From 83dfb6de85588c4c36bd3bcc1d42cbe2ec122aa8 Mon Sep 17 00:00:00 2001
From: jrobinso <933148+jrobinso@users.noreply.github.com>
Date: Thu, 8 Feb 2024 22:53:27 -0800
Subject: [PATCH] merge igv-widgets source into project
---
css/_file-load-widget.scss | 366 ++++++
css/_igv-widgets-alert-dialog.scss | 143 +++
css/app.css | 318 +++++-
css/app.css.map | 2 +-
css/app.scss | 8 +
js/app.js | 25 +-
js/genomeWidgets.js | 12 +-
js/shareHelper.js | 2 +-
js/shareWidgets.js | 3 +-
js/widgets/alertDialog.js | 92 ++
js/widgets/alertSingleton.js | 21 +
.../encodeTrackDatasourceConfigurator.js | 190 ++++
js/widgets/eventBus.js | 43 +
js/widgets/fileLoad.js | 88 ++
js/widgets/fileLoadManager.js | 62 +
js/widgets/fileLoadWidget.js | 224 ++++
js/widgets/genericSelectModal.js | 35 +
js/widgets/genomeFileLoad.js | 75 ++
js/widgets/markupFactory.js | 44 +
js/widgets/multipleTrackFileLoad.js | 181 +++
js/widgets/qrcode.js | 1000 +++++++++++++++++
js/widgets/sessionController.js | 79 ++
js/widgets/sessionFileLoad.js | 24 +
js/widgets/sessionWidgets.js | 150 +++
js/widgets/trackURLModal.js | 39 +
js/widgets/trackWidgets.js | 434 +++++++
js/widgets/urlModal.js | 38 +
js/widgets/utils.js | 40 +
js/widgets/utils/dom-utils.js | 120 ++
js/{ => widgets/utils}/draggable.js | 24 +-
js/widgets/utils/icons.js | 48 +
js/widgets/utils/ui-utils.js | 16 +
package.json | 3 +-
33 files changed, 3909 insertions(+), 40 deletions(-)
create mode 100644 css/_file-load-widget.scss
create mode 100644 css/_igv-widgets-alert-dialog.scss
create mode 100644 js/widgets/alertDialog.js
create mode 100644 js/widgets/alertSingleton.js
create mode 100644 js/widgets/encodeTrackDatasourceConfigurator.js
create mode 100644 js/widgets/eventBus.js
create mode 100644 js/widgets/fileLoad.js
create mode 100644 js/widgets/fileLoadManager.js
create mode 100644 js/widgets/fileLoadWidget.js
create mode 100644 js/widgets/genericSelectModal.js
create mode 100644 js/widgets/genomeFileLoad.js
create mode 100644 js/widgets/markupFactory.js
create mode 100644 js/widgets/multipleTrackFileLoad.js
create mode 100755 js/widgets/qrcode.js
create mode 100644 js/widgets/sessionController.js
create mode 100644 js/widgets/sessionFileLoad.js
create mode 100644 js/widgets/sessionWidgets.js
create mode 100644 js/widgets/trackURLModal.js
create mode 100644 js/widgets/trackWidgets.js
create mode 100644 js/widgets/urlModal.js
create mode 100644 js/widgets/utils.js
create mode 100644 js/widgets/utils/dom-utils.js
rename js/{ => widgets/utils}/draggable.js (77%)
create mode 100644 js/widgets/utils/icons.js
create mode 100644 js/widgets/utils/ui-utils.js
diff --git a/css/_file-load-widget.scss b/css/_file-load-widget.scss
new file mode 100644
index 00000000..d12a8a12
--- /dev/null
+++ b/css/_file-load-widget.scss
@@ -0,0 +1,366 @@
+$igv-flw-font-face: 'Open Sans', sans-serif;
+$igv-flw-font-size: .875rem;
+$igv-flw-light-grey-color: #bfbfbf;
+$igv-flw-grey-color: #7F7F7F;
+$igv-flw-dark-grey-color: #373737;
+
+$igv-flw-label-font-color: #242424;
+$igv-flw-outline-grey-color: #7c7c7c;
+
+$igv-flw-modal-button-dark-color: #6c757c;
+
+$igv-flw-button-ok-color: #5ea4e0;
+$igv-flw-button-ok-hover-color: #3b5c7f;
+$igv-flw-button-cancel-color: #c4c4c4;
+$igv-flw-button-cancel-hover-color: #7f7f7f;
+
+// helga dimensions
+$igv-flw-container-width: 720px;
+$igv-flw-container-height: 400px;
+
+$igv-flw-button-width: 75px;
+$igv-flw-button-height: 28px;
+
+$igv-flw-input-row-height: 36px;
+
+$igv-flw-border-radius: 2px;
+
+@mixin igv-flw-input {
+
+ input {
+ display: block;
+ height: 100%;
+ width: 100%;
+
+ padding-left: 4px;
+
+ color: $igv-flw-dark-grey-color;
+ font-size: $igv-flw-font-size;
+ font-family: $igv-flw-font-face;
+ font-weight: 400;
+ text-align: left;
+
+ outline: none;
+
+ border-style: solid;
+ border-width: thin;
+ border-color: #dee2e6;
+ border-radius: .25rem;
+
+ background-color: white;
+ }
+
+}
+
+.igv-file-load-widget-container {
+
+ position: relative;
+ border-color: transparent;
+ width:100%;
+
+ //padding-bottom: 20px;
+
+ color: $igv-flw-grey-color;
+ font-family: $igv-flw-font-face;
+ font-size: $igv-flw-font-size;
+ font-weight: 200;
+
+ border-style: solid;
+ border-width: thin;
+
+ background-color: white;
+
+ display: flex;
+ flex-flow: column;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+
+ // header
+ .igv-file-load-widget-header {
+ width: 100%;
+ height: 24px;
+
+ background-color: $igv-flw-light-grey-color;
+
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+ align-items: center;
+
+ // close button container
+ div {
+ height: 24px;
+ width: 16px;
+ margin-right: 6px;
+
+ text-align: center;
+ line-height: 24px;
+
+ color: $igv-flw-dark-grey-color;
+ }
+
+ div:hover {
+ cursor: pointer;
+ }
+
+ }
+
+ // input container
+ .igv-flw-input-container {
+
+ width: 95%;
+
+ margin-top: 24px;
+ margin-bottom: 0;
+
+ display: flex;
+ flex-flow: column;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+
+ // input row
+ .igv-flw-input-row {
+
+ height: $igv-flw-input-row-height;
+ width: 100%;
+
+ margin-top: 8px;
+
+ padding-top: 4px;
+ padding-bottom: 4px;
+
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+
+ border-color: white;
+ border-style: solid;
+ border-width: thin;
+ border-radius: calc(2 * #{$igv-flw-border-radius});
+
+ // label
+ .igv-flw-input-label {
+
+ //color: rgba(0, 0, 0, 0.76);
+ color: $igv-flw-modal-button-dark-color;
+ font-weight: 400;
+
+ //margin-left: 8px;
+ //width: 128px;
+ width: 136px;
+ height: $igv-flw-input-row-height;
+
+ line-height: $igv-flw-input-row-height;
+ text-align: left;
+ }
+
+ // url input
+ @include igv-flw-input;
+ input {
+ height: calc(#{$igv-flw-input-row-height} - 12px);
+ }
+
+ // local file chooser
+ .igv-flw-file-chooser-container {
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ align-items: center;
+
+ width: 130px;
+ height: calc(#{$igv-flw-input-row-height} - 8px);
+
+ border-color: $igv-flw-modal-button-dark-color;
+ border-style: solid;
+ border-width: thin;
+ border-radius: calc(2 * #{$igv-flw-border-radius});
+
+ background-color: white;
+
+ label {
+ display: block;
+ margin: unset;
+ }
+
+ label.igv-flw-label-color {
+ color: $igv-flw-modal-button-dark-color;
+ }
+
+ label.igv-flw-label-color-hover {
+ cursor: pointer;
+ }
+
+ input.igv-flw-file-chooser-input {
+ width: 0.1px;
+ height: 0.1px;
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ z-index: -1;
+ }
+
+ }
+
+ .igv-flw-file-chooser-container:hover {
+ cursor: pointer;
+ background-color: $igv-flw-modal-button-dark-color;
+ }
+
+ // button indicating drag/drop capability
+ .igv-flw-drag-drop-target {
+
+ cursor: default;
+
+ margin-left: 8px;
+
+ width: 120px;
+ height: calc(#{$igv-flw-input-row-height} - 8px);
+
+ line-height: calc(#{$igv-flw-input-row-height} - 8px);
+ text-align: center;
+
+ border-color: $igv-flw-grey-color;
+ border-style: dashed;
+ border-width: thin;
+ border-radius: calc(2 * #{$igv-flw-border-radius});
+
+ //background-color: rgba(173, 255, 47, 0.47);
+ }
+
+ // name of local file
+ .igv-flw-local-file-name-container {
+
+ max-width: 400px;
+ height: $igv-flw-input-row-height;
+
+ color: $igv-flw-dark-grey-color;
+ line-height: $igv-flw-input-row-height;
+ text-align: left;
+ font-weight: 400;
+
+ margin-left: 8px;
+
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ //background-color: rgba(128, 128, 128, 0.25);
+ }
+
+ }
+
+ .igv-flw-input-row-hover-state {
+ background-color: #efefef;
+ border-color: $igv-flw-grey-color;
+ }
+
+ }
+
+ .igv-flw-error-message-container {
+
+ margin-top: 8px;
+
+ width: 95%;
+ height: 24px;
+
+ padding-left: 8px;
+
+ color: white;
+ font-size: $igv-flw-font-size;
+
+ background-color: rgba(59, 92, 127, 0.5);
+
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
+
+ // message container
+ div:first-child.igv-flw-error-message {
+ height: 24px;
+ width: 600px;
+
+ font-style: italic;
+ line-height: 24px;
+ text-align: left;
+
+ //background-color: coral;
+ }
+
+ // close button container
+ div:last-child {
+ height: 24px;
+ width: 16px;
+ margin-right: 6px;
+
+ text-align: center;
+ line-height: 24px;
+
+ color: $igv-flw-dark-grey-color;
+ }
+
+ div:hover {
+ cursor: pointer;
+ }
+
+ }
+
+ // ok | cancel
+ .igv-file-load-widget-ok-cancel {
+
+ width: 100%;
+ height: $igv-flw-button-height;
+ margin-top: 32px;
+
+ color: white;
+ font-size: $igv-flw-font-size;
+
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+ align-items: center;
+
+ div {
+ width: $igv-flw-button-width;
+ height: $igv-flw-button-height;
+
+ line-height: $igv-flw-button-height;
+ text-align: center;
+
+ border-color: transparent;
+ border-style: solid;
+ border-width: thin;
+ border-radius: $igv-flw-border-radius;
+
+ margin-right: 16px;
+ }
+
+ div:first-child {
+ margin-right: 22px;
+ background-color: $igv-flw-button-cancel-color;
+ }
+
+ div:first-child:hover {
+ cursor: pointer;
+ background-color: $igv-flw-button-cancel-hover-color;
+ }
+
+ div:last-child {
+ background-color: $igv-flw-button-ok-color;
+ }
+
+ div:last-child:hover {
+ cursor: pointer;
+ background-color: $igv-flw-button-ok-hover-color;
+ }
+
+ }
+
+}
diff --git a/css/_igv-widgets-alert-dialog.scss b/css/_igv-widgets-alert-dialog.scss
new file mode 100644
index 00000000..408366f2
--- /dev/null
+++ b/css/_igv-widgets-alert-dialog.scss
@@ -0,0 +1,143 @@
+
+//$igv-alert-dialog-width: 300px;
+$igv-alert-dialog-width: 400px;
+$igv-alert-dialog-height: 200px;
+$igv-alert-dialog-margin: -150px;
+
+$igv-alert-dialog-header-height: 24px;
+$igv-alert-dialog-ok-container-height: 64px;
+$igv-alert-dialog-ok-button-height: 30px;
+$igv-alert-dialog-body-copy-margin: 16px;
+
+.igv-widgets-alert-dialog-container {
+
+ box-sizing: content-box;
+
+ position: absolute;
+ z-index: 2048;
+ top:50%;
+ left:50%;
+
+ width:$igv-alert-dialog-width;
+ height:$igv-alert-dialog-height;
+
+ border-color: $igv-trackgear-grey-color;
+ border-radius: $igv-trackgear-popover-border-radius;
+ border-style: solid;
+ border-width: thin;
+
+ outline: none;
+
+ font-family: $igv-default-font-face;
+ font-size: 15px;
+ font-weight: 400;
+
+ background-color: white;
+
+ display: flex;
+ flex-flow: column;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
+
+ // header
+ > div:first-child {
+
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+
+ width: 100%;
+ height: $igv-alert-dialog-header-height;
+ cursor: move;
+
+ border-top-left-radius: $igv-trackgear-popover-border-radius;
+ border-top-right-radius: $igv-trackgear-popover-border-radius;
+ border-bottom-color: $igv-trackgear-grey-color;
+ border-bottom-style: solid;
+ border-bottom-width: thin;
+
+ background-color: #eee;
+
+ div:first-child {
+ // ERROR
+ padding-left: 8px;
+ }
+
+ }
+
+ // body container
+ #igv-widgets-alert-dialog-body {
+ color: $igv-dark-grey-color;
+
+ width: 100%;
+ height: calc(100% - #{$igv-alert-dialog-header-height} - #{$igv-alert-dialog-ok-container-height});
+
+ overflow-y: scroll;
+
+ #igv-widgets-alert-dialog-body-copy {
+ cursor: pointer;
+ //user-select: none;
+
+ margin: $igv-alert-dialog-body-copy-margin;
+
+ width: auto;
+ height: auto;
+
+ //overflow-x: hidden;
+ //overflow-y: scroll;
+
+ overflow-wrap: break-word;
+ word-break: break-word;
+
+ background-color: white;
+ border: unset;
+ }
+ }
+
+ // ok - container
+ > div:last-child {
+
+ width: 100%;
+ margin-bottom: 10px;
+
+ background-color: white;
+
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: center;
+ align-items: center;
+
+ // ok - button
+ div {
+ margin: unset;
+ width: 40px;
+ height: $igv-alert-dialog-ok-button-height;
+
+ line-height: $igv-alert-dialog-ok-button-height;
+ text-align: center;
+
+ color: white;
+ font-family: $igv-default-font-face;
+ font-size: small;
+ font-weight: 400;
+
+ border-color: #2B81AF;
+ border-style: solid;
+ border-width: thin;
+ border-radius: 4px;
+ background-color: #2B81AF;
+ }
+
+ div:hover {
+ cursor: pointer;
+ border-color: #25597f;
+ background-color: #25597f;
+ }
+
+ }
+
+}
diff --git a/css/app.css b/css/app.css
index 58685e4b..5fee7b2b 100644
--- a/css/app.css
+++ b/css/app.css
@@ -129,7 +129,323 @@ select#igv-app-track-select-modal-select option {
font-size: 1rem;
}
select#igv-app-track-select-modal-select option[disabled] {
- color: rgb(175, 175, 175);
+ color: #afafaf;
+}
+
+.igv-widgets-alert-dialog-container {
+ box-sizing: content-box;
+ position: absolute;
+ z-index: 2048;
+ top: 50%;
+ left: 50%;
+ width: 400px;
+ height: 200px;
+ border-color: #7F7F7F;
+ border-radius: 4px;
+ border-style: solid;
+ border-width: thin;
+ outline: none;
+ font-family: "Open Sans", sans-serif;
+ font-size: 15px;
+ font-weight: 400;
+ background-color: white;
+ display: flex;
+ flex-flow: column;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
+}
+.igv-widgets-alert-dialog-container > div:first-child {
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+ width: 100%;
+ height: 24px;
+ cursor: move;
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ border-bottom-color: #7F7F7F;
+ border-bottom-style: solid;
+ border-bottom-width: thin;
+ background-color: #eee;
+}
+.igv-widgets-alert-dialog-container > div:first-child div:first-child {
+ padding-left: 8px;
+}
+.igv-widgets-alert-dialog-container #igv-widgets-alert-dialog-body {
+ color: #373737;
+ width: 100%;
+ height: calc(100% - 24px - 64px);
+ overflow-y: scroll;
+}
+.igv-widgets-alert-dialog-container #igv-widgets-alert-dialog-body #igv-widgets-alert-dialog-body-copy {
+ cursor: pointer;
+ margin: 16px;
+ width: auto;
+ height: auto;
+ overflow-wrap: break-word;
+ word-break: break-word;
+ background-color: white;
+ border: unset;
+}
+.igv-widgets-alert-dialog-container > div:last-child {
+ width: 100%;
+ margin-bottom: 10px;
+ background-color: white;
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: center;
+ align-items: center;
+}
+.igv-widgets-alert-dialog-container > div:last-child div {
+ margin: unset;
+ width: 40px;
+ height: 30px;
+ line-height: 30px;
+ text-align: center;
+ color: white;
+ font-family: "Open Sans", sans-serif;
+ font-size: small;
+ font-weight: 400;
+ border-color: #2B81AF;
+ border-style: solid;
+ border-width: thin;
+ border-radius: 4px;
+ background-color: #2B81AF;
+}
+.igv-widgets-alert-dialog-container > div:last-child div:hover {
+ cursor: pointer;
+ border-color: #25597f;
+ background-color: #25597f;
+}
+
+.igv-file-load-widget-container {
+ position: relative;
+ border-color: transparent;
+ width: 100%;
+ color: #7F7F7F;
+ font-family: "Open Sans", sans-serif;
+ font-size: 0.875rem;
+ font-weight: 200;
+ border-style: solid;
+ border-width: thin;
+ background-color: white;
+ display: flex;
+ flex-flow: column;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+}
+.igv-file-load-widget-container .igv-file-load-widget-header {
+ width: 100%;
+ height: 24px;
+ background-color: #bfbfbf;
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+ align-items: center;
+}
+.igv-file-load-widget-container .igv-file-load-widget-header div {
+ height: 24px;
+ width: 16px;
+ margin-right: 6px;
+ text-align: center;
+ line-height: 24px;
+ color: #373737;
+}
+.igv-file-load-widget-container .igv-file-load-widget-header div:hover {
+ cursor: pointer;
+}
+.igv-file-load-widget-container .igv-flw-input-container {
+ width: 95%;
+ margin-top: 24px;
+ margin-bottom: 0;
+ display: flex;
+ flex-flow: column;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row {
+ height: 36px;
+ width: 100%;
+ margin-top: 8px;
+ padding-top: 4px;
+ padding-bottom: 4px;
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+ border-color: white;
+ border-style: solid;
+ border-width: thin;
+ border-radius: calc(2 * 2px);
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-input-label {
+ color: #6c757c;
+ font-weight: 400;
+ width: 136px;
+ height: 36px;
+ line-height: 36px;
+ text-align: left;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row input {
+ display: block;
+ height: 100%;
+ width: 100%;
+ padding-left: 4px;
+ color: #373737;
+ font-size: 0.875rem;
+ font-family: "Open Sans", sans-serif;
+ font-weight: 400;
+ text-align: left;
+ outline: none;
+ border-style: solid;
+ border-width: thin;
+ border-color: #dee2e6;
+ border-radius: 0.25rem;
+ background-color: white;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row input {
+ height: calc(36px - 12px);
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container {
+ display: flex;
+ flex-flow: row;
+ justify-content: center;
+ align-items: center;
+ width: 130px;
+ height: calc(36px - 8px);
+ border-color: #6c757c;
+ border-style: solid;
+ border-width: thin;
+ border-radius: calc(2 * 2px);
+ background-color: white;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container label {
+ display: block;
+ margin: unset;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container label.igv-flw-label-color {
+ color: #6c757c;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container label.igv-flw-label-color-hover {
+ cursor: pointer;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container input.igv-flw-file-chooser-input {
+ width: 0.1px;
+ height: 0.1px;
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ z-index: -1;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-file-chooser-container:hover {
+ cursor: pointer;
+ background-color: #6c757c;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-drag-drop-target {
+ cursor: default;
+ margin-left: 8px;
+ width: 120px;
+ height: calc(36px - 8px);
+ line-height: calc(36px - 8px);
+ text-align: center;
+ border-color: #7F7F7F;
+ border-style: dashed;
+ border-width: thin;
+ border-radius: calc(2 * 2px);
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row .igv-flw-local-file-name-container {
+ max-width: 400px;
+ height: 36px;
+ color: #373737;
+ line-height: 36px;
+ text-align: left;
+ font-weight: 400;
+ margin-left: 8px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.igv-file-load-widget-container .igv-flw-input-container .igv-flw-input-row-hover-state {
+ background-color: #efefef;
+ border-color: #7F7F7F;
+}
+.igv-file-load-widget-container .igv-flw-error-message-container {
+ margin-top: 8px;
+ width: 95%;
+ height: 24px;
+ padding-left: 8px;
+ color: white;
+ font-size: 0.875rem;
+ background-color: rgba(59, 92, 127, 0.5);
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ align-items: center;
+}
+.igv-file-load-widget-container .igv-flw-error-message-container div:first-child.igv-flw-error-message {
+ height: 24px;
+ width: 600px;
+ font-style: italic;
+ line-height: 24px;
+ text-align: left;
+}
+.igv-file-load-widget-container .igv-flw-error-message-container div:last-child {
+ height: 24px;
+ width: 16px;
+ margin-right: 6px;
+ text-align: center;
+ line-height: 24px;
+ color: #373737;
+}
+.igv-file-load-widget-container .igv-flw-error-message-container div:hover {
+ cursor: pointer;
+}
+.igv-file-load-widget-container .igv-file-load-widget-ok-cancel {
+ width: 100%;
+ height: 28px;
+ margin-top: 32px;
+ color: white;
+ font-size: 0.875rem;
+ display: flex;
+ flex-flow: row;
+ flex-wrap: nowrap;
+ justify-content: flex-end;
+ align-items: center;
+}
+.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div {
+ width: 75px;
+ height: 28px;
+ line-height: 28px;
+ text-align: center;
+ border-color: transparent;
+ border-style: solid;
+ border-width: thin;
+ border-radius: 2px;
+ margin-right: 16px;
+}
+.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:first-child {
+ margin-right: 22px;
+ background-color: #c4c4c4;
+}
+.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:first-child:hover {
+ cursor: pointer;
+ background-color: #7f7f7f;
+}
+.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:last-child {
+ background-color: #5ea4e0;
+}
+.igv-file-load-widget-container .igv-file-load-widget-ok-cancel div:last-child:hover {
+ cursor: pointer;
+ background-color: #3b5c7f;
}
body {
diff --git a/css/app.css.map b/css/app.css.map
index 14114af8..18167d54 100644
--- a/css/app.css.map
+++ b/css/app.css.map
@@ -1 +1 @@
-{"version":3,"sourceRoot":"","sources":["_panel.scss","_color.scss","_share-modal.scss","_encode.scss","_track-select.scss","app.scss"],"names":[],"mappings":"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","file":"app.css"}
\ No newline at end of file
+{"version":3,"sourceRoot":"","sources":["_panel.scss","_color.scss","_share-modal.scss","_encode.scss","_track-select.scss","_igv-widgets-alert-dialog.scss","app.scss","_file-load-widget.scss"],"names":[],"mappings":"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E;EACA,kBAjV8B;;AAoVhC;EACE,kBAxVoB;;AA2VtB;EACE;EACA,kBA5V0B;;;ADchC;EACE,aAPkB;;;AAYlB;EACE;;;AAKJ;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAIA;EACE;;;AAIJ;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;;AAGF;EACE,YA/DkB;EAgElB,kBAvEmB;;;AA6Ef;EACE;EACA;EACA;;AAEF;EACE;;;AAMR;EACE;EAEA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;;;AAGF;EAEE;EACA;EACA;EACA;EACA;EAEA;EACA;EAEA;EACA;;AAEA;EACE;;AAEF;EACE;;;AAKJ;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKA;EACE;;AAEA;EACE;;;AAKN;EACE;;;AAGF;EACE;EACA;;;AAKA;EACE;;;AAOF;EACE;;;AAKJ;EAEE;EACA;EACA;EACA;EACA;EAEA;EACA;;;AAOE;EACE,OAxMe;EAyMf;EACA;EACA;;;AAKN;EACE;;;AAKA;EACE,OAvNiB;EAwNjB;EAEA;;AAGF;EAEE;;;AAKJ;EACE;EACA;;;AAGF;EACE,eArOsB;;;AAwOxB;EACE;EACA;EAEA;EACA,QA7OsB;EA8OtB,aA9OsB;EAgPtB;EACA;EACA;EACA;EACA;;AAEA;EACE,QAvPoB;EAwPpB,aAxPoB;EAyPpB;EACA;;AACA;EACE,OAjQe;EAkQf;;AAIJ;EACE,QAlQoB;EAmQpB,aAnQoB;EAoQpB;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;;;AAMN;EACE;EAEA,QA1RkC;EA4RlC;;;AAIF;EACE,kBArSmB;;;AAwSrB;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE,OAvTqB;EAwTrB;EACA;;;AAGF;EACE,OA5TmB;EA6TnB,cA9TqB;;;AAiUvB;EACE,kBAlUqB;;;AAqUvB;EACE,kBArUmB;;;AAwUrB;EACE;;;AAQA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAKJ;EACE;;;AAIF;EACE;EACA;;;AAIF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAKF;EACE;;;AAKA;EACE;EACA;EAEA;EACA;EAEA;EACA;EACA;EACA;;AAGF;EACE;;;AAIJ;EACE;EACA;;;AAGF;EAEE;IACE;IACA;IAEA;IACA,QA7ZoB;IA8ZpB,aA9ZoB;IAgapB;IACA;IACA;IACA;IACA;;EAEA;IACE,QAvakB;IAwalB,aAxakB;IAyalB;IACA;;EACA;IACE,OAjba;IAkbb;;EAIJ;IACE,QAlbkB;IAmblB,aAnbkB;IAoblB;IACA;;EAGF;IACE","file":"app.css"}
\ No newline at end of file
diff --git a/css/app.scss b/css/app.scss
index e11eb839..939ca494 100644
--- a/css/app.scss
+++ b/css/app.scss
@@ -5,6 +5,10 @@
@import "track-select";
//
+$igv-default-font-face: 'Open Sans', sans-serif;
+$igv-trackgear-grey-color: #7F7F7F;
+$igv-trackgear-popover-border-radius: 4px;
+$igv-dark-grey-color: #373737;
$igv-app-light-color: #f7f7f7;
$igv-app-medium-color: #a6a6a6;
$igv-app-dark-color: #5f5f5f;
@@ -16,6 +20,10 @@ $igv-app-footer-height: 48px;
$igv-app-logo-container-width: 20%;
$igv-header-height: 48px;
+@import "igv-widgets-alert-dialog";
+@import "file-load-widget";
+
+
body {
padding-top: $igv-header-height;
}
diff --git a/js/app.js b/js/app.js
index 3c6d4b78..a601b2c6 100644
--- a/js/app.js
+++ b/js/app.js
@@ -24,22 +24,15 @@
import igv from '../node_modules/igv/dist/igv.esm.min.js'
import * as GoogleAuth from '../node_modules/google-utils/src/googleAuth.js'
import * as GooglePicker from '../node_modules/google-utils/src/googleFilePicker.js'
-import {makeDraggable} from "./draggable.js"
-import {
- AlertSingleton,
- createSessionWidgets,
- createTrackWidgetsWithTrackRegistry,
- dropboxButtonImageBase64,
- dropboxDropdownItem,
- GenomeFileLoad,
- googleDriveButtonImageBase64,
- googleDriveDropdownItem,
- getPathsWithTrackRegistryFile,
- updateTrackMenusWithTrackConfigurations,
- FileLoadManager,
- FileLoadWidget,
- Utils
-} from '../node_modules/igv-widgets/dist/igv-widgets.js'
+import makeDraggable from "./widgets/utils/draggable.js"
+import AlertSingleton from "./widgets/alertSingleton.js"
+import {createSessionWidgets} from "./widgets/sessionWidgets.js"
+import {updateTrackMenusWithTrackConfigurations, createTrackWidgetsWithTrackRegistry, getPathsWithTrackRegistryFile} from "./widgets/trackWidgets.js"
+import {dropboxDropdownItem, dropboxButtonImageBase64, googleDriveButtonImageBase64, googleDriveDropdownItem} from "./widgets/markupFactory.js"
+import GenomeFileLoad from "./widgets/genomeFileLoad.js"
+import FileLoadManager from "./widgets/fileLoadManager.js"
+import FileLoadWidget from "./widgets/fileLoadWidget.js"
+import * as Utils from './widgets/utils.js'
import Globals from "./globals.js"
import {createGenomeWidgets, initializeGenomeWidgets, loadGenome} from './genomeWidgets.js'
import {createShareWidgets, shareWidgetConfigurator} from './shareWidgets.js'
diff --git a/js/genomeWidgets.js b/js/genomeWidgets.js
index 07dba523..1b53d26c 100644
--- a/js/genomeWidgets.js
+++ b/js/genomeWidgets.js
@@ -24,13 +24,11 @@
* THE SOFTWARE.
*/
-import {
- AlertSingleton,
- createURLModal,
- FileLoadManager,
- FileLoadWidget,
- Utils
-} from '../node_modules/igv-widgets/dist/igv-widgets.js'
+import AlertSingleton from "./widgets/alertSingleton.js"
+import {createURLModal} from "./widgets/urlModal.js"
+import FileLoadManager from "./widgets/fileLoadManager.js"
+import FileLoadWidget from "./widgets/fileLoadWidget.js"
+import * as Utils from './widgets/utils.js'
import Globals from "./globals.js"
const MAX_CUSTOM_GENOMES = 10
diff --git a/js/shareHelper.js b/js/shareHelper.js
index 3291694c..0be8e685 100644
--- a/js/shareHelper.js
+++ b/js/shareHelper.js
@@ -20,7 +20,7 @@
* THE SOFTWARE.
*
*/
-import {AlertSingleton} from '../node_modules/igv-widgets/dist/igv-widgets.js'
+import AlertSingleton from "./widgets/alertSingleton.js"
import {bitlyShortener, googleShortener, tinyURLShortener} from "./urlShortener.js";
import Globals from "./globals.js";
diff --git a/js/shareWidgets.js b/js/shareWidgets.js
index f4f6c3e8..0e91fe1e 100644
--- a/js/shareWidgets.js
+++ b/js/shareWidgets.js
@@ -22,7 +22,8 @@
*/
import igv from '../node_modules/igv/dist/igv.esm.min.js'
-import {AlertSingleton, QRCode} from '../node_modules/igv-widgets/dist/igv-widgets.js'
+import AlertSingleton from './widgets/alertSingleton.js'
+import {QRCode} from './widgets/qrcode.js'
import {setURLShortener, shortSessionURL} from './shareHelper.js'
function createShareWidgets({browser, container, modal, share_input, copy_link_button, email_button, qrcode_button, qrcode_image, embed_container, embed_button, embedTarget}) {
diff --git a/js/widgets/alertDialog.js b/js/widgets/alertDialog.js
new file mode 100644
index 00000000..06626764
--- /dev/null
+++ b/js/widgets/alertDialog.js
@@ -0,0 +1,92 @@
+import * as DOMUtils from "./utils/dom-utils.js"
+import makeDraggable from "./utils/draggable.js"
+
+const httpMessages =
+ {
+ "401": "Access unauthorized",
+ "403": "Access forbidden",
+ "404": "Not found"
+ }
+
+
+class AlertDialog {
+ constructor(parent) {
+
+ // container
+ this.container = DOMUtils.div({class: "igv-widgets-alert-dialog-container"})
+ parent.appendChild(this.container)
+ this.container.setAttribute('tabIndex', '-1')
+
+ // header
+ const header = DOMUtils.div()
+ this.container.appendChild(header)
+
+ this.errorHeadline = DOMUtils.div()
+ header.appendChild(this.errorHeadline)
+ this.errorHeadline.textContent = ''
+
+ // body container
+ let bodyContainer = DOMUtils.div({id: 'igv-widgets-alert-dialog-body'})
+ this.container.appendChild(bodyContainer)
+
+ // body copy
+ this.body = DOMUtils.div({id: 'igv-widgets-alert-dialog-body-copy'})
+ bodyContainer.appendChild(this.body)
+
+ // ok container
+ let ok_container = DOMUtils.div()
+ this.container.appendChild(ok_container)
+
+ // ok
+ this.ok = DOMUtils.div()
+ ok_container.appendChild(this.ok)
+ this.ok.textContent = 'OK'
+
+ const okHandler = () => {
+
+ if (typeof this.callback === 'function') {
+ this.callback("OK")
+ this.callback = undefined
+ }
+ this.body.innerHTML = ''
+ DOMUtils.hide(this.container)
+ }
+
+ this.ok.addEventListener('click', event => {
+
+ event.stopPropagation()
+
+ okHandler()
+ })
+
+ this.container.addEventListener('keypress', event => {
+
+ event.stopPropagation()
+
+ if ('Enter' === event.key) {
+ okHandler()
+ }
+ })
+
+ makeDraggable(this.container, header)
+
+ DOMUtils.hide(this.container)
+ }
+
+ present(alert, callback) {
+
+ this.errorHeadline.textContent = alert.message ? 'ERROR' : ''
+ let string = alert.message || alert
+
+ if (httpMessages.hasOwnProperty(string)) {
+ string = httpMessages[string]
+ }
+
+ this.body.innerHTML = string
+ this.callback = callback
+ DOMUtils.show(this.container, "flex")
+ this.container.focus()
+ }
+}
+
+export default AlertDialog
diff --git a/js/widgets/alertSingleton.js b/js/widgets/alertSingleton.js
new file mode 100644
index 00000000..23dd2426
--- /dev/null
+++ b/js/widgets/alertSingleton.js
@@ -0,0 +1,21 @@
+import AlertDialog from './alertDialog.js'
+
+class AlertSingleton {
+ constructor(root) {
+
+ if (root) {
+ this.alertDialog = undefined
+ }
+ }
+
+ init(root) {
+ this.alertDialog = new AlertDialog(root)
+ }
+
+ present(alert, callback) {
+ this.alertDialog.present(alert, callback)
+ }
+
+}
+
+export default new AlertSingleton()
diff --git a/js/widgets/encodeTrackDatasourceConfigurator.js b/js/widgets/encodeTrackDatasourceConfigurator.js
new file mode 100644
index 00000000..a49e501c
--- /dev/null
+++ b/js/widgets/encodeTrackDatasourceConfigurator.js
@@ -0,0 +1,190 @@
+/**
+ * Factory function to create a configuration object for the EncodeTrackDatasource given a genomicId and type
+ * @param genomeId
+ * @param type - 'signals' | 'other
+ * @returns {{genomeId: *, selectionHandler: (function(*): *|Uint8Array|BigInt64Array|{color, name, url}[]|Float64Array|Int8Array|Float32Array|Int32Array|Uint32Array|Uint8ClampedArray|BigUint64Array|Int16Array|Uint16Array), hiddenColumns: [string, string, string], addIndexColumn: boolean, parser: undefined, isJSON: boolean, urlPrefix: string, columns: string[], dataSetPath: undefined, titles: {AssayType: string, BioRep: string, OutputType: string, TechRep: string}, suffix: *, dataSetPathPrefix: string}}
+ */
+function encodeTrackDatasourceConfigurator(genomeId, type) {
+
+ const root = 'https://s3.amazonaws.com/igv.org.app/encode/'
+ let url
+
+ switch (type) {
+ case 'signals-chip':
+ url = `${root}${canonicalId(genomeId)}.signals.chip.txt`
+ break
+ case 'signals-other':
+ url = `${root}${canonicalId(genomeId)}.signals.other.txt`
+ break
+ case 'other':
+ url = `${root}${canonicalId(genomeId)}.other.txt`
+ break
+
+ }
+
+ return {
+ isJSON: false,
+ url,
+ sort: encodeSort,
+ columns:
+ [
+ //'ID', // hide
+ //'Assembly', // hide
+ 'Biosample',
+ 'AssayType',
+ 'Target',
+ 'BioRep',
+ 'TechRep',
+ 'OutputType',
+ 'Format',
+ 'Lab',
+ //'HREF', // hide
+ 'Accession',
+ 'Experiment'
+ ],
+ columnDefs:
+ {
+ AssayType: {title: 'Assay Type'},
+ OutputType: {title: 'Output Type'},
+ BioRep: {title: 'Bio Rep'},
+ TechRep: {title: 'Tech Rep'}
+ },
+
+ rowHandler: row => {
+ const name = constructName(row)
+ const url = `https://www.encodeproject.org${row['HREF']}`
+ const color = colorForTarget(row['Target'])
+ return {name, url, color}
+ }
+
+ }
+}
+
+
+function supportsGenome(genomeId) {
+ const knownGenomes = new Set(["ce10", "ce11", "dm3", "dm6", "GRCh38", "hg19", "mm9", "mm10"])
+ const id = canonicalId(genomeId)
+ return knownGenomes.has(id)
+}
+
+
+function canonicalId(genomeId) {
+
+ switch (genomeId) {
+ case "hg38":
+ return "GRCh38"
+ case "CRCh37":
+ return "hg19"
+ case "GRCm38":
+ return "mm10"
+ case "NCBI37":
+ return "mm9"
+ case "WBcel235":
+ return "ce11"
+ case "WS220":
+ return "ce10"
+ default:
+ return genomeId
+ }
+}
+
+function constructName(record) {
+
+ let name = record["Biosample"] || ""
+
+ if (record["Target"]) {
+ name += " " + record["Target"]
+ }
+ if (record["AssayType"].toLowerCase() !== "chip-seq") {
+ name += " " + record["AssayType"]
+ }
+
+
+ return name
+
+}
+
+// Longer form of constructName, not currently used
+function _constructName(record) {
+ let name = record["Cell Type"] || ""
+
+ if (record["Target"]) {
+ name += " " + record["Target"]
+ }
+ if (record["AssayType"] && record["AssayType"].toLowerCase() !== "chip-seq") {
+ name += " " + record["AssayType"]
+ }
+ if (record["BioRep"]) {
+ name += " " + record["BioRep"]
+ }
+ if (record["TechRep"]) {
+ name += (record["BioRep"] ? ":" : " 0:") + record["TechRep"]
+ }
+ if (record["OutputType"]) {
+ name += " " + record["OutputType"]
+ }
+ if (record["Accession"]) {
+ name += " " + record["Accession"]
+ }
+ return name
+}
+
+
+function encodeSort(a, b) {
+ var aa1,
+ aa2,
+ cc1,
+ cc2,
+ tt1,
+ tt2
+
+ aa1 = a['Assay Type']
+ aa2 = b['Assay Type']
+ cc1 = a['Biosample']
+ cc2 = b['Biosample']
+ tt1 = a['Target']
+ tt2 = b['Target']
+
+ if (aa1 === aa2) {
+ if (cc1 === cc2) {
+ if (tt1 === tt2) {
+ return 0
+ } else if (tt1 < tt2) {
+ return -1
+ } else {
+ return 1
+ }
+ } else if (cc1 < cc2) {
+ return -1
+ } else {
+ return 1
+ }
+ } else {
+ if (aa1 < aa2) {
+ return -1
+ } else {
+ return 1
+ }
+ }
+}
+
+function colorForTarget(target) {
+
+ const t = target.toLowerCase()
+ if (t.startsWith("h3k4")) {
+ return "rgb(0,150,0)"
+ } else if (t.startsWith("h3k27")) {
+ return "rgb(200,0,0)"
+ } else if (t.startsWith("h3k36")) {
+ return "rgb(0,0,150)"
+ } else if (t.startsWith("h3k9")) {
+ return "rgb(100,0,0)"
+ } else if (t === "ctcf") {
+ return "black"
+ } else {
+ return undefined
+ }
+}
+
+
+export {encodeTrackDatasourceConfigurator, supportsGenome}
diff --git a/js/widgets/eventBus.js b/js/widgets/eventBus.js
new file mode 100644
index 00000000..b0723793
--- /dev/null
+++ b/js/widgets/eventBus.js
@@ -0,0 +1,43 @@
+let subscribers = {}
+
+class EventBus {
+ constructor() {
+
+ }
+
+ subscribe(eventType, object) {
+
+ let subscriberList = subscribers[eventType]
+ if (undefined === subscriberList) {
+ subscriberList = []
+ subscribers[eventType] = subscriberList
+ }
+ subscriberList.push(object)
+ }
+
+ post(event) {
+
+ const subscriberList = subscribers[event.type]
+ if (subscriberList) {
+
+ for (let subscriber of subscriberList) {
+
+ if ("function" === typeof subscriber.receiveEvent) {
+ subscriber.receiveEvent(event)
+ } else if ("function" === typeof subscriber) {
+ subscriber(event)
+ }
+ }
+ }
+ }
+
+ static createEvent(type, data, propogate) {
+ return {type: type, data: data || {}, propogate: propogate !== undefined ? propogate : true}
+ }
+
+}
+
+// Global event bus
+EventBus.globalBus = new EventBus()
+
+export default EventBus
diff --git a/js/widgets/fileLoad.js b/js/widgets/fileLoad.js
new file mode 100644
index 00000000..74a4b5f0
--- /dev/null
+++ b/js/widgets/fileLoad.js
@@ -0,0 +1,88 @@
+import AlertSingleton from './alertSingleton.js'
+import * as DOMUtils from "./utils/dom-utils.js"
+import {GooglePicker} from "../../node_modules/igv-utils/src/index.js"
+
+class FileLoad {
+
+ constructor({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton}) {
+
+ localFileInput.addEventListener('change', async () => {
+
+ if (true === FileLoad.isValidLocalFileInput(localFileInput)) {
+
+ try {
+ await this.loadPaths(Array.from(localFileInput.files))
+ } catch (e) {
+ console.error(e)
+ AlertSingleton.present(e)
+ }
+ localFileInput.value = ''
+ }
+
+ })
+
+ if (dropboxButton) dropboxButton.addEventListener('click', async () => {
+
+ const result = await initializeDropbox()
+
+ if (true === result) {
+
+ const config =
+ {
+ success: async dbFiles => {
+ try {
+ await this.loadPaths(dbFiles.map(dbFile => dbFile.link))
+ } catch (e) {
+ console.error(e)
+ AlertSingleton.present(e)
+ }
+ },
+ cancel: () => {
+ },
+ linkType: 'preview',
+ multiselect: true,
+ folderselect: false,
+ }
+
+ Dropbox.choose(config)
+
+ } else {
+ AlertSingleton.present('Cannot connect to Dropbox')
+ }
+
+ })
+
+
+ if (false === googleEnabled) {
+ DOMUtils.hide(googleDriveButton.parentElement)
+ }
+
+ if (true === googleEnabled && googleDriveButton) {
+
+ googleDriveButton.addEventListener('click', () => {
+ GooglePicker.createDropdownButtonPicker(true, async responses => {
+
+ try {
+ await this.loadPaths(responses.map(({url}) => url))
+ } catch (e) {
+ console.error(e)
+ AlertSingleton.present(e)
+ }
+ })
+ })
+
+ }
+
+ }
+
+ async loadPaths(paths) {
+ //console.log('FileLoad: loadPaths(...)');
+ }
+
+ static isValidLocalFileInput(input) {
+ return (input.files && input.files.length > 0)
+ }
+
+}
+
+export default FileLoad
diff --git a/js/widgets/fileLoadManager.js b/js/widgets/fileLoadManager.js
new file mode 100644
index 00000000..ab1c6049
--- /dev/null
+++ b/js/widgets/fileLoadManager.js
@@ -0,0 +1,62 @@
+import {FileUtils} from "../../node_modules/igv-utils/src/index.js"
+
+class FileLoadManager {
+
+ constructor() {
+ this.dictionary = {}
+ }
+
+ inputHandler(path, isIndexFile) {
+ this.ingestPath(path, isIndexFile)
+ }
+
+ ingestPath(path, isIndexFile) {
+ let key = true === isIndexFile ? 'index' : 'data'
+
+ this.dictionary[key] = path.trim()
+ }
+
+ didDragDrop(dataTransfer) {
+ var files
+
+ files = dataTransfer.files
+
+ return (files && files.length > 0)
+ }
+
+ dragDropHandler(dataTransfer, isIndexFile) {
+ var url,
+ files,
+ isValid
+
+ url = dataTransfer.getData('text/uri-list')
+ files = dataTransfer.files
+
+ if (files && files.length > 0) {
+ this.ingestPath(files[0], isIndexFile)
+ } else if (url && '' !== url) {
+ this.ingestPath(url, isIndexFile)
+ }
+
+ }
+
+ indexName() {
+ return itemName(this.dictionary.index)
+ }
+
+ dataName() {
+ return itemName(this.dictionary.data)
+ }
+
+ reset() {
+ this.dictionary = {}
+ }
+
+}
+
+function itemName(item) {
+ return FileUtils.isFilePath(item) ? item.name : item
+}
+
+export default FileLoadManager
+
diff --git a/js/widgets/fileLoadWidget.js b/js/widgets/fileLoadWidget.js
new file mode 100644
index 00000000..02d1c219
--- /dev/null
+++ b/js/widgets/fileLoadWidget.js
@@ -0,0 +1,224 @@
+import * as DOMUtils from "./utils/dom-utils.js"
+import * as UIUtils from "./utils/ui-utils.js"
+
+class FileLoadWidget {
+
+ constructor({widgetParent, dataTitle, indexTitle, mode, fileLoadManager, dataOnly, doURL}) {
+
+ dataTitle = dataTitle || 'Data'
+
+ indexTitle = indexTitle || 'Index'
+
+ this.fileLoadManager = fileLoadManager
+
+ dataOnly = dataOnly || false
+
+ // TODO: Remove?
+ doURL = doURL || false
+
+ // file load widget
+ this.container = DOMUtils.div({class: 'igv-file-load-widget-container'})
+ widgetParent.appendChild(this.container)
+
+ let config
+ if ('localFile' === mode) {
+ // local data/index
+ config =
+ {
+ parent: this.container,
+ doURL: false,
+ dataTitle: dataTitle + ' file',
+ indexTitle: indexTitle + ' file',
+ dataOnly
+ }
+ } else {
+
+ // url data/index
+ config =
+ {
+ parent: this.container,
+ doURL: true,
+ dataTitle: dataTitle + ' URL',
+ indexTitle: indexTitle + ' URL',
+ dataOnly
+ }
+ }
+
+ this.createInputContainer(config)
+
+ // error message container
+ this.error_message = DOMUtils.div({class: 'igv-flw-error-message-container'})
+ this.container.appendChild(this.error_message)
+
+ // error message
+ this.error_message.appendChild(DOMUtils.div({class: 'igv-flw-error-message'}))
+
+ // error dismiss button
+ UIUtils.attachDialogCloseHandlerWithParent(this.error_message, () => {
+ this.dismissErrorMessage()
+ })
+
+ this.dismissErrorMessage()
+
+ }
+
+ retrievePaths() {
+
+ this.fileLoadManager.ingestPath(this.inputData.value, false)
+ if (this.inputIndex) {
+ this.fileLoadManager.ingestPath(this.inputIndex.value, true)
+ }
+
+ let paths = []
+ if (this.fileLoadManager.dictionary) {
+
+ if (this.fileLoadManager.dictionary.data) {
+ paths.push(this.fileLoadManager.dictionary.data)
+ }
+ if (this.fileLoadManager.dictionary.index) {
+ paths.push(this.fileLoadManager.dictionary.index)
+ }
+ }
+
+ // clear input elements
+ this.container.querySelectorAll('.igv-flw-input-row').forEach(div => {
+ div.querySelector('input').value = ''
+ })
+
+ return paths
+
+ }
+
+ presentErrorMessage(message) {
+ this.error_message.querySelector('.igv-flw-error-message').textContent = message
+ DOMUtils.show(this.error_message)
+ }
+
+ dismissErrorMessage() {
+ DOMUtils.hide(this.error_message)
+ this.error_message.querySelector('.igv-flw-error-message').textContent = ''
+ }
+
+ present() {
+ DOMUtils.show(this.container)
+ }
+
+ dismiss() {
+
+ this.dismissErrorMessage()
+
+ // const e = this.container.querySelector('.igv-flw-local-file-name-container');
+ // if (e) {
+ // DOMUtils.hide(e);
+ // }
+
+ // clear input elements
+ this.container.querySelectorAll('.igv-flw-input-row').forEach(div => {
+ div.querySelector('input').value = ''
+ })
+
+ this.fileLoadManager.reset()
+
+ }
+
+ createInputContainer({parent, doURL, dataTitle, indexTitle, dataOnly}) {
+
+ // container
+ const container = DOMUtils.div({class: 'igv-flw-input-container'})
+ parent.appendChild(container)
+
+ // data
+ const input_data_row = DOMUtils.div({class: 'igv-flw-input-row'})
+ container.appendChild(input_data_row)
+
+ let label
+
+ // label
+ label = DOMUtils.div({class: 'igv-flw-input-label'})
+ input_data_row.appendChild(label)
+ label.textContent = dataTitle
+
+ if (true === doURL) {
+ this.createURLContainer(input_data_row, 'igv-flw-data-url', false)
+ } else {
+ this.createLocalFileContainer(input_data_row, 'igv-flw-local-data-file', false)
+ }
+
+ if (true === dataOnly) {
+ return
+ }
+
+ // index
+ const input_index_row = DOMUtils.div({class: 'igv-flw-input-row'})
+ container.appendChild(input_index_row)
+
+ // label
+ label = DOMUtils.div({class: 'igv-flw-input-label'})
+ input_index_row.appendChild(label)
+ label.textContent = indexTitle
+
+ if (true === doURL) {
+ this.createURLContainer(input_index_row, 'igv-flw-index-url', true)
+ } else {
+ this.createLocalFileContainer(input_index_row, 'igv-flw-local-index-file', true)
+ }
+
+ }
+
+ createURLContainer(parent, id, isIndexFile) {
+
+ const input = DOMUtils.create('input')
+ input.setAttribute('type', 'text')
+ // input.setAttribute('placeholder', (true === isIndexFile ? 'Enter index URL' : 'Enter data URL'));
+ parent.appendChild(input)
+
+ if (isIndexFile) {
+ this.inputIndex = input
+ } else {
+ this.inputData = input
+ }
+
+ }
+
+ createLocalFileContainer(parent, id, isIndexFile) {
+
+ const file_chooser_container = DOMUtils.div({class: 'igv-flw-file-chooser-container'})
+ parent.appendChild(file_chooser_container)
+
+ const str = `${id}${DOMUtils.guid()}`
+
+ const label = DOMUtils.create('label')
+ label.setAttribute('for', str)
+
+ file_chooser_container.appendChild(label)
+ label.textContent = 'Choose file'
+
+ const input = DOMUtils.create('input', {class: 'igv-flw-file-chooser-input'})
+ input.setAttribute('id', str)
+ input.setAttribute('name', str)
+ input.setAttribute('type', 'file')
+ file_chooser_container.appendChild(input)
+
+ const file_name = DOMUtils.div({class: 'igv-flw-local-file-name-container'})
+ parent.appendChild(file_name)
+
+ DOMUtils.hide(file_name)
+
+ input.addEventListener('change', e => {
+
+ this.dismissErrorMessage()
+
+ const file = e.target.files[0]
+ this.fileLoadManager.inputHandler(file, isIndexFile)
+
+ const {name} = file
+ file_name.textContent = name
+ file_name.setAttribute('title', name)
+ DOMUtils.show(file_name)
+ })
+
+ }
+
+}
+
+export default FileLoadWidget
diff --git a/js/widgets/genericSelectModal.js b/js/widgets/genericSelectModal.js
new file mode 100644
index 00000000..9f5b8f4c
--- /dev/null
+++ b/js/widgets/genericSelectModal.js
@@ -0,0 +1,35 @@
+function createGenericSelectModal(id, select_id) {
+
+ return `
`
+
+}
+export {createGenericSelectModal}
diff --git a/js/widgets/genomeFileLoad.js b/js/widgets/genomeFileLoad.js
new file mode 100644
index 00000000..9058f6e6
--- /dev/null
+++ b/js/widgets/genomeFileLoad.js
@@ -0,0 +1,75 @@
+import {FileUtils, igvxhr, StringUtils} from "../../node_modules/igv-utils/src/index.js"
+import FileLoad from "./fileLoad.js"
+import MultipleTrackFileLoad from './multipleTrackFileLoad.js'
+
+class GenomeFileLoad extends FileLoad {
+
+ constructor({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton, loadHandler}) {
+ super({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton})
+ this.loadHandler = loadHandler
+ }
+
+ async loadPaths(paths) {
+
+ const status = await GenomeFileLoad.isGZip(paths)
+
+ if (true === status) {
+ throw new Error('Genome did not load - gzip files are not supported')
+ } else {
+
+ let configuration = undefined
+
+ const jsonFiles = paths.filter(path => 'json' === FileUtils.getExtension(path))
+ const hubFiles = paths.filter(path => StringUtils.isString(path) && path.endsWith("/hub.txt"))
+
+ // If one of the paths is .json, unpack and send to loader
+ // TODO -- what if multiple json files are selected? This is surely an error
+ if (jsonFiles.length >= 1) {
+ configuration = await igvxhr.loadJson(jsonFiles[0])
+ } else if (hubFiles.length >= 1) {
+ configuration = {url: hubFiles[0]}
+ } else if (2 === paths.length) {
+ const [_0, _1] = await GenomeFileLoad.getExtension(paths)
+ if ('fai' === _0) {
+ configuration = {fastaURL: paths[1], indexURL: paths[0]}
+ } else if ('fai' === _1) {
+ configuration = {fastaURL: paths[0], indexURL: paths[1]}
+ }
+ }
+
+ if (undefined === configuration) {
+ throw new Error('Genome requires either a single JSON file or a FASTA file & index file')
+ } else {
+ this.loadHandler(configuration)
+ }
+
+
+ }
+
+ }
+
+ static async isGZip(paths) {
+
+ for (let path of paths) {
+ const filename = await MultipleTrackFileLoad.getFilename(path)
+ if (true === filename.endsWith('.gz')) {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ static async getExtension(paths) {
+
+ const a = await MultipleTrackFileLoad.getFilename(paths[0])
+ const b = await MultipleTrackFileLoad.getFilename(paths[1])
+
+ return [a, b].map(name => FileUtils.getExtension(name))
+
+ }
+
+
+}
+
+export default GenomeFileLoad
diff --git a/js/widgets/markupFactory.js b/js/widgets/markupFactory.js
new file mode 100644
index 00000000..4a4a8431
--- /dev/null
+++ b/js/widgets/markupFactory.js
@@ -0,0 +1,44 @@
+const dropboxButtonImageLiteral =
+ ``
+
+const googleDriveImageLiteral =
+ ``
+
+const dropboxButtonImageBase64 = () => window.btoa(dropboxButtonImageLiteral)
+
+const googleDriveButtonImageBase64 = () => window.btoa(googleDriveImageLiteral)
+
+const dropboxDropdownItem = id => {
+
+ return `
+
+
Dropbox File
+
+
+
+
+
`
+}
+
+const googleDriveDropdownItem = id => {
+
+ return `
+
+
Google Drive File
+
+
+
+
+
`
+}
+
+export {dropboxButtonImageBase64, googleDriveButtonImageBase64, dropboxDropdownItem, googleDriveDropdownItem}
diff --git a/js/widgets/multipleTrackFileLoad.js b/js/widgets/multipleTrackFileLoad.js
new file mode 100644
index 00000000..b250e4a6
--- /dev/null
+++ b/js/widgets/multipleTrackFileLoad.js
@@ -0,0 +1,181 @@
+import AlertSingleton from './alertSingleton.js'
+import {FileUtils, URIUtils, GoogleUtils, GoogleDrive, GooglePicker} from "../../node_modules/igv-utils/src/index.js"
+
+class MultipleTrackFileLoad {
+
+ constructor({
+ $localFileInput,
+ initializeDropbox,
+ $dropboxButton,
+ $googleDriveButton,
+ fileLoadHandler,
+ multipleFileSelection
+ }) {
+
+ this.fileLoadHandler = fileLoadHandler
+
+ const localFileInput = $localFileInput.get(0)
+ const dropboxButton = $dropboxButton ? $dropboxButton.get(0) : undefined
+ const googleDriveButton = $googleDriveButton ? $googleDriveButton.get(0) : undefined
+
+ localFileInput.addEventListener('change', async () => {
+
+ if (true === MultipleTrackFileLoad.isValidLocalFileInput(localFileInput)) {
+ const {files} = localFileInput
+ const paths = Array.from(files)
+ localFileInput.value = ''
+ await this.loadPaths(paths)
+ }
+
+ })
+
+ if (dropboxButton) dropboxButton.addEventListener('click', async () => {
+
+ const result = await initializeDropbox()
+
+ if (true === result) {
+
+ const obj =
+ {
+ success: dbFiles => this.loadPaths(dbFiles.map(({link}) => link)),
+ cancel: () => {
+ },
+ linkType: "preview",
+ multiselect: multipleFileSelection,
+ folderselect: false,
+ }
+
+ Dropbox.choose(obj)
+
+ } else {
+ AlertSingleton.present('Cannot connect to Dropbox')
+ }
+ })
+
+
+ if (googleDriveButton) {
+
+ googleDriveButton.addEventListener('click', () => {
+ GooglePicker.createDropdownButtonPicker(multipleFileSelection,
+ async responses => await this.loadPaths(responses.map(({
+ name,
+ url
+ }) => url)))
+ })
+
+ }
+
+ }
+
+ async loadPaths(paths) {
+ await ingestPaths({paths, fileLoadHandler: this.fileLoadHandler})
+ }
+
+ static isValidLocalFileInput(input) {
+ return (input.files && input.files.length > 0)
+ }
+
+ static async getFilename(path) {
+
+ if (path instanceof File) {
+ return path.name
+ } else if (GoogleUtils.isGoogleDriveURL(path)) {
+ const info = await GoogleDrive.getDriveFileInfo(path)
+ return info.name || info.originalFileName
+ } else {
+ const result = URIUtils.parseUri(path)
+ return result.file
+ }
+
+ }
+
+ static isGoogleDrivePath(path) {
+ return path instanceof File ? false : GoogleUtils.isGoogleDriveURL(path)
+ }
+
+}
+
+async function ingestPaths({paths, fileLoadHandler}) {
+ try {
+ // Search for index files (.bai, .csi, .tbi, .idx)
+ const indexLUT = new Map()
+
+ const dataPaths = []
+ for (let path of paths) {
+
+ const name = await MultipleTrackFileLoad.getFilename(path)
+ const extension = FileUtils.getExtension(name)
+
+ if (indexExtensions.has(extension)) {
+
+ // key is the data file name
+ const key = createIndexLUTKey(name, extension)
+ indexLUT.set(key, {
+ indexURL: path,
+ indexFilename: MultipleTrackFileLoad.isGoogleDrivePath(path) ? name : undefined
+ })
+ } else {
+ dataPaths.push(path)
+ }
+
+ }
+
+ const configurations = []
+
+ for (let dataPath of dataPaths) {
+
+ const filename = await MultipleTrackFileLoad.getFilename(dataPath)
+
+ if (indexLUT.has(filename)) {
+
+ const {indexURL, indexFilename} = indexLUT.get(filename)
+ configurations.push({
+ url: dataPath,
+ filename,
+ indexURL,
+ indexFilename,
+ name: filename,
+ _derivedName: true
+ })
+
+ } else if (requireIndex.has(FileUtils.getExtension(filename))) {
+ throw new Error(`Unable to load track file ${filename} - you must select both ${filename} and its corresponding index file`)
+ } else {
+ configurations.push({url: dataPath, filename, name: filename, _derivedName: true})
+ }
+
+ }
+
+ if (configurations) {
+ fileLoadHandler(configurations)
+ }
+
+ } catch (e) {
+ console.error(e)
+ AlertSingleton.present(e.message)
+ }
+}
+
+const indexExtensions = new Set(['bai', 'csi', 'tbi', 'idx', 'crai', 'fai'])
+
+const requireIndex = new Set(['bam', 'cram', 'fa', 'fasta'])
+
+const createIndexLUTKey = (name, extension) => {
+
+ let key = name.substring(0, name.length - (extension.length + 1))
+
+ // bam and cram files (.bai, .crai) have 2 conventions:
+ // .bam.bai
+ // .bai - we will support this one
+
+ if ('bai' === extension && !key.endsWith('bam')) {
+ return `${key}.bam`
+ } else if ('crai' === extension && !key.endsWith('cram')) {
+ return `${key}.cram`
+ } else {
+ return key
+ }
+
+}
+
+export default MultipleTrackFileLoad
diff --git a/js/widgets/qrcode.js b/js/widgets/qrcode.js
new file mode 100755
index 00000000..e387e987
--- /dev/null
+++ b/js/widgets/qrcode.js
@@ -0,0 +1,1000 @@
+/**
+ * @fileoverview
+ * - Using the 'QRCode for Javascript library'
+ * - Fixed dataset of 'QRCode for Javascript library' for support full-spec.
+ * - this library has no dependencies.
+ *
+ * @author davidshimjs
+ * @see http://www.d-project.com/
+ * @see http://jeromeetienne.github.com/jquery-qrcode/
+ */
+
+
+//---------------------------------------------------------------------
+// QRCode for JavaScript
+//
+// Copyright (c) 2009 Kazuhiko Arase
+//
+// URL: http://www.d-project.com/
+//
+// Licensed under the MIT license:
+// http://www.opensource.org/licenses/mit-license.php
+//
+// The word "QR Code" is registered trademark of
+// DENSO WAVE INCORPORATED
+// http://www.denso-wave.com/qrcode/faqpatent-e.html
+//
+//---------------------------------------------------------------------
+function QR8bitByte(data) {
+ this.mode = QRMode.MODE_8BIT_BYTE
+ this.data = data
+ this.parsedData = []
+
+ // Added to support UTF-8 Characters
+ for (var i = 0, l = this.data.length; i < l; i++) {
+ var byteArray = []
+ var code = this.data.charCodeAt(i)
+
+ if (code > 0x10000) {
+ byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18)
+ byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12)
+ byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6)
+ byteArray[3] = 0x80 | (code & 0x3F)
+ } else if (code > 0x800) {
+ byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12)
+ byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6)
+ byteArray[2] = 0x80 | (code & 0x3F)
+ } else if (code > 0x80) {
+ byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6)
+ byteArray[1] = 0x80 | (code & 0x3F)
+ } else {
+ byteArray[0] = code
+ }
+
+ this.parsedData.push(byteArray)
+ }
+
+ this.parsedData = Array.prototype.concat.apply([], this.parsedData)
+
+ if (this.parsedData.length != this.data.length) {
+ this.parsedData.unshift(191)
+ this.parsedData.unshift(187)
+ this.parsedData.unshift(239)
+ }
+}
+
+QR8bitByte.prototype = {
+ getLength: function (buffer) {
+ return this.parsedData.length
+ },
+ write: function (buffer) {
+ for (var i = 0, l = this.parsedData.length; i < l; i++) {
+ buffer.put(this.parsedData[i], 8)
+ }
+ }
+}
+
+function QRCodeModel(typeNumber, errorCorrectLevel) {
+ this.typeNumber = typeNumber
+ this.errorCorrectLevel = errorCorrectLevel
+ this.modules = null
+ this.moduleCount = 0
+ this.dataCache = null
+ this.dataList = []
+}
+
+QRCodeModel.prototype = {
+ addData: function (data) {
+ var newData = new QR8bitByte(data)
+ this.dataList.push(newData)
+ this.dataCache = null
+ }, isDark: function (row, col) {
+ if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) {
+ throw new Error(row + "," + col)
+ }
+ return this.modules[row][col]
+ }, getModuleCount: function () {
+ return this.moduleCount
+ }, make: function () {
+ this.makeImpl(false, this.getBestMaskPattern())
+ }, makeImpl: function (test, maskPattern) {
+ this.moduleCount = this.typeNumber * 4 + 17
+ this.modules = new Array(this.moduleCount)
+ for (var row = 0; row < this.moduleCount; row++) {
+ this.modules[row] = new Array(this.moduleCount)
+ for (var col = 0; col < this.moduleCount; col++) {
+ this.modules[row][col] = null
+ }
+ }
+ this.setupPositionProbePattern(0, 0)
+ this.setupPositionProbePattern(this.moduleCount - 7, 0)
+ this.setupPositionProbePattern(0, this.moduleCount - 7)
+ this.setupPositionAdjustPattern()
+ this.setupTimingPattern()
+ this.setupTypeInfo(test, maskPattern)
+ if (this.typeNumber >= 7) {
+ this.setupTypeNumber(test)
+ }
+ if (this.dataCache == null) {
+ this.dataCache = QRCodeModel.createData(this.typeNumber, this.errorCorrectLevel, this.dataList)
+ }
+ this.mapData(this.dataCache, maskPattern)
+ }, setupPositionProbePattern: function (row, col) {
+ for (var r = -1; r <= 7; r++) {
+ if (row + r <= -1 || this.moduleCount <= row + r) continue
+ for (var c = -1; c <= 7; c++) {
+ if (col + c <= -1 || this.moduleCount <= col + c) continue
+ if ((0 <= r && r <= 6 && (c == 0 || c == 6)) || (0 <= c && c <= 6 && (r == 0 || r == 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4)) {
+ this.modules[row + r][col + c] = true
+ } else {
+ this.modules[row + r][col + c] = false
+ }
+ }
+ }
+ }, getBestMaskPattern: function () {
+ var minLostPoint = 0
+ var pattern = 0
+ for (var i = 0; i < 8; i++) {
+ this.makeImpl(true, i)
+ var lostPoint = QRUtil.getLostPoint(this)
+ if (i == 0 || minLostPoint > lostPoint) {
+ minLostPoint = lostPoint
+ pattern = i
+ }
+ }
+ return pattern
+ }, createMovieClip: function (target_mc, instance_name, depth) {
+ var qr_mc = target_mc.createEmptyMovieClip(instance_name, depth)
+ var cs = 1
+ this.make()
+ for (var row = 0; row < this.modules.length; row++) {
+ var y = row * cs
+ for (var col = 0; col < this.modules[row].length; col++) {
+ var x = col * cs
+ var dark = this.modules[row][col]
+ if (dark) {
+ qr_mc.beginFill(0, 100)
+ qr_mc.moveTo(x, y)
+ qr_mc.lineTo(x + cs, y)
+ qr_mc.lineTo(x + cs, y + cs)
+ qr_mc.lineTo(x, y + cs)
+ qr_mc.endFill()
+ }
+ }
+ }
+ return qr_mc
+ }, setupTimingPattern: function () {
+ for (var r = 8; r < this.moduleCount - 8; r++) {
+ if (this.modules[r][6] != null) {
+ continue
+ }
+ this.modules[r][6] = (r % 2 == 0)
+ }
+ for (var c = 8; c < this.moduleCount - 8; c++) {
+ if (this.modules[6][c] != null) {
+ continue
+ }
+ this.modules[6][c] = (c % 2 == 0)
+ }
+ }, setupPositionAdjustPattern: function () {
+ var pos = QRUtil.getPatternPosition(this.typeNumber)
+ for (var i = 0; i < pos.length; i++) {
+ for (var j = 0; j < pos.length; j++) {
+ var row = pos[i]
+ var col = pos[j]
+ if (this.modules[row][col] != null) {
+ continue
+ }
+ for (var r = -2; r <= 2; r++) {
+ for (var c = -2; c <= 2; c++) {
+ if (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
+ this.modules[row + r][col + c] = true
+ } else {
+ this.modules[row + r][col + c] = false
+ }
+ }
+ }
+ }
+ }
+ }, setupTypeNumber: function (test) {
+ var bits = QRUtil.getBCHTypeNumber(this.typeNumber)
+ for (var i = 0; i < 18; i++) {
+ var mod = (!test && ((bits >> i) & 1) == 1)
+ this.modules[Math.floor(i / 3)][i % 3 + this.moduleCount - 8 - 3] = mod
+ }
+ for (var i = 0; i < 18; i++) {
+ var mod = (!test && ((bits >> i) & 1) == 1)
+ this.modules[i % 3 + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod
+ }
+ }, setupTypeInfo: function (test, maskPattern) {
+ var data = (this.errorCorrectLevel << 3) | maskPattern
+ var bits = QRUtil.getBCHTypeInfo(data)
+ for (var i = 0; i < 15; i++) {
+ var mod = (!test && ((bits >> i) & 1) == 1)
+ if (i < 6) {
+ this.modules[i][8] = mod
+ } else if (i < 8) {
+ this.modules[i + 1][8] = mod
+ } else {
+ this.modules[this.moduleCount - 15 + i][8] = mod
+ }
+ }
+ for (var i = 0; i < 15; i++) {
+ var mod = (!test && ((bits >> i) & 1) == 1)
+ if (i < 8) {
+ this.modules[8][this.moduleCount - i - 1] = mod
+ } else if (i < 9) {
+ this.modules[8][15 - i - 1 + 1] = mod
+ } else {
+ this.modules[8][15 - i - 1] = mod
+ }
+ }
+ this.modules[this.moduleCount - 8][8] = (!test)
+ }, mapData: function (data, maskPattern) {
+ var inc = -1
+ var row = this.moduleCount - 1
+ var bitIndex = 7
+ var byteIndex = 0
+ for (var col = this.moduleCount - 1; col > 0; col -= 2) {
+ if (col == 6) col--
+ while (true) {
+ for (var c = 0; c < 2; c++) {
+ if (this.modules[row][col - c] == null) {
+ var dark = false
+ if (byteIndex < data.length) {
+ dark = (((data[byteIndex] >>> bitIndex) & 1) == 1)
+ }
+ var mask = QRUtil.getMask(maskPattern, row, col - c)
+ if (mask) {
+ dark = !dark
+ }
+ this.modules[row][col - c] = dark
+ bitIndex--
+ if (bitIndex == -1) {
+ byteIndex++
+ bitIndex = 7
+ }
+ }
+ }
+ row += inc
+ if (row < 0 || this.moduleCount <= row) {
+ row -= inc
+ inc = -inc
+ break
+ }
+ }
+ }
+ }
+}
+QRCodeModel.PAD0 = 0xEC
+QRCodeModel.PAD1 = 0x11
+QRCodeModel.createData = function (typeNumber, errorCorrectLevel, dataList) {
+ var rsBlocks = QRRSBlock.getRSBlocks(typeNumber, errorCorrectLevel)
+ var buffer = new QRBitBuffer()
+ for (var i = 0; i < dataList.length; i++) {
+ var data = dataList[i]
+ buffer.put(data.mode, 4)
+ buffer.put(data.getLength(), QRUtil.getLengthInBits(data.mode, typeNumber))
+ data.write(buffer)
+ }
+ var totalDataCount = 0
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalDataCount += rsBlocks[i].dataCount
+ }
+ if (buffer.getLengthInBits() > totalDataCount * 8) {
+ throw new Error("code length overflow. ("
+ + buffer.getLengthInBits()
+ + ">"
+ + totalDataCount * 8
+ + ")")
+ }
+ if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
+ buffer.put(0, 4)
+ }
+ while (buffer.getLengthInBits() % 8 != 0) {
+ buffer.putBit(false)
+ }
+ while (true) {
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break
+ }
+ buffer.put(QRCodeModel.PAD0, 8)
+ if (buffer.getLengthInBits() >= totalDataCount * 8) {
+ break
+ }
+ buffer.put(QRCodeModel.PAD1, 8)
+ }
+ return QRCodeModel.createBytes(buffer, rsBlocks)
+}
+QRCodeModel.createBytes = function (buffer, rsBlocks) {
+ var offset = 0
+ var maxDcCount = 0
+ var maxEcCount = 0
+ var dcdata = new Array(rsBlocks.length)
+ var ecdata = new Array(rsBlocks.length)
+ for (var r = 0; r < rsBlocks.length; r++) {
+ var dcCount = rsBlocks[r].dataCount
+ var ecCount = rsBlocks[r].totalCount - dcCount
+ maxDcCount = Math.max(maxDcCount, dcCount)
+ maxEcCount = Math.max(maxEcCount, ecCount)
+ dcdata[r] = new Array(dcCount)
+ for (var i = 0; i < dcdata[r].length; i++) {
+ dcdata[r][i] = 0xff & buffer.buffer[i + offset]
+ }
+ offset += dcCount
+ var rsPoly = QRUtil.getErrorCorrectPolynomial(ecCount)
+ var rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1)
+ var modPoly = rawPoly.mod(rsPoly)
+ ecdata[r] = new Array(rsPoly.getLength() - 1)
+ for (var i = 0; i < ecdata[r].length; i++) {
+ var modIndex = i + modPoly.getLength() - ecdata[r].length
+ ecdata[r][i] = (modIndex >= 0) ? modPoly.get(modIndex) : 0
+ }
+ }
+ var totalCodeCount = 0
+ for (var i = 0; i < rsBlocks.length; i++) {
+ totalCodeCount += rsBlocks[i].totalCount
+ }
+ var data = new Array(totalCodeCount)
+ var index = 0
+ for (var i = 0; i < maxDcCount; i++) {
+ for (var r = 0; r < rsBlocks.length; r++) {
+ if (i < dcdata[r].length) {
+ data[index++] = dcdata[r][i]
+ }
+ }
+ }
+ for (var i = 0; i < maxEcCount; i++) {
+ for (var r = 0; r < rsBlocks.length; r++) {
+ if (i < ecdata[r].length) {
+ data[index++] = ecdata[r][i]
+ }
+ }
+ }
+ return data
+}
+var QRMode = {MODE_NUMBER: 1 << 0, MODE_ALPHA_NUM: 1 << 1, MODE_8BIT_BYTE: 1 << 2, MODE_KANJI: 1 << 3}
+var QRErrorCorrectLevel = {L: 1, M: 0, Q: 3, H: 2}
+var QRMaskPattern = {
+ PATTERN000: 0,
+ PATTERN001: 1,
+ PATTERN010: 2,
+ PATTERN011: 3,
+ PATTERN100: 4,
+ PATTERN101: 5,
+ PATTERN110: 6,
+ PATTERN111: 7
+}
+var QRUtil = {
+ PATTERN_POSITION_TABLE: [[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90], [6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110], [6, 30, 58, 86, 114], [6, 34, 62, 90, 118], [6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134], [6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146], [6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158], [6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170]],
+ G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0),
+ G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0),
+ G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1),
+ getBCHTypeInfo: function (data) {
+ var d = data << 10
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) {
+ d ^= (QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15)))
+ }
+ return ((data << 10) | d) ^ QRUtil.G15_MASK
+ },
+ getBCHTypeNumber: function (data) {
+ var d = data << 12
+ while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) {
+ d ^= (QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18)))
+ }
+ return (data << 12) | d
+ },
+ getBCHDigit: function (data) {
+ var digit = 0
+ while (data != 0) {
+ digit++
+ data >>>= 1
+ }
+ return digit
+ },
+ getPatternPosition: function (typeNumber) {
+ return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]
+ },
+ getMask: function (maskPattern, i, j) {
+ switch (maskPattern) {
+ case QRMaskPattern.PATTERN000:
+ return (i + j) % 2 == 0
+ case QRMaskPattern.PATTERN001:
+ return i % 2 == 0
+ case QRMaskPattern.PATTERN010:
+ return j % 3 == 0
+ case QRMaskPattern.PATTERN011:
+ return (i + j) % 3 == 0
+ case QRMaskPattern.PATTERN100:
+ return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 == 0
+ case QRMaskPattern.PATTERN101:
+ return (i * j) % 2 + (i * j) % 3 == 0
+ case QRMaskPattern.PATTERN110:
+ return ((i * j) % 2 + (i * j) % 3) % 2 == 0
+ case QRMaskPattern.PATTERN111:
+ return ((i * j) % 3 + (i + j) % 2) % 2 == 0
+ default:
+ throw new Error("bad maskPattern:" + maskPattern)
+ }
+ },
+ getErrorCorrectPolynomial: function (errorCorrectLength) {
+ var a = new QRPolynomial([1], 0)
+ for (var i = 0; i < errorCorrectLength; i++) {
+ a = a.multiply(new QRPolynomial([1, QRMath.gexp(i)], 0))
+ }
+ return a
+ },
+ getLengthInBits: function (mode, type) {
+ if (1 <= type && type < 10) {
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 10
+ case QRMode.MODE_ALPHA_NUM:
+ return 9
+ case QRMode.MODE_8BIT_BYTE:
+ return 8
+ case QRMode.MODE_KANJI:
+ return 8
+ default:
+ throw new Error("mode:" + mode)
+ }
+ } else if (type < 27) {
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 12
+ case QRMode.MODE_ALPHA_NUM:
+ return 11
+ case QRMode.MODE_8BIT_BYTE:
+ return 16
+ case QRMode.MODE_KANJI:
+ return 10
+ default:
+ throw new Error("mode:" + mode)
+ }
+ } else if (type < 41) {
+ switch (mode) {
+ case QRMode.MODE_NUMBER:
+ return 14
+ case QRMode.MODE_ALPHA_NUM:
+ return 13
+ case QRMode.MODE_8BIT_BYTE:
+ return 16
+ case QRMode.MODE_KANJI:
+ return 12
+ default:
+ throw new Error("mode:" + mode)
+ }
+ } else {
+ throw new Error("type:" + type)
+ }
+ },
+ getLostPoint: function (qrCode) {
+ var moduleCount = qrCode.getModuleCount()
+ var lostPoint = 0
+ for (var row = 0; row < moduleCount; row++) {
+ for (var col = 0; col < moduleCount; col++) {
+ var sameCount = 0
+ var dark = qrCode.isDark(row, col)
+ for (var r = -1; r <= 1; r++) {
+ if (row + r < 0 || moduleCount <= row + r) {
+ continue
+ }
+ for (var c = -1; c <= 1; c++) {
+ if (col + c < 0 || moduleCount <= col + c) {
+ continue
+ }
+ if (r == 0 && c == 0) {
+ continue
+ }
+ if (dark == qrCode.isDark(row + r, col + c)) {
+ sameCount++
+ }
+ }
+ }
+ if (sameCount > 5) {
+ lostPoint += (3 + sameCount - 5)
+ }
+ }
+ }
+ for (var row = 0; row < moduleCount - 1; row++) {
+ for (var col = 0; col < moduleCount - 1; col++) {
+ var count = 0
+ if (qrCode.isDark(row, col)) count++
+ if (qrCode.isDark(row + 1, col)) count++
+ if (qrCode.isDark(row, col + 1)) count++
+ if (qrCode.isDark(row + 1, col + 1)) count++
+ if (count == 0 || count == 4) {
+ lostPoint += 3
+ }
+ }
+ }
+ for (var row = 0; row < moduleCount; row++) {
+ for (var col = 0; col < moduleCount - 6; col++) {
+ if (qrCode.isDark(row, col) && !qrCode.isDark(row, col + 1) && qrCode.isDark(row, col + 2) && qrCode.isDark(row, col + 3) && qrCode.isDark(row, col + 4) && !qrCode.isDark(row, col + 5) && qrCode.isDark(row, col + 6)) {
+ lostPoint += 40
+ }
+ }
+ }
+ for (var col = 0; col < moduleCount; col++) {
+ for (var row = 0; row < moduleCount - 6; row++) {
+ if (qrCode.isDark(row, col) && !qrCode.isDark(row + 1, col) && qrCode.isDark(row + 2, col) && qrCode.isDark(row + 3, col) && qrCode.isDark(row + 4, col) && !qrCode.isDark(row + 5, col) && qrCode.isDark(row + 6, col)) {
+ lostPoint += 40
+ }
+ }
+ }
+ var darkCount = 0
+ for (var col = 0; col < moduleCount; col++) {
+ for (var row = 0; row < moduleCount; row++) {
+ if (qrCode.isDark(row, col)) {
+ darkCount++
+ }
+ }
+ }
+ var ratio = Math.abs(100 * darkCount / moduleCount / moduleCount - 50) / 5
+ lostPoint += ratio * 10
+ return lostPoint
+ }
+}
+var QRMath = {
+ glog: function (n) {
+ if (n < 1) {
+ throw new Error("glog(" + n + ")")
+ }
+ return QRMath.LOG_TABLE[n]
+ }, gexp: function (n) {
+ while (n < 0) {
+ n += 255
+ }
+ while (n >= 256) {
+ n -= 255
+ }
+ return QRMath.EXP_TABLE[n]
+ }, EXP_TABLE: new Array(256), LOG_TABLE: new Array(256)
+}
+for (var i = 0; i < 8; i++) {
+ QRMath.EXP_TABLE[i] = 1 << i
+}
+for (var i = 8; i < 256; i++) {
+ QRMath.EXP_TABLE[i] = QRMath.EXP_TABLE[i - 4] ^ QRMath.EXP_TABLE[i - 5] ^ QRMath.EXP_TABLE[i - 6] ^ QRMath.EXP_TABLE[i - 8]
+}
+for (var i = 0; i < 255; i++) {
+ QRMath.LOG_TABLE[QRMath.EXP_TABLE[i]] = i
+}
+
+function QRPolynomial(num, shift) {
+ if (num.length == undefined) {
+ throw new Error(num.length + "/" + shift)
+ }
+ var offset = 0
+ while (offset < num.length && num[offset] == 0) {
+ offset++
+ }
+ this.num = new Array(num.length - offset + shift)
+ for (var i = 0; i < num.length - offset; i++) {
+ this.num[i] = num[i + offset]
+ }
+}
+
+QRPolynomial.prototype = {
+ get: function (index) {
+ return this.num[index]
+ }, getLength: function () {
+ return this.num.length
+ }, multiply: function (e) {
+ var num = new Array(this.getLength() + e.getLength() - 1)
+ for (var i = 0; i < this.getLength(); i++) {
+ for (var j = 0; j < e.getLength(); j++) {
+ num[i + j] ^= QRMath.gexp(QRMath.glog(this.get(i)) + QRMath.glog(e.get(j)))
+ }
+ }
+ return new QRPolynomial(num, 0)
+ }, mod: function (e) {
+ if (this.getLength() - e.getLength() < 0) {
+ return this
+ }
+ var ratio = QRMath.glog(this.get(0)) - QRMath.glog(e.get(0))
+ var num = new Array(this.getLength())
+ for (var i = 0; i < this.getLength(); i++) {
+ num[i] = this.get(i)
+ }
+ for (var i = 0; i < e.getLength(); i++) {
+ num[i] ^= QRMath.gexp(QRMath.glog(e.get(i)) + ratio)
+ }
+ return new QRPolynomial(num, 0).mod(e)
+ }
+}
+
+function QRRSBlock(totalCount, dataCount) {
+ this.totalCount = totalCount
+ this.dataCount = dataCount
+}
+
+QRRSBlock.RS_BLOCK_TABLE = [[1, 26, 19], [1, 26, 16], [1, 26, 13], [1, 26, 9], [1, 44, 34], [1, 44, 28], [1, 44, 22], [1, 44, 16], [1, 70, 55], [1, 70, 44], [2, 35, 17], [2, 35, 13], [1, 100, 80], [2, 50, 32], [2, 50, 24], [4, 25, 9], [1, 134, 108], [2, 67, 43], [2, 33, 15, 2, 34, 16], [2, 33, 11, 2, 34, 12], [2, 86, 68], [4, 43, 27], [4, 43, 19], [4, 43, 15], [2, 98, 78], [4, 49, 31], [2, 32, 14, 4, 33, 15], [4, 39, 13, 1, 40, 14], [2, 121, 97], [2, 60, 38, 2, 61, 39], [4, 40, 18, 2, 41, 19], [4, 40, 14, 2, 41, 15], [2, 146, 116], [3, 58, 36, 2, 59, 37], [4, 36, 16, 4, 37, 17], [4, 36, 12, 4, 37, 13], [2, 86, 68, 2, 87, 69], [4, 69, 43, 1, 70, 44], [6, 43, 19, 2, 44, 20], [6, 43, 15, 2, 44, 16], [4, 101, 81], [1, 80, 50, 4, 81, 51], [4, 50, 22, 4, 51, 23], [3, 36, 12, 8, 37, 13], [2, 116, 92, 2, 117, 93], [6, 58, 36, 2, 59, 37], [4, 46, 20, 6, 47, 21], [7, 42, 14, 4, 43, 15], [4, 133, 107], [8, 59, 37, 1, 60, 38], [8, 44, 20, 4, 45, 21], [12, 33, 11, 4, 34, 12], [3, 145, 115, 1, 146, 116], [4, 64, 40, 5, 65, 41], [11, 36, 16, 5, 37, 17], [11, 36, 12, 5, 37, 13], [5, 109, 87, 1, 110, 88], [5, 65, 41, 5, 66, 42], [5, 54, 24, 7, 55, 25], [11, 36, 12], [5, 122, 98, 1, 123, 99], [7, 73, 45, 3, 74, 46], [15, 43, 19, 2, 44, 20], [3, 45, 15, 13, 46, 16], [1, 135, 107, 5, 136, 108], [10, 74, 46, 1, 75, 47], [1, 50, 22, 15, 51, 23], [2, 42, 14, 17, 43, 15], [5, 150, 120, 1, 151, 121], [9, 69, 43, 4, 70, 44], [17, 50, 22, 1, 51, 23], [2, 42, 14, 19, 43, 15], [3, 141, 113, 4, 142, 114], [3, 70, 44, 11, 71, 45], [17, 47, 21, 4, 48, 22], [9, 39, 13, 16, 40, 14], [3, 135, 107, 5, 136, 108], [3, 67, 41, 13, 68, 42], [15, 54, 24, 5, 55, 25], [15, 43, 15, 10, 44, 16], [4, 144, 116, 4, 145, 117], [17, 68, 42], [17, 50, 22, 6, 51, 23], [19, 46, 16, 6, 47, 17], [2, 139, 111, 7, 140, 112], [17, 74, 46], [7, 54, 24, 16, 55, 25], [34, 37, 13], [4, 151, 121, 5, 152, 122], [4, 75, 47, 14, 76, 48], [11, 54, 24, 14, 55, 25], [16, 45, 15, 14, 46, 16], [6, 147, 117, 4, 148, 118], [6, 73, 45, 14, 74, 46], [11, 54, 24, 16, 55, 25], [30, 46, 16, 2, 47, 17], [8, 132, 106, 4, 133, 107], [8, 75, 47, 13, 76, 48], [7, 54, 24, 22, 55, 25], [22, 45, 15, 13, 46, 16], [10, 142, 114, 2, 143, 115], [19, 74, 46, 4, 75, 47], [28, 50, 22, 6, 51, 23], [33, 46, 16, 4, 47, 17], [8, 152, 122, 4, 153, 123], [22, 73, 45, 3, 74, 46], [8, 53, 23, 26, 54, 24], [12, 45, 15, 28, 46, 16], [3, 147, 117, 10, 148, 118], [3, 73, 45, 23, 74, 46], [4, 54, 24, 31, 55, 25], [11, 45, 15, 31, 46, 16], [7, 146, 116, 7, 147, 117], [21, 73, 45, 7, 74, 46], [1, 53, 23, 37, 54, 24], [19, 45, 15, 26, 46, 16], [5, 145, 115, 10, 146, 116], [19, 75, 47, 10, 76, 48], [15, 54, 24, 25, 55, 25], [23, 45, 15, 25, 46, 16], [13, 145, 115, 3, 146, 116], [2, 74, 46, 29, 75, 47], [42, 54, 24, 1, 55, 25], [23, 45, 15, 28, 46, 16], [17, 145, 115], [10, 74, 46, 23, 75, 47], [10, 54, 24, 35, 55, 25], [19, 45, 15, 35, 46, 16], [17, 145, 115, 1, 146, 116], [14, 74, 46, 21, 75, 47], [29, 54, 24, 19, 55, 25], [11, 45, 15, 46, 46, 16], [13, 145, 115, 6, 146, 116], [14, 74, 46, 23, 75, 47], [44, 54, 24, 7, 55, 25], [59, 46, 16, 1, 47, 17], [12, 151, 121, 7, 152, 122], [12, 75, 47, 26, 76, 48], [39, 54, 24, 14, 55, 25], [22, 45, 15, 41, 46, 16], [6, 151, 121, 14, 152, 122], [6, 75, 47, 34, 76, 48], [46, 54, 24, 10, 55, 25], [2, 45, 15, 64, 46, 16], [17, 152, 122, 4, 153, 123], [29, 74, 46, 14, 75, 47], [49, 54, 24, 10, 55, 25], [24, 45, 15, 46, 46, 16], [4, 152, 122, 18, 153, 123], [13, 74, 46, 32, 75, 47], [48, 54, 24, 14, 55, 25], [42, 45, 15, 32, 46, 16], [20, 147, 117, 4, 148, 118], [40, 75, 47, 7, 76, 48], [43, 54, 24, 22, 55, 25], [10, 45, 15, 67, 46, 16], [19, 148, 118, 6, 149, 119], [18, 75, 47, 31, 76, 48], [34, 54, 24, 34, 55, 25], [20, 45, 15, 61, 46, 16]]
+QRRSBlock.getRSBlocks = function (typeNumber, errorCorrectLevel) {
+ var rsBlock = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectLevel)
+ if (rsBlock == undefined) {
+ throw new Error("bad rs block @ typeNumber:" + typeNumber + "/errorCorrectLevel:" + errorCorrectLevel)
+ }
+ var length = rsBlock.length / 3
+ var list = []
+ for (var i = 0; i < length; i++) {
+ var count = rsBlock[i * 3 + 0]
+ var totalCount = rsBlock[i * 3 + 1]
+ var dataCount = rsBlock[i * 3 + 2]
+ for (var j = 0; j < count; j++) {
+ list.push(new QRRSBlock(totalCount, dataCount))
+ }
+ }
+ return list
+}
+QRRSBlock.getRsBlockTable = function (typeNumber, errorCorrectLevel) {
+ switch (errorCorrectLevel) {
+ case QRErrorCorrectLevel.L:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 0]
+ case QRErrorCorrectLevel.M:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 1]
+ case QRErrorCorrectLevel.Q:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 2]
+ case QRErrorCorrectLevel.H:
+ return QRRSBlock.RS_BLOCK_TABLE[(typeNumber - 1) * 4 + 3]
+ default:
+ return undefined
+ }
+}
+
+function QRBitBuffer() {
+ this.buffer = []
+ this.length = 0
+}
+
+QRBitBuffer.prototype = {
+ get: function (index) {
+ var bufIndex = Math.floor(index / 8)
+ return ((this.buffer[bufIndex] >>> (7 - index % 8)) & 1) == 1
+ }, put: function (num, length) {
+ for (var i = 0; i < length; i++) {
+ this.putBit(((num >>> (length - i - 1)) & 1) == 1)
+ }
+ }, getLengthInBits: function () {
+ return this.length
+ }, putBit: function (bit) {
+ var bufIndex = Math.floor(this.length / 8)
+ if (this.buffer.length <= bufIndex) {
+ this.buffer.push(0)
+ }
+ if (bit) {
+ this.buffer[bufIndex] |= (0x80 >>> (this.length % 8))
+ }
+ this.length++
+ }
+}
+var QRCodeLimitLength = [[17, 14, 11, 7], [32, 26, 20, 14], [53, 42, 32, 24], [78, 62, 46, 34], [106, 84, 60, 44], [134, 106, 74, 58], [154, 122, 86, 64], [192, 152, 108, 84], [230, 180, 130, 98], [271, 213, 151, 119], [321, 251, 177, 137], [367, 287, 203, 155], [425, 331, 241, 177], [458, 362, 258, 194], [520, 412, 292, 220], [586, 450, 322, 250], [644, 504, 364, 280], [718, 560, 394, 310], [792, 624, 442, 338], [858, 666, 482, 382], [929, 711, 509, 403], [1003, 779, 565, 439], [1091, 857, 611, 461], [1171, 911, 661, 511], [1273, 997, 715, 535], [1367, 1059, 751, 593], [1465, 1125, 805, 625], [1528, 1190, 868, 658], [1628, 1264, 908, 698], [1732, 1370, 982, 742], [1840, 1452, 1030, 790], [1952, 1538, 1112, 842], [2068, 1628, 1168, 898], [2188, 1722, 1228, 958], [2303, 1809, 1283, 983], [2431, 1911, 1351, 1051], [2563, 1989, 1423, 1093], [2699, 2099, 1499, 1139], [2809, 2213, 1579, 1219], [2953, 2331, 1663, 1273]]
+
+var useSVG = document.documentElement.tagName.toLowerCase() === "svg"
+
+let Drawing
+
+if (useSVG) {
+
+ Drawing = function (el, htOption) {
+ this._el = el
+ this._htOption = htOption
+ }
+
+ Drawing.prototype.draw = function (oQRCode) {
+ var _htOption = this._htOption
+ var _el = this._el
+ var nCount = oQRCode.getModuleCount()
+ var nWidth = Math.floor(_htOption.width / nCount)
+ var nHeight = Math.floor(_htOption.height / nCount)
+
+ this.clear()
+
+ function makeSVG(tag, attrs) {
+ var el = document.createElementNS('http://www.w3.org/2000/svg', tag)
+ for (var k in attrs)
+ if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k])
+ return el
+ }
+
+ var svg = makeSVG("svg", {
+ 'viewBox': '0 0 ' + String(nCount) + " " + String(nCount),
+ 'width': '100%',
+ 'height': '100%',
+ 'fill': _htOption.colorLight
+ })
+ svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink")
+ _el.appendChild(svg)
+
+ svg.appendChild(makeSVG("rect", {"fill": _htOption.colorLight, "width": "100%", "height": "100%"}))
+ svg.appendChild(makeSVG("rect", {
+ "fill": _htOption.colorDark,
+ "width": "1",
+ "height": "1",
+ "id": "template"
+ }))
+
+ for (var row = 0; row < nCount; row++) {
+ for (var col = 0; col < nCount; col++) {
+ if (oQRCode.isDark(row, col)) {
+ var child = makeSVG("use", {"x": String(col), "y": String(row)})
+ child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template")
+ svg.appendChild(child)
+ }
+ }
+ }
+ }
+ Drawing.prototype.clear = function () {
+ while (this._el.hasChildNodes())
+ this._el.removeChild(this._el.lastChild)
+ }
+} else {
+
+ /**
+ * Drawing QRCode by using canvas
+ *
+ * @constructor
+ * @param {HTMLElement} el
+ * @param {Object} htOption QRCode Options
+ */
+ Drawing = function (el, htOption) {
+ this._bIsPainted = false
+ this._htOption = htOption
+ this._elCanvas = document.createElement("canvas")
+ this._elCanvas.width = htOption.width
+ this._elCanvas.height = htOption.height
+ el.appendChild(this._elCanvas)
+ this._el = el
+ this._oContext = this._elCanvas.getContext("2d")
+ this._bIsPainted = false
+ this._elImage = document.createElement("img")
+ this._elImage.alt = "Scan me!"
+ this._elImage.style.display = "none"
+ this._el.appendChild(this._elImage)
+ this._bSupportDataURI = null
+ }
+
+ /**
+ * Draw the QRCode
+ *
+ * @param {QRCode} oQRCode
+ */
+ Drawing.prototype.draw = function (oQRCode) {
+ var _elImage = this._elImage
+ var _oContext = this._oContext
+ var _htOption = this._htOption
+
+ var nCount = oQRCode.getModuleCount()
+ var nWidth = _htOption.width / nCount
+ var nHeight = _htOption.height / nCount
+ var nRoundedWidth = Math.round(nWidth)
+ var nRoundedHeight = Math.round(nHeight)
+
+ _elImage.style.display = "none"
+ this.clear()
+
+ for (var row = 0; row < nCount; row++) {
+ for (var col = 0; col < nCount; col++) {
+ var bIsDark = oQRCode.isDark(row, col)
+ var nLeft = col * nWidth
+ var nTop = row * nHeight
+ _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight
+ _oContext.lineWidth = 1
+ _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight
+ _oContext.fillRect(nLeft, nTop, nWidth, nHeight)
+
+ // 안티 앨리어싱 방지 처리
+ _oContext.strokeRect(
+ Math.floor(nLeft) + 0.5,
+ Math.floor(nTop) + 0.5,
+ nRoundedWidth,
+ nRoundedHeight
+ )
+
+ _oContext.strokeRect(
+ Math.ceil(nLeft) - 0.5,
+ Math.ceil(nTop) - 0.5,
+ nRoundedWidth,
+ nRoundedHeight
+ )
+ }
+ }
+
+ this._bIsPainted = true
+ }
+
+ /**
+ * Make the image from Canvas if the browser supports Data URI.
+ */
+ Drawing.prototype.makeImage = function () {
+ if (this._bIsPainted) {
+ this._elImage.src = this._elCanvas.toDataURL("image/png")
+ this._elImage.style.display = "block"
+ this._elCanvas.style.display = "none"
+ }
+ }
+
+ /**
+ * Return whether the QRCode is painted or not
+ *
+ * @return {Boolean}
+ */
+ Drawing.prototype.isPainted = function () {
+ return this._bIsPainted
+ }
+
+ /**
+ * Clear the QRCode
+ */
+ Drawing.prototype.clear = function () {
+ this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height)
+ this._bIsPainted = false
+ }
+
+ /**
+ * @private
+ * @param {Number} nNumber
+ */
+ Drawing.prototype.round = function (nNumber) {
+ if (!nNumber) {
+ return nNumber
+ }
+
+ return Math.floor(nNumber * 1000) / 1000
+ }
+}
+
+/**
+ * Get the type by string length
+ *
+ * @private
+ * @param {String} sText
+ * @param {Number} nCorrectLevel
+ * @return {Number} type
+ */
+function _getTypeNumber(sText, nCorrectLevel) {
+ var nType = 1
+ var length = _getUTF8Length(sText)
+
+ for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) {
+ var nLimit = 0
+
+ switch (nCorrectLevel) {
+ case QRErrorCorrectLevel.L :
+ nLimit = QRCodeLimitLength[i][0]
+ break
+ case QRErrorCorrectLevel.M :
+ nLimit = QRCodeLimitLength[i][1]
+ break
+ case QRErrorCorrectLevel.Q :
+ nLimit = QRCodeLimitLength[i][2]
+ break
+ case QRErrorCorrectLevel.H :
+ nLimit = QRCodeLimitLength[i][3]
+ break
+ }
+
+ if (length <= nLimit) {
+ break
+ } else {
+ nType++
+ }
+ }
+
+ if (nType > QRCodeLimitLength.length) {
+ throw new Error("Too long data")
+ }
+
+ return nType
+}
+
+function _getUTF8Length(sText) {
+ var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a')
+ return replacedText.length + (replacedText.length != sText ? 3 : 0)
+}
+
+/**
+ * @class QRCode
+ * @constructor
+ * @example
+ * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie");
+ *
+ * @example
+ * var oQRCode = new QRCode("test", {
+ * text : "http://naver.com",
+ * width : 128,
+ * height : 128
+ * });
+ *
+ * oQRCode.clear(); // Clear the QRCode.
+ * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode.
+ *
+ * @param {HTMLElement|String} el target element or 'id' attribute of element.
+ * @param {Object|String} vOption
+ * @param {String} vOption.text QRCode link data
+ * @param {Number} [vOption.width=256]
+ * @param {Number} [vOption.height=256]
+ * @param {String} [vOption.colorDark="#000000"]
+ * @param {String} [vOption.colorLight="#ffffff"]
+ * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H]
+ */
+const QRCode = function (el, vOption) {
+ this._htOption = {
+ width: 256,
+ height: 256,
+ typeNumber: 4,
+ colorDark: "#000000",
+ colorLight: "#ffffff",
+ correctLevel: QRErrorCorrectLevel.H
+ }
+
+ if (typeof vOption === 'string') {
+ vOption = {
+ text: vOption
+ }
+ }
+
+ // Overwrites options
+ if (vOption) {
+ for (var i in vOption) {
+ this._htOption[i] = vOption[i]
+ }
+ }
+
+ if (typeof el == "string") {
+ el = document.getElementById(el)
+ }
+
+ if (this._htOption.useSVG) {
+ Drawing = svgDrawer
+ }
+
+ this._el = el
+ this._oQRCode = null
+ this._oDrawing = new Drawing(this._el, this._htOption)
+
+ if (this._htOption.text) {
+ this.makeCode(this._htOption.text)
+ }
+}
+
+/**
+ * Make the QRCode
+ *
+ * @param {String} sText link data
+ */
+QRCode.prototype.makeCode = function (sText) {
+ this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel)
+ this._oQRCode.addData(sText)
+ this._oQRCode.make()
+ this._el.title = sText
+ this._oDrawing.draw(this._oQRCode)
+ this.makeImage()
+}
+
+/**
+ * Make the Image from Canvas element
+ * - It occurs automatically
+ * - Android below 3 doesn't support Data-URI spec.
+ *
+ * @private
+ */
+QRCode.prototype.makeImage = function () {
+ if (typeof this._oDrawing.makeImage == "function") {
+ this._oDrawing.makeImage()
+ }
+}
+
+/**
+ * Clear the QRCode
+ */
+QRCode.prototype.clear = function () {
+ this._oDrawing.clear()
+}
+
+/**
+ * @name QRCode.CorrectLevel
+ */
+QRCode.CorrectLevel = QRErrorCorrectLevel
+
+export {QRCode}
\ No newline at end of file
diff --git a/js/widgets/sessionController.js b/js/widgets/sessionController.js
new file mode 100644
index 00000000..5c5730b4
--- /dev/null
+++ b/js/widgets/sessionController.js
@@ -0,0 +1,79 @@
+import FileLoadWidget from "./fileLoadWidget.js"
+import FileLoadManager from "./fileLoadManager.js"
+import * as Utils from './utils.js'
+import {FileUtils} from '../node_modules/igv-utils/src/index.js'
+
+class SessionController {
+
+ constructor({prefix, sessionLoadModal, sessionSaveModal, sessionFileLoad, JSONProvider}) {
+
+ let config =
+ {
+ widgetParent: sessionLoadModal.querySelector('.modal-body'),
+ dataTitle: 'Load Session',
+ indexTitle: undefined,
+ mode: 'url',
+ fileLoadManager: new FileLoadManager(),
+ dataOnly: true,
+ doURL: undefined
+ }
+
+ this.urlWidget = new FileLoadWidget(config)
+
+ // Configure load session modal
+ Utils.configureModal(this.urlWidget, sessionLoadModal, async fileLoadWidget => {
+ await sessionFileLoad.loadPaths(fileLoadWidget.retrievePaths())
+ return true
+ })
+
+ // Configure save session modal
+ configureSaveSessionModal(prefix, JSONProvider, sessionSaveModal)
+
+ }
+
+}
+
+
+function configureSaveSessionModal(prefix, JSONProvider, sessionSaveModal) {
+
+ let input = sessionSaveModal.querySelector('input')
+
+ let okHandler = () => {
+
+ const extensions = new Set(['json', 'xml'])
+
+ let filename = input.value
+
+ if (undefined === filename || '' === filename) {
+ filename = input.getAttribute('placeholder')
+ } else if (false === extensions.has(FileUtils.getExtension(filename))) {
+ filename = filename + '.json'
+ }
+
+ const json = JSONProvider()
+ const jsonString = JSON.stringify(json, null, '\t')
+ const data = URL.createObjectURL(new Blob([jsonString], {type: "application/octet-stream"}))
+
+ FileUtils.download(filename, data)
+
+ $(sessionSaveModal).modal('hide')
+ }
+
+ const $ok = $(sessionSaveModal).find('.modal-footer button:nth-child(2)')
+ $ok.on('click', okHandler)
+
+ $(sessionSaveModal).on('show.bs.modal', (e) => {
+ input.value = `${prefix}-session.json`
+ })
+
+ input.addEventListener('keyup', e => {
+
+ // enter key key-up
+ if (13 === e.keyCode) {
+ okHandler()
+ }
+ })
+
+}
+
+export default SessionController
diff --git a/js/widgets/sessionFileLoad.js b/js/widgets/sessionFileLoad.js
new file mode 100644
index 00000000..a07cf142
--- /dev/null
+++ b/js/widgets/sessionFileLoad.js
@@ -0,0 +1,24 @@
+import FileLoad from "./fileLoad.js"
+
+class SessionFileLoad extends FileLoad {
+
+ constructor({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton, loadHandler}) {
+ super({localFileInput, initializeDropbox, dropboxButton, googleEnabled, googleDriveButton})
+ this.loadHandler = loadHandler
+ }
+
+ async loadPaths(paths) {
+
+ const path = paths[0]
+
+ try {
+ this.loadHandler({url: path})
+
+ } catch (e) {
+ throw new Error('Session file did not load' + e.message)
+ }
+ };
+
+}
+
+export default SessionFileLoad
diff --git a/js/widgets/sessionWidgets.js b/js/widgets/sessionWidgets.js
new file mode 100644
index 00000000..da74136f
--- /dev/null
+++ b/js/widgets/sessionWidgets.js
@@ -0,0 +1,150 @@
+import {FileUtils} from '../../node_modules/igv-utils/src/index.js'
+import FileLoadManager from './fileLoadManager.js'
+import FileLoadWidget from './fileLoadWidget.js'
+import SessionFileLoad from "./sessionFileLoad.js"
+import {createURLModal} from './urlModal.js'
+import * as Utils from './utils.js'
+
+let fileLoadWidget
+
+function createSessionWidgets($rootContainer,
+ prefix,
+ localFileInputId,
+ initializeDropbox,
+ dropboxButtonId,
+ googleDriveButtonId,
+ urlModalId,
+ sessionSaveModalId,
+ googleEnabled,
+ loadHandler,
+ JSONProvider) {
+
+ const urlModal = createURLModal(urlModalId, 'Session URL')
+ $rootContainer.get(0).appendChild(urlModal)
+
+ if (!googleEnabled) {
+ $(`#${googleDriveButtonId}`).parent().hide()
+ }
+
+ const fileLoadWidgetConfig =
+ {
+ widgetParent: urlModal.querySelector('.modal-body'),
+ dataTitle: 'Session',
+ indexTitle: undefined,
+ mode: 'url',
+ fileLoadManager: new FileLoadManager(),
+ dataOnly: true,
+ doURL: undefined
+ }
+
+ fileLoadWidget = new FileLoadWidget(fileLoadWidgetConfig)
+
+ const sessionFileLoadConfig =
+ {
+ localFileInput: document.querySelector(`#${localFileInputId}`),
+ initializeDropbox,
+ dropboxButton: dropboxButtonId ? document.querySelector(`#${dropboxButtonId}`) : undefined,
+ googleEnabled,
+ googleDriveButton: document.querySelector(`#${googleDriveButtonId}`),
+ loadHandler
+ }
+
+ const sessionFileLoad = new SessionFileLoad(sessionFileLoadConfig)
+
+ Utils.configureModal(fileLoadWidget, urlModal, async fileLoadWidget => {
+ await sessionFileLoad.loadPaths(fileLoadWidget.retrievePaths())
+ return true
+ })
+
+ configureSaveSessionModal($rootContainer, prefix, JSONProvider, sessionSaveModalId)
+
+}
+
+function configureSaveSessionModal($rootContainer, prefix, JSONProvider, sessionSaveModalId) {
+
+ const modal =
+ `
+
+
+
+
+
+
+
+
+
+
+
+ Enter session filename with .json suffix
+
+
+
+
+
+
+
+
+
+
+
`
+
+ const $modal = $(modal)
+ $rootContainer.append($modal)
+
+ let $input = $modal.find('input')
+
+ let okHandler = () => {
+
+ const extensions = new Set(['json', 'xml'])
+
+ let filename = $input.val()
+
+ if (undefined === filename || '' === filename) {
+ filename = $input.attr('placeholder')
+ } else if (false === extensions.has(FileUtils.getExtension(filename))) {
+ filename = filename + '.json'
+ }
+
+ const json = JSONProvider()
+
+ if (json) {
+ const jsonString = JSON.stringify(json, null, '\t')
+ const data = URL.createObjectURL(new Blob([jsonString], {type: "application/octet-stream"}))
+ FileUtils.download(filename, data)
+ }
+
+ $modal.modal('hide')
+ }
+
+ const $ok = $modal.find('.modal-footer button:nth-child(2)')
+ $ok.on('click', okHandler)
+
+ $modal.on('show.bs.modal', (e) => {
+ $input.val(`${prefix}-session.json`)
+ })
+
+ $input.on('keyup', e => {
+
+ // enter key
+ if (13 === e.keyCode) {
+ okHandler()
+ }
+ })
+
+}
+
+export {createSessionWidgets}
diff --git a/js/widgets/trackURLModal.js b/js/widgets/trackURLModal.js
new file mode 100644
index 00000000..6ef16201
--- /dev/null
+++ b/js/widgets/trackURLModal.js
@@ -0,0 +1,39 @@
+const createTrackURLModal = id => {
+
+ const html =
+ ``
+
+ const fragment = document.createRange().createContextualFragment(html)
+
+ return fragment.firstChild
+
+}
+
+export {createTrackURLModal}
diff --git a/js/widgets/trackWidgets.js b/js/widgets/trackWidgets.js
new file mode 100644
index 00000000..c7d8b8a8
--- /dev/null
+++ b/js/widgets/trackWidgets.js
@@ -0,0 +1,434 @@
+import {ModalTable, GenericDataSource} from '../../node_modules/data-modal/src/index.js'
+import {encodeTrackDatasourceConfigurator, supportsGenome} from './encodeTrackDatasourceConfigurator.js'
+import AlertSingleton from './alertSingleton.js'
+import {createGenericSelectModal} from './genericSelectModal.js'
+import {createTrackURLModal} from './trackURLModal.js'
+import FileLoadManager from "./fileLoadManager.js"
+import FileLoadWidget from "./fileLoadWidget.js"
+import MultipleTrackFileLoad from "./multipleTrackFileLoad.js"
+import * as Utils from './utils.js'
+
+let fileLoadWidget
+let multipleTrackFileLoad
+let encodeModalTables = []
+let customModalTable
+let $genericSelectModal = undefined
+
+const defaultCustomModalTableConfig =
+ {
+ // id: modalID,
+ // title: 'ENCODE',
+ selectionStyle: 'multi',
+ pageLength: 100
+ }
+
+function createTrackWidgetsWithTrackRegistry($igvMain,
+ $dropdownMenu,
+ $localFileInput,
+ initializeDropbox,
+ $dropboxButton,
+ googleEnabled,
+ $googleDriveButton,
+ encodeTrackModalIds,
+ urlModalId,
+ selectModalIdOrUndefined,
+ GtexUtilsOrUndefined,
+ trackRegistryFile,
+ trackLoadHandler,
+ trackMenuHandler) {
+
+ const urlModal = createTrackURLModal(urlModalId)
+ $igvMain.get(0).appendChild(urlModal)
+
+ let fileLoadWidgetConfig =
+ {
+ widgetParent: urlModal.querySelector('.modal-body'),
+ dataTitle: 'Track',
+ indexTitle: 'Index',
+ mode: 'url',
+ fileLoadManager: new FileLoadManager(),
+ dataOnly: false,
+ doURL: true
+ }
+
+ fileLoadWidget = new FileLoadWidget(fileLoadWidgetConfig)
+
+ Utils.configureModal(fileLoadWidget, urlModal, async fileLoadWidget => {
+ const paths = fileLoadWidget.retrievePaths()
+ await multipleTrackFileLoad.loadPaths(paths)
+ return true
+ })
+
+ if ($googleDriveButton && !googleEnabled) {
+ $googleDriveButton.parent().hide()
+ }
+
+ const multipleTrackFileLoadConfig =
+ {
+ $localFileInput,
+ initializeDropbox,
+ $dropboxButton,
+ $googleDriveButton: googleEnabled ? $googleDriveButton : undefined,
+ fileLoadHandler: trackLoadHandler,
+ multipleFileSelection: true
+ }
+
+ multipleTrackFileLoad = new MultipleTrackFileLoad(multipleTrackFileLoadConfig)
+
+ for (let modalID of encodeTrackModalIds) {
+
+ const encodeModalTableConfig =
+ {
+ id: modalID,
+ title: 'ENCODE',
+ selectionStyle: 'multi',
+ pageLength: 100,
+ okHandler: trackLoadHandler
+ }
+
+ encodeModalTables.push(new ModalTable(encodeModalTableConfig))
+
+ }
+
+ customModalTable = new ModalTable({
+ id: 'igv-custom-modal',
+ title: 'UNTITLED',
+ okHandler: trackLoadHandler, ...defaultCustomModalTableConfig
+ })
+
+ if (selectModalIdOrUndefined) {
+ createGenericSelectModalWidget($igvMain, selectModalIdOrUndefined, trackLoadHandler, trackMenuHandler)
+ }
+
+}
+
+function createGenericSelectModalWidget($igvMain, selectModalIdOrUndefined, trackLoadHandler, trackMenuHandler) {
+
+ $genericSelectModal = $(createGenericSelectModal(selectModalIdOrUndefined, `${selectModalIdOrUndefined}-select`))
+
+ $igvMain.append($genericSelectModal)
+ const $select = $genericSelectModal.find('select')
+
+ const $dismiss = $genericSelectModal.find('.modal-footer button:nth-child(1)')
+ $dismiss.on('click', () => $genericSelectModal.modal('hide'))
+
+ const $ok = $genericSelectModal.find('.modal-footer button:nth-child(2)')
+
+ const okHandler = () => {
+
+ const configurations = []
+ const $selectedOptions = $select.find('option:selected')
+ $selectedOptions.each(function () {
+ // console.log(`${ $(this).val() } was selected`)
+ configurations.push($(this).data('track'))
+ $(this).removeAttr('selected')
+ })
+
+ if (configurations.length > 0) {
+ trackLoadHandler(configurations)
+ }
+
+ $genericSelectModal.modal('hide')
+
+ }
+
+ $ok.on('click', okHandler)
+
+ $genericSelectModal.get(0).addEventListener('keypress', event => {
+ if ('Enter' === event.key) {
+ okHandler()
+ }
+ })
+
+ $genericSelectModal.on('show.bs.modal', () => {
+
+ const urlList = []
+ $genericSelectModal.find('select').find('option').each(function () {
+
+ const {url} = $(this).data('track')
+ urlList.push({element: $(this).get(0), url})
+ })
+
+ trackMenuHandler(urlList)
+
+ })
+
+
+}
+
+async function updateTrackMenusWithTrackConfigurations(genomeID, GtexUtilsOrUndefined, trackConfigurations, $dropdownMenu) {
+
+ const id_prefix = 'genome_specific_'
+
+ const $divider = $dropdownMenu.find('.dropdown-divider')
+
+ const searchString = '[id^=' + id_prefix + ']'
+ const $found = $dropdownMenu.find(searchString)
+ $found.remove()
+
+ let buttonConfigurations = []
+
+ for (const trackConfiguration of trackConfigurations) {
+
+ if (true === supportsGenome(genomeID) && 'ENCODE' === trackConfiguration.type) {
+ encodeModalTables[0].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-chip')))
+ encodeModalTables[1].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-other')))
+ encodeModalTables[2].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'other')))
+ } else if (GtexUtilsOrUndefined && 'GTEX' === trackConfiguration.type) {
+
+ let info = undefined
+ try {
+ info = await GtexUtilsOrUndefined.getTissueInfo(trackConfiguration.datasetId)
+ } catch (e) {
+ AlertSingleton.present(e.message)
+ }
+
+ if (info) {
+ trackConfiguration.tracks = info.tissueInfo.map(tissue => GtexUtilsOrUndefined.trackConfiguration(tissue))
+ }
+
+ }
+
+ buttonConfigurations.push(trackConfiguration)
+
+ } // for(jsons)
+
+ for (let buttonConfiguration of buttonConfigurations.reverse()) {
+
+ if (buttonConfiguration.type && 'custom-data-modal' === buttonConfiguration.type) {
+
+ createDropdownButton($divider, buttonConfiguration.label, id_prefix)
+ .on('click', () => {
+
+ if (buttonConfiguration.description) {
+ customModalTable.setDescription(buttonConfiguration.description)
+ }
+
+ customModalTable.setDatasource(new GenericDataSource(buttonConfiguration))
+ customModalTable.setTitle(buttonConfiguration.label)
+ customModalTable.$modal.modal('show')
+ })
+
+ } else if (buttonConfiguration.type && 'ENCODE' === buttonConfiguration.type) {
+
+ if (true === supportsGenome(genomeID)) {
+
+ if (buttonConfiguration.description) {
+ encodeModalTables[0].setDescription(buttonConfiguration.description)
+ encodeModalTables[1].setDescription(buttonConfiguration.description)
+ encodeModalTables[2].setDescription(buttonConfiguration.description)
+ }
+
+ createDropdownButton($divider, 'ENCODE Other', id_prefix)
+ .on('click', () => encodeModalTables[2].$modal.modal('show'))
+
+ createDropdownButton($divider, 'ENCODE Signals - Other', id_prefix)
+ .on('click', () => encodeModalTables[1].$modal.modal('show'))
+
+ createDropdownButton($divider, 'ENCODE Signals - ChIP', id_prefix)
+ .on('click', () => encodeModalTables[0].$modal.modal('show'))
+
+ }
+
+ } else if ($genericSelectModal) {
+
+ createDropdownButton($divider, buttonConfiguration.label, id_prefix)
+ .on('click', () => {
+ configureSelectModal($genericSelectModal, buttonConfiguration)
+ $genericSelectModal.modal('show')
+ })
+
+ }
+ } // for (buttonConfigurations)
+
+}
+
+async function updateTrackMenus(genomeID, GtexUtilsOrUndefined, trackRegistryFile, $dropdownMenu) {
+
+ const id_prefix = 'genome_specific_'
+
+ const $divider = $dropdownMenu.find('.dropdown-divider')
+
+ const searchString = '[id^=' + id_prefix + ']'
+ const $found = $dropdownMenu.find(searchString)
+ $found.remove()
+
+ const paths = await getPathsWithTrackRegistryFile(genomeID, trackRegistryFile)
+
+ if (undefined === paths) {
+ console.warn(`There are no tracks in the track registry for genome ${genomeID}`)
+ return
+ }
+
+ let responses = []
+ try {
+ responses = await Promise.all(paths.map(path => fetch(path)))
+ } catch (e) {
+ AlertSingleton.present(e.message)
+ }
+
+ let jsons = []
+ try {
+ jsons = await Promise.all(responses.map(response => response.json()))
+ } catch (e) {
+ AlertSingleton.present(e.message)
+ }
+
+ let buttonConfigurations = []
+
+ for (let json of jsons) {
+
+ if (true === supportsGenome(genomeID) && 'ENCODE' === json.type) {
+ encodeModalTables[0].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-chip')))
+ encodeModalTables[1].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'signals-other')))
+ encodeModalTables[2].setDatasource(new GenericDataSource(encodeTrackDatasourceConfigurator(genomeID, 'other')))
+ } else if (GtexUtilsOrUndefined && 'GTEX' === json.type) {
+
+ let info = undefined
+ try {
+ info = await GtexUtilsOrUndefined.getTissueInfo(json.datasetId)
+ } catch (e) {
+ AlertSingleton.present(e.message)
+ }
+
+ if (info) {
+ json.tracks = info.tissueInfo.map(tissue => GtexUtilsOrUndefined.trackConfiguration(tissue))
+ }
+
+ }
+
+ buttonConfigurations.push(json)
+
+ } // for(jsons)
+
+ for (let buttonConfiguration of buttonConfigurations.reverse()) {
+
+ if (buttonConfiguration.type && 'custom-data-modal' === buttonConfiguration.type) {
+
+ createDropdownButton($divider, buttonConfiguration.label, id_prefix)
+ .on('click', () => {
+
+ if (buttonConfiguration.description) {
+ customModalTable.setDescription(buttonConfiguration.description)
+ }
+
+ customModalTable.setDatasource(new GenericDataSource(buttonConfiguration))
+ customModalTable.setTitle(buttonConfiguration.label)
+ customModalTable.$modal.modal('show')
+ })
+
+ } else if (buttonConfiguration.type && 'ENCODE' === buttonConfiguration.type) {
+
+ if (true === supportsGenome(genomeID)) {
+
+ if (buttonConfiguration.description) {
+ encodeModalTables[0].setDescription(buttonConfiguration.description)
+ encodeModalTables[1].setDescription(buttonConfiguration.description)
+ encodeModalTables[2].setDescription(buttonConfiguration.description)
+ }
+
+ createDropdownButton($divider, 'ENCODE Other', id_prefix)
+ .on('click', () => encodeModalTables[2].$modal.modal('show'))
+
+ createDropdownButton($divider, 'ENCODE Signals - Other', id_prefix)
+ .on('click', () => encodeModalTables[1].$modal.modal('show'))
+
+ createDropdownButton($divider, 'ENCODE Signals - ChIP', id_prefix)
+ .on('click', () => encodeModalTables[0].$modal.modal('show'))
+
+ }
+
+ } else if ($genericSelectModal) {
+
+ createDropdownButton($divider, buttonConfiguration.label, id_prefix)
+ .on('click', () => {
+ configureSelectModal($genericSelectModal, buttonConfiguration)
+ $genericSelectModal.modal('show')
+ })
+
+ }
+ } // for (buttonConfigurations)
+
+}
+
+function createDropdownButton($divider, buttonText, id_prefix) {
+ const $button = $('