diff --git a/.babelrc b/.babelrc
deleted file mode 100644
index c6c9efb75d8001d08bbb16daebd9fb6291b838fb..0000000000000000000000000000000000000000
--- a/.babelrc
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-"presets": ["es2015"]
-}
\ No newline at end of file
diff --git a/css/all-ol-style.less b/css/all-ol-style.less
new file mode 100644
index 0000000000000000000000000000000000000000..41a007c06853fb79e6e445543311fc09e2d0c87c
--- /dev/null
+++ b/css/all-ol-style.less
@@ -0,0 +1,3 @@
+@import (inline) "../node_modules/openlayers/dist/ol.css";
+@import "legend";
+@import "ol-popup";
diff --git a/css/glrtoc/glrtoc-opsmap.less b/css/glrtoc/glrtoc-opsmap.less
new file mode 100644
index 0000000000000000000000000000000000000000..6e0df6500e34a488e616043f022aeea75e89c88e
--- /dev/null
+++ b/css/glrtoc/glrtoc-opsmap.less
@@ -0,0 +1,132 @@
+@import "../all-ol-style";
+@import "../media-control";
+
+
+body, html {
+  height: 100%;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+}
+
+#main-container {
+  height: 100%;
+}
+
+.flex-container {
+  display: flex;
+
+  //> div {
+  //  flex-grow: 1;
+  //}
+}
+
+#map{
+  flex-grow: 1;
+}
+
+
+#sidebar {
+  height: 100%;
+  width: 350px;
+  flex-grow: initial;
+  position: relative;
+}
+
+#logo-container {
+    > div {
+    flex-grow: 1;
+  }
+
+  div {
+    margin: 7px;
+    height: 150px;
+  }
+
+  div:first-child {
+    background: url('img/glrtoc-logo.png') no-repeat center;
+    background-size: contain;
+  }
+
+  div:nth-child(2) {
+    background: url('img/tops-logo.png') no-repeat center;
+    background-size: contain;
+  }
+}
+
+#map {
+  position: relative;
+}
+
+#tabs {
+  height: 100%;
+  padding: 0;
+  border: none;
+  width: 100%;
+
+
+  > div {
+    padding: 0;
+    height: 98%;
+
+    p {
+      text-align: justify;
+      padding: 7px 12px;
+      margin: 10px 0;
+    }
+
+  }
+
+  .ui-tab {
+    padding: 3px 7px !important;
+  }
+}
+
+#operations-tab {
+  h3 {
+    margin-top: 0;
+    padding-top: 5px;
+    padding-bottom: 5px;
+  }
+
+  > div{
+    padding: 0;
+  }
+}
+
+#hide-sidebar, #show-sidebar {
+  z-index: 5;
+  position: absolute;
+  color: white;
+  cursor: pointer;
+}
+
+//hide sidebar span
+#hide-sidebar {
+  right: 4px;
+  top: 9px;
+}
+
+#show-sidebar {
+  display: none;
+  left: -12px;
+  top: 74px;
+  background: linear-gradient(#74BBD8, #3FA3CA);
+  width: 40px;
+  text-align: right;
+  padding: 10px 4px;
+  border-radius: 5px;
+  border: solid #4297D7 1px;
+}
+
+#operations > div {
+  padding: 0;
+}
+
+#animation-control {
+  text-align: center;
+  padding: 10px;
+}
+
+
+
diff --git a/css/glrtoc/img/glrtoc-logo.png b/css/glrtoc/img/glrtoc-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..e703e4220e4f9bfc334eb49e95d19b8e9720aefe
Binary files /dev/null and b/css/glrtoc/img/glrtoc-logo.png differ
diff --git a/css/glrtoc/img/tops-logo.png b/css/glrtoc/img/tops-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..436dbf5a4ff00a9dcea8dd51add6f68b5943c4f7
Binary files /dev/null and b/css/glrtoc/img/tops-logo.png differ
diff --git a/css/itsMap.less b/css/itsMap.less
new file mode 100644
index 0000000000000000000000000000000000000000..7cbd153a7077b453bf003a168e0e6907670bed6d
--- /dev/null
+++ b/css/itsMap.less
@@ -0,0 +1,116 @@
+@import "./all-ol-style";
+
+
+@headerHeight: 80px;
+
+body, html {
+  height: 100%;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+}
+
+#header {
+  height: @headerHeight;
+  background-color: #808080;
+
+  span {
+    display: inline-block;
+    margin: 9px;
+    font-size: 28px;
+    line-height: 30px;
+  }
+
+  a {
+    float: right;
+  }
+
+  img {
+    margin: 3px 10px;
+    max-height: @headerHeight - 10px;
+    width: auto;
+  }
+}
+
+#container {
+  display: flex;
+  height: ~"calc(100% - " @headerHeight ~")";
+  width: 100%;
+}
+
+#left {
+  height: 100%;
+  width: 320px;
+  overflow-y: auto;
+}
+
+#map {
+  flex: 1
+}
+
+.legend-container {
+  border: none;
+}
+
+@media screen and (max-height: 650px) {
+
+}
+
+@media screen and (max-height: 640px), (max-width: 650px) {
+
+  #header a {
+    display: none;
+  }
+
+  #header{
+    display: none;
+  }
+
+  #container {
+    //display: block;
+    height: 100%;
+  }
+
+  #left {
+    width: 246px;
+    height: auto;
+    position: absolute;
+    top: 5px;
+    right: 5px;
+    z-index: 10;
+    background-color: white;
+    -webkit-border-radius:5px;
+    -moz-border-radius:5px;
+    border-radius: 5px;
+    max-height: 250px;
+    overflow-y: scroll;
+  }
+
+  #map{
+    height: 100%;
+  }
+
+  .legend-container ul{
+    padding-left: 5px;
+  }
+
+}
+
+@media screen and (max-height: 500px), (max-width: 346px) {
+  #left{
+    width: 204px;
+    max-height: 204px;
+  }
+
+  .ol-popup{
+    width: 240px;
+    min-width: 240px;
+
+    iframe{
+      width: 240px;
+    }
+  }
+}
+
+
+
diff --git a/css/legend.less b/css/legend.less
new file mode 100644
index 0000000000000000000000000000000000000000..504f2813186f2ee31441e54f5d17ede363f69ad4
--- /dev/null
+++ b/css/legend.less
@@ -0,0 +1,116 @@
+@legendKeyOffset: 7px;
+
+@rectangleImg: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHQAAAATCAMAAAB2tdn3AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAMAUExURQAAAAcHBwoKCgsLCw0NDQ4ODg8PDxAQEBYWFhcXFxsbGxwcHB0dHR4eHh8fHyAgICEhISIiIiQkJCUlJSYmJicnJygoKCkpKSoqKiwsLC0tLS4uLi8vLzAwMDQ0NDU1NTc3Nzg4ODk5OT09PT4+Pj8/P0FBQUJCQkNDQ0REREZGRkdHR0hISElJSUpKSktLS0xMTE1NTU5OTk9PT1FRUVJSUlNTU1RUVFZWVldXV1hYWFlZWVpaWl5eXmBgYGFhYWJiYmRkZGZmZmhoaGlpaWpqam1tbW5ubm9vb3Nzc3V1dXd3d3l5eXx8fH5+fn9/fwCADwGBEQKCEgGJEwKKEwSMFQGRFAKSFAKTFQiXGguUHRifKAynGAipFROjJhukLR+iMSaWNCuVOCGhMSSmND+oTT20SwnQGwzRHRDCJRPDKADjHQDmGgDmGwDlHQHpHwDyFwDzGQD1GAD2GQD3HQD3HgD/HgD/HwLqIAD/IAD/IQD/IkeFTkiFUEiGUFeNXlmPX0SrU0i4VVajX2CpaW2kdF7MbGHPbnyxgoCAgIGBgYKCgoODg4SEhIWFhYiIiImJiYuLi4yMjI6OjpCQkJWVlZiYmJqampubm5ycnImwjI+zkZqomKSkpKampqioqKurq6ysrK2tra6urq+vr6SwogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP4rSb8AAAEAdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wBT9wclAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGHRFWHRTb2Z0d2FyZQBwYWludC5uZXQgNC4wLjOM5pdQAAAB+UlEQVRIS5WV6VfTQBTFA7YgoICsFtmxgLRShQIqCLKDKEFQ3FlERdlxwwTBBQRMKVqQRdSyCH/n803yevj65ndy8iY55957ZpJ5owDAgdceJkVSE6pMWlPoFRN7yS6qMNSbahlIUJdg1dM1VpXAUSRCL7fQoxQx4hZrDiVpvQjK/kkxajyf7Ujj4Eg/V7WBgp1KgMogDg6/9N7pus3iwcC8yDq1p3hEdRX7Aqs8At9rM9tQEglwHMvSo1eaPs1Dnxp+gpINjxKBpfqqX+Xju5mPGlxfsUZD73QZ3nxGjU0JA1h3Gt8kUI16nGo8wAmAxQlyY6K9+A1wTITWuVZUMmShGhUAcWboU7mJ6vrIJ4BwEVpe4ic7Jv7cUGinRmZMtPFZCvWWLpMbk5WcUGgHmbF5/4FCy2RDj2Z6i7y4aOMzFFrr9kt+07JQaN8UuXEZ/UihP/N8ZMdCNRqvh/7ehUky4zK4Lf5eG26cqmsy+9RoL0BNtLVPX8pN9e0z1NgV7L8AhaXLAW5L+tGUJbo1dqQoLF8fv5boSGP9KPnlVoK4TgA1ZzPTWZzJcJavCcUFvP5h/fO85173XRb3H84JZeJfBdz4geQxTxnzrJDlhlOcp66ETXpmc4XO0+RLVuWzlZJnHuKw67GFS5HYYDkANCfTKyY2VxAA/gOxObo6iNgi8wAAAABJRU5ErkJggg==');
+
+
+.legend-container {
+  list-style: none;
+  border: solid black 1px;
+  border-radius: 5px;
+  background-color: rgba(211, 211, 211, 0.2);
+  margin: 2px;
+  padding: 2px;
+
+  ul {
+    list-style: none;
+    padding-left: 17px;
+  }
+
+  li {
+    border-radius: 7px;
+    margin: 2px;
+    padding: 2px 4px;
+  }
+
+  hr {
+    display: inline-block;
+    width: 40px;
+    height: 10px;
+    background-color: blue;
+    border: none;
+    margin: 0 0 3px @legendKeyOffset;
+  }
+
+  > li:first-child {
+    font-weight: bold;
+    min-height: 25px;
+
+    input {
+      display: none;
+    }
+
+    input + label {
+      width: 100px;
+      height: 19px;
+      margin: 5px;
+      display: inline-block;
+    }
+
+    input + label > span {
+      display: inline-block;
+      width: 59px;
+      height: 19px;
+      background: @rectangleImg no-repeat 0 0;
+    }
+
+
+
+    input:checked + label > span {
+      background: @rectangleImg no-repeat -59px 0;
+    }
+  }
+}
+
+
+.legend-layer-subitem {
+  display: inline-block;
+  max-width: 148px;
+  overflow: hidden;
+  max-height: 20px;
+}
+
+.legend-layer-icon {
+  margin-left: @legendKeyOffset;
+}
+
+.legend-layer-div li {
+  margin: 0;
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+.layer-not-visible {
+  background-color: gray;
+  display: none;
+}
+
+.layer-force-show {
+  display: inherit;
+}
+
+.layer-group-expander, .map-server-expander, .unique-symbol-expander, .legend-items-expander {
+  color: #59AFEE;
+  cursor: pointer;
+  padding-left: 4px;
+  font-size: 14px;
+}
+
+.esri-popup-table{
+  border-collapse: collapse;
+
+  td, th {
+    border: solid black 1px;
+    padding: 0 4px;
+  }
+
+  tr:nth-child(even){
+    background-color: lightblue;
+  }
+}
+
+.legend-layer-name, .legend-check{
+  cursor: pointer
+}
+
+
+
diff --git a/css/media-control.less b/css/media-control.less
new file mode 100644
index 0000000000000000000000000000000000000000..9233513556e87c5b315d1333e0de9c7c33a1984e
--- /dev/null
+++ b/css/media-control.less
@@ -0,0 +1,68 @@
+@mediaImg: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJEAAAAdCAMAAACddH6yAAAABGdBTUEAALGPC/xhBQAAAwBQTFRFAAAAJycmKygmLCknKSkpKSkqLysqLCwsLS0tLi4uLi8vMCwrMi0sMS8tNCwpMTAvNzAvMDAwMTExMTEyMjIxMjIyMzMzMzM0MzQ0NjExNzMxNDQ0NTU1NTY2Njc3OTMxOTc2PDg2OTk5OTo7PTo5Pz4+Pj8/RDs2R0A9REA+QkNDQkRFR0hIS0NBS0VCTUdES0tLTU5NVFRUVVdYYFlXZl9caF9cYWBgZ2Jham1ubGxsc3JycXR2Zn6HaYCIcpSegHx7hIWFjo+PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAptsNGgAAAQB0Uk5T////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AFP3ByUAAAAJcEhZcwAADsIAAA7CARUoSoAAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuM4zml1AAAAIpSURBVFhH7ZdrV9QwEIYriLDgbQsdUfCKchEVBBVUFOH//6c482a627Qz2VVzjl94PiQznZP02TRNz1bhETnUIYRa4wEbXNzUeEAza+S2xgP4ntWqxhb+tEwYaWARGg0sQvaelQYmT7U32dH+z3mmvUktRuu8jgPecmHLK4qNzCuPZ8A7XR+zeMgF+aFm8ZhHNmxkFrER2MgpfohGmvV4JT5ecR9GzrS70cgZGmC0rEmfnBEXmRVN+sBI3hqLOYz8eWcZjTTpk1ujG6PI3Ebj2+g+ob0vTdfomr7EoCUx+jUh5nMa3UO+iraJO6tr1CxI+xw7eemutKlR72clRjAAnbw10qud4sQIbY3WM6rxbtVkGRGNYwyKGBF9lbtJ4hjxAcVG3NpGRA9jxhQyosf4Ul04RhyykbSeEdFlzLlYxojW8O08qA0jKbT4Ru2jK2bUYhh1lXyj5iJeKGY0xhqNrTVawDLNeGrvYyrFMkZ3sI/OvJ29jp3NVxyjzstWyOgIX3SO/+rtxywtRYzQ5s+jcIQTctEyOo+RUsQIeVx4w2h0C91PtHvSJEYPYj8hMerDlalRn6mRQddoSNdowI0R+H9GTvUNjJzij5zRCYyckZ9zRqfRiF5rnsIFNqKXmqbwYSpGLzRN0dP/iaYpXGQj2tY0hQswosZAruOvp+YJM4tA8wS5nh3ZVNMJhuCE90i/zT3+YWSVGft98gAMPuL8d/iWKx7nilfhN6Q1MB5pcaF/AAAAAElFTkSuQmCC');
+
+.media-control-container {
+  padding: 10px;
+  text-align: center;
+
+  input[type=range]{
+    width: 80%;
+  }
+
+  input[type=range]:disabled{
+      cursor: not-allowed;
+      background-color: lightgray;
+  }
+}
+
+.media-player-button {
+  background: @mediaImg no-repeat;
+  height: 29px;
+  width: 30px;
+  display: inline-block;
+  margin: 0 2px;
+  cursor: pointer;
+}
+
+.media-player-button:hover {
+  background-color: lightblue;
+}
+
+.media-pause {
+  background-position: -86px 0;
+}
+
+.media-play {
+  background-position: -28px 0;
+}
+
+.media-stop {
+  background-position: -57px 0;
+}
+
+.media-ahead {
+  background-position: -116px 0;
+}
+
+.media-back {
+  background-position: 1px 0;
+}
+
+.media-disabled {
+  display: none;
+}
+
+.media-locked{
+  background-color: lightgray !important;
+  cursor: not-allowed;
+}
+
+.media-control-value-label-container{
+  display: flex;
+  justify-content: space-between;
+
+  span{
+    display: block;
+    font-size: small;
+  }
+}
+
diff --git a/css/npmrds-heatmap.less b/css/npmrds-heatmap.less
new file mode 100644
index 0000000000000000000000000000000000000000..16ffa8b039e04cf59bfad30c56ebf0b4f931c989
--- /dev/null
+++ b/css/npmrds-heatmap.less
@@ -0,0 +1,174 @@
+@heatMapControlHeight: 135px;
+@heatMapSliderHeight: 30px;
+@import "./all-ol-style";
+
+
+html, body {
+  height: 100%;
+  width: 100%;
+  margin: 0;
+  /*position: relative;*/
+}
+
+#container {
+  display: flex;
+  height: 100%;
+  width: 100%;
+}
+
+#map {
+  flex: 1;
+}
+
+#left {
+  flex: initial;
+  width: 300px;
+  min-width: 100px;
+
+  > div {
+    margin: 10px;
+  }
+}
+
+#ul-hwy-dirs {
+  list-style: none;
+  padding-left: 5px;
+  max-height: 300px;
+  overflow-y: auto;
+  background-color: lightgrey;
+
+  li {
+    padding: 2px;
+  }
+}
+
+h4 {
+  margin: 10px 5px;
+}
+
+#right {
+  width: 60%;
+  height: 100%;
+
+  input[type=button] {
+    margin: 3px 0;
+  }
+}
+
+#heat-map-control {
+  display: flex;
+  flex-wrap: wrap;
+  height: @heatMapControlHeight;
+
+  > div {
+    margin: 0 5px;
+  }
+
+  ul {
+    list-style: none;
+    padding-left: 0;
+    margin-top: 14px;
+  }
+}
+
+#heat-slider-container {
+  height: @heatMapSliderHeight;
+}
+
+#heat-map-slider {
+  border: none;
+  width: 95%;
+  padding: 0;
+  margin: 0 0 0 18px;
+}
+
+#heatmap-canvas {
+  position: absolute;
+  top: 0;
+  left: 22px;
+  cursor: crosshair;
+}
+
+#heat-slider-container, #canvas-container {
+  width: 100%;
+  position: relative;
+}
+
+#canvas-container {
+  height: ~"calc(100% - " @heatMapControlHeight + @heatMapSliderHeight ~")";
+  overflow-y: auto;
+}
+
+#heat-map-vertical-bar {
+  position: absolute;
+  /*z-index: 4;*/
+  border-right: solid blue 2px;
+  left: 22px;
+  top: 0;
+  width: 0;
+  height: 0;
+  background-color: rgba(100, 100, 100, 0.3);
+}
+
+#date-ul {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  position: absolute;
+  left: 0;
+  top: 0;
+  z-index: -5;
+
+  li {
+    border-bottom: solid #000000 1px;
+    height: 575px;
+  }
+
+  li:nth-child(odd) {
+    background-color: lightblue;
+  }
+
+  li div {
+    color: blue;
+    height: 150px;
+    transform: rotate(-90deg) translate(-259px, 0px);
+    transform-origin: left top 0;
+    float: left;
+  }
+}
+
+#canvas-tooltip-span {
+  display: none;
+  z-index: 100;
+  position: fixed;
+  background-color: lightgrey;
+  border: solid black 1px;
+  border-radius: 5px;
+  padding: 5px;
+  /*height: 50px;*/
+  width: 160px;
+  overflow: hidden;
+  text-align: center;
+}
+
+#progress-indicator-div {
+  z-index: 200;
+  background-color: lightblue;
+  border: solid darkslategray 1px;
+  border-radius: 5px;
+  width: 200px;
+  padding: 10px;
+  margin: 20px;
+  display: none;
+}
+
+#sel-hwy {
+  padding-left: 14px;
+  padding-top: 9px;
+  font-weight: bold;
+  display: inline-block;
+}
+
+/*.ui-datepicker{*/
+/*z-index: 100 !important;*/
+/*}*/
\ No newline at end of file
diff --git a/css/ol-popup.less b/css/ol-popup.less
new file mode 100644
index 0000000000000000000000000000000000000000..8e13d3ef8c4902942dd8240840a3f77a60f84fd6
--- /dev/null
+++ b/css/ol-popup.less
@@ -0,0 +1,68 @@
+.ol-popup {
+  position: absolute;
+  background-color: white;
+  -webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
+  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
+  padding: 22px 5px 5px 5px;
+  border-radius: 10px;
+  border: 1px solid #cccccc;
+  bottom: 12px;
+  left: -50px;
+  min-width: 280px;
+  cursor: default;
+}
+
+.ol-popup:after, .ol-popup:before {
+  top: 100%;
+  border: solid transparent;
+  content: " ";
+  height: 0;
+  width: 0;
+  position: absolute;
+  pointer-events: none;
+}
+
+.ol-popup:after {
+  border-top-color: white;
+  border-width: 10px;
+  left: 48px;
+  margin-left: -10px;
+}
+
+.ol-popup:before {
+  border-top-color: #cccccc;
+  border-width: 11px;
+  left: 48px;
+  margin-left: -11px;
+}
+
+.ol-popup-closer {
+  text-decoration: none;
+  position: absolute;
+  top: 2px;
+  right: 8px;
+}
+
+.ol-popup-closer:after {
+  content: "X";
+}
+
+.ol-popup-nav {
+  background-color: lightgray;
+  padding: 3px;
+  margin-bottom: 7px;
+}
+
+.ol-popup-nav-arrow{
+  color: #419CC4;
+  cursor: pointer;
+  margin: 0 3px;
+  font-size: larger;
+}
+
+.ol-inner-inner {
+  height: 300px;
+  //width: 392px;
+  //padding-right: 25px;
+  overflow-y: auto;
+}
diff --git a/css/peergroup.less b/css/peergroup.less
new file mode 100644
index 0000000000000000000000000000000000000000..e0fb5e7106e18ac9dd306a3168a4534acfc38315
--- /dev/null
+++ b/css/peergroup.less
@@ -0,0 +1,173 @@
+@import './all-ol-style';
+
+@sidebarWidth: 280px;
+
+html, body {
+  margin: 0;
+  padding: 0;
+  height: 100%;
+  width: 100%;
+}
+
+#app-container {
+  display: flex;
+  height: 100%;
+
+  > div {
+    height: 100%;
+    overflow-y: auto;
+  }
+}
+
+#left-sidebar {
+  width: @sidebarWidth;
+
+  label {
+    display: inline-block;
+    margin-left: 4px;
+  }
+
+  h6 {
+    margin-top: 5px;
+    margin-bottom: 2px;
+  }
+}
+
+#accordion {
+  font-size: 0.9em !important;
+
+  h3 {
+    padding-top: 1px;
+    padding-bottom: 1px;
+    margin-bottom: 2px;
+  }
+}
+
+#right-sidebar {
+  width: 400px;
+}
+
+#map {
+  flex: 1;
+}
+
+#dialog{
+  display: none;
+}
+//
+.ui-accordion-content {
+  padding: 5px 10px !important;
+}
+
+select {
+  margin: 4px;
+  padding: 0;
+}
+
+input[type=text] {
+  margin: 2px 7px;
+  padding: 5px;
+}
+
+.divUnd {
+  width: 100px;
+
+}
+
+.equality-compare {
+  width: 45px;
+}
+
+.class-adtVal {
+  width: 90px;
+}
+
+.class-laneVal, .class-speedVal {
+  width: 50px;
+}
+
+.comma-list {
+  width: 250px;
+
+}
+
+#summary {
+  border-collapse: collapse;
+}
+
+#summary tr {
+  height: 30px;
+}
+
+#summary th, td {
+  border: 1px solid black;
+  padding: 1px 4px;
+  text-align: center;
+  //color: #636363;
+}
+
+.invalid {
+  border: solid red 1px !important;
+}
+
+.popup-table {
+  border-collapse: collapse;
+  margin-top: 5px;
+
+}
+
+.popup-table th, td {
+  border: 1px solid black;
+}
+
+//
+//.ol-popup {
+//    display: none;
+//    position: absolute;
+//    background-color: white;
+//    -moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
+//    -webkit-filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
+//    filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2));
+//    padding: 15px;
+//    border-radius: 10px;
+//    border: 1px solid #cccccc;
+//    bottom: 12px;
+//    left: -50px;
+//}
+//
+//.ol-popup:after, .ol-popup:before {
+//    top: 100%;
+//    border: solid transparent;
+//    content: " ";
+//    height: 0;
+//    width: 0;
+//    position: absolute;
+//    pointer-events: none;
+//}
+//
+//.ol-popup:after {
+//    border-top-color: white;
+//    border-width: 10px;
+//    left: 48px;
+//    margin-left: -10px;
+//}
+//
+//.ol-popup:before {
+//    border-top-color: #cccccc;
+//    border-width: 11px;
+//    left: 48px;
+//    margin-left: -11px;
+//}
+//
+//.ol-popup-closer {
+//    text-decoration: none;
+//    position: absolute;
+//    top: 2px;
+//    right: 8px;
+//}
+//
+//.ol-popup-closer:after {
+//    content: "✖";
+//}
+//
+//
diff --git a/css/tip-colors.less b/css/tip-colors.less
new file mode 100644
index 0000000000000000000000000000000000000000..669e4c57f0c18015eda76686173593d0d5b80086
--- /dev/null
+++ b/css/tip-colors.less
@@ -0,0 +1,30 @@
+.cell-min {
+    background-color: #00ff00 !important;
+}
+
+.cell-very-low {
+    background-color: #55ff00  !important;
+}
+
+.cell-low {
+    background-color: #aaff00  !important;
+}
+
+.cell-mid {
+    background-color: #ffff00  !important;
+}
+
+.cell-high {
+    background-color: #ffaa00  !important;
+}
+
+.cell-very-high {
+    background-color: #ff5500  !important;
+    font-weight: bold;
+}
+
+.cell-max {
+    background-color: #ff0000  !important;
+    font-weight: bold;
+    text-decoration: underline;
+}
\ No newline at end of file
diff --git a/css/tip-results.less b/css/tip-results.less
new file mode 100644
index 0000000000000000000000000000000000000000..8a04f94700692c9c3a6d0a6b5e24e2b95ec744f7
--- /dev/null
+++ b/css/tip-results.less
@@ -0,0 +1,136 @@
+@import "tip-colors";
+@import "./all-ol-style";
+
+body, html, #container {
+  height: 100%;
+  width: 100%;
+  margin: 0;
+  padding: 0;
+}
+
+#container, #summary {
+  display: flex;
+}
+
+#summary {
+  height: 38%;
+
+  > div {
+    flex-grow: 1;
+    height: 100%;
+    overflow-y: auto;
+  }
+}
+
+#crash-data {
+  table {
+    border-collapse: collapse;
+    margin: 7px 4px 0 4px;
+  }
+
+  th, td {
+    border: solid black 1px;
+    text-align: center;
+    padding: 0 5px;
+  }
+}
+
+#right {
+  flex: 1;
+}
+
+#map {
+  height: 60%;
+  position: relative;
+}
+
+#legend-container {
+  position: absolute;
+  width: 250px;
+  max-height: 80%;
+  min-height: 30px;
+  overflow-y: auto;
+  background-color: rgba(255, 255, 255, 0.8);
+  z-index: 10;
+  top: 5px;
+  right: 5px;
+  border-radius: 7px;
+
+  > ul {
+    > li:first-child {
+      display: none;
+    }
+  }
+}
+
+#summary {
+  h3, h4, ul {
+    margin: 5px 10px;
+  }
+}
+
+#results-container {
+  width: 936px;
+  padding-right: 20px;
+  height: 100%;
+  overflow: auto;
+  position: relative;
+  table {
+    border-collapse: collapse;
+    margin: 4px;
+    width: 100%;
+    //height: 100%;
+    position: relative;
+    table-layout: fixed;
+
+    th, td {
+      border: solid black 1px;
+      padding: 3px;
+      text-align: center;
+    }
+
+    > *{
+      width: 100%;
+    }
+
+    th {
+      cursor: pointer;
+    }
+
+    thead {
+
+      display: block;
+      width: 100%;
+
+      tr {
+        background-color: #4a4a4a;
+        color: #FDFDFD;
+        width: 100%;
+      }
+    }
+
+    tbody {
+      display: block;
+      height: 400px;
+      overflow-y: auto;
+      width: 100%;
+    }
+  }
+}
+
+.selectable-row:hover {
+  background-color: #00ffd0;
+  cursor: pointer;
+}
+
+.selected-row {
+  background-color: #0074ff !important;
+}
+
+.score-under-one {
+  display: none;
+}
+
+#show-all-toggle, #show-all-toggle-label {
+  cursor: pointer;
+}
diff --git a/css/tip.less b/css/tip.less
new file mode 100644
index 0000000000000000000000000000000000000000..09ac1b59db406b49d1151b8fb966fe131bbba2a3
--- /dev/null
+++ b/css/tip.less
@@ -0,0 +1,159 @@
+@import "./tip-colors";
+@import "./all-ol-style";
+
+html, body {
+  width: 100%;
+  height: 100%;
+  margin: 0;
+}
+
+#container {
+  display: flex;
+  height: 100%;
+  width: 100%;
+}
+
+#left {
+  overflow-y: scroll;
+  background-color: lightblue;
+  height: 100%;
+  width: 320px;
+}
+
+// suppress first li in legend
+.legend-container {
+  > li:first-child {
+    display: none;
+  }
+}
+
+//tip legend style
+#tip-segments-layer-li {
+  display: inherit;
+
+  > div {
+    height: 80px;
+
+    hr {
+      height: 4px;
+    }
+
+    ul:first-of-type {
+      float: right;
+      padding-left: 0;
+
+      li {
+        height: 10px;
+      }
+    }
+
+    ul:last-of-type {
+      float: right;
+      margin-top: 5px;
+
+      li {
+        margin: 3px 0;
+        text-align: right;
+        border-right: solid black 1px;
+        border-radius: 0;
+      }
+    }
+  }
+}
+
+//common border styling of legend and preset container
+#preset-wrapper, .legend-container {
+  border: solid black 1px;
+  border-radius: 5px;
+}
+
+#preset-wrapper {
+  margin: 3px;
+  padding: 3px;
+  border: solid black 1px;
+  border-radius: 5px;
+
+  h4 {
+    display: inline-block;
+    margin-top: 0;
+    margin-bottom: 0;
+  }
+
+  select {
+    width: 141px;
+  }
+
+  input[type=button] {
+    width: 88px;
+    margin: 0;
+  }
+}
+
+.slider-container {
+
+  > div {
+    padding: 2px;
+    border-bottom: solid darkblue 1px;
+    //min-height: 46px;
+  }
+
+  label {
+    display: inline-block;
+  }
+
+  .slider-label {
+    width: 97px;
+  }
+
+  .hidden-select {
+    display: none;
+  }
+
+  .percent-label {
+    padding: 2px 3px;
+    margin-left: 2px;
+    background-color: lightgray;
+    font-weight: bold;
+    color: orangered;
+  }
+
+  input[type=range] {
+    width: 153px;
+    padding: 1px;
+
+  }
+
+  input[type=range][disabled] {
+    background-color: #59afee;
+    cursor: not-allowed;
+  }
+
+  select {
+    margin-bottom: 2px;
+    width: 46px;
+  }
+
+  .low-high {
+    font-size: 12px;
+  }
+}
+
+// offset around show help button
+#tip-help {
+  margin-left: 10px;
+  margin-top: 4px;
+}
+
+#map {
+  background-color: slategray;
+  height: 100%;
+  flex: 1;
+  position: relative;
+}
+
+#loading-gif {
+  left: 10px;
+  top: 80px;
+  z-index: 10;
+  position: absolute;
+}
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000000000000000000000000000000000000..8ba80e870929c0a1c86423c4b8b95f2c0f6c185c
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,230 @@
+const gulp = require('gulp');
+const babel = require('gulp-babel');
+const babelify = require('babelify');
+const browserify = require('browserify');
+const buffer = require('vinyl-buffer');
+const source = require('vinyl-source-stream');
+const sourcemaps = require('gulp-sourcemaps');
+const minify = require('gulp-minify');
+const less = require('gulp-less');
+const cssmin = require('gulp-cssmin');
+const rename = require('gulp-rename');
+
+
+/**
+ *
+ * @param {string} inputFile - input file
+ * @param {string} outputFile - output file
+ * @param {boolean|*} runMinify - if should minify
+ * @returns {object} return stream
+ */
+function processJsFile(inputFile, outputFile, runMinify) {
+    "use strict";
+    runMinify = typeof runMinify == 'boolean' ? runMinify : false;
+    let pathParts = outputFile.split('/');
+    let outFileName = pathParts[pathParts.length - 1];
+    pathParts.splice(pathParts.length - 1, 1);
+    let outDir = pathParts.length === 0 ? '.' : pathParts.join('/');
+
+    let bundler = browserify({entries: inputFile, "debug": true, "extensions": ["js"]});
+
+    bundler.transform(babelify.configure({
+        presets: ["es2015"],
+        "ignore": /custom-ol-build|jquery.min/
+    }));
+
+
+    //bundler.transform(babelify.configure({
+    //    ignore: /custom-ol-build|jquery.min/
+    //}));
+    //let bundler = browserify({entries: inputFile, extensions: ['.js'], debug: true})
+    //    .transform(babelify.configure({
+    //        ignore: /custom-ol-build.js|jquery.min|/,
+    //        presets: ["es2015"],
+    //        extensions: [".ts", ".js"]
+    //    }));
+
+
+    //bundler.transform("babelify", {presets: ["es2015"], ignore: /custom-ol-build.js|jquery.min|/}, extensions: ['.js', '.ts']);
+
+
+    if (runMinify) {
+        return bundler.bundle()
+            .on('error', function (err) {
+                console.error(err);
+            })
+            .pipe(source(outFileName))
+            .pipe(buffer())
+            .pipe(sourcemaps.init({loadMaps: true}))
+            .pipe(minify({
+                ext: {
+                    src: '-debug.js',
+                    min: '.js'
+                },
+                exclude: ['tasks'],
+                ignoreFiles: ['.combo.js', '-min.js']
+            }))
+            .pipe(sourcemaps.write('./'))
+            .pipe(gulp.dest(outDir));
+    } else {
+        return bundler.bundle()
+            .on('error', function (err) {
+                console.error(err);
+            })
+            .pipe(source(outFileName))
+            .pipe(buffer())
+            .pipe(sourcemaps.init({loadMaps: true}))
+            .pipe(sourcemaps.write('./'))
+            .pipe(gulp.dest(outDir));
+    }
+}
+
+function processLessFile(inputFile, outputFile) {
+    "use strict";
+    let pathParts = outputFile.split('/');
+    let outFileName = pathParts[pathParts.length - 1];
+    pathParts.splice(pathParts.length - 1, 1);
+    let outDir = pathParts.length === 0 ? '.' : pathParts.join('/');
+
+    let fileNameParts = outFileName.split('.');
+
+    return gulp.src(inputFile)
+        .pipe(less().on('error', function (err) {
+            console.log(err);
+        }))
+        .pipe(cssmin().on('error', function (err) {
+            console.log(err);
+        }))
+        .pipe(rename({
+            basename: fileNameParts[0],
+            extname: '.' + fileNameParts[1],
+            suffix: '.min'
+        }))
+        .pipe(gulp.dest(outDir));
+}
+
+//gulp.task('default', function () {
+//    "use strict";
+//    return gulp.src('src/app.js')
+//        .pipe(babel())
+//        .pipe(gulp.dest('dist'))
+//});
+
+function _itsInventory(doMinify) {
+    "use strict";
+    //processLessFile('./flaskApp/blueprints/its_inventory/static/css/itsMap.less', './flaskApp/blueprints/its_inventory/static/_build/itsMap.css');
+
+    return processJsFile('./projects/itsMap.js', './build/itsMap.js', doMinify);
+}
+
+gulp.task('itsInventory-dev', () => {
+    return _itsInventory(false);
+});
+
+gulp.task('itsInventory-prod', () => {
+    return _itsInventory(true);
+});
+
+function _glrtoc(doMinify) {
+    "use strict";
+    processJsFile('./projects/glrtoc/main.js', './build/glrtoc/main.js', doMinify);
+
+    return processJsFile('./projects/glrtoc/legendTest.js', './build/glrtoc/legendTest.js', doMinify);
+}
+
+gulp.task('glrtoc-dev', () => {
+    "use strict";
+
+    return _glrtoc(false);
+});
+
+gulp.task('glrtoc-prod', () => {
+    "use strict";
+
+    return _glrtoc(true);
+});
+
+function _tsmo(doMinify) {
+    "use strict";
+    processJsFile('./projects/tsmo/legend-test.js', './build/legend-test.js', doMinify);
+    //processJsFile('./projects/tsmo/slider-test.js', './build/slider-test.js', doMinify);
+    //processJsFile('./projects/tsmo/main.js', './build/main.js', doMinify);
+    //return processJsFile('./projects/tsmo/main-report.js', './build/main-report.js', doMinify);
+}
+
+gulp.task('tsmo-dev', () => {
+    "use strict";
+
+    return _tsmo(false);
+});
+
+gulp.task('tsmo-prod', () => {
+    "use strict";
+
+    return _tsmo(true);
+});
+
+function _npmrds(doMinify) {
+    "use strict";
+
+    return processJsFile('./flaskApp/blueprints/npmrds/static/js/heatmap/main.js', './flaskApp/blueprints/npmrds/static/_build/heatmap-main.js', doMinify);
+}
+
+gulp.task('npmrds-dev', () => {
+    "use strict";
+
+    return _npmrds(false);
+});
+
+gulp.task('npmrds-prod', () => {
+    "use strict";
+
+    return _npmrds(true);
+});
+
+function _ssa(doMinify) {
+    "use strict";
+    processLessFile('./flaskApp/blueprints/testing/static/css/ssa-corridor.less', './flaskApp/blueprints/testing/static/_build/ssa-corridor.css');
+
+    return processJsFile('./flaskApp/blueprints/testing/static/js/ssa-main.js', './flaskApp/blueprints/testing/static/_build/ssa-main.js', doMinify);
+}
+
+gulp.task('ssa-dev', () => {
+    "use strict";
+
+    return _ssa(false);
+});
+
+gulp.task('ssa-prod', () => {
+    "use strict";
+
+    return _ssa(true);
+});
+
+gulp.task('peerGroup-dev', () => {
+    "use strict";
+
+    return processJsFile('./flaskApp/blueprints/peerGroup/static/js/main.js', './flaskApp/blueprints/peerGroup/static/_build/main.js', false);
+});
+
+gulp.task('peerGroup-prod', () => {
+    "use strict";
+
+    return processJsFile('./flaskApp/blueprints/peerGroup/static/js/main.js', './flaskApp/blueprints/peerGroup/static/_build/main.js', true);
+});
+
+function _buildTestApps() {
+    "use strict";
+    //processJsFile('./flaskApp/blueprints/testing/static/js/test-custom-build.js', './flaskApp/blueprints/testing/static/_build/test-custom-build.js', false);
+
+    return processJsFile('./flaskApp/blueprints/testing/static/js/test-corridor-layer.js', './flaskApp/blueprints/testing/static/_build/test-corridor-layer.js', false);
+
+}
+
+gulp.task('buildTestApps', () => {
+    "use strict";
+
+    return _buildTestApps(false);
+});
+
+gulp.task('build-prod', ['glrtoc-prod', 'tsmo-prod', 'npmrds-prod', 'ssa-prod', 'itsInventory-prod', 'peerGroup-prod']);
diff --git a/lib/jquery.floatThead.js b/lib/jquery.floatThead.js
new file mode 100644
index 0000000000000000000000000000000000000000..42eba92d4e2345f0efc9ecdbb4c5b56213508749
--- /dev/null
+++ b/lib/jquery.floatThead.js
@@ -0,0 +1,967 @@
+// @preserve jQuery.floatThead 1.3.2 - http://mkoryak.github.io/floatThead/ - Copyright (c) 2012 - 2015 Misha Koryak
+// @license MIT
+
+/* @author Misha Koryak
+ * @projectDescription lock a table header in place while scrolling - without breaking styles or events bound to the header
+ *
+ * Dependencies:
+ * jquery 1.9.0 + [required] OR jquery 1.7.0 + jquery UI core
+ *
+ * http://mkoryak.github.io/floatThead/
+ *
+ * Tested on FF13+, Chrome 21+, IE8, IE9, IE10, IE11
+ *
+ */
+let jQuery = require('jQuery');
+
+(function( $ ) {
+  /**
+   * provides a default config object. You can modify this after including this script if you want to change the init defaults
+   * @type {Object}
+   */
+  $.floatThead = $.floatThead || {};
+  $.floatThead.defaults = {
+    headerCellSelector: 'tr:visible:first>*:visible', //thead cells are this.
+    zIndex: 1001, //zindex of the floating thead (actually a container div)
+    position: 'auto', // 'fixed', 'absolute', 'auto'. auto picks the best for your table scrolling type.
+    top: 0, //String or function($table) - offset from top of window where the header should not pass above
+    bottom: 0, //String or function($table) - offset from the bottom of the table where the header should stop scrolling
+    scrollContainer: function($table){
+      return $([]); //if the table has horizontal scroll bars then this is the container that has overflow:auto and causes those scroll bars
+    },
+    getSizingRow: function($table, $cols, $fthCells){ // this is only called when using IE,
+      // override it if the first row of the table is going to contain colgroups (any cell spans greater than one col)
+      // it should return a jquery object containing a wrapped set of table cells comprising a row that contains no col spans and is visible
+      return $table.find('tbody tr:visible:first>*:visible');
+    },
+    floatTableClass: 'floatThead-table',
+    floatWrapperClass: 'floatThead-wrapper',
+    floatContainerClass: 'floatThead-container',
+    copyTableClass: true, //copy 'class' attribute from table into the floated table so that the styles match.
+    enableAria: false, //will copy header text from the floated header back into the table for screen readers. Might cause the css styling to be off. beware!
+    autoReflow: false, //(undocumented) - use MutationObserver api to reflow automatically when internal table DOM changes
+    debug: false //print possible issues (that don't prevent script loading) to console, if console exists.
+  };
+
+  var util = window._;
+
+  var canObserveMutations = typeof MutationObserver !== 'undefined';
+
+
+  //browser stuff
+  var ieVersion = function(){for(var a=3,b=document.createElement("b"),c=b.all||[];a = 1+a,b.innerHTML="<!--[if gt IE "+ a +"]><i><![endif]-->",c[0];);return 4<a?a:document.documentMode}();
+  var isFF = /Gecko\//.test(navigator.userAgent);
+  var isWebkit = /WebKit\//.test(navigator.userAgent);
+
+  //safari 7 (and perhaps others) reports table width to be parent container's width if max-width is set on table. see: https://github.com/mkoryak/floatThead/issues/108
+  var isTableWidthBug = function(){
+    if(isWebkit) {
+      var $test = $('<div style="width:0px"><table style="max-width:100%"><tr><th><div style="min-width:100px;">X</div></th></tr></table></div>');
+      $("body").append($test);
+      var ret = ($test.find("table").width() == 0);
+      $test.remove();
+      return ret;
+    }
+    return false;
+  };
+
+  var createElements = !isFF && !ieVersion; //FF can read width from <col> elements, but webkit cannot
+
+  var $window = $(window);
+
+  /**
+   * @param debounceMs
+   * @param cb
+   */
+  function windowResize(eventName, cb){
+    if(ieVersion == 8){ //ie8 is crap: https://github.com/mkoryak/floatThead/issues/65
+      var winWidth = $window.width();
+      var debouncedCb = util.debounce(function(){
+        var winWidthNew = $window.width();
+        if(winWidth != winWidthNew){
+          winWidth = winWidthNew;
+          cb();
+        }
+      }, 1);
+      $window.on(eventName, debouncedCb);
+    } else {
+      $window.on(eventName, util.debounce(cb, 1));
+    }
+  }
+
+
+  function debug(str){
+    window && window.console && window.console.error && window.console.error("jQuery.floatThead: " + str);
+  }
+
+  //returns fractional pixel widths
+  function getOffsetWidth(el) {
+    var rect = el.getBoundingClientRect();
+    return rect.width || rect.right - rect.left;
+  }
+
+  /**
+   * try to calculate the scrollbar width for your browser/os
+   * @return {Number}
+   */
+  function scrollbarWidth() {
+    var $div = $( //borrowed from anti-scroll
+        '<div style="width:50px;height:50px;overflow-y:scroll;'
+        + 'position:absolute;top:-200px;left:-200px;"><div style="height:100px;width:100%">'
+        + '</div>'
+    );
+    $('body').append($div);
+    var w1 = $div.innerWidth();
+    var w2 = $('div', $div).innerWidth();
+    $div.remove();
+    return w1 - w2;
+  }
+  /**
+   * Check if a given table has been datatableized (http://datatables.net)
+   * @param $table
+   * @return {Boolean}
+   */
+  function isDatatable($table){
+    if($table.dataTableSettings){
+      for(var i = 0; i < $table.dataTableSettings.length; i++){
+        var table = $table.dataTableSettings[i].nTable;
+        if($table[0] == table){
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  function tableWidth($table, $fthCells, isOuter){
+    // see: https://github.com/mkoryak/floatThead/issues/108
+    var fn = isOuter ? "outerWidth": "width";
+    if(isTableWidthBug && $table.css("max-width")){
+      var w = 0;
+      if(isOuter) {
+        w += parseInt($table.css("borderLeft"), 10);
+        w += parseInt($table.css("borderRight"), 10);
+      }
+      for(var i=0; i < $fthCells.length; i++){
+        w += $fthCells.get(i).offsetWidth;
+      }
+      return w;
+    } else {
+      return $table[fn]();
+    }
+  }
+  $.fn.floatThead = function(map){
+    map = map || {};
+    if(!util){ //may have been included after the script? lets try to grab it again.
+      util = window._ || $.floatThead._;
+      if(!util){
+        throw new Error("jquery.floatThead-slim.js requires underscore. You should use the non-lite version since you do not have underscore.");
+      }
+    }
+
+    if(ieVersion < 8){
+      return this; //no more crappy browser support.
+    }
+
+    var mObs = null; //mutation observer lives in here if we can use it / make it
+
+    if(util.isFunction(isTableWidthBug)) {
+      isTableWidthBug = isTableWidthBug();
+    }
+
+    if(util.isString(map)){
+      var command = map;
+      var ret = this;
+      this.filter('table').each(function(){
+        var $this = $(this);
+        var opts = $this.data('floatThead-lazy');
+        if(opts){
+          $this.floatThead(opts);
+        }
+        var obj = $this.data('floatThead-attached');
+        if(obj && util.isFunction(obj[command])){
+          var r = obj[command]();
+          if(typeof r !== 'undefined'){
+            ret = r;
+          }
+        }
+      });
+      return ret;
+    }
+    var opts = $.extend({}, $.floatThead.defaults || {}, map);
+
+    $.each(map, function(key, val){
+      if((!(key in $.floatThead.defaults)) && opts.debug){
+        debug("Used ["+key+"] key to init plugin, but that param is not an option for the plugin. Valid options are: "+ (util.keys($.floatThead.defaults)).join(', '));
+      }
+    });
+    if(opts.debug){
+      var v = $.fn.jquery.split(".");
+      if(parseInt(v[0], 10) == 1 && parseInt(v[1], 10) <= 7){
+        debug("jQuery version "+$.fn.jquery+" detected! This plugin supports 1.8 or better, or 1.7.x with jQuery UI 1.8.24 -> http://jqueryui.com/resources/download/jquery-ui-1.8.24.zip")
+      }
+    }
+
+    this.filter(':not(.'+opts.floatTableClass+')').each(function(){
+      var floatTheadId = util.uniqueId();
+      var $table = $(this);
+      if($table.data('floatThead-attached')){
+        return true; //continue the each loop
+      }
+      if(!$table.is('table')){
+        throw new Error('jQuery.floatThead must be run on a table element. ex: $("table").floatThead();');
+      }
+      canObserveMutations = opts.autoReflow && canObserveMutations; //option defaults to false!
+      var $header = $table.children('thead:first');
+      var $tbody = $table.children('tbody:first');
+      if($header.length == 0 || $tbody.length == 0){
+        $table.data('floatThead-lazy', opts);
+        $table.unbind("reflow").one('reflow', function(){
+          $table.floatThead(opts);
+        });
+        return;
+      }
+      if($table.data('floatThead-lazy')){
+        $table.unbind("reflow");
+      }
+      $table.data('floatThead-lazy', false);
+
+      var headerFloated = true;
+      var scrollingTop, scrollingBottom;
+      var scrollbarOffset = {vertical: 0, horizontal: 0};
+      var scWidth = scrollbarWidth();
+      var lastColumnCount = 0; //used by columnNum()
+      var $scrollContainer = opts.scrollContainer($table) || $([]); //guard against returned nulls
+      var locked = $scrollContainer.length > 0;
+
+      var useAbsolutePositioning = null;
+      if(typeof opts.useAbsolutePositioning !== 'undefined'){
+        opts.position = 'auto';
+        if(opts.useAbsolutePositioning){
+          opts.position = opts.useAbsolutePositioning ? 'absolute' : 'fixed';
+        }
+        debug("option 'useAbsolutePositioning' has been removed in v1.3.0, use `position:'"+opts.position+"'` instead. See docs for more info: http://mkoryak.github.io/floatThead/#options")
+      }
+      if(typeof opts.scrollingTop !== 'undefined'){
+        opts.top = opts.scrollingTop;
+        debug("option 'scrollingTop' has been renamed to 'top' in v1.3.0. See docs for more info: http://mkoryak.github.io/floatThead/#options");
+      }
+      if(typeof opts.scrollingBottom !== 'undefined'){
+          opts.bottom = opts.scrollingBottom;
+          debug("option 'scrollingBottom' has been renamed to 'bottom' in v1.3.0. See docs for more info: http://mkoryak.github.io/floatThead/#options");
+      }
+
+
+      if (opts.position == 'auto') {
+        useAbsolutePositioning = null;
+      } else if (opts.position == 'fixed') {
+        useAbsolutePositioning = false;
+      } else if (opts.position == 'absolute'){
+        useAbsolutePositioning = true;
+      } else if (opts.debug) {
+        debug('Invalid value given to "position" option, valid is "fixed", "absolute" and "auto". You passed: ', opts.position);
+      }
+
+      if(useAbsolutePositioning == null){ //defaults: locked=true, !locked=false
+        useAbsolutePositioning = locked;
+      }
+      var $caption = $table.find("caption");
+      var haveCaption = $caption.length == 1;
+      if(haveCaption){
+        var captionAlignTop = ($caption.css("caption-side") || $caption.attr("align") || "top") === "top";
+      }
+
+      var $fthGrp = $('<fthfoot style="display:table-footer-group;border-spacing:0;height:0;border-collapse:collapse;visibility:hidden"/>');
+
+      var wrappedContainer = false; //used with absolute positioning enabled. did we need to wrap the scrollContainer/table with a relative div?
+      var $wrapper = $([]); //used when absolute positioning enabled - wraps the table and the float container
+      var absoluteToFixedOnScroll = ieVersion <= 9 && !locked && useAbsolutePositioning; //on IE using absolute positioning doesn't look good with window scrolling, so we change position to fixed on scroll, and then change it back to absolute when done.
+      var $floatTable = $("<table/>");
+      var $floatColGroup = $("<colgroup/>");
+      var $tableColGroup = $table.children('colgroup:first');
+      var existingColGroup = true;
+      if($tableColGroup.length == 0){
+        $tableColGroup = $("<colgroup/>");
+        existingColGroup = false;
+      }
+      var $fthRow = $('<fthtr style="display:table-row;border-spacing:0;height:0;border-collapse:collapse"/>'); //created unstyled elements (used for sizing the table because chrome can't read <col> width)
+      var $floatContainer = $('<div style="overflow: hidden;" aria-hidden="true"></div>');
+      var floatTableHidden = false; //this happens when the table is hidden and we do magic when making it visible
+      var $newHeader = $("<thead/>");
+      var $sizerRow = $('<tr class="size-row"/>');
+      var $sizerCells = $([]);
+      var $tableCells = $([]); //used for sizing - either $sizerCells or $tableColGroup cols. $tableColGroup cols are only created in chrome for borderCollapse:collapse because of a chrome bug.
+      var $headerCells = $([]);
+      var $fthCells = $([]); //created elements
+
+      $newHeader.append($sizerRow);
+      $table.prepend($tableColGroup);
+      if(createElements){
+        $fthGrp.append($fthRow);
+        $table.append($fthGrp);
+      }
+
+      $floatTable.append($floatColGroup);
+      $floatContainer.append($floatTable);
+      if(opts.copyTableClass){
+        $floatTable.attr('class', $table.attr('class'));
+      }
+      $floatTable.attr({ //copy over some deprecated table attributes that people still like to use. Good thing people don't use colgroups...
+        'cellpadding': $table.attr('cellpadding'),
+        'cellspacing': $table.attr('cellspacing'),
+        'border': $table.attr('border')
+      });
+      var tableDisplayCss = $table.css('display');
+      $floatTable.css({
+        'borderCollapse': $table.css('borderCollapse'),
+        'border': $table.css('border'),
+        'display': tableDisplayCss
+      });
+      if(tableDisplayCss == 'none'){
+        floatTableHidden = true;
+      }
+
+      $floatTable.addClass(opts.floatTableClass).css({'margin': 0, 'border-bottom-width': 0}); //must have no margins or you won't be able to click on things under floating table
+
+      if(useAbsolutePositioning){
+        var makeRelative = function($container, alwaysWrap){
+          var positionCss = $container.css('position');
+          var relativeToScrollContainer = (positionCss == "relative" || positionCss == "absolute");
+          var $containerWrap = $container;
+          if(!relativeToScrollContainer || alwaysWrap){
+            var css = {"paddingLeft": $container.css('paddingLeft'), "paddingRight": $container.css('paddingRight')};
+            $floatContainer.css(css);
+            $containerWrap = $container.data('floatThead-containerWrap') || $container.wrap("<div class='"+opts.floatWrapperClass+"' style='position: relative; clear:both;'></div>").parent();
+            $container.data('floatThead-containerWrap', $containerWrap); //multiple tables inside one scrolling container - #242
+            wrappedContainer = true;
+          }
+          return $containerWrap;
+        };
+        if(locked){
+          $wrapper = makeRelative($scrollContainer, true);
+          $wrapper.prepend($floatContainer);
+        } else {
+          $wrapper = makeRelative($table);
+          $table.before($floatContainer);
+        }
+      } else {
+        $table.before($floatContainer);
+      }
+
+
+      $floatContainer.css({
+        position: useAbsolutePositioning ? 'absolute' : 'fixed',
+        marginTop: 0,
+        top:  useAbsolutePositioning ? 0 : 'auto',
+        zIndex: opts.zIndex
+      });
+      $floatContainer.addClass(opts.floatContainerClass);
+      updateScrollingOffsets();
+
+      var layoutFixed = {'table-layout': 'fixed'};
+      var layoutAuto = {'table-layout': $table.css('tableLayout') || 'auto'};
+      var originalTableWidth = $table[0].style.width || ""; //setting this to auto is bad: #70
+      var originalTableMinWidth = $table.css('minWidth') || "";
+
+      function eventName(name){
+        return name+'.fth-'+floatTheadId+'.floatTHead'
+      }
+
+      function setHeaderHeight(){
+        var headerHeight = 0;
+        $header.children("tr:visible").each(function(){
+          headerHeight += $(this).outerHeight(true);
+        });
+        if($table.css('border-collapse') == 'collapse') {
+          var tableBorderTopHeight = parseInt($table.css('border-top-width'), 10);
+          var cellBorderTopHeight = parseInt($table.find("thead tr:first").find(">*:first").css('border-top-width'), 10);
+          if(tableBorderTopHeight > cellBorderTopHeight) {
+            headerHeight -= (tableBorderTopHeight / 2); //id love to see some docs where this magic recipe is found..
+          }
+        }
+        $sizerRow.outerHeight(headerHeight);
+        $sizerCells.outerHeight(headerHeight);
+      }
+
+
+      function setFloatWidth(){
+        var tw = tableWidth($table, $fthCells, true);
+        var width = $scrollContainer.width() || tw;
+        var floatContainerWidth = $scrollContainer.css("overflow-y") != 'hidden' ? width - scrollbarOffset.vertical : width;
+        $floatContainer.width(floatContainerWidth);
+        if(locked){
+          var percent = 100 * tw / (floatContainerWidth);
+          $floatTable.css('width', percent+'%');
+        } else {
+          $floatTable.outerWidth(tw);
+        }
+      }
+
+      function updateScrollingOffsets(){
+        scrollingTop = (util.isFunction(opts.top) ? opts.top($table) : opts.top) || 0;
+        scrollingBottom = (util.isFunction(opts.bottom) ? opts.bottom($table) : opts.bottom) || 0;
+      }
+
+      /**
+       * get the number of columns and also rebuild resizer rows if the count is different than the last count
+       */
+      function columnNum(){
+        var count;
+        var $headerColumns = $header.find(opts.headerCellSelector);
+        if(existingColGroup){
+          count = $tableColGroup.find('col').length;
+        } else {
+          count = 0;
+          $headerColumns.each(function () {
+              count += parseInt(($(this).attr('colspan') || 1), 10);
+          });
+        }
+        if(count != lastColumnCount){
+          lastColumnCount = count;
+          var cells = [], cols = [], psuedo = [], content;
+          for(var x = 0; x < count; x++){
+            if (opts.enableAria && (content = $headerColumns.eq(x).text()) ) {
+              cells.push('<th scope="col" class="floatThead-col">' + content + '</th>');
+            } else {
+              cells.push('<th class="floatThead-col"/>');
+            }
+            cols.push('<col/>');
+            psuedo.push("<fthtd style='display:table-cell;height:0;width:auto;'/>");
+          }
+
+          cols = cols.join('');
+          cells = cells.join('');
+
+          if(createElements){
+            psuedo = psuedo.join('');
+            $fthRow.html(psuedo);
+            $fthCells = $fthRow.find('fthtd');
+          }
+
+          $sizerRow.html(cells);
+          $sizerCells = $sizerRow.find("th");
+          if(!existingColGroup){
+            $tableColGroup.html(cols);
+          }
+          $tableCells = $tableColGroup.find('col');
+          $floatColGroup.html(cols);
+          $headerCells = $floatColGroup.find("col");
+
+        }
+        return count;
+      }
+
+      function refloat(){ //make the thing float
+        if(!headerFloated){
+          headerFloated = true;
+          if(useAbsolutePositioning){ //#53, #56
+            var tw = tableWidth($table, $fthCells, true);
+            var wrapperWidth = $wrapper.width();
+            if(tw > wrapperWidth){
+              $table.css('minWidth', tw);
+            }
+          }
+          $table.css(layoutFixed);
+          $floatTable.css(layoutFixed);
+          $floatTable.append($header); //append because colgroup must go first in chrome
+          $tbody.before($newHeader);
+          setHeaderHeight();
+        }
+      }
+      function unfloat(){ //put the header back into the table
+        if(headerFloated){
+          headerFloated = false;
+          if(useAbsolutePositioning){ //#53, #56
+            $table.width(originalTableWidth);
+          }
+          $newHeader.detach();
+          $table.prepend($header);
+          $table.css(layoutAuto);
+          $floatTable.css(layoutAuto);
+          $table.css('minWidth', originalTableMinWidth); //this looks weird, but it's not a bug. Think about it!!
+          $table.css('minWidth', tableWidth($table, $fthCells)); //#121
+        }
+      }
+      var isHeaderFloatingLogical = false; //for the purpose of this event, the header is/isnt floating, even though the element
+                                           //might be in some other state. this is what the header looks like to the user
+      function triggerFloatEvent(isFloating){
+        if(isHeaderFloatingLogical != isFloating){
+          isHeaderFloatingLogical = isFloating;
+          $table.triggerHandler("floatThead", [isFloating, $floatContainer])
+        }
+      }
+      function changePositioning(isAbsolute){
+        if(useAbsolutePositioning != isAbsolute){
+          useAbsolutePositioning = isAbsolute;
+          $floatContainer.css({
+            position: useAbsolutePositioning ? 'absolute' : 'fixed'
+          });
+        }
+      }
+      function getSizingRow($table, $cols, $fthCells, ieVersion){
+        if(createElements){
+          return $fthCells;
+        } else if(ieVersion) {
+          return opts.getSizingRow($table, $cols, $fthCells);
+        } else {
+          return $cols;
+        }
+      }
+
+      /**
+       * returns a function that updates the floating header's cell widths.
+       * @return {Function}
+       */
+      function reflow(){
+        var i;
+        var numCols = columnNum(); //if the tables columns changed dynamically since last time (datatables), rebuild the sizer rows and get a new count
+
+        return function(){
+          $tableCells = $tableColGroup.find('col');
+          var $rowCells = getSizingRow($table, $tableCells, $fthCells, ieVersion);
+
+          if($rowCells.length == numCols && numCols > 0){
+            if(!existingColGroup){
+              for(i=0; i < numCols; i++){
+                $tableCells.eq(i).css('width', '');
+              }
+            }
+            unfloat();
+            var widths = [];
+            for(i=0; i < numCols; i++){
+              widths[i] = getOffsetWidth($rowCells.get(i));
+            }
+            for(i=0; i < numCols; i++){
+              $headerCells.eq(i).width(widths[i]);
+              $tableCells.eq(i).width(widths[i]);
+            }
+            refloat();
+          } else {
+            $floatTable.append($header);
+            $table.css(layoutAuto);
+            $floatTable.css(layoutAuto);
+            setHeaderHeight();
+          }
+          $table.triggerHandler("reflowed", [$floatContainer]);
+        };
+      }
+
+      function floatContainerBorderWidth(side){
+        var border = $scrollContainer.css("border-"+side+"-width");
+        var w = 0;
+        if (border && ~border.indexOf('px')) {
+          w = parseInt(border, 10);
+        }
+        return w;
+      }
+      /**
+       * first performs initial calculations that we expect to not change when the table, window, or scrolling container are scrolled.
+       * returns a function that calculates the floating container's top and left coords. takes into account if we are using page scrolling or inner scrolling
+       * @return {Function}
+       */
+      function calculateFloatContainerPosFn(){
+        var scrollingContainerTop = $scrollContainer.scrollTop();
+
+        //this floatEnd calc was moved out of the returned function because we assume the table height doesn't change (otherwise we must reinit by calling calculateFloatContainerPosFn)
+        var floatEnd;
+        var tableContainerGap = 0;
+        var captionHeight = haveCaption ? $caption.outerHeight(true) : 0;
+        var captionScrollOffset = captionAlignTop ? captionHeight : -captionHeight;
+
+        var floatContainerHeight = $floatContainer.height();
+        var tableOffset = $table.offset();
+        var tableLeftGap = 0; //can be caused by border on container (only in locked mode)
+        var tableTopGap = 0;
+        if(locked){
+          var containerOffset = $scrollContainer.offset();
+          tableContainerGap = tableOffset.top - containerOffset.top + scrollingContainerTop;
+          if(haveCaption && captionAlignTop){
+            tableContainerGap += captionHeight;
+          }
+          tableLeftGap = floatContainerBorderWidth('left');
+          tableTopGap = floatContainerBorderWidth('top');
+          tableContainerGap -= tableTopGap;
+        } else {
+          floatEnd = tableOffset.top - scrollingTop - floatContainerHeight + scrollingBottom + scrollbarOffset.horizontal;
+        }
+        var windowTop = $window.scrollTop();
+        var windowLeft = $window.scrollLeft();
+        var scrollContainerLeft =  $scrollContainer.scrollLeft();
+
+        return function(eventType){
+          var isTableHidden = $table[0].offsetWidth <= 0 && $table[0].offsetHeight <= 0;
+          if(!isTableHidden && floatTableHidden) {
+            floatTableHidden = false;
+            setTimeout(function(){
+              $table.triggerHandler("reflow");
+            }, 1);
+            return null;
+          }
+          if(isTableHidden){ //it's hidden
+            floatTableHidden = true;
+            if(!useAbsolutePositioning){
+              return null;
+            }
+          }
+
+          if(eventType == 'windowScroll'){
+            windowTop = $window.scrollTop();
+            windowLeft = $window.scrollLeft();
+          } else if(eventType == 'containerScroll'){
+            scrollingContainerTop = $scrollContainer.scrollTop();
+            scrollContainerLeft =  $scrollContainer.scrollLeft();
+          } else if(eventType != 'init') {
+            windowTop = $window.scrollTop();
+            windowLeft = $window.scrollLeft();
+            scrollingContainerTop = $scrollContainer.scrollTop();
+            scrollContainerLeft =  $scrollContainer.scrollLeft();
+          }
+          if(isWebkit && (windowTop < 0 || windowLeft < 0)){ //chrome overscroll effect at the top of the page - breaks fixed positioned floated headers
+            return;
+          }
+
+          if(absoluteToFixedOnScroll){
+            if(eventType == 'windowScrollDone'){
+              changePositioning(true); //change to absolute
+            } else {
+              changePositioning(false); //change to fixed
+            }
+          } else if(eventType == 'windowScrollDone'){
+            return null; //event is fired when they stop scrolling. ignore it if not 'absoluteToFixedOnScroll'
+          }
+
+          tableOffset = $table.offset();
+          if(haveCaption && captionAlignTop){
+            tableOffset.top += captionHeight;
+          }
+          var top, left;
+          var tableHeight = $table.outerHeight();
+
+          if(locked && useAbsolutePositioning){ //inner scrolling, absolute positioning
+            if (tableContainerGap >= scrollingContainerTop) {
+              var gap = tableContainerGap - scrollingContainerTop + tableTopGap;
+              top = gap > 0 ? gap : 0;
+              triggerFloatEvent(false);
+            } else {
+              top = wrappedContainer ? tableTopGap : scrollingContainerTop;
+              //headers stop at the top of the viewport
+              triggerFloatEvent(true);
+            }
+            left = tableLeftGap;
+          } else if(!locked && useAbsolutePositioning) { //window scrolling, absolute positioning
+            if(windowTop > floatEnd + tableHeight + captionScrollOffset){
+              top = tableHeight - floatContainerHeight + captionScrollOffset; //scrolled past table
+            } else if (tableOffset.top >= windowTop + scrollingTop) {
+              top = 0; //scrolling to table
+              unfloat();
+              triggerFloatEvent(false);
+            } else {
+              top = scrollingTop + windowTop - tableOffset.top + tableContainerGap + (captionAlignTop ? captionHeight : 0);
+              refloat(); //scrolling within table. header floated
+              triggerFloatEvent(true);
+            }
+            left =  0;
+          } else if(locked && !useAbsolutePositioning){ //inner scrolling, fixed positioning
+            if (tableContainerGap > scrollingContainerTop || scrollingContainerTop - tableContainerGap > tableHeight) {
+              top = tableOffset.top - windowTop;
+              unfloat();
+              triggerFloatEvent(false);
+            } else {
+              top = tableOffset.top + scrollingContainerTop  - windowTop - tableContainerGap;
+              refloat();
+              triggerFloatEvent(true);
+              //headers stop at the top of the viewport
+            }
+            left = tableOffset.left + scrollContainerLeft - windowLeft;
+          } else if(!locked && !useAbsolutePositioning) { //window scrolling, fixed positioning
+            if(windowTop > floatEnd + tableHeight + captionScrollOffset){
+              top = tableHeight + scrollingTop - windowTop + floatEnd + captionScrollOffset;
+              //scrolled past the bottom of the table
+            } else if (tableOffset.top > windowTop + scrollingTop) {
+              top = tableOffset.top - windowTop;
+              refloat();
+              triggerFloatEvent(false); //this is a weird case, the header never gets unfloated and i have no no way to know
+              //scrolled past the top of the table
+            } else {
+              //scrolling within the table
+              top = scrollingTop;
+              triggerFloatEvent(true);
+            }
+            left = tableOffset.left - windowLeft;
+          }
+          return {top: top, left: left};
+        };
+      }
+      /**
+       * returns a function that caches old floating container position and only updates css when the position changes
+       * @return {Function}
+       */
+      function repositionFloatContainerFn(){
+        var oldTop = null;
+        var oldLeft = null;
+        var oldScrollLeft = null;
+        return function(pos, setWidth, setHeight){
+          if(pos != null && (oldTop != pos.top || oldLeft != pos.left)){
+            $floatContainer.css({
+              top: pos.top,
+              left: pos.left
+            });
+            oldTop = pos.top;
+            oldLeft = pos.left;
+          }
+          if(setWidth){
+            setFloatWidth();
+          }
+          if(setHeight){
+            setHeaderHeight();
+          }
+          var scrollLeft = $scrollContainer.scrollLeft();
+          if(!useAbsolutePositioning || oldScrollLeft != scrollLeft){
+            $floatContainer.scrollLeft(scrollLeft);
+            oldScrollLeft = scrollLeft;
+          }
+        }
+      }
+
+      /**
+       * checks if THIS table has scrollbars, and finds their widths
+       */
+      function calculateScrollBarSize(){ //this should happen after the floating table has been positioned
+        if($scrollContainer.length){
+          if($scrollContainer.data().perfectScrollbar){
+            scrollbarOffset = {horizontal:0, vertical:0};
+          } else {
+            var sw = $scrollContainer.width(), sh = $scrollContainer.height(), th = $table.height(), tw = tableWidth($table, $fthCells);
+            var offseth = sw < tw ? scWidth : 0;
+            var offsetv = sh < th ? scWidth : 0;
+            scrollbarOffset.horizontal = sw - offsetv < tw ? scWidth : 0;
+            scrollbarOffset.vertical = sh - offseth < th ? scWidth : 0;
+          }
+        }
+      }
+      //finish up. create all calculation functions and bind them to events
+      calculateScrollBarSize();
+
+      var flow;
+
+      var ensureReflow = function(){
+        flow = reflow();
+        flow();
+      };
+
+      ensureReflow();
+
+      var calculateFloatContainerPos = calculateFloatContainerPosFn();
+      var repositionFloatContainer = repositionFloatContainerFn();
+
+      repositionFloatContainer(calculateFloatContainerPos('init'), true); //this must come after reflow because reflow changes scrollLeft back to 0 when it rips out the thead
+
+      var windowScrollDoneEvent = util.debounce(function(){
+        repositionFloatContainer(calculateFloatContainerPos('windowScrollDone'), false);
+      }, 1);
+
+      var windowScrollEvent = function(){
+        repositionFloatContainer(calculateFloatContainerPos('windowScroll'), false);
+        if(absoluteToFixedOnScroll){
+          windowScrollDoneEvent();
+        }
+      };
+      var containerScrollEvent = function(){
+        repositionFloatContainer(calculateFloatContainerPos('containerScroll'), false);
+      };
+
+
+      var windowResizeEvent = function(){
+        if($table.is(":hidden")){
+          return;
+        }
+        updateScrollingOffsets();
+        calculateScrollBarSize();
+        ensureReflow();
+        calculateFloatContainerPos = calculateFloatContainerPosFn();
+        repositionFloatContainer = repositionFloatContainerFn();
+        repositionFloatContainer(calculateFloatContainerPos('resize'), true, true);
+      };
+      var reflowEvent = util.debounce(function(){
+        if($table.is(":hidden")){
+          return;
+        }
+        calculateScrollBarSize();
+        updateScrollingOffsets();
+        ensureReflow();
+        calculateFloatContainerPos = calculateFloatContainerPosFn();
+        repositionFloatContainer(calculateFloatContainerPos('reflow'), true);
+      }, 1);
+      if(locked){ //internal scrolling
+        if(useAbsolutePositioning){
+          $scrollContainer.on(eventName('scroll'), containerScrollEvent);
+        } else {
+          $scrollContainer.on(eventName('scroll'), containerScrollEvent);
+          $window.on(eventName('scroll'), windowScrollEvent);
+        }
+      } else { //window scrolling
+        $window.on(eventName('scroll'), windowScrollEvent);
+      }
+
+      $window.on(eventName('load'), reflowEvent); //for tables with images
+
+      windowResize(eventName('resize'), windowResizeEvent);
+      $table.on('reflow', reflowEvent);
+      if(isDatatable($table)){
+        $table
+          .on('filter', reflowEvent)
+          .on('sort',   reflowEvent)
+          .on('page',   reflowEvent);
+      }
+
+      $window.on(eventName('shown.bs.tab'), reflowEvent); // people cant seem to figure out how to use this plugin with bs3 tabs... so this :P
+      $window.on(eventName('tabsactivate'), reflowEvent); // same thing for jqueryui
+
+
+      if (canObserveMutations) {
+        var mutationElement = null;
+        if(util.isFunction(opts.autoReflow)){
+          mutationElement = opts.autoReflow($table, $scrollContainer)
+        }
+        if(!mutationElement) {
+          mutationElement = $scrollContainer.length ? $scrollContainer[0] : $table[0]
+        }
+        mObs = new MutationObserver(function(e){
+          var wasTableRelated = function(nodes){
+            return nodes && nodes[0] && (nodes[0].nodeName == "THEAD" || nodes[0].nodeName == "TD"|| nodes[0].nodeName == "TH");
+          };
+          for(var i=0; i < e.length; i++){
+            if(!(wasTableRelated(e[i].addedNodes) || wasTableRelated(e[i].removedNodes))){
+              reflowEvent();
+              break;
+            }
+          }
+        });
+        mObs.observe(mutationElement, {
+            childList: true,
+            subtree: true
+        });
+      }
+
+      //attach some useful functions to the table.
+      $table.data('floatThead-attached', {
+        destroy: function(){
+          var ns = '.fth-'+floatTheadId;
+          unfloat();
+          $table.css(layoutAuto);
+          $tableColGroup.remove();
+          createElements && $fthGrp.remove();
+          if($newHeader.parent().length){ //only if it's in the DOM
+            $newHeader.replaceWith($header);
+          }
+          if(canObserveMutations){
+            mObs.disconnect();
+            mObs = null;
+          }
+          $table.off('reflow reflowed');
+          $scrollContainer.off(ns);
+          if (wrappedContainer) {
+            if ($scrollContainer.length) {
+              $scrollContainer.unwrap();
+            }
+            else {
+              $table.unwrap();
+            }
+          }
+          if(locked){
+            $scrollContainer.data('floatThead-containerWrap', false);
+          } else {
+            $table.data('floatThead-containerWrap', false);
+          }
+          $table.css('minWidth', originalTableMinWidth);
+          $floatContainer.remove();
+          $table.data('floatThead-attached', false);
+          $window.off(ns);
+        },
+        reflow: function(){
+          reflowEvent();
+        },
+        setHeaderHeight: function(){
+          setHeaderHeight();
+        },
+        getFloatContainer: function(){
+          return $floatContainer;
+        },
+        getRowGroups: function(){
+          if(headerFloated){
+            return $floatContainer.find('>table>thead').add($table.children("tbody,tfoot"));
+          } else {
+            return $table.children("thead,tbody,tfoot");
+          }
+        }
+      });
+    });
+    return this;
+  };
+})(jQuery);
+
+/* jQuery.floatThead.utils - http://mkoryak.github.io/floatThead/ - Copyright (c) 2012 - 2014 Misha Koryak
+ * License: MIT
+ *
+ * This file is required if you do not use underscore in your project and you want to use floatThead.
+ * It contains functions from underscore that the plugin uses.
+ *
+ * YOU DON'T NEED TO INCLUDE THIS IF YOU ALREADY INCLUDE UNDERSCORE!
+ *
+ */
+
+(function($){
+
+  $.floatThead = $.floatThead || {};
+
+  $.floatThead._  = window._ || (function(){
+    var that = {};
+    var hasOwnProperty = Object.prototype.hasOwnProperty, isThings = ['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'];
+    that.has = function(obj, key) {
+      return hasOwnProperty.call(obj, key);
+    };
+    that.keys = function(obj) {
+      if (obj !== Object(obj)) throw new TypeError('Invalid object');
+      var keys = [];
+      for (var key in obj) if (that.has(obj, key)) keys.push(key);
+      return keys;
+    };
+    var idCounter = 0;
+    that.uniqueId = function(prefix) {
+      var id = ++idCounter + '';
+      return prefix ? prefix + id : id;
+    };
+    $.each(isThings, function(){
+      var name = this;
+      that['is' + name] = function(obj) {
+        return Object.prototype.toString.call(obj) == '[object ' + name + ']';
+      };
+    });
+    that.debounce = function(func, wait, immediate) {
+      var timeout, args, context, timestamp, result;
+      return function() {
+        context = this;
+        args = arguments;
+        timestamp = new Date();
+        var later = function() {
+          var last = (new Date()) - timestamp;
+          if (last < wait) {
+            timeout = setTimeout(later, wait - last);
+          } else {
+            timeout = null;
+            if (!immediate) result = func.apply(context, args);
+          }
+        };
+        var callNow = immediate && !timeout;
+        if (!timeout) {
+          timeout = setTimeout(later, wait);
+        }
+        if (callNow) result = func.apply(context, args);
+        return result;
+      };
+    };
+    return that;
+  })();
+})(jQuery);
+
+export default undefined;
\ No newline at end of file
diff --git a/package.json b/package.json
index df446ff00c4d4b8c771fcbdceee169911de38e0a..bef02ff148d8e986135115f659a812abf0e2f16e 100644
--- a/package.json
+++ b/package.json
@@ -12,11 +12,34 @@
   "author": "TOPS Lab",
   "license": "ISC",
   "devDependencies": {
+    "angular2": "^2.0.0-beta.17",
+    "babel-cli": "^6.8.0",
     "babel-preset-es2015": "^6.6.0",
-    "openlayers": "^3.15.1"
+    "babelify": "^7.3.0",
+    "browserify": "^13.0.0",
+    "es6-mixins": "^1.0.2",
+    "es6-shim": "^0.35.0",
+    "gulp": "^3.9.1",
+    "gulp-babel": "^6.1.2",
+    "gulp-cssmin": "^0.1.7",
+    "gulp-less": "^3.0.5",
+    "gulp-minify": "0.0.11",
+    "gulp-rename": "^1.2.2",
+    "gulp-sourcemaps": "^2.0.0-alpha",
+    "jquery": "^2.2.3",
+    "jquery-ui": "^1.10.5",
+    "openlayers": "^3.15.1",
+    "reflect-metadata": "^0.1.2",
+    "rxjs": "^5.0.0-beta.6",
+    "vinyl-buffer": "^1.0.0",
+    "vinyl-source-stream": "^1.1.0",
+    "zone.js": "^0.6.12"
   },
   "repository": {
     "type": "git",
     "url": "https://github.com/glennvorhes/webmapsjs.git"
+  },
+  "dependencies": {
+    "angular2": "^2.0.0-beta.17"
   }
 }
diff --git a/projects/glrtoc/appConfig.js b/projects/glrtoc/appConfig.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc44356dede6665076d4b7e70bd8e8694e33fd4c
--- /dev/null
+++ b/projects/glrtoc/appConfig.js
@@ -0,0 +1,150 @@
+/**
+ * Created by gavorhes on 12/8/2015.
+ */
+
+import mapPopup from '../../src/olHelpers/mapPopup';
+import $ from '../../src/jquery';
+let homeTabId = 'home-tab';
+let coordinationTabId = 'coordination-tab';
+let operationsTabId = 'operations-tab';
+
+let operationsStaticPanelId = 'operations-static';
+let operationsAnimatedPanelId = 'operations-animated';
+
+class AppConfig {
+    /**
+     * app configuration object
+     */
+    constructor() {
+        this.$opsAccordion = $("#" + operationsTabId);
+        this.debug = true;
+        this.map = undefined;
+        this._coordinationLayer = undefined;
+        this.lyrIds = [];
+        this.lyrLookup = {};
+        this.lyrArray = [];
+        this.mediaControl = undefined;
+        this.animationLayers = [];
+        this.coordinationLayerId = 'coorindation-layer';
+
+        /**
+         *
+         * @type {Array<LayerBase>}
+         */
+        this.operationsLayersStatic = [];
+
+        /**
+         *
+         * @type {Array<LayerBase>}
+         */
+        this.operationsLayersAnimated = [];
+
+        this.currentOperationsLayers = this.operationsLayersStatic;
+    }
+
+    init() {
+        for (let l of this.operationsLayersStatic) {
+            this.map.removeLayer(l['olLayer']);
+        }
+
+        for (let l of this.operationsLayersAnimated) {
+            this.map.removeLayer(l['olLayer']);
+        }
+
+        this.currentTabId = homeTabId;
+    }
+
+    /**
+     * Add the layer to the config object
+     * @param {LayerBase|*} lyr - base layer
+     */
+    _addLayer(lyr) {
+        this.lyrIds.push(lyr.id);
+        this.lyrLookup[lyr.id] = lyr;
+        this.lyrArray.push(lyr);
+    }
+
+    get coordinationLayer() {
+        return this._coordinationLayer;
+    }
+
+    set coordinationLayer(coordLayer) {
+        this._coordinationLayer = coordLayer;
+        this._addLayer(coordLayer);
+    }
+
+    addOperationsLayerStatic(lyr) {
+        this.operationsLayersStatic.push(lyr);
+        this._addLayer(lyr);
+    }
+
+    addOperationsLayerAnimate(lyr) {
+        this.operationsLayersAnimated.push(lyr);
+        this._addLayer(lyr);
+    }
+
+    set currentTabId(tabId) {
+        mapPopup.closePopup();
+        this.mediaControl.stopPlaying();
+        if (tabId == operationsTabId) {
+            for (let l of this.currentOperationsLayers) {
+                this.map.addLayer(l['olLayer']);
+            }
+        } else {
+            for (let l of this.currentOperationsLayers) {
+                this.map.removeLayer(l['olLayer']);
+            }
+        }
+
+        switch (tabId) {
+            case homeTabId:
+                this.coordinationLayer.visible = false;
+                break;
+            case coordinationTabId:
+                this.coordinationLayer.visible = true;
+                break;
+            case operationsTabId:
+                this.coordinationLayer.visible = false;
+                this.$opsAccordion.accordion("refresh");
+                break;
+            default:
+                throw tabId + ' tab id not found'
+        }
+    }
+
+    set currentOperationsPanelId(panelId) {
+        mapPopup.closePopup();
+        this.mediaControl.stopPlaying();
+
+        for (let l of this.currentOperationsLayers) {
+            this.map.removeLayer(l['olLayer']);
+        }
+
+        switch (panelId) {
+            case operationsStaticPanelId:
+                this.currentOperationsLayers = this.operationsLayersStatic;
+                break;
+
+            case operationsAnimatedPanelId:
+                this.currentOperationsLayers = this.operationsLayersAnimated;
+                break;
+            default:
+                throw panelId + ' panel not found';
+        }
+        for (let l of this.currentOperationsLayers) {
+            this.map.addLayer(l['olLayer']);
+        }
+
+    }
+
+    ///**
+    // * trick to trigger the map move event
+    // */
+    //forceRefresh() {
+    //    if (this.map) {
+    //        this.map.getView().setZoom(this.map.getView().getZoom());
+    //    }
+    //}
+}
+
+export default new AppConfig();
diff --git a/projects/glrtoc/layerPopups.js b/projects/glrtoc/layerPopups.js
new file mode 100644
index 0000000000000000000000000000000000000000..4362c1cf8e9be7d5a1d3cd3e7f3f5601004363a8
--- /dev/null
+++ b/projects/glrtoc/layerPopups.js
@@ -0,0 +1,37 @@
+/**
+ * Created by gavorhes on 12/7/2015.
+ */
+
+
+/**
+ * coordination layer popup config
+ * @param {object} props
+ * @returns {string}
+ */
+export function coordination(props) {
+    "use strict";
+    return `<iframe src="${window.location.href.replace('#', '') +
+    '/../toc?toc=' + props['toc']}" width="368" height="292"></iframe>`;
+}
+
+/**
+ * wrs layer popup config
+ * @param {object} props
+ * @returns {string}
+ */
+export function wrs(props) {
+    "use strict";
+    return `<p style="text-align: center">${props['WMS_INFO'].replace(/\n/g, '<br>')}</p>`
+}
+
+export function specialEventWorkZone(props) {
+    "use strict";
+    let startDate = new Date(props['EventStartDate']);
+    let endDate = new Date(props['EventEndDate']);
+    let theContent = '<p style="text-align:center">';
+    theContent += props['EventDescription'] + '<br>';
+    theContent += `${props['HwyName']} ${props['Location']}` + '<br>';
+    theContent += `${startDate.toLocaleDateString()}&nbsp;-&nbsp;${endDate.toLocaleDateString()}`;
+    theContent += '</p>';
+    return theContent;
+}
diff --git a/projects/glrtoc/layerStyles.js b/projects/glrtoc/layerStyles.js
new file mode 100644
index 0000000000000000000000000000000000000000..7b2eaed96ac0a82e79fa30377741b8aea787e63d
--- /dev/null
+++ b/projects/glrtoc/layerStyles.js
@@ -0,0 +1,39 @@
+/**
+ * Created by gavorhes on 12/3/2015.
+ */
+import ol from '../../src/custom-ol';
+
+export const workZoneAndEventStyle = new ol.style.Style({
+    stroke: new ol.style.Stroke({
+        color: 'red',
+        width: 7
+    }),
+    image: new ol.style.Circle({
+        radius: 8,
+        fill: new ol.style.Fill({
+            color: 'magenta'
+        }),
+        stroke: new ol.style.Stroke({
+            color: 'magenta'
+        })
+    })
+});
+
+export function wrsStyle(feature, resolution) {
+    let colorString = feature.getProperties()['COLOR'];
+
+    let c;
+    if (typeof colorString === 'string') {
+        c = colorString.split(' ');
+    } else {
+        c = [125, 125, 125];
+    }
+
+    return [new ol.style.Style({
+        stroke: new ol.style.Stroke({
+            color: `rgba(${c[0]},${c[1]},${c[2]},0)`,
+            width: 5
+        })
+    })];
+}
+
diff --git a/projects/glrtoc/legendTest.js b/projects/glrtoc/legendTest.js
new file mode 100644
index 0000000000000000000000000000000000000000..71ecba4373efc13af700eaad09ceed4e44dd8941
--- /dev/null
+++ b/projects/glrtoc/legendTest.js
@@ -0,0 +1,129 @@
+/**
+ * Created by gavorhes on 1/4/2016.
+ */
+import quickMap from '../../src/olHelpers/quickMap';
+import LayerItsInventory from '../../src/layers/LayerItsInventory';
+import LayerBaseVectorEsri from '../../src/layers/LayerBaseVectorEsri';
+import LayerEsriMapServer from '../../src/layers/LayerEsriMapServer';
+import LayerBaseXyzTile from '../../src/layers/LayerBaseXyzTile';
+import LayerLegend from '../../src/collections/LayerLegend';
+import ItsLayerCollection from '../../src/collections/ItsLayerCollection';
+import mapPopup from '../../src/olHelpers/mapPopup';
+
+
+(function () {
+    "use strict";
+    let map = quickMap({center: {x: -9907589, y: 5232317}, zoom: 12, minZoom: 3, maxZoom: 19});
+
+    let itsLayerCollection = new ItsLayerCollection(map);
+
+    let oakRidgeCams = new LayerEsriMapServer(
+        `http://itsdpro.ornl.gov/arcgis/rest/services/ITSPublic/cameras33/MapServer`,
+        {
+            id: 'cameras33',
+            name: 'Oak Cameras',
+            visible: true,
+            minZoom: 7,
+            zIndex: 20,
+            addPopup: true
+        }
+    );
+
+    map.addLayer(oakRidgeCams.olLayer);
+
+    glob.cat = (bird) => bird * 2;
+
+
+
+
+    glob.map = map;
+
+    let itsLayer = new LayerItsInventory({itsType: 'CCTV', itsIcon: 'cctv.png', name: 'Camera', visible: false});
+
+    map.addLayer(itsLayer.olLayer);
+
+    let legend = new LayerLegend(
+        [
+            {groupName: 'its layers', collapse: true, items: itsLayerCollection.layers},
+            itsLayer, oakRidgeCams
+        ],
+        'legend-container', {});
+
+
+
+    //let legend = new LayerLegend(
+    //    [
+    //        {groupName: 'its layers', expand: true, items: itsLayerCollection.layers},
+    //        itsLayer, testEsri, msg, tower, xyzTile,
+    //        {groupName: 'new group', expand: true, items: [rwis, coord, seg, esriMap]}
+    //    ],
+    //    'legend-container', {});
+
+    //let legend = new LayerLegend(
+    //    [
+    //        itsLayerCollection.layers[1]
+    //    ],
+    //    'legend-container', {});
+
+    map.getView().setZoom(13);
+
+    return;
+
+    let testEsri = new LayerBaseVectorEsri('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer/3',
+        {name: 'test esri', useEsriStyle: true, visible: false}
+    );
+    map.addLayer(testEsri.olLayer);
+
+    let msg = new LayerBaseVectorEsri('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer/8',
+        {name: 'message', useEsriStyle: true, visible: false}
+    );
+    map.addLayer(msg.olLayer);
+
+    let tower = new LayerBaseVectorEsri('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer/0',
+        {name: 'tower', useEsriStyle: true, visible: false}
+    );
+    map.addLayer(tower.olLayer);
+
+
+    let rwis = new LayerBaseVectorEsri('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer/1',
+        {name: 'rwis', useEsriStyle: true, visible: false}
+    );
+    map.addLayer(rwis.olLayer);
+
+    let coord = new LayerBaseVectorEsri('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer/10',
+        {name: 'coord', useEsriStyle: true, minZoom: 6, visible: false, collapseLegend: true}
+    );
+    map.addLayer(coord.olLayer);
+
+    //let coord2 = new LayerBaseVectorEsri('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer/11',
+    //    {name: 'coord2', useEsriStyle: true}
+    //);
+    //map.addLayer(coord.olLayer);
+
+    let seg = new LayerBaseVectorEsri('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer/9',
+        {name: 'seg', useEsriStyle: true, visible: false}
+    );
+    map.addLayer(seg.olLayer);
+
+    let esriMap = new LayerEsriMapServer('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/LegendTest/MapServer', {
+        name: 'esri map',
+        legendCollapse: true,
+        legendCheckbox: false,
+        visible: false
+    });
+
+    map.addLayer(esriMap.olLayer);
+
+    let xyzTile = new LayerBaseXyzTile('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/NPMRDS/npmrds_tile/MapServer/tile/{z}/{y}/{x}',
+        {minZoom: 4, maxZoom: 11, name: "NPMRDS", useEsriStyle: true, collapseLegend: true});
+
+    //let esriMapServer = new LayerEsriMapServer('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/NPMRDS/npmrds_dynamic/MapServer',
+    //    {minZoom: 12, maxZoom: 18});
+
+    map.addLayer(xyzTile.olLayer);
+    //map.addLayer(esriMapServer.olLayer);
+
+    glob.itsCollection = itsLayerCollection;
+
+
+})();
diff --git a/projects/glrtoc/main.js b/projects/glrtoc/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..2613e08113cbadf982ee28c1ffad45dc0e544c62
--- /dev/null
+++ b/projects/glrtoc/main.js
@@ -0,0 +1,319 @@
+/**
+ * Created by gavorhes on 12/2/2015.
+ */
+
+// <editor-fold desc="imports">
+
+import quickMap from '../../src/olHelpers/quickMap';
+import LayerBase from '../../src/layers/LayerBase';
+import LayerBaseVectorGeoJson from '../../src/layers/LayerBaseVectorGeoJson';
+import LayerBaseVectorEsri from  '../../src/layers/LayerBaseVectorEsri';
+import LayerRealEarthTile from  '../../src/layers/LayerRealEarthTile';
+import LayerRealEarthVector from '../../src/layers/LayerRealEarthVector';
+import LayerEsriMapServer from '../../src/layers/LayerEsriMapServer';
+import * as layerStyles from './layerStyles';
+import * as layerPopups from './layerPopups';
+import mapPopup from '../../src/olHelpers/mapPopup';
+import mapMove from '../../src/olHelpers/mapMove';
+import * as dteConvert from '../../src/util/dateConvert';
+import * as colors from '../../src/util/colors';
+import appConfig from './appConfig';
+import * as uiSetup from './mainUi';
+import LayerLegend from '../../src/collections/LayerLegend';
+// </editor-fold>
+
+(function () {
+    "use strict";
+    uiSetup.uiInit();
+
+    let legendItemsStatic = [];
+    let legendItemsAnimated = [];
+    let oakRidgeGroup = {groupName: 'Oak Ridge ITS', collapse: false, addCheck: true, items: []};
+    let realEarthGroup = {groupName: 'Real Earth', collapse: false, items: []};
+    let workZoneEventGroup = {groupName: 'Work Zones / Events', collapse: false, items: []};
+    legendItemsStatic.push(oakRidgeGroup);
+    legendItemsStatic.push(realEarthGroup);
+    legendItemsStatic.push(workZoneEventGroup);
+
+    function animationLoadCallback() {
+        appConfig.animationLayers.push(this);
+    }
+
+    let map = quickMap({center: {x: -85.413, y: 43.29320}, zoom: 6, minZoom: 3, maxZoom: 19});
+    appConfig.map = map;
+
+    // <editor-fold desc="Coordination Layer">
+    let coordinationLayer = new LayerBaseVectorEsri(
+        'http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/GlrtocCoordination/MapServer/0',
+        {
+            id: appConfig.coordinationLayerId,
+            visible: true,
+            autoLoad: true,
+            name: 'Coordination',
+            useEsriStyle: true
+        }
+    );
+
+    map.addLayer(coordinationLayer.olLayer);
+    mapPopup.addVectorPopup(coordinationLayer, layerPopups.coordination);
+    appConfig.coordinationLayer = coordinationLayer;
+
+
+    //appConfig.addLayer(coordinationLayer);
+    // </editor-fold>
+
+    // <editor-fold desc="Oak Ridge Layers ">
+    let oakRidgeLayers = [
+        ['Cameras', 'cameras33'],
+        ['HAR', 'HAR33'],
+        ['DMS', 'MessageSigns33'],
+        //['State Summary', 'statesummary'],
+        ['Traffic Control', 'TrafficControl33'],
+        ['Traffic Detection', 'TrafficDetection33'],
+        ['Weather', 'Weather33']
+    ];
+
+
+    for (let i = 0; i < oakRidgeLayers.length; i++) {
+        let oakRidgeLayer = new LayerEsriMapServer(
+            `http://itsdpro.ornl.gov/arcgis/rest/services/ITSPublic/${oakRidgeLayers[i][1]}/MapServer`,
+            {
+                id: oakRidgeLayers[i][1],
+                name: oakRidgeLayers[i][0],
+                visible: false,
+                minZoom: 7,
+                zIndex: 20,
+                addPopup: true
+            }
+        );
+        oakRidgeGroup.items.push(oakRidgeLayer);
+        map.addLayer(oakRidgeLayer.olLayer);
+        appConfig.addOperationsLayerStatic(oakRidgeLayer);
+    }
+    // </editor-fold>
+
+    // <editor-fold desc="WRS Segments">
+
+    let wrsConfigVector = {
+        products: 'ROADS',
+        style: layerStyles.wrsStyle,
+        animate: false,
+        id: 'wrs-segments-vector-static',
+        name: 'Winter Roads',
+        minZoom: 5,
+        maxZoom: 13,
+        zIndex: 3
+    };
+
+    let wrsSegmentLayerVectorStatic = new LayerRealEarthVector(wrsConfigVector);
+    map.addLayer(wrsSegmentLayerVectorStatic.olLayer);
+    mapPopup.addVectorPopup(wrsSegmentLayerVectorStatic, layerPopups.wrs);
+    appConfig.addOperationsLayerStatic(wrsSegmentLayerVectorStatic);
+    //realEarthGroup.items.push(wrsSegmentLayerVectorStatic);
+
+    wrsConfigVector.animate = true;
+    wrsConfigVector.id = 'wrs-segments-vector-animate';
+    wrsConfigVector.loadCallback = animationLoadCallback;
+
+    let wrsSegmentLayerVectorAnimate = new LayerRealEarthVector(wrsConfigVector);
+    map.addLayer(wrsSegmentLayerVectorAnimate.olLayer);
+    mapPopup.addVectorPopup(wrsSegmentLayerVectorAnimate, layerPopups.wrs);
+    appConfig.addOperationsLayerAnimate(wrsSegmentLayerVectorAnimate);
+
+    let wrsConfigTile = {
+        products: 'ROADS',
+        id: 'roads-tile-static',
+        opacity: 0.6,
+        animate: false,
+        name: 'Winter Roads',
+        zIndex: 10
+    };
+
+    let wrsSegmentLayerTileStatic = new LayerRealEarthTile(wrsConfigTile);
+    map.addLayer(wrsSegmentLayerTileStatic.olLayer);
+    appConfig.addOperationsLayerStatic(wrsSegmentLayerTileStatic);
+    realEarthGroup.items.push(wrsSegmentLayerTileStatic);
+
+    wrsConfigTile.animate = true;
+    wrsConfigTile.id = 'roads-tile-animate';
+    wrsConfigTile.loadCallback = animationLoadCallback;
+
+    let wrsSegmentLayerTileAnimate = new LayerRealEarthTile(wrsConfigTile);
+    map.addLayer(wrsSegmentLayerTileAnimate.olLayer);
+    appConfig.addOperationsLayerAnimate(wrsSegmentLayerTileAnimate);
+    legendItemsAnimated.push(wrsSegmentLayerTileAnimate);
+
+    // </editor-fold>
+
+    // <editor-fold desc="24 hour snow">
+
+    let snow24Config = {
+        products: 'SNOWDEPTH24',
+        id: 'snowdepth24-static',
+        opacity: 0.5,
+        animate: false,
+        name: '24HR Snow',
+        maxZoom: 9
+    };
+
+
+    let snowDepthStatic = new LayerRealEarthTile(snow24Config);
+    map.addLayer(snowDepthStatic.olLayer);
+    appConfig.addOperationsLayerStatic(snowDepthStatic);
+    realEarthGroup.items.push(snowDepthStatic);
+
+    //snow24Config.animate = true;
+    //snow24Config.id = 'snowdepth24-animate';
+    //snow24Config.loadCallback = animationLoadCallback;
+    //
+    //let snowDepthAnimate = new LayerRealEarthTile(snow24Config);
+    //map.addLayer(snowDepthAnimate.olLayer);
+    //appConfig.addOperationsLayerAnimate(snowDepthAnimate);
+    //legendItemsAnimated.push(snowDepthAnimate);
+
+    // </editor-fold>
+
+    // <editor-fold desc="nexrhres and precipitation layers">
+    let nexrhresConfig = {
+        products: 'nexrhres',
+        id: 'nexrhres-static',
+        opacity: 0.6,
+        animate: false,
+        name: 'Hybrid Reflectivity',
+        maxZoom: 10
+    };
+
+    let nexrhresStatic = new LayerRealEarthTile(nexrhresConfig);
+    map.addLayer(nexrhresStatic.olLayer);
+    appConfig.addOperationsLayerStatic(nexrhresStatic);
+    realEarthGroup.items.push(nexrhresStatic);
+
+    nexrhresConfig.animate = true;
+    nexrhresConfig.id = 'nexrhres-animate';
+    nexrhresConfig.loadCallback = animationLoadCallback;
+
+    let nexrhresAnimate = new LayerRealEarthTile(nexrhresConfig);
+    map.addLayer(nexrhresAnimate.olLayer);
+    appConfig.addOperationsLayerAnimate(nexrhresAnimate);
+    legendItemsAnimated.push(nexrhresAnimate);
+
+    let precipRateConfig = {
+        products: 'nexrcomp',
+        id: 'precip-rate-static',
+        opacity: 0.6,
+        animate: false,
+        name: 'Precipitation Rate',
+        maxZoom: 10
+    };
+
+    let precipRateStatic = new LayerRealEarthTile(precipRateConfig);
+    map.addLayer(precipRateStatic.olLayer);
+    appConfig.addOperationsLayerStatic(precipRateStatic);
+    realEarthGroup.items.push(precipRateStatic);
+
+    precipRateConfig.animate = true;
+    precipRateConfig.id = 'precip-rate-animate';
+    precipRateConfig.loadCallback = animationLoadCallback;
+
+    let precipRateAnimate = new LayerRealEarthTile(precipRateConfig);
+    map.addLayer(precipRateAnimate.olLayer);
+    appConfig.addOperationsLayerAnimate(precipRateAnimate);
+    legendItemsAnimated.push(precipRateAnimate);
+
+
+    // </editor-fold>
+
+    // <editor-fold desc="Work Zones and special events">
+    let d = new Date();
+    d.setSeconds(0);
+    let endDate = dteConvert.dateToYyyyMmDdHhMmSs(d);
+    d.setYear(d.getYear() + 1901);
+    var startDate = dteConvert.dateToYyyyMmDdHhMmSs(d);
+
+    let workZoneSegLayer = new LayerBaseVectorEsri(
+        'http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/GLRTOC_WZ_SE/MapServer/1',
+        {
+            where: `EventStartDate < '${startDate}' AND EventEndDate > '${endDate}' AND Impact IN('High', 'XXX')`,
+            name: "Work Zone Segments",
+            style: layerStyles.workZoneAndEventStyle,
+            id: 'work-zone-segments',
+            minZoom: 5,
+            maxZoom: 13
+        }
+    );
+
+    map.addLayer(workZoneSegLayer.olLayer);
+    appConfig.addOperationsLayerStatic(workZoneSegLayer);
+    mapPopup.addVectorPopup(workZoneSegLayer, layerPopups.specialEventWorkZone);
+    workZoneEventGroup.items.push(workZoneSegLayer);
+
+    let specialEventsLayer = new LayerBaseVectorEsri(
+        'http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/GLRTOC/GLRTOC_WZ_SE/MapServer/2',
+        {
+            where: `EventStartDate < '${startDate}' AND EventEndDate > '${endDate}' AND Impact IN('High', 'XXX')`,
+            //where: '1=1',
+            name: "Special Events",
+            style: layerStyles.workZoneAndEventStyle,
+            id: 'special-event-points',
+            minZoom: 5,
+            maxZoom: 13
+        }
+    );
+
+    map.addLayer(specialEventsLayer.olLayer);
+    appConfig.addOperationsLayerStatic(specialEventsLayer);
+    mapPopup.addVectorPopup(specialEventsLayer, layerPopups.specialEventWorkZone);
+    workZoneEventGroup.items.push(specialEventsLayer);
+
+    // </editor-fold>
+
+    let legendStatic = new LayerLegend(legendItemsStatic, 'legend-container-static', {});
+    let legendAnimate = new LayerLegend(legendItemsAnimated, 'legend-container-animate', {});
+
+    appConfig.init();
+
+    uiSetup.uiAfterMap();
+})();
+
+
+// <editor-fold desc="Promise example">
+
+//var promise = new Promise(function (resolve, reject) {
+//    console.log('doing stuff');
+//
+//    $.get('http://realearth.ssec.wisc.edu:80/api/products', {products: 'ROADS'}, function (d) {
+//        //if (d.length == 0) {
+//        //    console.log(`${this._products} layer not available or does not have times`);
+//        //    return;
+//        //}
+//        d = d[0];
+//        console.log(d);
+//        resolve("Stuff worked!");
+//
+//
+//        //for (let i = 0; i < d['times'].length; i++) {
+//        //    _this._loadDates.call(_this, d['times'][i]);
+//        //}
+//        //console.log(_this._localDates);
+//
+//        //_this._loadAtTimeIndex.call(_this, _this._localDates.length - 1)
+//
+//    }, 'json').fail(function () {
+//        reject(Error("It broke"));
+//    });
+//
+//
+//});
+//
+//
+//promise.then(
+//    function (result) {
+//        console.log(result); // "Stuff worked!"
+//    },
+//    function (err) {
+//        console.log(err); // Error: "It broke"
+//    }
+//);
+// </editor-fold>
+
+
diff --git a/projects/glrtoc/mainUi.js b/projects/glrtoc/mainUi.js
new file mode 100644
index 0000000000000000000000000000000000000000..9e4f17c4dc682b1de35401b06d468a78943087c4
--- /dev/null
+++ b/projects/glrtoc/mainUi.js
@@ -0,0 +1,105 @@
+/**
+ * Created by gavorhes on 12/7/2015.
+ */
+
+import {} from '../../src/jquery-plugin/mediaControl';
+import {} from '../../src/jquery-plugin/rangeChange';
+import appConfig from './appConfig';
+import $ from '../../src/jquery';
+require('jquery-ui/tabs');
+require('jquery-ui/accordion');
+
+
+/**
+ * Set up the UI
+ */
+export function uiInit() {
+    "use strict";
+
+    //$("#operations").accordion('refresh');
+
+
+    let $sidebar = $('#sidebar');
+    let $hideSideBar = $('#hide-sidebar');
+    let $showSideBar = $('#show-sidebar');
+
+    let $tabs = $("#tabs");
+
+    let sidebarWidth = $sidebar.width();
+
+    //apply tab layout
+    $tabs.tabs({
+        heightStyle: "fill",
+        activate: function (event, ui) {
+            appConfig.currentTabId = ui.newPanel[0].id;
+        }
+    });
+
+    //apply accordion
+    appConfig.$opsAccordion.accordion({
+        heightStyle: "fill",
+        activate: function (event, ui) {
+            appConfig.currentOperationsPanelId = ui.newPanel[0].id;
+        }
+    });
+
+    $(window).resize(function () {
+        $tabs.tabs('refresh');
+        setTimeout(function () {
+            appConfig.$opsAccordion.accordion("refresh");
+        }, 50);
+    });
+
+    $hideSideBar.click(function () {
+        var mapCenter = appConfig.map.getView().getCenter();
+
+        appConfig.map.beforeRender(function () {
+            $sidebar.animate({'margin-left': -1 * sidebarWidth}, 200,
+                function () {
+                    $hideSideBar.hide();
+                    $showSideBar.show();
+                    appConfig.map.updateSize();
+                }
+            );
+        });
+        appConfig.map.getView().setCenter(mapCenter);
+    });
+
+    $showSideBar.click(function () {
+        var mapCenter = appConfig.map.getView().getCenter();
+
+        appConfig.map.beforeRender(function () {
+            $sidebar.animate({'margin-left': 0}, 200,
+                function () {
+                    $showSideBar.hide();
+                    $hideSideBar.show();
+                    appConfig.map.updateSize();
+                }
+            );
+        });
+        appConfig.map.getView().setCenter(mapCenter);
+    });
+
+    let d = new Date();
+    let endTime = d.getTime();
+    d.setHours(d.getHours() - 4);
+    let startTime = d.getTime();
+    let rangeStep = Math.round((endTime - startTime) / 8);
+
+    appConfig.mediaControl = $('#animation-control').mediaControl(startTime, endTime, endTime, rangeStep,
+        function (t) {
+            for (let l of appConfig.animationLayers) {
+                l.setLayerTime(t);
+            }
+        },
+        750, true);
+}
+
+
+//http://realearth.ssec.wisc.edu/api/image?products=nexrhres_20160108_180000&x=5&y=5&z=4
+
+export function uiAfterMap() {
+    "use strict";
+
+
+}
\ No newline at end of file
diff --git a/projects/itsMap.js b/projects/itsMap.js
new file mode 100644
index 0000000000000000000000000000000000000000..d31b6c55c261faf2095522c102093dd723fd5355
--- /dev/null
+++ b/projects/itsMap.js
@@ -0,0 +1,27 @@
+/**
+ * Created by gavorhes on 12/18/2015.
+ */
+
+import quickMap from '../src/olHelpers/quickMap';
+import mapMove from '../src/olHelpers/mapMove';
+import mapPopup from '../src/olHelpers/mapPopup';
+import ItsLayerCollection from '../src/collections/ItsLayerCollection';
+import LayerLegend from '../src/collections/LayerLegend';
+
+let map = quickMap();
+mapMove.init(map);
+mapPopup.init(map);
+
+let itsLayerCollection = new ItsLayerCollection(map);
+
+let layerArray = [
+    {
+        groupName: 'ITS Inventory Layers',
+        collapse: false,
+        addCheck: true,
+        items: itsLayerCollection.layers
+    }
+];
+
+
+let legend = new LayerLegend(layerArray, 'legend-container', {});
diff --git a/projects/npmrds/delay/delay-config.js b/projects/npmrds/delay/delay-config.js
new file mode 100644
index 0000000000000000000000000000000000000000..5be9a94e0e24857e670c42b0da45a6a41434b8fc
--- /dev/null
+++ b/projects/npmrds/delay/delay-config.js
@@ -0,0 +1,3 @@
+/**
+ * Created by gavorhes on 2/9/2016.
+ */
diff --git a/projects/npmrds/delay/delay-main.js b/projects/npmrds/delay/delay-main.js
new file mode 100644
index 0000000000000000000000000000000000000000..b0783f402689ece7127771e36855c344f53ee45b
--- /dev/null
+++ b/projects/npmrds/delay/delay-main.js
@@ -0,0 +1,32 @@
+/**
+ * Created by gavorhes on 2/9/2016.
+ */
+
+import quickMap from '../../../src/olHelpers/quickMap';
+const angular = require('angular2');
+
+let map = quickMap();
+
+let app = angular.module('myApp', []);
+
+
+app.controller('myCtrl', function($scope) {
+    $scope.firstName = "John";
+    $scope.lastName = "Doe";
+
+    $scope.myFunction = function(){
+        "use strict";
+        console.log((new Date()).getTime());
+    };
+});
+
+
+app.controller('myCtrl2', function($scope) {
+    //$scope.firstName = "John";
+    //$scope.lastName = "Doe";
+    //
+    //$scope.myFunction = function(){
+    //    "use strict";
+    //    console.log((new Date()).getTime());
+    //}
+});
diff --git a/projects/npmrds/heatmap/appConfig.js b/projects/npmrds/heatmap/appConfig.js
new file mode 100644
index 0000000000000000000000000000000000000000..32d4fcbdc123a3e2c47d27e67f5e65e998de8310
--- /dev/null
+++ b/projects/npmrds/heatmap/appConfig.js
@@ -0,0 +1,50 @@
+/**
+ * Created by gavorhes on 12/23/2015.
+ */
+
+class NpmrdsHeatmapConfig {
+    constructor() {
+
+        this.map = undefined;
+
+        /**
+         *
+         * @type {LayerBaseVectorGeoJson}
+         */
+        this.featureOverlay = undefined;
+
+        /**
+         *
+         * @type {LayerBaseVectorGeoJson}
+         */
+        this.lineLayer = undefined;
+
+
+        /**
+         *
+         * @type {LayerBaseVectorGeoJson}
+         */
+        this.pointLayer = undefined;
+
+
+        /**
+         *
+         * @type {LayerBaseVectorGeoJson}
+         */
+        this.trackerLayer = undefined;
+    }
+
+
+}
+
+///**
+// *
+// * @type {NpmrdsHeatmapConfig}
+// */
+//const npmrdsHeatmapConfig = new NpmrdsHeatmapConfig();
+
+/**
+ * @type {NpmrdsHeatmapConfig}
+ */
+export default new NpmrdsHeatmapConfig();
+
diff --git a/projects/npmrds/heatmap/layerStyles.js b/projects/npmrds/heatmap/layerStyles.js
new file mode 100644
index 0000000000000000000000000000000000000000..e16bd69ff74e2f6797d6d2902e73e1c1046eebf9
--- /dev/null
+++ b/projects/npmrds/heatmap/layerStyles.js
@@ -0,0 +1,43 @@
+/**
+ * Created by gavorhes on 12/22/2015.
+ */
+import ol from '../../../src/custom-ol';
+
+
+export const overlayStyle = new ol.style.Style({
+    fill: new ol.style.Fill({
+        color: 'rgba(255, 0, 237, 0.1)'
+    }),
+    stroke: new ol.style.Stroke({
+        color: 'rgb(255, 0, 237)',
+        width: 2
+    })
+});
+
+
+export const lineIndicator = new ol.style.Style({
+    stroke: new ol.style.Stroke({
+        color: 'red',
+        width: 7
+    })
+});
+
+
+export const pointIndices = new ol.style.Style({
+    image: new ol.style.Circle({
+        radius: 4,
+        fill: new ol.style.Fill({
+            color: 'blue'
+        })
+    })
+});
+
+
+export const trackerPoint = new ol.style.Style({
+    image: new ol.style.Circle({
+        radius: 8,
+        fill: new ol.style.Fill({
+            color: 'blue'
+        })
+    })
+});
diff --git a/projects/npmrds/heatmap/main-ui.js b/projects/npmrds/heatmap/main-ui.js
new file mode 100644
index 0000000000000000000000000000000000000000..7f6a44a225255824550a32012f197d205b285f29
--- /dev/null
+++ b/projects/npmrds/heatmap/main-ui.js
@@ -0,0 +1,368 @@
+/**
+ * Created by gavorhes on 12/22/2015.
+ */
+
+import npmrdsHeatmapConfig from './appConfig';
+import {} from '../../../src/jquery-plugin/dayRange';
+import {} from '../../../src/jquery-plugin/rangeChange';
+import * as colors from '../../../src/util/colors';
+import SortedFeatures from '../../../src/olHelpers/SortedFeatures';
+import $ from '../../../src/jquery';
+import ol from '../../../src/custom-ol';
+
+
+let $btnCancelArea = $('#btn-cancel-area');
+let $btnSelectArea = $('#btn-select-area');
+let $divSelectArea = $('#div-select-area');
+let $ulHwyDirs = $('#ul-hwy-dirs');
+let $btnSelectHwy = $('#btn-select-hwy');
+//let $btnSelectHwyBack = $('#btn-select-hwy-back');
+let $divHighwaySelection = $('#div-highway-selection');
+let $divRight = $('#right');
+let $divLeft = $('#left');
+let $ckmapTrack = $('#chk-map-track');
+let $canvasToolTipSpan = $('#canvas-tooltip-span');
+let $canv = $('#heatmap-canvas');
+/**
+ *
+ * @type {*|jQuery|HTMLElement}
+ */
+let $canvContainer = $('#canvas-container');
+let $dateUl = $('#date-ul');
+let $heatMapVerticalBar = $('#heat-map-vertical-bar');
+let $selectedTmcs = $('#selected-tmcs');
+
+let sldrHeatMapId = 'heat-map-slider';
+let $sldrHeatMap = $('#' + sldrHeatMapId);
+
+
+let heatMapReady = false;
+
+/**
+ * @type {SortedFeatures}
+ */
+let sortedFeatures;
+
+/**
+ * @type {DayRange}
+ */
+let dayRange;
+
+function _leadingPad(num) {
+    let out = num.toFixed();
+    if (out.length == 1) {
+        out = '0' + out;
+    }
+    
+    return out;
+}
+
+function _formatDate(d) {
+    return `${_leadingPad(d.getMonth() + 1)}/${_leadingPad(d.getDate())}/${d.getYear() + 1900} ` +
+        `${_leadingPad(d.getHours())}:${_leadingPad(d.getMinutes())}:${_leadingPad(d.getSeconds())}`;
+}
+
+
+
+function clearCanvas() {
+    $dateUl.html('');
+    $canv.off('mousemove');
+    let canv = $canv[0];
+    let ctx = canv.getContext("2d");
+    ctx.clearRect(0, 0, canv.width, canv.height);
+
+    $canv.attr('width', $('#heat-map-slider').width());
+    $canv.attr('height', 100);
+    $heatMapVerticalBar.height(0);
+}
+
+
+export function startUi() {
+    "use strict";
+    dayRange = $('#div-date-range').dayRange(10);
+}
+
+export function drawSetup() {
+    "use strict";
+    let draw = new ol.interaction.Draw({
+        source: npmrdsHeatmapConfig.featureOverlay.source,
+        type: 'Polygon'
+    });
+
+    draw.on('drawend', function (evt) {
+        //$divSelectArea.hide();
+        $btnSelectArea.prop('disabled', false);
+        let geom = evt.feature.getGeometry();
+        let geomClone = geom.clone();
+
+        geomClone.transform('EPSG:3857', 'EPSG:4326');
+
+        setTimeout(function () {
+            npmrdsHeatmapConfig.map.removeInteraction(draw);
+        }, 100);
+        $btnCancelArea.hide();
+
+        $.get('npmrds/gethighways', {polygon: JSON.stringify(geomClone.getCoordinates())},
+            function (d) {
+                $ulHwyDirs.html(d);
+                $divHighwaySelection.show();
+
+                $('input[name=hwydirs]').change(function () {
+                    $btnSelectHwy.prop('disabled', false);
+                });
+            }, 'text');
+    });
+
+    $btnCancelArea.click(function () {
+        npmrdsHeatmapConfig.map.removeInteraction(draw);
+        $btnSelectArea.prop('disabled', false);
+        $btnCancelArea.hide();
+    });
+
+    $btnSelectArea.click(function () {
+        npmrdsHeatmapConfig.featureOverlay.source.clear();
+        npmrdsHeatmapConfig.map.addInteraction(draw);
+        $btnSelectArea.prop('disabled', true);
+        $btnCancelArea.show();
+        $ulHwyDirs.html('');
+        $divHighwaySelection.hide();
+        $btnSelectHwy.prop('disabled', true);
+    });
+}
+
+
+export function endUi() {
+    "use strict";
+
+    $divSelectArea.show();
+    $btnSelectArea.prop('disabled', false);
+    $btnCancelArea.hide();
+    $divHighwaySelection.hide();
+
+    $btnSelectHwy.click(function () {
+        let selectedHwy = $('input[name=hwydirs]:checked:first').val();
+        $('#sel-hwy').html(selectedHwy);
+        $.get('npmrds/getroute',
+            {
+                hwyDir: selectedHwy
+            }, function (d) {
+                npmrdsHeatmapConfig.lineLayer.clear();
+                npmrdsHeatmapConfig.lineLayer.addFeatures(d['line']);
+
+                npmrdsHeatmapConfig.pointLayer.clear();
+                npmrdsHeatmapConfig.pointLayer.addFeatures(d['points']);
+
+                sortedFeatures = new SortedFeatures(npmrdsHeatmapConfig.pointLayer.source.getFeatures(), 'distReal');
+
+                npmrdsHeatmapConfig.trackerLayer.clear();
+                npmrdsHeatmapConfig.trackerLayer.source.addFeature(sortedFeatures.sortedFeatures[0]);
+
+
+                $divLeft.hide();
+                $divRight.show();
+                npmrdsHeatmapConfig.map.updateSize();
+
+                let mapView = npmrdsHeatmapConfig.map.getView();
+
+                let panAnimation = ol.animation.pan({
+                    duration: 500,
+                    source: mapView.getCenter()
+                });
+                npmrdsHeatmapConfig.map.beforeRender(panAnimation);
+
+                mapView.fit(
+                    npmrdsHeatmapConfig.featureOverlay.source.getFeatures()[0].getGeometry().getExtent(),
+                    npmrdsHeatmapConfig.map.getSize()
+                );
+
+                mapView.setZoom(mapView.getZoom() - 1);
+
+                $('#vehicle-all').prop('checked', true);
+
+                npmrdsHeatmapConfig.featureOverlay.visible = false;
+                npmrdsHeatmapConfig.lineLayer.visible = true;
+                npmrdsHeatmapConfig.pointLayer.visible = true;
+                npmrdsHeatmapConfig.trackerLayer.visible = true;
+
+                $sldrHeatMap.prop('max', d['totalDistance']);
+                $sldrHeatMap.val(0);
+                $heatMapVerticalBar.css('left', '22px');
+                $selectedTmcs.val(JSON.stringify(d['tmcs']));
+
+                //console.log(d);
+            }, 'json').fail(function () {
+            alert("something went wrong, try another area selection");
+        });
+    });
+}
+
+
+export function heatMapUi() {
+    "use strict";
+
+    $('#heat-map-back').click(function () {
+        $divLeft.show();
+        $divRight.hide();
+        npmrdsHeatmapConfig.map.updateSize();
+        npmrdsHeatmapConfig.featureOverlay.visible = true;
+        npmrdsHeatmapConfig.lineLayer.visible = false;
+        npmrdsHeatmapConfig.pointLayer.visible = false;
+        npmrdsHeatmapConfig.trackerLayer.visible = false;
+
+        clearCanvas();
+    });
+
+    $canv.mouseenter(function () {
+        if (heatMapReady) {
+            $canvasToolTipSpan.css('display', 'block');
+        }
+    });
+
+    $canv.mouseleave(function () {
+        $canvasToolTipSpan.css('display', 'none');
+        $canvasToolTipSpan.html('');
+    });
+
+    //add the slider change interaction
+    $('#' + sldrHeatMapId).rangeChange(function(newVal, percent, evt){
+
+        let canvWidth = parseFloat($canv.width());
+
+        $heatMapVerticalBar.css('left', (canvWidth * percent + 22).toFixed() + 'px');
+
+        let selectedFeature = sortedFeatures.getFeature(newVal);
+
+        npmrdsHeatmapConfig.trackerLayer.clear();
+        npmrdsHeatmapConfig.trackerLayer.source.addFeature(selectedFeature);
+
+        if ($ckmapTrack.prop('checked')) {
+            let panAnimation = ol.animation.pan({
+                duration: 500,
+                source: npmrdsHeatmapConfig.map.getView().getCenter()
+            });
+            npmrdsHeatmapConfig.map.beforeRender(panAnimation);
+            npmrdsHeatmapConfig.map.getView().setCenter(selectedFeature.getGeometry().getCoordinates());
+        }
+    }, 25);
+
+
+    $('#heat-map-confirm').click(function () {
+        clearCanvas();
+        let canv = $canv[0];
+        let ctx = canv.getContext("2d");
+        let $progressDiv = $('#progress-indicator-div');
+        $dateUl.html('');
+
+        $progressDiv.show();
+
+        $.get('npmrds/getheatmap',
+            {
+                vehicleType: $('input[name=vehicle-type]:checked:first').val(),
+                startDate: _formatDate(dayRange.startDate),
+                endDate: _formatDate(dayRange.endDate),
+                tmcs: $selectedTmcs.val()
+            }, function (d) {
+                if (d['error']) {
+                    alert(d['error']);
+
+                    return;
+                }
+
+                let canvasWidth = parseInt($canv.attr('width'));
+                let colorGradient = colors.makeBlueGreenRedGradient(d['minSpeedVsFree'], d['maxSpeedVsFree'], true);
+                let colorGradientStdDev = colors.makeBlueGreenRedGradientZScore(d['medianSpeedVsFree'], d['stdDevSpeedVsFree'], true);
+
+                let recordLength = d['tmcResult'][0].values.length;
+                $canv.attr('height', (recordLength * 2).toFixed());
+                $heatMapVerticalBar.height(recordLength * 2);
+
+                let columnPosition = 0;
+                let tmcEndPositionArray = [];
+
+                for (let i = 0; i < d['tmcResult'].length; i++) {
+                    let tmc = d['tmcResult'][i];
+                    let rectangleWidth = canvasWidth * tmc['distPercent'] / 100;
+                    let valLength = tmc['values'].length;
+
+                    for (let j = 0; j < valLength; j++) {
+                        let speedVFreeSpeed = (tmc['values'][j] == null ? null : tmc['values'][j] / tmc['freeFlowSpeed']);
+                        let speedVFreeZScore = (speedVFreeSpeed - d['medianSpeedVsFree']) / d['stdDevSpeedVsFree'];
+                        //console.log(colorGradientStdDev(speedVFreeSpeed));
+                        //let z_score =
+                        //let z_score =
+
+                        ctx.fillStyle = colorGradientStdDev(speedVFreeSpeed);
+                        ctx.fillRect(columnPosition, j * 2, rectangleWidth, 2);
+                    }
+
+                    columnPosition += rectangleWidth;
+                    tmcEndPositionArray.push([tmc['tmc'], columnPosition]);
+                }
+
+                for (let i = 0; i < d['dateList'].length; i++) {
+                    $dateUl.append('<li><div>' + d['dateList'][i] + '</div></ul>');
+                }
+
+                // set the date li height
+                //$('#date-ul li').css('height', (2 + (parseInt($canv.attr('height')) / d['dateList'].length) - d['dateList'].length).toFixed());
+
+                let tmcResultDict = {};
+                for (let i = 0; i < d['tmcResult'].length; i++) {
+                    tmcResultDict[d['tmcResult'][i]['tmc']] = d['tmcResult'][i];
+                }
+
+                $canv.mousemove(function (evt) {
+
+                    // coordinates relative the canvas
+                    let offsetX = evt.offsetX;
+                    let offsetY = evt.offsetY;
+
+                    // coordinates relative the window
+                    let clientX = evt.clientX;
+                    let clientY = evt.clientY;
+
+                    // get the tmc
+                    let tmcId = null;
+                    for (let i = 0; i < tmcEndPositionArray.length; i++) {
+                        if (offsetX < tmcEndPositionArray[i][1]) {
+                            tmcId = tmcEndPositionArray[i][0];
+                            break;
+                        }
+                    }
+
+                    if (tmcId == null) {
+                        tmcId = tmcEndPositionArray[tmcEndPositionArray.length - 1][0];
+                    }
+
+                    // since each retangle in the heatmap is 2px high, get the index by divding by 2, round down
+                    let rowIndex = Math.floor(offsetY / 2);
+
+                    // make a date equal to the query start date
+                    let dte = new Date(d['dateList'][0]);
+                    // add minutes as 5X the row index
+                    dte.setMinutes(dte.getMinutes() + rowIndex * 5);
+
+                    let tmcObj = tmcResultDict[tmcId];
+
+                    //set the html
+                    $canvasToolTipSpan.html(`TMC: ${tmcId}<br>${dte.toLocaleDateString()} ${dte.toLocaleTimeString()}<br>` +
+                    `Speed: ${(tmcObj['values'][rowIndex] == null ? 'null' : tmcObj['values'][rowIndex].toFixed(1))}, Free: ${tmcObj['freeFlowSpeed'].toFixed(1)}`
+                    );
+
+                    //set the position
+                    let canvasToolipSpan = $canvasToolTipSpan[0];
+                    canvasToolipSpan.style.left = (clientX - 150).toFixed() + 'px';
+                    canvasToolipSpan.style.top = (clientY - 85).toFixed() + 'px';
+
+                });
+                //console.log(d);
+
+                $canvContainer.scrollTop(0);
+                heatMapReady = true;
+            }, 'json').fail(function(){
+                alert('Something went wrong with the request');
+            }).always(function(){
+                $progressDiv.hide();
+            });
+    });
+}
diff --git a/projects/npmrds/heatmap/main.js b/projects/npmrds/heatmap/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..23973777d705e6d5e2d8600450ae3aeb257d490e
--- /dev/null
+++ b/projects/npmrds/heatmap/main.js
@@ -0,0 +1,47 @@
+/**
+ * Created by gavorhes on 12/22/2015.
+ */
+
+import npmrdsHeatmapConfig from './appConfig';
+import quickMap from '../../../src/olHelpers/quickMap';
+import LayerBaseXyzTile from '../../../src/layers/LayerBaseXyzTile';
+import LayerEsriMapServer from '../../../src/layers/LayerEsriMapServer';
+import LayerBaseVectorGeoJson from '../../../src/layers/LayerBaseVectorGeoJson';
+import * as layerStyles from './layerStyles';
+import * as uiSetup from './main-ui';
+
+//glob.appConfig = npmrdsHeatmapConfig;
+
+(function(){
+    "use strict";
+    uiSetup.startUi();
+    let map = quickMap({center: {x: -10012438, y: 5548095}, zoom: 8, minZoom: 5});
+
+    npmrdsHeatmapConfig.map = map;
+
+    let xyzTile = new LayerBaseXyzTile('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/NPMRDS/npmrds_tile/MapServer/tile/{z}/{y}/{x}',
+        {minZoom: 4, maxZoom: 11});
+
+    let esriMapServer = new LayerEsriMapServer('http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/NPMRDS/npmrds_dynamic/MapServer',
+        {minZoom: 12, maxZoom: 18});
+
+    map.addLayer(xyzTile.olLayer);
+    map.addLayer(esriMapServer.olLayer);
+
+    let transform = {dataProjection: 'EPSG:3857', featureProjection: 'EPSG:3857'};
+
+    npmrdsHeatmapConfig.featureOverlay = new LayerBaseVectorGeoJson('', {style: layerStyles.overlayStyle, transform: transform});
+    npmrdsHeatmapConfig.lineLayer = new LayerBaseVectorGeoJson('', {style: layerStyles.lineIndicator, transform: transform});
+    npmrdsHeatmapConfig.pointLayer = new LayerBaseVectorGeoJson('', {style: layerStyles.pointIndices, transform: transform});
+    npmrdsHeatmapConfig.trackerLayer = new LayerBaseVectorGeoJson('', {style: layerStyles.trackerPoint, transform: transform});
+
+    map.addLayer(npmrdsHeatmapConfig.featureOverlay.olLayer);
+    map.addLayer(npmrdsHeatmapConfig.lineLayer.olLayer);
+    //map.addLayer(npmrdsHeatmapConfig.pointLayer.olLayer);
+    map.addLayer(npmrdsHeatmapConfig.trackerLayer.olLayer);
+
+    uiSetup.drawSetup();
+    uiSetup.heatMapUi();
+    uiSetup.endUi();
+})();
+
diff --git a/projects/tsmo/TipConfig.js b/projects/tsmo/TipConfig.js
new file mode 100644
index 0000000000000000000000000000000000000000..705a404b87a43624e54becc69c7311d596699029
--- /dev/null
+++ b/projects/tsmo/TipConfig.js
@@ -0,0 +1,149 @@
+/**
+ * Created by gavorhes on 12/14/2015.
+ */
+
+/**
+ * Tip config object
+ */
+class TipConfig {
+    constructor() {
+
+        /**
+         *
+         * @type {Sliders}
+         */
+        this.sliders = undefined;
+
+        /**
+         *
+         * @type {Map}
+         */
+        this.map = null;
+
+        this.$loadingGif = null;
+        this.$regionSelector = undefined;
+
+        /**
+         *
+         * @type {TipSegmentLayer}
+         */
+        this.tipSegmentLayer = undefined;
+
+
+        /**
+         *
+         * @type {LayerBase|*}
+         */
+        this.metamanagerSegments = undefined;
+        this.tipSegmentLayerMinZoom = 10;
+
+        /**
+         * its layer collection
+         * @type {ItsLayerCollection}
+         */
+        this.itsLayerCollection = undefined;
+
+        /**
+         * param list to set up the sliders, initial weight and drop down selection added later
+         */
+        this._sliderParamArray = [
+            ['AADT', [['aadtyr_1', '1']]],
+            ['AADT Future', [['aadtyr_5', '5'], ['aadtyr_10', '10'], ['aadtyr_15', '15'], ['aadtyr_20', '20']]],
+            ['Growth', [['growth_5', '5'], ['growth_10', '10'], ['growth_15', '15'], ['growth_20', '20']]],
+            ['Truck', [['trkdyr_1', '1'], ['trkdyr_20', '20']]],
+            ['LOS', [['losyr_1', '1']]],
+            ['LOS Future', [['losyr_5', '5'], ['losyr_10', '10'], ['losyr_15', '15'], ['losyr_20', '20']]],
+            ['Crash Rate', [['crash_rate', '1']]],
+            ['Severity', [['crash_severity', '1']]],
+            ['Weather', [['weather', '1']]],
+            ['Event', [['event', '1']]]
+        ];
+
+        /**
+         * Presets list, order of the parameters is important, must match that of the slider param list
+         */
+        this._presetArray =
+            [
+                ['Default TIP', [
+                    [10.0, 'aadtyr_1'],
+                    [7.0, 'aadtyr_20'],
+                    [7.0, 'growth_20'],
+                    [4.0, 'trkdyr_1'],
+                    [12.0, 'losyr_1'],
+                    [12.0, 'losyr_20'],
+                    [15.0, 'crash_rate'],
+                    [13.0, 'crash_severity'],
+                    [9.0, 'weather'],
+                    [11.0, 'event']]
+                ],
+                ['Safety', [
+                    [20.0, 'aadtyr_1'],
+                    [0.0, 'aadtyr_20'],
+                    [0.0, 'growth_20'],
+                    [0.0, 'trkdyr_1'],
+                    [0.0, 'losyr_1'],
+                    [0.0, 'losyr_20'],
+                    [40.0, 'crash_rate'],
+                    [40.0, 'crash_severity'],
+                    [0.0, 'weather'],
+                    [0.0, 'event']]
+                ],
+                ['Mobility Present', [
+                    [25.0, 'aadtyr_1'],
+                    [25.0, 'aadtyr_5'],
+                    [0.0, 'growth_20'],
+                    [25.0, 'trkdyr_1'],
+                    [25.0, 'losyr_1'],
+                    [0.0, 'losyr_20'],
+                    [0.0, 'crash_rate'],
+                    [0.0, 'crash_severity'],
+                    [0.0, 'weather'],
+                    [0.0, 'event']]
+                ],
+                ['Mobility Future', [
+                    [0.0, 'aadtyr_1'],
+                    [25.0, 'aadtyr_20'],
+                    [25.0, 'growth_20'],
+                    [25.0, 'trkdyr_20'],
+                    [0.0, 'losyr_1'],
+                    [25.0, 'losyr_20'],
+                    [0.0, 'crash_rate'],
+                    [0.0, 'crash_severity'],
+                    [0.0, 'weather'],
+                    [0.0, 'event']]
+                ],
+                ['Service', [
+                    [30.0, 'aadtyr_1'],
+                    [0.0, 'aadtyr_20'],
+                    [10.0, 'growth_20'],
+                    [0.0, 'trkdyr_1'],
+                    [30.0, 'losyr_1'],
+                    [30.0, 'losyr_20'],
+                    [0.0, 'crash_rate'],
+                    [0.0, 'crash_severity'],
+                    [0.0, 'weather'],
+                    [0.0, 'event']]
+                ],
+                ['Freight Performance', [
+                    [20.0, 'aadtyr_1'],
+                    [0.0, 'aadtyr_20'],
+                    [0.0, 'growth_20'],
+                    [60.0, 'trkdyr_1'],
+                    [20.0, 'losyr_1'],
+                    [0.0, 'losyr_20'],
+                    [0.0, 'crash_rate'],
+                    [0.0, 'crash_severity'],
+                    [0.0, 'weather'],
+                    [0.0, 'event']]
+                ]
+            ];
+
+        for (var i = 0; i < this._sliderParamArray.length; i++) {
+            this._sliderParamArray[i].push(this._presetArray[0][1][i][0]);
+            this._sliderParamArray[i].push(this._presetArray[0][1][i][1]);
+        }
+
+    }
+}
+
+export default new TipConfig();
diff --git a/projects/tsmo/TipSegmentLayer.js b/projects/tsmo/TipSegmentLayer.js
new file mode 100644
index 0000000000000000000000000000000000000000..60f299b904280e56583071187d14a8d0f806bd36
--- /dev/null
+++ b/projects/tsmo/TipSegmentLayer.js
@@ -0,0 +1,191 @@
+/**
+ * Created by gavorhes on 12/10/2015.
+ */
+
+import LayerBaseVectorGeoJson from '../../src/layers/LayerBaseVectorGeoJson';
+import mapMove from '../../src/olHelpers/mapMove';
+import mapPopup from '../../src/olHelpers/mapPopup';
+import tipConfig from './TipConfig'
+import * as tipLayerStyles from './tipStyleFunction';
+const $ = require('jquery');
+
+/**
+ * Tip Segment layer with a bunch of configuration applied, extends base GeoJSON
+ */
+class TipSegmentLayer extends LayerBaseVectorGeoJson {
+
+    /**
+     * ITS device layer, types available at http://transportal.cee.wisc.edu/its/inventory/
+     * @param {Sliders} sliders
+     * @param {object} options
+     * @param {boolean} [options.visible=true]
+     * @param {boolean} [options.onDemand=true]
+     * @param {number} [options.onDemandDelay=300] delay before the map move callback should be called
+     * @param [options.$loadingGif=undefined] jquery reference to loading gif
+     * @param [options.$regionSelector=undefined] jquery reference to region selector
+     *
+     * @param {object} [options.selectionStyle={}] the selection style configuration
+     * @param {string} [options.selectionStyle.color=rgba(255,170,0,0.5)] the selection color
+     * @param {number} [options.selectionStyle.width=10] the selection width for linear features
+     * @param {object} [options.selectionStyle.olStyle=undefined] an openlayers style object
+     * @param {propertiesZoomStyle} [options.selectionStyle.olStyleFunction=undefined] modified OL style function
+     */
+    constructor(sliders, options) {
+        options['name'] = "TIP Segments";
+        options.onDemand = true;
+        options.style = tipLayerStyles.tipStyle;
+        options['minZoom'] = tipConfig.tipSegmentLayerMinZoom;
+        options['transform'] = {};
+        options.transform.dataProjection = 'EPSG:3857';
+        options.transform.featureProjection = 'EPSG:3857';
+        options['id'] = 'tip-segments';
+        super('tip/gettipfeatures?callback=?', options);
+        this._$regionSelector = options.$regionSelector || undefined;
+
+
+        this._$loadingGif = options.$loadingGif;
+
+        this.legendContent += '<ul><li><hr class="cell-min"></li><li><hr class="cell-very-low"></li>' +
+            '<li><hr class="cell-low"></li><li><hr class="cell-mid"></li><li><hr class="cell-high"></li>' +
+            '<li><hr class="cell-very-high"></li><li><hr class="cell-max">' +
+            '</li></ul>';
+
+        this.legendContent += '<ul>';
+        this.legendContent += '<li style="height: 15px;">low</li>';
+        this.legendContent += '<li style="height: 25px; margin-top: 4px;">medium</li>';
+        this.legendContent += '<li style="height: 15px; margin-top: 5px;">high</li>';
+        this.legendContent += '</ul>';
+
+        this.legendContent += '<span style="margin: 12px 0 0 52px; display: block">Relative Need</span>';
+
+        this._geometry_cache = [];
+
+        this.sliders = sliders;
+        tipConfig.tipSegmentLayer = this;
+
+        let _this = this;
+
+        mapPopup.addVectorPopup(this, function (props) {
+
+            let parms = {
+                pdpId: props['p'],
+                paramWeights: JSON.stringify(_this.sliders.getParams())
+            };
+
+            return '<iframe style="margin-top:5px;" ' +
+                `src="tip/getsegprops?${$.param(parms)}" ` +
+                `height="250" width="350"></iframe>`;
+        }, options.selectionStyle);
+    }
+
+    /**
+     * override base method to include show or hide loading indicator
+     * @param {number} zoom
+     * @param {string} [evtType=undefined] undefined for initial load, otherwise one of 'change:center', 'change:resolution'
+     * @returns {boolean}
+     */
+    mapMoveBefore(zoom, evtType) {
+        let returnVal = super.mapMoveBefore(zoom, evtType);
+
+        if (this._$loadingGif) {
+            if (returnVal) {
+                this._$loadingGif.fadeIn();
+            } else {
+                this._$loadingGif.fadeOut();
+            }
+        }
+        return returnVal;
+    }
+
+    /**
+     * callback to generate the parameters passed in the get request
+     * @param {object} extent
+     * @param {number} extent.minX
+     * @param {number} extent.minY
+     * @param {number} extent.maxX
+     * @param {number} extent.maxY
+     * @param {number} zoomLevel
+     */
+    mapMoveMakeGetParams(extent, zoomLevel) {
+        super.mapMoveMakeGetParams(extent, zoomLevel);
+        $.extend(this.mapMoveParams, extent);
+        this.mapMoveParams['param_weights'] = JSON.stringify(this.sliders.getParams());
+        if (this._$regionSelector){
+            this.mapMoveParams['region'] = this._$regionSelector.val();
+        }
+    }
+
+
+    /**
+     * callback function on map move
+     * @param d the json response
+     */
+    mapMoveCallback(d) {
+        if (d['geojson'].features.length > 0) {
+            this._geometry_cache = this._geometry_cache.concat(d['geojson'].features);
+            this._geometry_cache.sort(function (a, b) {
+                return (a.properties['p'] - b.properties['p']);
+            });
+        }
+
+        let passFeatures = [];
+
+        for (let s of d['scores']) {
+
+            let feat = this._set_pdp_score(s['p'], s['z']);
+            if (feat) {
+                passFeatures.push(feat);
+            }
+        }
+
+        d['geojson'].features = passFeatures;
+        super.mapMoveCallback(d['geojson']);
+        if (this._$loadingGif) {
+            this._$loadingGif.fadeOut();
+        }
+    }
+
+    /**
+     * set the score for the feature
+     * @param {number} pdp pdp_id
+     * @param {number} score to assign
+     * @param {object} [in_array=this._geometry_cache]
+     * @returns {*} the feature with the score assigned
+     * @private
+     */
+    _set_pdp_score(pdp, score, in_array) {
+        if (typeof in_array == 'undefined') {
+            in_array = this._geometry_cache;
+        }
+
+        if (in_array.length == 0) {
+            return undefined;
+        }
+
+        if (in_array.length == 1) {
+            let feat = in_array[0];
+            if (feat.properties['p'] == pdp) {
+                feat.properties['z'] = score;
+                return feat;
+            } else {
+                return undefined;
+            }
+        }
+
+        let midPoint = Math.floor(in_array.length / 2);
+
+        let midFeature = in_array[midPoint];
+        if (midFeature.properties['p'] == pdp) {
+            midFeature.properties['z'] = score;
+            return midFeature;
+        } else if (pdp < midFeature.properties['p']) {
+            return this._set_pdp_score(pdp, score, in_array.slice(0, midPoint));
+        } else {
+            return this._set_pdp_score(pdp, score, in_array.slice(midPoint + 1));
+        }
+    }
+
+}
+
+
+export default TipSegmentLayer;
diff --git a/projects/tsmo/legend-test.js b/projects/tsmo/legend-test.js
new file mode 100644
index 0000000000000000000000000000000000000000..361da6767f3f3946e51754dfb6f7b3ee68672b55
--- /dev/null
+++ b/projects/tsmo/legend-test.js
@@ -0,0 +1,84 @@
+/**
+ * Created by gavorhes on 12/18/2015.
+ */
+import quickMap from '../../src/olHelpers/quickMap';
+import mapMove from '../../src/olHelpers/mapMove';
+import mapPopup from '../../src/olHelpers/mapPopup';
+import ItsLayerCollection from '../../src/collections/ItsLayerCollection';
+import LayerLegend from '../../src/collections/LayerLegend';
+
+let map = quickMap();
+
+mapMove.init(map);
+mapPopup.init(map);
+
+let itsLayerCollection = new ItsLayerCollection(map);
+
+let layerArray = [
+    {
+        groupName: 'ITS Inventory Layers',
+        collapse: false,
+        addCheck: false,
+        items: itsLayerCollection.layers
+    }
+];
+
+let legend = new LayerLegend(layerArray, 'legend-container', {});
+
+//
+//let layerArry = itsLayerCollection.layers.slice(0, 3);
+//
+//layerArry.push(
+//    {
+//        groupName: 'Group 1',
+//        collapse: false,
+//        items: itsLayerCollection.layers.slice(3, 6)
+//    }
+//);
+//
+//layerArry.push(itsLayerCollection.layers[6]);
+//
+//let collection2 = itsLayerCollection.layers.slice(7, 9);
+//collection2.push({
+//
+//    groupName: 'Group 3',
+//    collapse: false,
+//    items: itsLayerCollection.layers.slice(10, 12)
+//
+//});
+//
+//layerArry.push(
+//    {
+//        groupName: 'Group 2',
+//        collapse: true,
+//        items: collection2
+//    }
+//);
+//
+//let layerArray = [
+//    itsLayerCollection.layers[0],
+//    itsLayerCollection.layers[1],
+//    {
+//
+//        groupName: 'Group 1',
+//        collapse: false,
+//        items: [itsLayerCollection.layers[2], itsLayerCollection.layers[3]]
+//    },
+//    {
+//        groupName: 'Group 2',
+//        collapse: true,
+//        addCheck: false,
+//        items: [
+//            itsLayerCollection.layers[4],
+//            itsLayerCollection.layers[5],
+//            {
+//                groupName: 'Group 3',
+//                collapse: false,
+//                items: [itsLayerCollection.layers[6], itsLayerCollection.layers[7]]
+//            }
+//        ]
+//    }
+//];
+
+
+
diff --git a/projects/tsmo/main-report.js b/projects/tsmo/main-report.js
new file mode 100644
index 0000000000000000000000000000000000000000..6b8d9489a6d0f66010cca739f8aa83cf6c6924c9
--- /dev/null
+++ b/projects/tsmo/main-report.js
@@ -0,0 +1,198 @@
+/**
+ * Created by gavorhes on 12/22/2015.
+ */
+
+import quickMap from '../../src/olHelpers/quickMap';
+import LayerBaseVectorGeoJson from '../../src/layers/LayerBaseVectorGeoJson';
+import mapPopup from '../../src/olHelpers/mapPopup';
+import * as tipLayerStyles from './tipStyleFunction';
+import LayerEsriMapServer from '../../src/layers/LayerEsriMapServer';
+import ItsLayerCollection from '../../src/collections/ItsLayerCollection';
+import LayerLegend from '../../src/collections/LayerLegend';
+import {} from '../../lib/jquery.floatThead';
+let $ = require('jquery');
+
+let suppressLowScores = true;
+
+function tipStyleReport(feature, resolution) {
+    "use strict";
+
+    let seg_score = feature.getProperties()['z'];
+
+    if (seg_score < 1 && suppressLowScores) {
+        return null;
+    } else {
+        return tipLayerStyles.tipStyle(feature, resolution);
+    }
+}
+
+
+(function () {
+    "use strict";
+
+    let $crashData = $('#crash-data');
+
+    function getCrashData(pdpId) {
+        $crashData.html('');
+
+        $.get('getcrash', {pdpId: pdpId}, function (d) {
+            $crashData.html(d);
+        }, 'text')
+    }
+
+    // add the cell classes
+    $('.param-val').each(function () {
+        let z = parseFloat($(this).attr('data-crumb'));
+        let cellClass = 'cell-max';
+
+        if (z < -2.5) {
+            cellClass = 'cell-min';
+        } else if (z < -1.5) {
+            cellClass = 'cell-very-low';
+        } else if (z < -0.5) {
+            cellClass = 'cell-low';
+        } else if (z < 0.5) {
+            cellClass = 'cell-mid';
+        } else if (z < 1.5) {
+            cellClass = 'cell-high';
+        } else if (z < 2.5) {
+            cellClass = 'cell-very-high';
+        }
+
+        $(this).addClass(cellClass);
+    });
+
+    let map = quickMap({center: {x: -10012438, y: 5548095}, zoom: 6, minZoom: 6});
+
+    let wisDotRegions = new LayerEsriMapServer(
+        'http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/MetaManager/Metamanager_regions/MapServer',
+        {
+            minZoom: 6,
+            maxZoom: 15,
+            opacity: 0.4
+        });
+
+    map.addLayer(wisDotRegions.olLayer);
+
+    let reportSegments = new LayerBaseVectorGeoJson('',
+        {
+            loadCallback: function (theLayer) {
+                map.getView().fit(theLayer.source.getExtent(), map.getSize());
+            },
+            transform: {featureProjection: 'EPSG:3857', dataProjection: 'EPSG:3857'},
+            style: tipStyleReport,
+            params: {d: (new Date()).getTime().toString()},
+            zIndex: 10
+        }
+    );
+
+    let geoJsonData = JSON.parse($('#geojson-data').val());
+    reportSegments.addFeatures(geoJsonData);
+
+    map.addLayer(reportSegments.olLayer);
+    map.getView().fit(reportSegments.source.getExtent(), map.getSize());
+
+    let itsLayerCollection = new ItsLayerCollection(map);
+
+    for (let l of itsLayerCollection.layers) {
+        l.zIndex = 10;
+        l.opacity = 0.5;
+    }
+
+    let layerArray = [
+        {
+            groupName: 'ITS Inventory Layers',
+            collapse: true,
+            addCheck: true,
+            items: itsLayerCollection.layers
+        }
+    ];
+
+    let legend = new LayerLegend(layerArray, 'legend-container', {});
+
+    let $resultsTable = $('#results-table');
+    let $resultsContainer = $('#results-container');
+
+
+
+    mapPopup.addMapClickFunction(function () {
+        $('.selected-row').removeClass('selected-row');
+    });
+
+    //return undefined in popup callback to prevent default popup display
+    let selectionLayer = mapPopup.addVectorPopup(reportSegments, function (props) {
+            let pdp = props['p'];
+            getCrashData(pdp);
+            let $selectedRow = $resultsTable.find(`#${pdp}`);
+            $selectedRow.addClass('selected-row');
+            $resultsContainer.find('table tbody').scrollTop(0);
+            $resultsContainer.find('table tbody').scrollTop($selectedRow.offset().top - 60);
+            return undefined;
+        },
+        {olStyle: tipLayerStyles.tipStyleSelection}
+    );
+
+    mapPopup.addMapClickFunction(function () {
+        $crashData.html('');
+    });
+
+    $('.selectable-row').click(function () {
+        let mapView = map.getView();
+        selectionLayer.getSource().clear();
+        $('.selected-row').removeClass('selected-row');
+        $(this).addClass('selected-row');
+        let pdp = parseInt(this.id);
+        let feats = reportSegments.source.getFeatures();
+        getCrashData(pdp);
+        for (let feature of feats) {
+            if (feature.getProperties()['p'] == pdp) {
+                selectionLayer.getSource().addFeature(feature);
+                mapView.fit(feature.getGeometry().getExtent(), map.getSize());
+                mapView.setZoom(mapView.getZoom() - 2);
+                return;
+            }
+        }
+    });
+
+    let $scoreUnderOneRows = $('.score-under-one-flag');
+
+    $('#show-all-toggle').click(function () {
+        if (this.checked) {
+            $scoreUnderOneRows.removeClass('score-under-one');
+            suppressLowScores = false;
+        } else {
+            $scoreUnderOneRows.addClass('score-under-one');
+            suppressLowScores = true;
+        }
+        reportSegments.refresh();
+        mapPopup.closePopup();
+        $('.selected-row').removeClass('selected-row');
+        $('#crash-table').html('');
+
+    }).prop('checked', false);
+
+    let oneRowTd = $resultsTable.find('tr:nth-of-type(2)').children('td');
+    let $firstTrTh = $resultsTable.find('tr:first-of-type').children('th');
+
+    function updateTableColumnWidth(){
+
+        let colWidths = [];
+
+        $firstTrTh.each(function(i, el){
+            colWidths.push($(el).width());
+        });
+
+        oneRowTd.each(function(i, el){
+            $(el).width(colWidths[i]);
+        });
+    }
+
+    updateTableColumnWidth();
+
+    $resultsTable.find('tr:first-of-type').click(function(){
+       setTimeout(updateTableColumnWidth, 50);
+
+    });
+
+
+})();
diff --git a/projects/tsmo/main-ui.js b/projects/tsmo/main-ui.js
new file mode 100644
index 0000000000000000000000000000000000000000..48be215361fda62917210e195d6fbd79426932b0
--- /dev/null
+++ b/projects/tsmo/main-ui.js
@@ -0,0 +1,126 @@
+/**
+ * Created by gavorhes on 12/14/2015.
+ */
+import tipConfig from './TipConfig';
+import mapMove from '../../src/olHelpers/mapMove';
+import LayerLegend from '../../src/collections/LayerLegend';
+const $ = require('jquery');
+
+/**
+ * start the UI setup
+ */
+export function startUi() {
+
+    tipConfig.$loadingGif = $('#loading-gif');
+    tipConfig.$loadingGif.fadeIn();
+
+    tipConfig.$regionSelector = $('#region-selector');
+    tipConfig.$regionSelector.val('all');
+
+    // configure the preset selector
+    let $presetSelector = $('#preset-selector');
+    for (let i = 0; i < tipConfig._presetArray.length; i++) {
+        let weights = {};
+        let sumCheck = 0;
+        for (let j = 0; j < tipConfig._sliderParamArray.length; j++) {
+            sumCheck += tipConfig._presetArray[i][1][j][0];
+            weights[tipConfig._sliderParamArray[j][0].toLowerCase().replace(/ /g, '-')] = tipConfig._presetArray[i][1][j];
+        }
+        if (sumCheck != 100) {
+            alert('Sum not equal to 100 for preset ' + tipConfig._presetArray[i][0]);
+        }
+
+        let optionHtml = '<option value="';
+        optionHtml += JSON.stringify(weights).replace(/"/g, '&quot;');
+        optionHtml += '"';
+        optionHtml += (i == 0 ? ' selected="selected"' : '') + '>' + tipConfig._presetArray[i][0] + '</option>';
+
+        $presetSelector.append(optionHtml);
+    }
+
+    //enable get help button
+    $('#tip-help').click(function () {
+
+        let win = window.open('tip/help', '_blank');
+        if (win) {
+            //Browser has allowed it to be opened
+            win.focus();
+        } else {
+            //Browser has blocked it
+            alert('Please allow popups for this site');
+        }
+    });
+}
+
+
+/**
+ * finish the UI setup
+ */
+export function endUi() {
+    tipConfig.$loadingGif.fadeOut();
+
+    $('#make-report').click(function () {
+        let params = mapMove.mapExtent;
+        params['paramWeights'] = JSON.stringify(tipConfig.sliders.getParams());
+        params['region'] = $('#region-selector').val();
+        params['preset'] = $('#preset-selector option:selected').text();
+
+        let win = window.open('tip/getreport?' + $.param(params), '_blank');
+        if (win) {
+            //Browser has allowed it to be opened
+            win.focus();
+        } else {
+            //Browser has blocked it
+            alert('Please allow popups for this site');
+        }
+    });
+
+    //add events to the preset selector
+    let $presetSelector = $('#preset-selector');
+
+    // add preset selector change handler
+    let first = true;
+    $presetSelector.change(function () {
+
+        var weightSelection = JSON.parse(this.value);
+        tipConfig.sliders.setValues(weightSelection);
+        if (first) {
+            first = false;
+        } else {
+            mapMove.triggerLyrLoad(tipConfig.tipSegmentLayer);
+        }
+    });
+
+    $presetSelector.trigger('change');
+
+    $('#region-selector').change(function(){
+        "use strict";
+        mapMove.triggerLyrLoad(tipConfig.tipSegmentLayer);
+    });
+
+
+    tipConfig.sliders.addSlideFinishedFunction(function () {
+        $presetSelector[0].selectedIndex = 0;
+        mapMove.triggerLyrLoad(tipConfig.tipSegmentLayer);
+    });
+}
+
+export function endUiMap() {
+
+    mapMove.addCallback(function (ext, zoom, evt) {
+        $('#make-report').prop('disabled', !(zoom >= tipConfig.tipSegmentLayerMinZoom));
+    });
+
+    let layerArray = [
+        tipConfig.tipSegmentLayer,
+        tipConfig.metamanagerSegments,
+        {
+            groupName: 'ITS Inventory Layers',
+            collapse: true,
+            addCheck: false,
+            items: tipConfig.itsLayerCollection.layers
+        }
+    ];
+
+    let legend = new LayerLegend(layerArray, 'legend-container', {});
+}
diff --git a/projects/tsmo/main.js b/projects/tsmo/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..3a73607be42fef3053324fbb9fc39728e370899d
--- /dev/null
+++ b/projects/tsmo/main.js
@@ -0,0 +1,166 @@
+/**
+ * Created by gavorhes on 12/14/2015.
+ */
+import Sliders from '../../src/collections/Sliders';
+import tipConfig from './TipConfig';
+import mapPopup from '../../src/olHelpers/mapPopup';
+import * as uiSetup from './main-ui';
+import LayerEsriMapServer from '../../src/layers/LayerEsriMapServer';
+import ItsLayerCollection from '../../src/collections/ItsLayerCollection';
+import quickMap from '../../src/olHelpers/quickMap';
+import TipSegmentLayer from './TipSegmentLayer';
+
+(function () {
+    "use strict";
+
+    glob.config = tipConfig;
+
+    uiSetup.startUi();
+
+    let sliders = new Sliders(tipConfig._sliderParamArray, 'slider-container');
+    tipConfig.sliders = sliders;
+
+    let map = quickMap({minZoom: 7});
+
+    tipConfig.map = map;
+
+    let wisDotRegions = new LayerEsriMapServer(
+        'http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/MetaManager/Metamanager_regions/MapServer',
+        {
+            minZoom: 6,
+            maxZoom: 12,
+            name: 'WisDOT Regions',
+            useEsriStyle: true
+        });
+
+    map.addLayer(wisDotRegions.olLayer);
+
+    //initialize the tip segment layer
+    let tipSegmentLayer = new TipSegmentLayer(sliders,
+        {
+            selectionStyle: {
+                color: 'rgba(0,0,255,0.5)',
+                width: 7
+            },
+            $loadingGif: tipConfig.$loadingGif,
+            $regionSelector: tipConfig.$regionSelector
+        }
+    );
+    map.addLayer(tipSegmentLayer.olLayer);
+    tipConfig.tipSegmentLayer = tipSegmentLayer;
+
+    let metamanagerSegments = new LayerEsriMapServer(
+        'http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/MetaManager/MM_All_Segments/MapServer',
+        {
+            minZoom: 7,
+            visible: false,
+            name: 'Metamanager Segments'
+        });
+
+    map.addLayer(metamanagerSegments.olLayer);
+    tipConfig.metamanagerSegments = metamanagerSegments;
+
+    //initialize the ITS layer collection
+    tipConfig.itsLayerCollection = new ItsLayerCollection(map);
+
+    uiSetup.endUi();
+    uiSetup.endUiMap();
+
+})();
+
+
+//
+//let sliders;
+//    let app;
+//    let extraLayers = {};
+//
+//    $(function () {
+//        return;
+//        // create the sliders object
+//        sliders = new SliderGroup(tipConfig._sliderParamArray, 'slider-container');
+//
+//        $('#btnResetDefault').click(function () {
+//            sliders.reset();
+//        });
+//
+
+//
+//        let geoJsonArray = [];
+//
+//        for (i = 0; i < itsLayerConfig.length; i++) {
+//            geoJsonArray.push(new ItsGeoJsonLayer(itsLayerConfig[i], app.map, 'its-legend-items'));
+//        }
+//
+//        $('#showVis').prop('checked', true);
+//
+//        $('input[name=showAll]').change(function () {
+//            let $legendDivs = $('#its-legend-items > div');
+//            if (this.id == 'showAll' && this.checked) {
+//                $legendDivs.addClass('force-show');
+//            } else {
+//                $legendDivs.removeClass('force-show');
+//            }
+//        });
+//
+//        function showHide() {
+//            let shown = true;
+//            let $legend = $('#its-legend-items');
+//            let $pic = $('#show-hide');
+//            return function () {
+//                if (shown) {
+//                    $legend.slideUp();
+//                    shown = false;
+//                    $pic.attr('src', $pic.attr('src').replace('up', 'down'));
+//                } else {
+//                    $legend.slideDown();
+//                    shown = true;
+//                    $pic.attr('src', $pic.attr('src').replace('down', 'up'));
+//                }
+//            }
+//        }
+//
+//
+//        $('#show-hide').click(showHide()).trigger('click');
+//
+//        $('#show-hide-all-its').prop('checked', true).change(function () {
+//
+//            for (let i = 0; i < geoJsonArray.length; i++) {
+//                geoJsonArray[i].groupVisible = this.checked;
+//            }
+//
+//        });
+//
+//
+//        let url = 'http://transportal.cee.wisc.edu/applications/arcgis2/rest/services/MetaManager/MM_All_Segments/MapServer';
+//
+//        extraLayers['mm-layer'] = new ol.layer.Tile({
+//            source: new ol.source.TileArcGISRest({
+//                url: url
+//            })
+//        });
+//
+//        let layerCheckboxes = $('#layer-switch').find('input[type="checkbox"]');
+//
+//        layerCheckboxes.each(function (idx, val) {
+//            val.checked = false;
+//        });
+//
+//        layerCheckboxes.click(function () {
+//            if (this.checked) {
+//                app.map.addLayer(extraLayers[this.id])
+//            } else {
+//                app.map.removeLayer(extraLayers[this.id])
+//            }
+//        });
+//
+//        //fade out the loading gif when complete
+//        $gif.fadeOut();
+//
+
+//
+
+//    });
+
+
+
+
diff --git a/projects/tsmo/slider-test.js b/projects/tsmo/slider-test.js
new file mode 100644
index 0000000000000000000000000000000000000000..6bf939d2b395e422fde1a898ff5ce6b7dfdc7523
--- /dev/null
+++ b/projects/tsmo/slider-test.js
@@ -0,0 +1,29 @@
+/**
+ * Created by gavorhes on 12/14/2015.
+ */
+
+import Sliders from '../../src/collections/Sliders';
+import {} from '../../src/jquery-plugin/dayRange';
+import tipConfig from './TipConfig';
+const $ = require('jquery');
+
+(function(){
+    //alert('here');
+
+    let dayRange = $('#dates').dayRange(10);
+
+
+    //let sliders = new Sliders(tipConfig._sliderParamArray, 'slider-container');
+    //glob.sliders = sliders;
+    //
+    //
+    //
+    //
+    //console.log(tipConfig);
+})();
+
+
+
+
+
+
diff --git a/projects/tsmo/tipStyleFunction.js b/projects/tsmo/tipStyleFunction.js
new file mode 100644
index 0000000000000000000000000000000000000000..33d6c2f262ed33015c29c3a06307a397af0790c9
--- /dev/null
+++ b/projects/tsmo/tipStyleFunction.js
@@ -0,0 +1,84 @@
+/**
+ * Created by gavorhes on 12/22/2015.
+ */
+import * as zoomResolutionConvert from '../../src/olHelpers/zoomResolutionConvert';
+import ol from '../../src/custom-ol';
+
+
+/**
+ * tip style function
+ * @param feature
+ * @param resolution
+ * @returns {*[]}
+ */
+export function tipStyle(feature, resolution) {
+
+    let zoom = zoomResolutionConvert.resolutionToZoom(resolution);
+
+    let width = 5;
+
+    if (zoom >= 18){
+        width = 10;
+    } else if (zoom >= 14) {
+        width = 8;
+    } else if (zoom >= 12) {
+        width = 7;
+    } else if (zoom >= 10) {
+        width = 6;
+    }
+
+    let seg_score = feature.getProperties()['z'];
+    let hex_color;
+    if (seg_score < -2.5) {
+        hex_color = '#00ff00';
+    } else if (seg_score < -1.5) {
+        hex_color = '#55ff00';
+    } else if (seg_score < -0.5) {
+        hex_color = '#aaff00';
+    } else if (seg_score < 0.5) {
+        hex_color = '#ffff00';
+    } else if (seg_score < 1.5) {
+        hex_color = '#ffaa00';
+    } else if (seg_score < 2.5) {
+        hex_color = '#ff5500';
+    } else {
+        hex_color = '#ff0000'
+    }
+
+    return [new ol.style.Style({
+        stroke: new ol.style.Stroke({
+            color: hex_color,
+            width: width
+        })
+    })];
+}
+
+/**
+ * selection style
+ * @param feature
+ * @param resolution
+ * @returns {*[]}
+ */
+export function tipStyleSelection (feature, resolution) {
+
+    let zoom = zoomResolutionConvert.resolutionToZoom(resolution);
+
+    let width = 5;
+
+    if (zoom >= 18){
+        width = 10;
+    } else if (zoom >= 14) {
+        width = 8;
+    } else if (zoom >= 12) {
+        width = 7;
+    } else if (zoom >= 10) {
+        width = 6;
+    }
+
+    return [new ol.style.Style({
+        stroke: new ol.style.Stroke({
+            color: '#0074ff',
+            width: width
+        })
+    })];
+}
\ No newline at end of file
diff --git a/src/collections/ItsLayerCollection.js b/src/collections/ItsLayerCollection.js
index 78d40781ef13c7469e06d7555ad873f08d724857..d446ae3978ac1fd69e130175d74cda76ca84ddb2 100644
--- a/src/collections/ItsLayerCollection.js
+++ b/src/collections/ItsLayerCollection.js
@@ -123,7 +123,7 @@ class ItsLayerCollection {
 
     /**
      * Create a collection of all ITS layers
-     * @param {Map} theMap the openlayers map
+     * @param {ol.Map} theMap the openlayers map
      * @param {Array} [exclude=[]] array of Its layer identifiers to exclude
      *
      * BLUE Bluetooth Detector - Bluetooth Detector
diff --git a/src/collections/LayerLegend.js b/src/collections/LayerLegend.js
index 6d7bb329006305e1be9b5ad88544ae2a72fa1153..d8526850460bf073231053d387f822778ec2b8cc 100644
--- a/src/collections/LayerLegend.js
+++ b/src/collections/LayerLegend.js
@@ -1,7 +1,7 @@
 /**
  * Created by gavorhes on 12/16/2015.
  */
-const $ = require('jquery');
+import $ from '../jquery';
 import provide from '../util/provide';
 import makeGuid from '../util/makeGuid';
 import mapMove from '../olHelpers/mapMove';
diff --git a/src/collections/Sliders.js b/src/collections/Sliders.js
index 3ea46e358ceba175f01f7ca63cb9bfe5c7afd325..6254f48dd6c71e14660f8a42847b1cb41af5e977 100644
--- a/src/collections/Sliders.js
+++ b/src/collections/Sliders.js
@@ -3,7 +3,7 @@
  */
 import provide from '../util/provide';
 let nm = provide('collections');
-let $ = require('jquery');
+import $ from '../jquery';
 
 class _Slider {
 
diff --git a/src/jquery-plugin/dayRange.js b/src/jquery-plugin/dayRange.js
index 21037cddb487ad792c7d12391890fcf091a9efc0..75b3b515e31d698daa0ce0fa8f53ec37b58204ca 100644
--- a/src/jquery-plugin/dayRange.js
+++ b/src/jquery-plugin/dayRange.js
@@ -1,4 +1,4 @@
-let $ = require('jquery');
+import $ from '../jquery';
 require('jquery-ui/datepicker');
 import provide from '../util/provide';
 let nm = provide('jQueryPlugin');
diff --git a/src/jquery-plugin/mediaControl.js b/src/jquery-plugin/mediaControl.js
index 10565eac19b4e573422a5d37e07a664a83018cde..5fbe63aefa9bfdf0c94f1371ccda053d66223e35 100644
--- a/src/jquery-plugin/mediaControl.js
+++ b/src/jquery-plugin/mediaControl.js
@@ -1,4 +1,4 @@
-const $ = require('jquery');
+import $ from '../jquery';
 import provide from '../util/provide';
 let nm = provide('jQueryPlugin');
 
diff --git a/src/jquery-plugin/rangeChange.js b/src/jquery-plugin/rangeChange.js
index 19d8f69b241cc88343f9003707751fe206bf2b88..eea8b887fe1ee65b007acf17f004d488cfedb0dc 100644
--- a/src/jquery-plugin/rangeChange.js
+++ b/src/jquery-plugin/rangeChange.js
@@ -4,7 +4,7 @@
  * Created by gavorhes on 11/2/2015.
  */
 
-const $ = require('jquery');
+import $ from '../jquery';
 let mouseIn = false;
 let mouseDown = false;
 let timeout = null;
diff --git a/src/jquery-plugin/rpPicker.js b/src/jquery-plugin/rpPicker.js
index dc7d9586733babe6792346c777fb8c15047d77df..05ffd495216cec300b2e9277fe5031d8113db9e5 100644
--- a/src/jquery-plugin/rpPicker.js
+++ b/src/jquery-plugin/rpPicker.js
@@ -1,4 +1,4 @@
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 
 import quickMap from '../olHelpers/quickMap';
diff --git a/src/jquery-plugin/ssaCorridorPicker.js b/src/jquery-plugin/ssaCorridorPicker.js
index eaded167b22e150df80579251386e2e949217f26..67855fb46976609139b2ed524c309de59a9ba49c 100644
--- a/src/jquery-plugin/ssaCorridorPicker.js
+++ b/src/jquery-plugin/ssaCorridorPicker.js
@@ -1,4 +1,4 @@
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 
 import quickMap from '../olHelpers/quickMap';
diff --git a/src/jquery.js b/src/jquery.js
new file mode 100644
index 0000000000000000000000000000000000000000..c863dfde2f8fe7d5dc7916d3711bcef5368b8ecf
--- /dev/null
+++ b/src/jquery.js
@@ -0,0 +1,6 @@
+/**
+ * Created by gavorhes on 5/3/2016.
+ */
+const $ = require('jquery/dist/jquery.min');
+
+export default $;
diff --git a/src/layers/LayerBase.js b/src/layers/LayerBase.js
index ed07436559c94fe7e060a59e885fe646d0a8ca01..699e4a96f13667a9c28b76f05fc6f5e6cf3a73b5 100644
--- a/src/layers/LayerBase.js
+++ b/src/layers/LayerBase.js
@@ -1,4 +1,4 @@
-const $ = require('jquery');
+import $ from '../jquery';
 import makeGuid from '../util/makeGuid';
 import * as zoomResolutionConvert from '../olHelpers/zoomResolutionConvert';
 import provide from '../util/provide';
diff --git a/src/layers/LayerBaseVector.js b/src/layers/LayerBaseVector.js
index 70af8274212c8afbd2004a2bda8b1e855ae9ff63..8dc214ae8f3ad7b4180892971876e16dddd85361 100644
--- a/src/layers/LayerBaseVector.js
+++ b/src/layers/LayerBaseVector.js
@@ -1,4 +1,4 @@
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import LayerBase from './LayerBase';
 import mapMove from '../olHelpers/mapMove';
@@ -183,7 +183,7 @@ class LayerBaseVector extends LayerBase {
 
     /**
      * get the style definition
-     * @type {ol.Style|function}
+     * @type {ol.Style|styleFunc}
      */
     get style() {
         return this._style;
diff --git a/src/layers/LayerBaseVectorEsri.js b/src/layers/LayerBaseVectorEsri.js
index 941cd3a6c2de4da8912a57c4c26107922393e0ab..70862a35bc3ca20244b9b284d1b1f13c1df86f02 100644
--- a/src/layers/LayerBaseVectorEsri.js
+++ b/src/layers/LayerBaseVectorEsri.js
@@ -1,7 +1,7 @@
 /**
  * Created by gavorhes on 11/2/2015.
  */
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import LayerBaseVector from './LayerBaseVector';
 import * as esriToOl from '../olHelpers/esriToOlStyle';
diff --git a/src/layers/LayerBaseVectorGeoJson.js b/src/layers/LayerBaseVectorGeoJson.js
index 73c381c1fe76d0c3bcd0029ddf57b87f50e2bedd..884a280b8d73dee149904398de5a42c93c17bf69 100644
--- a/src/layers/LayerBaseVectorGeoJson.js
+++ b/src/layers/LayerBaseVectorGeoJson.js
@@ -1,7 +1,7 @@
 /**
  * Created by gavorhes on 11/2/2015.
  */
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import LayerBaseVector from './LayerBaseVector';
 import provide from '../util/provide';
diff --git a/src/layers/LayerBaseXyzTile.js b/src/layers/LayerBaseXyzTile.js
index 3ccefd89a1dc0ae847cffc6fbc915f8b0ad00c09..5b6ec6ebf37bd8105ccec6d15f87e31723b878e9 100644
--- a/src/layers/LayerBaseXyzTile.js
+++ b/src/layers/LayerBaseXyzTile.js
@@ -1,7 +1,7 @@
 /**
  * Created by gavorhes on 12/4/2015.
  */
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import LayerBase from './LayerBase';
 import * as esriToOl from '../olHelpers/esriToOlStyle';
diff --git a/src/layers/LayerEsriMapServer.js b/src/layers/LayerEsriMapServer.js
index 47a23d5bbafae211f13f35fe3ac32da8e03ed766..759775abdde1237b9a1acbe30066ae5de4e812b0 100644
--- a/src/layers/LayerEsriMapServer.js
+++ b/src/layers/LayerEsriMapServer.js
@@ -1,7 +1,7 @@
 /**
  * Created by gavorhes on 12/7/2015.
  */
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import LayerBase from './LayerBase';
 import * as esriToOl from '../olHelpers/esriToOlStyle';
diff --git a/src/layers/LayerItsInventory.js b/src/layers/LayerItsInventory.js
index cb793ff76363300e1a42061579f5d9638ba03cde..e4405436e30a8fa1842918f47669c3730f06fda1 100644
--- a/src/layers/LayerItsInventory.js
+++ b/src/layers/LayerItsInventory.js
@@ -2,7 +2,7 @@
  * Created by gavorhes on 12/8/2015.
  */
 
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import LayerBaseVectorGeoJson from './LayerBaseVectorGeoJson';
 import mapMove from '../olHelpers/mapMove';
diff --git a/src/layers/LayerRealEarthTile.js b/src/layers/LayerRealEarthTile.js
index c7bdddda6528d8f9469d9ece223e851ea52caa3a..de867095bb3feb82c85250bf1e121f566ab5e80d 100644
--- a/src/layers/LayerRealEarthTile.js
+++ b/src/layers/LayerRealEarthTile.js
@@ -2,7 +2,7 @@
  * Created by gavorhes on 11/4/2015.
  */
 
-const $ = require('jquery');
+import $ from '../jquery';
 import LayerBaseXyzTile from './LayerBaseXyzTile';
 import RealEarthAnimateTile from '../mixin/RealEarthAnimateTile';
 let mixIns = require('es6-mixins');
diff --git a/src/mixin/RealEarthAnimate.js b/src/mixin/RealEarthAnimate.js
index 900105d72c679d8ba991953a638571d16ae8d36f..d2f38fc576a04fcd4f962490b1bf59fe9ef2b9c1 100644
--- a/src/mixin/RealEarthAnimate.js
+++ b/src/mixin/RealEarthAnimate.js
@@ -3,7 +3,7 @@
  */
 import provide from '../util/provide';
 import mapPopup from '../olHelpers/mapPopup';
-const $ = require('jquery');
+import $ from '../jquery';
 let nm = provide('mixin');
 
 
diff --git a/src/olHelpers/mapMoveCls.js b/src/olHelpers/mapMoveCls.js
index c93ba7f06549f898fe53347a78117bb0c58cf9b8..127ac8b64f8d5525341d010331ad9901d37e9e0e 100644
--- a/src/olHelpers/mapMoveCls.js
+++ b/src/olHelpers/mapMoveCls.js
@@ -3,7 +3,7 @@
  */
 
 
-const $ = require('jquery');
+import $ from '../jquery';
 import MapInteractionBase from './mapInteractionBase';
 import * as checkDefined from '../util/checkDefined';
 import provide from '../util/provide';
diff --git a/src/olHelpers/mapPopupCls.js b/src/olHelpers/mapPopupCls.js
index 86c92562cc661ad6c6f3a79e89d3de39a73083d0..8519140d138de1b8f4d443f9fc9c312e139ff411 100644
--- a/src/olHelpers/mapPopupCls.js
+++ b/src/olHelpers/mapPopupCls.js
@@ -2,7 +2,7 @@
  * Created by gavorhes on 11/3/2015.
  */
 
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import MapInteractionBase from './mapInteractionBase';
 import propertiesZoomStyle from '../olHelpers/propertiesZoomStyle';
diff --git a/src/olHelpers/quickMapBase.js b/src/olHelpers/quickMapBase.js
index 099014061fd1718d3eb83d7aa79e6ca29e5a9ce8..338af5717ce9b93ad5a84d4e0b73cec307e050e3 100644
--- a/src/olHelpers/quickMapBase.js
+++ b/src/olHelpers/quickMapBase.js
@@ -6,7 +6,7 @@
  * Created by gavorhes on 12/15/2015.
  */
 
-const $ = require('jquery');
+import $ from '../jquery';
 import ol from '../custom-ol';
 import provide from '../util/provide';
 import mapMove from './mapMove';