<commit_before><!DOCTYPE html>
<html>
<!--
# Copyright 2016 NeuroData (http://neurodata.io)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Designed, Developed, and Maintained by <NAME>
# <EMAIL>
# github.com/alexbaden

!-->
<head>
  <title>NeuroDataViz: viewing {{ project_name }}</title>
  {% load staticfiles %}
  {# boostrap files #}
  <link rel="stylesheet" type ="text/css" href="{% static 'bootstrap/css/bootstrap.css' %}" />

  {# jquery UI #}
  <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">

  {# leaflet files #}
  <link rel="stylesheet" href="{% static 'ndv/leaflet/leaflet.css' %}" />
  <link rel="stylesheet" href="{% static 'ndv/css/Control.Loading.css' %}" />

  {# buttons, buttons, buttons #}
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">

  {# css file #}
  <link rel="stylesheet" href="{% static 'ndv/css/viewer.css' %}" />

  {% if GOOGLE_ANALYTICS_PROPERTY_ID %}
    {% include "ndv/analytics.html" %}
  {% endif %}

  <script src="{% static 'ndv/leaflet/leaflet-src.js' %}"></script>
  {#<script src="{% static 'ndv/leaflet/leaflet.js' %}"></script>#}
  <script src="{% static 'ndv/js/Control.Loading.js' %}"></script>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
  <script src="{% static 'ndv/js/spin.min.js' %}"></script>
  <script src="{% static 'ndv/js/easy-button.js' %}"></script>
  <script src="{% static 'ndv/js/ocp-leaflet.js' %}"></script>
  {# threejs #}
  <script src="{% static 'ndv/js/threejs/three.min.js' %}"></script>

	</head>
<body>

  {# Begin Modal Code Includes #}
  {% include "ndv/help.html" %}

  {% include "ndv/nav.html" %}

  {% include "ndv/add_data.html" %}

  {% include "ndv/login.html" %}
  {# End Modal Code Includes #}

  <div id="map"></div>
  <script id="vertexShader" type="x-shader/x-vertex">
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  </script>

  <script id="fragmentShader" type="x-shader/x-fragment">
    varying vec2 vUv;

    uniform vec3 color;
    uniform sampler2D texture;

    uniform float opacity;

    // remapping
    uniform float minval;
    uniform float maxval;

    // gamma
    uniform float gamma;

    void main() {
      vec4 tmpColor;

      #ifdef REMAP
      tmpColor = texture2D(texture, vUv) * vec4(color.x, color.y, color.z, opacity);
      // remap color to new intensity range
      tmpColor.x = min( max( (tmpColor.x - minval)/maxval, 0.0), 1.0 );
      tmpColor.y = min( max( (tmpColor.y - minval)/maxval, 0.0), 1.0 );
      tmpColor.z = min( max( (tmpColor.z - minval)/maxval, 0.0), 1.0 );
      #else
      tmpColor = texture2D(texture, vUv) * vec4(color.x, color.y, color.z, opacity);
      #endif

      #ifdef GAMMACOR
      tmpColor.rgb = pow(tmpColor.rgb, vec3(1.0 / gamma));
      #endif

      gl_FragColor = tmpColor;

    }
  </script>
  <script>
    // AB TODO use strict

    // global variables
    var ndv = {};
    ndv.querytool = false;
    ndv.querytoken = 'none';
    ndv.querychannel = 'none';
    ndv.queryserver = 'none';

    // timeseries globals
    ndv.playdelay = 1;

    // marker array
    ndv.markers = [];

    // upsample settings
    ndv.upsample_num = 2;

    // dataset info
    ndv.xstart = {{ xstart }};
    ndv.xmin = {{ xoffset }};
    ndv.xmax = {{ xsize }} + {{ xoffset }} - 1;

    ndv.ystart = {{ ystart }};
    ndv.ymin = {{ yoffset }};
    ndv.ymax = {{ ysize }} + {{ yoffset }} - 1;

    ndv.zmin = {{ zoffset }};
    ndv.zmax = {{ zsize }} + {{ zoffset }} - 1;

    ndv.minres = {{ minres }};
    ndv.maxres = {{ maxres }};
    ndv.res = {{ res }};
    // set zindex to some default (we pick the lowest slice)
    ndv.zindex = {{ zstart }};

     // create the map first
		var map = L.map('map', {
      crs: L.CRS.Simple,
			doubleClickZoom: false,
    });

    // add loading symbol
    var spinOpts = {
      lines: 13,
      length: 3,
      width: 3,
      radius: 5,
      rotate: 13,
      speed: 1.7,
      top: "83%",
    };
    var loadingControl = L.Control.loading({
      spinjs: true,
      spin: spinOpts
    });
    map.addControl(loadingControl);

    // define layers (namespaced with a layers. prefix)
    var layers = {};

    // add layer function
    function addLayer(layername, layertype, layerpropagate, url, server, token, channel, color) {

      layers[layername] = {
        'tileLayer': null,
        'server': server,
        'token': token,
        'channel': channel,
        'type': layertype,
        'color': color,
        'opacity': 1.0,
        'min': 0.0,
        'max': 1.0,
        'gamma': 1.0,
        'blending': THREE.NormalBlending,
      }

      var opacity;
      var propagate;
      if (layertype == 'annotation') {
        opacity = 0.4; // annotations loaded at 40% opacity by default
        layers[layername].opacity = 0.4;
        propagate = 2; // annotations propagation status doesn't matter
      }
      else
      {
        opacity = 1.0;
        propagate = layerpropagate;
      }

      var curtime = 0;
      if (layertype == 'timeseries') {
        curtime = {{ starttime }};
      }

      layers[layername]['tileLayer'] = L.tileLayer.OCPLayer(
        url,
        {
          zindex: ndv.zindex,
          zoomReverse: true,
          minZoom: ndv.maxres,
          maxZoom: ndv.maxres + ndv.maxres + ndv.upsample_num,
          maxNativeZoom: ndv.maxres + ndv.maxres,
          tileSize: 512,
          opacity: opacity,
          propagate: propagate,
          curtime: curtime,
          continuousWorld: true,
        }
      );

      layers[layername].tileLayer.addTo(map);
    }

    function updateLayerUrl(layername, newUrl) {
      layers[layername].tileLayer.setUrl(newUrl);
      //layers[layername].tileLayer.smoothRedraw();
    }

    // load image layers first
    {% for layer in layers %}
      {% if layer.layertype != 'annotation' %}
      {# setup the URL #}

        {% if layer.channel %}
          var webtoken = '{{ layer.token }}/{{ layer.channel }}';
        {% else %}
          var webtoken = '{{ layer.token }}';
        {% endif %}

        {% if layer.tilecache %}
          {% if layer.layertype == 'timeseries' %}
            {% if layer.tilecache_server %}
              var url = 'http://{{ layer.tilecache_server }}/ocptilecache/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
            {% else %}
              var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
            {% endif %}
          {% else %}
            {% if layer.tilecache_server %}
            {# note: presently unclear what the tilecache URL endpoint should be #}
              var url = 'http://{{ layer.tilecache_server }}/ocptilecache/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
            {% else %}
              var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
            {% endif %}
          {% endif %}

        {% else %}
          {% if layer.layertype == 'timeseries' %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
          {% else %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% endif %}
        {% endif %}

        {# add the layer #}
        addLayer( "{{ layer.layer_name|cut:" " }}", "{{ layer.layertype }}",  "{{ layer.propagate }}", url, "{{ layer.server }}", "{{ layer.token }}", "{{ layer.channel }}", "{{ layer.color }}");
        {% endif %}
    {% endfor %}

    // now load annotations
    {% for layer in layers %}
      {% if layer.layertype == 'annotation' %}
      {# setup the URL #}

        {% if layer.channel %}
          var webtoken = '{{ layer.token }}/{{ layer.channel }}';
          {% if layer.color %}
            webtoken = webtoken + ':{{layer.color}}';
          {% endif %}
        {% else %}
          var webtoken = '{{ layer.token }}';
        {% endif %}

        {% if layer.tilecache %}
          {% if layer.layertype == 'timeseries' %}
            var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
          {% else %}
            var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% endif %}

        {% else %}
          {% if layer.color %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/mcfc/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% elif layer.layertype == 'timeseries' %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
          {% else %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% endif %}
        {% endif %}

        {# add the layer #}
        addLayer( "{{ layer.layer_name|cut:" " }}", "{{ layer.layertype }}", "{{ layer.propagate }}", url, "{{ layer.server }}", "{{ layer.token }}", "{{ layer.channel }}", "{{ layer.color }}");
        {% endif %}
    {% endfor %}


    function setThreeColor(color) {
      // given a color tag ('r','g','b','c','m','y') set the appropriate three color
      switch(color) {
        case 'R':
          return new THREE.Color("rgb(255,0,0)");
        case 'G':
          return new THREE.Color("rgb(0, 255, 0)");
        case 'B':
          return new THREE.Color("rgb(0, 0, 255)");
        case 'C':
          return new THREE.Color("rgb(0, 255, 255)");
        case 'M':
          return new THREE.Color("rgb(255, 0, 255)");
        case 'Y':
          return new THREE.Color("rgb(255, 255, 0)");
        default:
          return new THREE.Color("rgb(255, 255, 255)");
      }
    }

    // testing the webGL layer interface
    var glLayer = new L.WebGLLayer({
      zindex: ndv.zindex,
      zoomReverse: true,
      minZoom: ndv.maxres,
      maxZoom: ndv.maxres + ndv.maxres + ndv.upsample_num,
      maxNativeZoom: ndv.maxres + ndv.maxres,
      tileSize: 512,
    }).addTo(map);

    glLayer.draw = function() {
      var scene = new THREE.Scene();

      var worldsize = this._map.getSize();
      var origin = this._map.getPixelOrigin();
      var shift = this._map.getPixelBounds().min.subtract(origin);
      var camera = this._camera;

      // get size from local function since layers may not have loaded
      var size = this._getTileSize();

      var vertexShader = document.getElementById('vertexShader').text;
      var fragmentShader = document.getElementById('fragmentShader').text;

      function loadTexture(img, layer, offsetx, offsety, color, opacity, minval, maxval, gamma, blending) {
        // use dom element for textures
        var texture = new THREE.Texture({
          repeat: 1,
        });

        texture.image = img;
        texture.name = layer;
        if ( $(img).hasClass('leaflet-tile-loaded') ) {
          texture.needsUpdate = true;
        } else {
          layer.on('tileload', function(e) {
            if (e.tile == texture.image) {
              texture.needsUpdate = true;
            }
          }, this);
        }

        var geometry = new THREE.PlaneBufferGeometry(size, size);

        // some default uniforms
        var uniforms = {
          color: { type: "c", value: color },
          texture: { type: "t", value: texture },
          opacity: { type: "f", value: opacity },
          minval: { type: "f", value: minval },
          maxval: { type: "f", value: maxval },
          gamma: { type: "f", value: gamma },
        };

        // TODO only enable these when sliders have been moved
        var defines = {
          REMAP: true,
          GAMMACOR: true,
        }

        var material = new THREE.ShaderMaterial({
          uniforms: uniforms,
          defines: defines,
          vertexShader: vertexShader,
          fragmentShader: fragmentShader,
          transparent: true,
          blending: blending,
        });

        var plane = new THREE.Mesh( geometry, material );

        // dispose of material, geometry, and mesh on unload
        layer.on('tileunload', function(e) {
          if (e.tile == texture.image) {
            texture.dispose();
            geometry.dispose();
            geometry = undefined;
            material.dispose();
            material = undefined;
            scene.remove(plane);
            plane = undefined;
          }
        }, this);

        // offset tile
        plane.position.set(offsetx, offsety, 0);

        scene.add(plane);
      }


      if (Object.keys(layers).length == 0) {
        return;
      }


      for (var layer in layers) {
        var color = setThreeColor(layers[layer].color);
        var opacity = layers[layer].opacity;
        var minval = layers[layer].min;
        var maxval = layers[layer].max;
        var gamma = layers[layer].gamma;
        var blending = parseInt(layers[layer].blending);

        for (var tile in layers[layer].tileLayer._tiles) {
          var img = layers[layer].tileLayer._tiles[tile];

          var offset = this._getTilePos(tile);

          loadTexture(img, layers[layer].tileLayer, offset.x, offset.y, color, opacity, minval, maxval, gamma, blending);
        }
      }

      this._camera.position.z = 1;

      this._renderTarget = new THREE.WebGLRenderTarget( worldsize.x, worldsize.y );

      function render() {
        if (this._repeater) {
          cancelAnimationFrame(this._repeater);
        }
        this._repeater = requestAnimationFrame( render.bind(this) );
        this._renderer.render(scene, this._camera, this._renderTarget);
        this._renderToScreen();
      }
      render.bind(this)();

      //this._renderToScreen();
    }

    function coordinatesDebug(event) {
      // place a leaflet marker and a webgl marker at the click

      var point = map.project(event.latlng);
      var marker_options = {
        title: point.x + " " + point.y + " " + ndv.zindex,
      }

      // add leaflet marker
      L.marker(event.latlng, marker_options).addTo(map);

      var projPoint = map._getCenterOffset(event.latlng);
      // add GL marker
      glLayer._drawCircle( L.point([projPoint.x, -projPoint.y])  );
    };

    //map.on('click', coordinatesDebug);

    // set the initial view
    var center = map.unproject(L.point( ndv.xstart, ndv.ystart ), ndv.maxres + ndv.maxres - ndv.res);
		map.setView(center, ndv.maxres + ndv.maxres - ndv.res);

    // BEGIN propagate warning box
    var propwarn = L.control();
    propwarn.options.position = 'topright';
    propwarn.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'propwarnbox');
      this._div.innerHTML = '<div class="alert alert-danger" role="alert"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span><span class="sr-only">Warning:</span> The following layers are not propagated: <strong><span class="notpropchanlist"></span></strong>.<br />Image data may be missing at some resolutions!';
      return this._div;
    };
    propwarn.addTo(map);

    function checkPropagate() {
      $( ".propwarnbox" ).hide();
      $( ".notpropchanlist" ).empty();
      for (layer in layers) {
        if (layers[layer].tileLayer.options.propagate != 2) {
          $( ".propwarnbox" ).show();
          if ($( ".notpropchanlist" ).text().length > 0)  {
            $( ".notpropchanlist" ).append(", ");
          }
          $( ".notpropchanlist" ).append(layer);
        }
      }
    }
    checkPropagate();

    // END propagate warning box

    // BEGIN query result palette
    var queryres = L.control();
    queryres.options.position = 'topright';
    queryres.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'queryres');
      var header = '<div class="closequery" style="float:right;"><a id="closequerybox" href="#"><i class="fa fa-times fa-lg"></a></i></div><h4>Query Result</h4>';
      var querybody = '<div class="querybody"></div>';
      this._div.innerHTML = header + querybody;
      return this._div;
    };
		queryres.addTo(map);

    // disable (and then re-enable) clicking on mouseover
    queryres.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    queryres.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });
    // END query result palette

    // BEGIN project info palette
    var projinfo = L.control();
    projinfo.options.position = 'topright';
    projinfo.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'projinfo');
      var showicon = '<div id="projinfoicon" style="float:left;"><a id="showprojinfo" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-info fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="projinfobox" style="display:none;">';
      var title = '<div style="float:right;"><a id="hideprojinfo" href="#"><i class="fa fa-times fa-lg"></a></i></div><div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-info fa-stack-1x"></i></span></div><div class="headertext">Project Info</div></div>';
      var projinfodiv = '<div id="projinfotxt"></div>'
      var hiddendivend = '</div>'
      this._div.innerHTML = showicon + hiddendivstart + title  + projinfodiv + hiddendivend;
			return this._div;
    };
		projinfo.addTo(map);
    $( "#showprojinfo" ).on('click', function () {
      $( "#projinfobox" ).toggle( "slow", function () {
        $( "#projinfoicon" ).toggle( );
      });
    });
    $( "#hideprojinfo" ).on('click', function () {
      $( "#projinfobox" ).toggle( "slow" , function () {
        $( "#projinfoicon" ).toggle( );
      });
    });
    // disable (and then re-enable) clicking on mouseover
    projinfo.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    projinfo.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });
    function getProjInfo() {
      // we use the token from the first layer (for now)
      var infoToken = "{{ layers.0.token }}";
      var infoUrl = "http://{{ request.get_host }}{% url 'projinfo' queryargs='placeholder' %}";
      infoUrl = infoUrl.replace('placeholder', 'localhost/' + infoToken + '/')

      $( "#projinfotxt" ).load(infoUrl);
		}
    getProjInfo();

    // END project info palette

    // BEGIN image control palette
    var imagecontrols = L.control();
    imagecontrols.options.position = 'topright';
    imagecontrols.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'imagecontrols');
      var showicon = '<div id="imagecontrolsshow" style="float:left;"><a id="showimagecontrols" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-picture-o fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="imagecontrolshide" style="display:none;">';
      var title = '<div style="float:right;"><a id="hideimagecontrols" href="#"><i class="fa fa-times fa-lg"></a></i></div><div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-picture-o fa-stack-1x"></i></span></div><div class="headertext">Image Controls</div></div>';

      var imgsliderdiv = '<div id="image-sliders"></div>'

      var blendmode = '<br /><div id="blendmode">Blending: <select name="blendmode"><option value="1">Normal</option><option value="2">Additive</option><option value="3">Subtractive</option><option value="4">Multiply</option><option value="0">None</option></select><span style="float: right;"><a href="#"><i class="fa fa-info-circle fa-lg"></i></a></span></div>';

      var hiddendivend = '</div>';

      this._div.innerHTML = showicon + hiddendivstart + title + imgsliderdiv + blendmode + hiddendivend;
      return this._div;
    };
    imagecontrols.addTo(map);
    // update sliders (removes all layers and adds from layers var)
    function updateImageControlSliders() {
      $( "#image-sliders" ).empty();
      for (layer in layers) {
        var tmphtml = '<div class="controls-' + layer + '"><div class="controls-layer-name"><a href="#" class="remove-layer ' + layer + '"><i class="fa fa-times-circle"></i></a><a href="#">' + layer + '</a></div><div class="controls-slider-name">Opacity</div><div id="slider"><span class="opacity">' + layer + '</span></div><br /><div class="controls-slider-name">Min</div><div id="slider"><span class="minslider">' + layer + '</span></div><br /><div class="controls-slider-name">Max</div><div id="slider"><span class="maxslider">' + layer + '</span></div><br /><div class="controls-slider-name">Gamma</div><div id="slider"><span class="gammaslider">' + layer + '</span></div><br /><div id="' + layer + '" class="colorbox"><div class="controls-slider-name">Color</div><div class="btn-group btn-group-xs"><button class="btn btn-default btn-color btn-r" value="R">R</button><button class="btn btn-default btn-color btn-g " value="G">G</button><button class="btn btn-default btn-color btn-b" value="B">B</button><button class="btn btn-default btn-color btn-c" value="C">C</button><button class="btn btn-default btn-color btn-m" value="M">M</button><button class="btn btn-default btn-color btn-y" value="Y">Y</button><button class="btn btn-default btn-color" value=""><span class="glyphicon glyphicon-remove-sign" aria-hidden="true"></span></button></div></div><br /> </div>';
        $( "#image-sliders" ).append(tmphtml);
      }
      // min controls
      $(function() {
        $( "#slider > .minslider" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 255,
            value: 0,
            slide: function ( event, ui ) {
              layers[layer].min = ui.value/255.0;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end min controls
      // max controls
      $(function() {
        $( "#slider > .maxslider" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 255,
            value: 255,
            slide: function ( event, ui ) {
              layers[layer].max = ui.value/255.0;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end max controls
      // gamma controls
      $(function() {
        $( "#slider > .gammaslider" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 2,
            value: 1,
            step: 0.1,
            slide: function ( event, ui ) {
              layers[layer].gamma = ui.value;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end gamma controls
      // opacity controls
      $(function() {
        $( "#slider > .opacity" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 100,
            value: layers[layer].opacity*100,
            slide: function ( event, ui ) {
                layers[layer].opacity = ui.value/100;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end opacity controls
      // color controls
      $(function () {
        $( ".colorbox" ).each( function() {
          var layer = $( this ).attr('id');
          $( this ).children('.btn-group').children( '.btn' ).each(function() {
            $( this ).click(function() {
              layers[layer].color = $(this).val();
              glLayer.draw();
            });
          });
        });
      });

      // end color controls
    };
    updateImageControlSliders();
    // toggle image controls
    $( "#showimagecontrols" ).on('click', function () {
      $( "#imagecontrolshide" ).toggle( "slow" );
      $( "#imagecontrolsshow" ).toggle();
    });
    $( "#hideimagecontrols" ).on('click', function () {
      $( "#imagecontrolshide" ).toggle( "slow", function () {
        $( "#imagecontrolsshow" ).toggle( );
      });
    });
    // disable (and then re-enable) clicking on mouseover
    imagecontrols.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    imagecontrols.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });
    // END image control palette

    // BEGIN projection service palette
    /*
    var projectModeButton = L.control();
    projectModeButton.options.position = 'topright';
    projectModeButton.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'projectMode');
      var showicon = '<div id="projectModeIcon" style="float:left;"><a id="showProjectModeBox" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-sort-amount-asc fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="projectModeBox" style="display:none;">';
      var title = '<div style="float:right;"><a id="hideProjectModeBox" href="#"><i class="fa fa-times fa-lg"></a></i></div><div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-sort-amount-asc fa-stack-1x"></i></span></div><div class="headertext">Projection View</div></div>';
      var width = '<br /><div id="projectModeWidth">Width: <input id="projectModeWidthInput" type="number" min="0" max="50" value="5" /></div>';
      //var type = '<br /><div id="projectModeType">Type: <select id="projectModeTypeInput"><option value="max">Max Projection</option><option value="min">Min Projection</option></select></div>';
      var apply = '<br /><button class="btn btn-primary" id="projectModeApply" >Load Projection</button>'
      var hiddendivend = '</div>'
      this._div.innerHTML = showicon + hiddendivstart + title  + width + apply + hiddendivend;
      return this._div;
    };
    projectModeButton.addTo(map);
    $( "#showProjectModeBox" ).on('click', function () {
      $( "#projectModeBox" ).toggle( "slow", function () {
        $( "#projectModeIcon" ).toggle( );
      });
    });
    $( "#hideProjectModeBox" ).on('click', function () {
      $( "#projectModeBox" ).toggle( "slow" , function () {
        $( "#projectModeIcon" ).toggle( );
      });
    });
    // disable (and then re-enable) clicking on mouseover
    projectModeButton.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    projectModeButton.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });

    $( "#projectModeApply" ).click( function() {
      var width = $( "#projectModeWidthInput" ).val();

      for (var layername in layers) {
        if (layers[layername].color == '') {
          var url = 'http://' + layers[layername].server + '/nd/catmaid/maxproj/' + layers[layername].token + '/' + layers[layername].channel + '/width:' + width + '/xy/{zindex}/{y}_{x}_{z}.png';
        }
        else {
          var url = 'http://' + layers[layername].server + '/nd/catmaid/maxproj/' + layers[layername].token + '/' + layers[layername].channel + ':' + layers[layername].color + '/width:' + width + '/xy/{zindex}/{y}_{x}_{z}.png';
        }
        updateLayerUrl( layername, url )
      }
    });

    // END projection service palette
    */

    // BEGIN query tool palette
    var queryToolControl = L.control();
    queryToolControl.options.position = 'topright';
    queryToolControl.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'queryToolControl');
      var showicon = '<div id="queryToolControlIcon" style="float: left;"><a id="showQueryToolBox" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-search fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="queryToolBox" style="display:none;">';

      var title = '<div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-map-marker fa-stack-1x"></i></span></div><div class="headertext">Query Tool</div></div>';

      var formdiv = '<div id="queryToolForm">';

      var endformdiv = '</div>';

      var hiddendivend = '</div>'

      this._div.innerHTML = showicon + hiddendivstart + title + formdiv + endformdiv + hiddendivend;
      return this._div;
    };
		queryToolControl.addTo(map);

    var updateQueryToolOptions = function() {
      $( "#queryToolForm").empty();

      // use the form class to get bootstrap formatting
      var formstart = '<form><div class="form-group"><select class="form-control" id="queryLayerSelect">';

      var layerDropdown = '<option value="none" selected>None</option>';
      for (layer in layers) {
        if (layers[layer].type == 'annotation') {
          var newOpt =  '<option value="' + layer + '">' + layer + '</option>';
          layerDropdown += newOpt;
        }
      }

      var formend = '</select></div></form>';

      $( "#queryToolForm" ).html(formstart + layerDropdown + formend);
    }
    updateQueryToolOptions();

    $(function() {
      $( "#queryLayerSelect" ).change(function(e) {
        var changed = $(e.target).val();
        if (changed == 'none') {
          // disable query tool
          ndv.querytool = false;
          ndv.querytoken = 'none';
          ndv.querychannel = 'none';
          ndv.queryserver = 'none';
          return;
        }
        ndv.querytoken = layers[changed].token;
        ndv.querychannel = layers[changed].channel;
        ndv.queryserver = layers[changed].server;
        ndv.querytool = true;
      });
    })



    // disable (and then re-enable) clicking on mouseover
    queryToolControl.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
      $( "#queryToolBox" ).show();
      $( "#queryToolControlIcon" ).hide();
    });

    queryToolControl.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
      $( "#queryToolControlIcon" ).show();
      $( "#queryToolBox" ).hide();
    });

    // END query tool palette

    // BEGIN marker tool palette
    var markerToolControl = L.control();
    markerToolControl.options.position = 'topright';
    markerToolControl.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'markerToolControl');

      var showicon = '<div id="markerToolControlIcon" style="float: left;"><a id="showMarkerControlBox" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-map-marker fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="markerToolBox" style="display:none;">';

      var title = '<div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-map-marker fa-stack-1x"></i></span></div><div class="headertext">Markers</div></div>';

      var click = '<div class="form-group"><button id="markerModeClick" class="btn btn-primary btn-block">On Click</button></div>';

      var center = '<div class="form-group"><button id="markerModeCenter" class="btn btn-primary btn-block">Center</button></div>';

      var clear = '<div class="form-group"><button id="clearmarkers" class="btn btn-danger btn-block">Clear Markers</button></div>';

      var hiddendivend = '</div>'

      this._div.innerHTML = showicon + hiddendivstart + title + click + center + clear + hiddendivend;
      return this._div;
    };
    markerToolControl.addTo(map);

    // disable (and then re-enable) clicking on mouseover
    markerToolControl.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
      $( "#markerToolBox" ).show();
      $( "#markerToolControlIcon" ).hide();
    });

    markerToolControl.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
      $( "#markerToolBox" ).hide();
      $( "#markerToolControlIcon" ).show();
    });

    // marker controls
    var enableMarkerOnClick = function() {
      map.on('click', markerOnClick);
      // turn off query tool
      map.off('click', onMapClick);
    }

    // button actions
    $( "#markerModeClick" ).on('click', function() {
      if ( $( "#markerModeClick").hasClass('btn-primary') ) {
        // enable marker tool on click
        //map.on('click', markerOnClick);
        setTimeout(enableMarkerOnClick, 300);
        // change button
        $( "#markerModeClick" ).removeClass('btn-primary');
        $( "#markerModeClick" ).addClass('btn-success');
      }
      else {
        // disable marker tool on click
        map.off('click', markerOnClick);
        // change button
        $( "#markerModeClick" ).removeClass('btn-success');
        $( "#markerModeClick" ).addClass('btn-primary');
      }
    });

    $( "#markerModeCenter" ).on('click', function() {
      addMarkerCenter();
    });
    $( "#clearmarkers" ).click(function( event ) {
      var index;
      for (index = 0; index < ndv.markers.length; index++) {
        map.removeLayer(ndv.markers[index]);
      }
    });
    //end button actions

    var addMarkerCenter = function() {
      // place a marker at the current map center
      var center = map.getCenter();
      var point = map.project(center);
      var marker_options = {
        title: point.x + " " + point.y + " " + ndv.zindex,
      }

      // keep track of markers by storing in an array
      ndv.markers.push(L.marker(center, marker_options).addTo(map));

      // get current url
      var markerUrl = getCurrentURL(point);

      var popup_options = {
        maxWidth: 750,
      }

      ndv.markers[ndv.markers.length - 1].bindPopup('<a href="' + markerUrl + '">' + markerUrl + '</a>', popup_options);
    }
    {% if marker == True %}
      // add a marker to the center on page load
      addMarkerCenter();
    {% endif %}

    function markerOnClick(event) {
      // place a marker at the click

      var point = map.project(event.latlng);
      var marker_options = {
        title: point.x + " " + point.y + " " + ndv.zindex,
      }

      // keep track of markers by storing in an array
      ndv.markers.push(L.marker(event.latlng, marker_options).addTo(map));

      // get current url
      var markerUrl = getCurrentURL(point);
      var popup_options = {
        maxWidth: 750,
      }

      ndv.markers[ndv.markers.length - 1].bindPopup('<a href="' + markerUrl + '">' + markerUrl + '</a>', popup_options);

      // markerOnClick causes lots of problems with controls, so only one marker per click!
      map.off('click', markerOnClick);
      // change button
      $( "#markerModeClick" ).removeClass('btn-success');
      $( "#markerModeClick" ).addClass('btn-primary');
      // turn query tool back on
      map.on('click', onMapClick);

    };

    function getCurrentURL(point) {
      var script_name = '{{ request.META.SCRIPT_NAME }}';
      var pathRaw = '{{ request.path }}';<commit_msg>Fixed bug where clicking with the query tool and marker tool selected caused both a marker placement and a query<commit_after><!DOCTYPE html>
<html>
<!--
# Copyright 2016 NeuroData (http://neurodata.io)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Designed, Developed, and Maintained by <NAME>
# <EMAIL>
# github.com/alexbaden

!-->
<head>
  <title>NeuroDataViz: viewing {{ project_name }}</title>
  {% load staticfiles %}
  {# boostrap files #}
  <link rel="stylesheet" type ="text/css" href="{% static 'bootstrap/css/bootstrap.css' %}" />

  {# jquery UI #}
  <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.4/themes/smoothness/jquery-ui.css">

  {# leaflet files #}
  <link rel="stylesheet" href="{% static 'ndv/leaflet/leaflet.css' %}" />
  <link rel="stylesheet" href="{% static 'ndv/css/Control.Loading.css' %}" />

  {# buttons, buttons, buttons #}
  <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">

  {# css file #}
  <link rel="stylesheet" href="{% static 'ndv/css/viewer.css' %}" />

  {% if GOOGLE_ANALYTICS_PROPERTY_ID %}
    {% include "ndv/analytics.html" %}
  {% endif %}

  <script src="{% static 'ndv/leaflet/leaflet-src.js' %}"></script>
  {#<script src="{% static 'ndv/leaflet/leaflet.js' %}"></script>#}
  <script src="{% static 'ndv/js/Control.Loading.js' %}"></script>
	<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
  <script src="{% static 'ndv/js/spin.min.js' %}"></script>
  <script src="{% static 'ndv/js/easy-button.js' %}"></script>
  <script src="{% static 'ndv/js/ocp-leaflet.js' %}"></script>
  {# threejs #}
  <script src="{% static 'ndv/js/threejs/three.min.js' %}"></script>

	</head>
<body>

  {# Begin Modal Code Includes #}
  {% include "ndv/help.html" %}

  {% include "ndv/nav.html" %}

  {% include "ndv/add_data.html" %}

  {% include "ndv/login.html" %}
  {# End Modal Code Includes #}

  <div id="map"></div>
  <script id="vertexShader" type="x-shader/x-vertex">
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  </script>

  <script id="fragmentShader" type="x-shader/x-fragment">
    varying vec2 vUv;

    uniform vec3 color;
    uniform sampler2D texture;

    uniform float opacity;

    // remapping
    uniform float minval;
    uniform float maxval;

    // gamma
    uniform float gamma;

    void main() {
      vec4 tmpColor;

      #ifdef REMAP
      tmpColor = texture2D(texture, vUv) * vec4(color.x, color.y, color.z, opacity);
      // remap color to new intensity range
      tmpColor.x = min( max( (tmpColor.x - minval)/maxval, 0.0), 1.0 );
      tmpColor.y = min( max( (tmpColor.y - minval)/maxval, 0.0), 1.0 );
      tmpColor.z = min( max( (tmpColor.z - minval)/maxval, 0.0), 1.0 );
      #else
      tmpColor = texture2D(texture, vUv) * vec4(color.x, color.y, color.z, opacity);
      #endif

      #ifdef GAMMACOR
      tmpColor.rgb = pow(tmpColor.rgb, vec3(1.0 / gamma));
      #endif

      gl_FragColor = tmpColor;

    }
  </script>
  <script>
    // AB TODO use strict

    // global variables
    var ndv = {};
    ndv.querytool = false;
    ndv.querytoken = 'none';
    ndv.querychannel = 'none';
    ndv.queryserver = 'none';

    // timeseries globals
    ndv.playdelay = 1;

    // marker array
    ndv.markers = [];

    // upsample settings
    ndv.upsample_num = 2;

    // dataset info
    ndv.xstart = {{ xstart }};
    ndv.xmin = {{ xoffset }};
    ndv.xmax = {{ xsize }} + {{ xoffset }} - 1;

    ndv.ystart = {{ ystart }};
    ndv.ymin = {{ yoffset }};
    ndv.ymax = {{ ysize }} + {{ yoffset }} - 1;

    ndv.zmin = {{ zoffset }};
    ndv.zmax = {{ zsize }} + {{ zoffset }} - 1;

    ndv.minres = {{ minres }};
    ndv.maxres = {{ maxres }};
    ndv.res = {{ res }};
    // set zindex to some default (we pick the lowest slice)
    ndv.zindex = {{ zstart }};

     // create the map first
		var map = L.map('map', {
      crs: L.CRS.Simple,
			doubleClickZoom: false,
    });

    // add loading symbol
    var spinOpts = {
      lines: 13,
      length: 3,
      width: 3,
      radius: 5,
      rotate: 13,
      speed: 1.7,
      top: "83%",
    };
    var loadingControl = L.Control.loading({
      spinjs: true,
      spin: spinOpts
    });
    map.addControl(loadingControl);

    // define layers (namespaced with a layers. prefix)
    var layers = {};

    // add layer function
    function addLayer(layername, layertype, layerpropagate, url, server, token, channel, color) {

      layers[layername] = {
        'tileLayer': null,
        'server': server,
        'token': token,
        'channel': channel,
        'type': layertype,
        'color': color,
        'opacity': 1.0,
        'min': 0.0,
        'max': 1.0,
        'gamma': 1.0,
        'blending': THREE.NormalBlending,
      }

      var opacity;
      var propagate;
      if (layertype == 'annotation') {
        opacity = 0.4; // annotations loaded at 40% opacity by default
        layers[layername].opacity = 0.4;
        propagate = 2; // annotations propagation status doesn't matter
      }
      else
      {
        opacity = 1.0;
        propagate = layerpropagate;
      }

      var curtime = 0;
      if (layertype == 'timeseries') {
        curtime = {{ starttime }};
      }

      layers[layername]['tileLayer'] = L.tileLayer.OCPLayer(
        url,
        {
          zindex: ndv.zindex,
          zoomReverse: true,
          minZoom: ndv.maxres,
          maxZoom: ndv.maxres + ndv.maxres + ndv.upsample_num,
          maxNativeZoom: ndv.maxres + ndv.maxres,
          tileSize: 512,
          opacity: opacity,
          propagate: propagate,
          curtime: curtime,
          continuousWorld: true,
        }
      );

      layers[layername].tileLayer.addTo(map);
    }

    function updateLayerUrl(layername, newUrl) {
      layers[layername].tileLayer.setUrl(newUrl);
      //layers[layername].tileLayer.smoothRedraw();
    }

    // load image layers first
    {% for layer in layers %}
      {% if layer.layertype != 'annotation' %}
      {# setup the URL #}

        {% if layer.channel %}
          var webtoken = '{{ layer.token }}/{{ layer.channel }}';
        {% else %}
          var webtoken = '{{ layer.token }}';
        {% endif %}

        {% if layer.tilecache %}
          {% if layer.layertype == 'timeseries' %}
            {% if layer.tilecache_server %}
              var url = 'http://{{ layer.tilecache_server }}/ocptilecache/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
            {% else %}
              var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
            {% endif %}
          {% else %}
            {% if layer.tilecache_server %}
            {# note: presently unclear what the tilecache URL endpoint should be #}
              var url = 'http://{{ layer.tilecache_server }}/ocptilecache/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
            {% else %}
              var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
            {% endif %}
          {% endif %}

        {% else %}
          {% if layer.layertype == 'timeseries' %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
          {% else %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% endif %}
        {% endif %}

        {# add the layer #}
        addLayer( "{{ layer.layer_name|cut:" " }}", "{{ layer.layertype }}",  "{{ layer.propagate }}", url, "{{ layer.server }}", "{{ layer.token }}", "{{ layer.channel }}", "{{ layer.color }}");
        {% endif %}
    {% endfor %}

    // now load annotations
    {% for layer in layers %}
      {% if layer.layertype == 'annotation' %}
      {# setup the URL #}

        {% if layer.channel %}
          var webtoken = '{{ layer.token }}/{{ layer.channel }}';
          {% if layer.color %}
            webtoken = webtoken + ':{{layer.color}}';
          {% endif %}
        {% else %}
          var webtoken = '{{ layer.token }}';
        {% endif %}

        {% if layer.tilecache %}
          {% if layer.layertype == 'timeseries' %}
            var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
          {% else %}
            var url = 'http://{{ layer.server }}/ocptilecache/tilecache/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% endif %}

        {% else %}
          {% if layer.color %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/mcfc/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% elif layer.layertype == 'timeseries' %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{{ starttime }}/{zindex}/{y}_{x}_{z}.png';
          {% else %}
            var url = 'http://{{ layer.server }}/ocp/catmaid/' + webtoken + '/xy/{zindex}/{y}_{x}_{z}.png';
          {% endif %}
        {% endif %}

        {# add the layer #}
        addLayer( "{{ layer.layer_name|cut:" " }}", "{{ layer.layertype }}", "{{ layer.propagate }}", url, "{{ layer.server }}", "{{ layer.token }}", "{{ layer.channel }}", "{{ layer.color }}");
        {% endif %}
    {% endfor %}


    function setThreeColor(color) {
      // given a color tag ('r','g','b','c','m','y') set the appropriate three color
      switch(color) {
        case 'R':
          return new THREE.Color("rgb(255,0,0)");
        case 'G':
          return new THREE.Color("rgb(0, 255, 0)");
        case 'B':
          return new THREE.Color("rgb(0, 0, 255)");
        case 'C':
          return new THREE.Color("rgb(0, 255, 255)");
        case 'M':
          return new THREE.Color("rgb(255, 0, 255)");
        case 'Y':
          return new THREE.Color("rgb(255, 255, 0)");
        default:
          return new THREE.Color("rgb(255, 255, 255)");
      }
    }

    // testing the webGL layer interface
    var glLayer = new L.WebGLLayer({
      zindex: ndv.zindex,
      zoomReverse: true,
      minZoom: ndv.maxres,
      maxZoom: ndv.maxres + ndv.maxres + ndv.upsample_num,
      maxNativeZoom: ndv.maxres + ndv.maxres,
      tileSize: 512,
    }).addTo(map);

    glLayer.draw = function() {
      var scene = new THREE.Scene();

      var worldsize = this._map.getSize();
      var origin = this._map.getPixelOrigin();
      var shift = this._map.getPixelBounds().min.subtract(origin);
      var camera = this._camera;

      // get size from local function since layers may not have loaded
      var size = this._getTileSize();

      var vertexShader = document.getElementById('vertexShader').text;
      var fragmentShader = document.getElementById('fragmentShader').text;

      function loadTexture(img, layer, offsetx, offsety, color, opacity, minval, maxval, gamma, blending) {
        // use dom element for textures
        var texture = new THREE.Texture({
          repeat: 1,
        });

        texture.image = img;
        texture.name = layer;
        if ( $(img).hasClass('leaflet-tile-loaded') ) {
          texture.needsUpdate = true;
        } else {
          layer.on('tileload', function(e) {
            if (e.tile == texture.image) {
              texture.needsUpdate = true;
            }
          }, this);
        }

        var geometry = new THREE.PlaneBufferGeometry(size, size);

        // some default uniforms
        var uniforms = {
          color: { type: "c", value: color },
          texture: { type: "t", value: texture },
          opacity: { type: "f", value: opacity },
          minval: { type: "f", value: minval },
          maxval: { type: "f", value: maxval },
          gamma: { type: "f", value: gamma },
        };

        // TODO only enable these when sliders have been moved
        var defines = {
          REMAP: true,
          GAMMACOR: true,
        }

        var material = new THREE.ShaderMaterial({
          uniforms: uniforms,
          defines: defines,
          vertexShader: vertexShader,
          fragmentShader: fragmentShader,
          transparent: true,
          blending: blending,
        });

        var plane = new THREE.Mesh( geometry, material );

        // dispose of material, geometry, and mesh on unload
        layer.on('tileunload', function(e) {
          if (e.tile == texture.image) {
            texture.dispose();
            geometry.dispose();
            geometry = undefined;
            material.dispose();
            material = undefined;
            scene.remove(plane);
            plane = undefined;
          }
        }, this);

        // offset tile
        plane.position.set(offsetx, offsety, 0);

        scene.add(plane);
      }

      if (Object.keys(layers).length == 0) {
        return;
      }

      for (var layer in layers) {
        var color = setThreeColor(layers[layer].color);
        var opacity = layers[layer].opacity;
        var minval = layers[layer].min;
        var maxval = layers[layer].max;
        var gamma = layers[layer].gamma;
        var blending = parseInt(layers[layer].blending);

        for (var tile in layers[layer].tileLayer._tiles) {
          var img = layers[layer].tileLayer._tiles[tile];

          var offset = this._getTilePos(tile);

          loadTexture(img, layers[layer].tileLayer, offset.x, offset.y, color, opacity, minval, maxval, gamma, blending);
        }
      }

      this._camera.position.z = 1;

      this._renderTarget = new THREE.WebGLRenderTarget( worldsize.x, worldsize.y );

      function render() {
        if (this._repeater) {
          cancelAnimationFrame(this._repeater);
        }
        this._repeater = requestAnimationFrame( render.bind(this) );
        this._renderer.render(scene, this._camera, this._renderTarget);
        this._renderToScreen();
      }
      render.bind(this)();

      //this._renderToScreen();
    }

    function coordinatesDebug(event) {
      // place a leaflet marker and a webgl marker at the click

      var point = map.project(event.latlng);
      var marker_options = {
        title: point.x + " " + point.y + " " + ndv.zindex,
      }

      // add leaflet marker
      L.marker(event.latlng, marker_options).addTo(map);

      var projPoint = map._getCenterOffset(event.latlng);
      // add GL marker
      glLayer._drawCircle( L.point([projPoint.x, -projPoint.y])  );
    };

    //map.on('click', coordinatesDebug);

    // set the initial view
    var center = map.unproject(L.point( ndv.xstart, ndv.ystart ), ndv.maxres + ndv.maxres - ndv.res);
		map.setView(center, ndv.maxres + ndv.maxres - ndv.res);

    // BEGIN propagate warning box
    var propwarn = L.control();
    propwarn.options.position = 'topright';
    propwarn.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'propwarnbox');
      this._div.innerHTML = '<div class="alert alert-danger" role="alert"><span class="glyphicon glyphicon-alert" aria-hidden="true"></span><span class="sr-only">Warning:</span> The following layers are not propagated: <strong><span class="notpropchanlist"></span></strong>.<br />Image data may be missing at some resolutions!';
      return this._div;
    };
    propwarn.addTo(map);

    function checkPropagate() {
      $( ".propwarnbox" ).hide();
      $( ".notpropchanlist" ).empty();
      for (layer in layers) {
        if (layers[layer].tileLayer.options.propagate != 2) {
          $( ".propwarnbox" ).show();
          if ($( ".notpropchanlist" ).text().length > 0)  {
            $( ".notpropchanlist" ).append(", ");
          }
          $( ".notpropchanlist" ).append(layer);
        }
      }
    }
    checkPropagate();

    // END propagate warning box

    // BEGIN query result palette
    var queryres = L.control();
    queryres.options.position = 'topright';
    queryres.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'queryres');
      var header = '<div class="closequery" style="float:right;"><a id="closequerybox" href="#"><i class="fa fa-times fa-lg"></a></i></div><h4>Query Result</h4>';
      var querybody = '<div class="querybody"></div>';
      this._div.innerHTML = header + querybody;
      return this._div;
    };
		queryres.addTo(map);

    // disable (and then re-enable) clicking on mouseover
    queryres.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    queryres.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });
    // END query result palette

    // BEGIN project info palette
    var projinfo = L.control();
    projinfo.options.position = 'topright';
    projinfo.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'projinfo');
      var showicon = '<div id="projinfoicon" style="float:left;"><a id="showprojinfo" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-info fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="projinfobox" style="display:none;">';
      var title = '<div style="float:right;"><a id="hideprojinfo" href="#"><i class="fa fa-times fa-lg"></a></i></div><div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-info fa-stack-1x"></i></span></div><div class="headertext">Project Info</div></div>';
      var projinfodiv = '<div id="projinfotxt"></div>'
      var hiddendivend = '</div>'
      this._div.innerHTML = showicon + hiddendivstart + title  + projinfodiv + hiddendivend;
			return this._div;
    };
		projinfo.addTo(map);
    $( "#showprojinfo" ).on('click', function () {
      $( "#projinfobox" ).toggle( "slow", function () {
        $( "#projinfoicon" ).toggle( );
      });
    });
    $( "#hideprojinfo" ).on('click', function () {
      $( "#projinfobox" ).toggle( "slow" , function () {
        $( "#projinfoicon" ).toggle( );
      });
    });
    // disable (and then re-enable) clicking on mouseover
    projinfo.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    projinfo.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });
    function getProjInfo() {
      // we use the token from the first layer (for now)
      var infoToken = "{{ layers.0.token }}";
      var infoUrl = "http://{{ request.get_host }}{% url 'projinfo' queryargs='placeholder' %}";
      infoUrl = infoUrl.replace('placeholder', 'localhost/' + infoToken + '/')

      $( "#projinfotxt" ).load(infoUrl);
		}
    getProjInfo();

    // END project info palette

    // BEGIN image control palette
    var imagecontrols = L.control();
    imagecontrols.options.position = 'topright';
    imagecontrols.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'imagecontrols');
      var showicon = '<div id="imagecontrolsshow" style="float:left;"><a id="showimagecontrols" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-picture-o fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="imagecontrolshide" style="display:none;">';
      var title = '<div style="float:right;"><a id="hideimagecontrols" href="#"><i class="fa fa-times fa-lg"></a></i></div><div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-picture-o fa-stack-1x"></i></span></div><div class="headertext">Image Controls</div></div>';

      var imgsliderdiv = '<div id="image-sliders"></div>'

      var blendmode = '<br /><div id="blendmode">Blending: <select name="blendmode"><option value="1">Normal</option><option value="2">Additive</option><option value="3">Subtractive</option><option value="4">Multiply</option><option value="0">None</option></select><span style="float: right;"><a href="#"><i class="fa fa-info-circle fa-lg"></i></a></span></div>';

      var hiddendivend = '</div>';

      this._div.innerHTML = showicon + hiddendivstart + title + imgsliderdiv + blendmode + hiddendivend;
      return this._div;
    };
    imagecontrols.addTo(map);
    // update sliders (removes all layers and adds from layers var)
    function updateImageControlSliders() {
      $( "#image-sliders" ).empty();
      for (layer in layers) {
        var tmphtml = '<div class="controls-' + layer + '"><div class="controls-layer-name"><a href="#" class="remove-layer ' + layer + '"><i class="fa fa-times-circle"></i></a><a href="#">' + layer + '</a></div><div class="controls-slider-name">Opacity</div><div id="slider"><span class="opacity">' + layer + '</span></div><br /><div class="controls-slider-name">Min</div><div id="slider"><span class="minslider">' + layer + '</span></div><br /><div class="controls-slider-name">Max</div><div id="slider"><span class="maxslider">' + layer + '</span></div><br /><div class="controls-slider-name">Gamma</div><div id="slider"><span class="gammaslider">' + layer + '</span></div><br /><div id="' + layer + '" class="colorbox"><div class="controls-slider-name">Color</div><div class="btn-group btn-group-xs"><button class="btn btn-default btn-color btn-r" value="R">R</button><button class="btn btn-default btn-color btn-g " value="G">G</button><button class="btn btn-default btn-color btn-b" value="B">B</button><button class="btn btn-default btn-color btn-c" value="C">C</button><button class="btn btn-default btn-color btn-m" value="M">M</button><button class="btn btn-default btn-color btn-y" value="Y">Y</button><button class="btn btn-default btn-color" value=""><span class="glyphicon glyphicon-remove-sign" aria-hidden="true"></span></button></div></div><br /> </div>';
        $( "#image-sliders" ).append(tmphtml);
      }
      // min controls
      $(function() {
        $( "#slider > .minslider" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 255,
            value: 0,
            slide: function ( event, ui ) {
              layers[layer].min = ui.value/255.0;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end min controls
      // max controls
      $(function() {
        $( "#slider > .maxslider" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 255,
            value: 255,
            slide: function ( event, ui ) {
              layers[layer].max = ui.value/255.0;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end max controls
      // gamma controls
      $(function() {
        $( "#slider > .gammaslider" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 2,
            value: 1,
            step: 0.1,
            slide: function ( event, ui ) {
              layers[layer].gamma = ui.value;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end gamma controls
      // opacity controls
      $(function() {
        $( "#slider > .opacity" ).each(function() {
          var layer = $( this ).text();
          $( this ).empty().slider({
            min: 0,
            max: 100,
            value: layers[layer].opacity*100,
            slide: function ( event, ui ) {
                layers[layer].opacity = ui.value/100;
              },
            start: function ( event, ui ) { map.dragging.disable(); },
            stop: function ( event, ui ) { map.dragging.enable(); glLayer.draw(); },
          });
        });
      });// end opacity controls
      // color controls
      $(function () {
        $( ".colorbox" ).each( function() {
          var layer = $( this ).attr('id');
          $( this ).children('.btn-group').children( '.btn' ).each(function() {
            $( this ).click(function() {
              layers[layer].color = $(this).val();
              glLayer.draw();
            });
          });
        });
      });

      // end color controls
    };
    updateImageControlSliders();
    // toggle image controls
    $( "#showimagecontrols" ).on('click', function () {
      $( "#imagecontrolshide" ).toggle( "slow" );
      $( "#imagecontrolsshow" ).toggle();
    });
    $( "#hideimagecontrols" ).on('click', function () {
      $( "#imagecontrolshide" ).toggle( "slow", function () {
        $( "#imagecontrolsshow" ).toggle( );
      });
    });
    // disable (and then re-enable) clicking on mouseover
    imagecontrols.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    imagecontrols.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });
    // END image control palette

    // BEGIN projection service palette
    /*
    var projectModeButton = L.control();
    projectModeButton.options.position = 'topright';
    projectModeButton.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'projectMode');
      var showicon = '<div id="projectModeIcon" style="float:left;"><a id="showProjectModeBox" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-sort-amount-asc fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="projectModeBox" style="display:none;">';
      var title = '<div style="float:right;"><a id="hideProjectModeBox" href="#"><i class="fa fa-times fa-lg"></a></i></div><div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-sort-amount-asc fa-stack-1x"></i></span></div><div class="headertext">Projection View</div></div>';
      var width = '<br /><div id="projectModeWidth">Width: <input id="projectModeWidthInput" type="number" min="0" max="50" value="5" /></div>';
      //var type = '<br /><div id="projectModeType">Type: <select id="projectModeTypeInput"><option value="max">Max Projection</option><option value="min">Min Projection</option></select></div>';
      var apply = '<br /><button class="btn btn-primary" id="projectModeApply" >Load Projection</button>'
      var hiddendivend = '</div>'
      this._div.innerHTML = showicon + hiddendivstart + title  + width + apply + hiddendivend;
      return this._div;
    };
    projectModeButton.addTo(map);
    $( "#showProjectModeBox" ).on('click', function () {
      $( "#projectModeBox" ).toggle( "slow", function () {
        $( "#projectModeIcon" ).toggle( );
      });
    });
    $( "#hideProjectModeBox" ).on('click', function () {
      $( "#projectModeBox" ).toggle( "slow" , function () {
        $( "#projectModeIcon" ).toggle( );
      });
    });
    // disable (and then re-enable) clicking on mouseover
    projectModeButton.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
    });

    projectModeButton.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
    });

    $( "#projectModeApply" ).click( function() {
      var width = $( "#projectModeWidthInput" ).val();

      for (var layername in layers) {
        if (layers[layername].color == '') {
          var url = 'http://' + layers[layername].server + '/nd/catmaid/maxproj/' + layers[layername].token + '/' + layers[layername].channel + '/width:' + width + '/xy/{zindex}/{y}_{x}_{z}.png';
        }
        else {
          var url = 'http://' + layers[layername].server + '/nd/catmaid/maxproj/' + layers[layername].token + '/' + layers[layername].channel + ':' + layers[layername].color + '/width:' + width + '/xy/{zindex}/{y}_{x}_{z}.png';
        }
        updateLayerUrl( layername, url )
      }
    });

    // END projection service palette
    */

    // BEGIN query tool palette
    var queryToolControl = L.control();
    queryToolControl.options.position = 'topright';
    queryToolControl.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'queryToolControl');
      var showicon = '<div id="queryToolControlIcon" style="float: left;"><a id="showQueryToolBox" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-search fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="queryToolBox" style="display:none;">';

      var title = '<div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-map-marker fa-stack-1x"></i></span></div><div class="headertext">Query Tool</div></div>';

      var formdiv = '<div id="queryToolForm">';

      var endformdiv = '</div>';

      var hiddendivend = '</div>'

      this._div.innerHTML = showicon + hiddendivstart + title + formdiv + endformdiv + hiddendivend;
      return this._div;
    };
		queryToolControl.addTo(map);

    var updateQueryToolOptions = function() {
      $( "#queryToolForm").empty();

      // use the form class to get bootstrap formatting
      var formstart = '<form><div class="form-group"><select class="form-control" id="queryLayerSelect">';

      var layerDropdown = '<option value="none" selected>None</option>';
      for (layer in layers) {
        if (layers[layer].type == 'annotation') {
          var newOpt =  '<option value="' + layer + '">' + layer + '</option>';
          layerDropdown += newOpt;
        }
      }

      var formend = '</select></div></form>';

      $( "#queryToolForm" ).html(formstart + layerDropdown + formend);
    }
    updateQueryToolOptions();

    $(function() {
      $( "#queryLayerSelect" ).change(function(e) {
        var changed = $(e.target).val();
        if (changed == 'none') {
          // disable query tool
          ndv.querytool = false;
          ndv.querytoken = 'none';
          ndv.querychannel = 'none';
          ndv.queryserver = 'none';
          return;
        }
        ndv.querytoken = layers[changed].token;
        ndv.querychannel = layers[changed].channel;
        ndv.queryserver = layers[changed].server;
        ndv.querytool = true;
      });
    })



    // disable (and then re-enable) clicking on mouseover
    queryToolControl.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
      $( "#queryToolBox" ).show();
      $( "#queryToolControlIcon" ).hide();
    });

    queryToolControl.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
      $( "#queryToolControlIcon" ).show();
      $( "#queryToolBox" ).hide();
    });

    // END query tool palette

    // BEGIN marker tool palette
    var markerToolControl = L.control();
    markerToolControl.options.position = 'topright';
    markerToolControl.onAdd = function (map) {
      this._div = L.DomUtil.create('div', 'markerToolControl');

      var showicon = '<div id="markerToolControlIcon" style="float: left;"><a id="showMarkerControlBox" href="#"><span class="fa-stack fa-2x"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-map-marker fa-stack-1x"></i></span></a></div>';

      var hiddendivstart = '<div id="markerToolBox" style="display:none;">';

      var title = '<div id="controlpanelheader"><div style="float:left;"><span class="fa-stack fa-2x" style="padding-bottom: 5px;"><i class="fa fa-square fa-stack-2x fa-inverse"></i><i class="fa fa-map-marker fa-stack-1x"></i></span></div><div class="headertext">Markers</div></div>';

      var click = '<div class="form-group"><button id="markerModeClick" class="btn btn-primary btn-block">On Click</button></div>';

      var center = '<div class="form-group"><button id="markerModeCenter" class="btn btn-primary btn-block">Center</button></div>';

      var clear = '<div class="form-group"><button id="clearmarkers" class="btn btn-danger btn-block">Clear Markers</button></div>';

      var hiddendivend = '</div>'

      this._div.innerHTML = showicon + hiddendivstart + title + click + center + clear + hiddendivend;
      return this._div;
    };
    markerToolControl.addTo(map);

    // disable (and then re-enable) clicking on mouseover
    markerToolControl.getContainer().addEventListener('mouseover', function() {
      map.off('click', onMapClick);
      $( "#markerToolBox" ).show();
      $( "#markerToolControlIcon" ).hide();
    });

    markerToolControl.getContainer().addEventListener('mouseout', function() {
      map.on('click', onMapClick);
      $( "#markerToolBox" ).hide();
      $( "#markerToolControlIcon" ).show();
    });

    // marker controls
    var enableMarkerOnClick = function() {
      map.on('click', markerOnClick);
      // turn off query tool
      map.off('click', onMapClick);
    }

    // button actions
    $( "#markerModeClick" ).on('click', function() {
      if ( $( "#markerModeClick").hasClass('btn-primary') ) {
        // enable marker tool on click
        //map.on('click', markerOnClick);
        setTimeout(enableMarkerOnClick, 300);
        // change button
        $( "#markerModeClick" ).removeClass('btn-primary');
        $( "#markerModeClick" ).addClass('btn-success');
      }
      else {
        // disable marker tool on click
        map.off('click', markerOnClick);
        // change button
        $( "#markerModeClick" ).removeClass('btn-success');
        $( "#markerModeClick" ).addClass('btn-primary');
      }
    });

    $( "#markerModeCenter" ).on('click', function() {
      addMarkerCenter();
    });
    $( "#clearmarkers" ).click(function( event ) {
      var index;
      for (index = 0; index < ndv.markers.length; index++) {
        map.removeLayer(ndv.markers[index]);
      }
    });
    //end button actions

    var addMarkerCenter = function() {
      // place a marker at the current map center
      var center = map.getCenter();
      var point = map.project(center);
      var marker_options = {
        title: point.x + " " + point.y + " " + ndv.zindex,
      }

      // keep track of markers by storing in an array
      ndv.markers.push(L.marker(center, marker_options).addTo(map));

      // get current url
      var markerUrl = getCurrentURL(point);

      var popup_options = {
        maxWidth: 750,
      }

      ndv.markers[ndv.markers.length - 1].bindPopup('<a href="' + markerUrl + '">' + markerUrl + '</a>', popup_options);
    }
    {% if marker == True %}
      // add a marker to the center on page load
      addMarkerCenter();
    {% endif %}

    function markerOnClick(event) {
      // place a marker at the click

      var point = map.project(event.latlng);
      var marker_options = {
        title: point.x + " " + point.y + " " + ndv.zindex,
      }

      // keep track of markers by storing in an array
      ndv.markers.push(L.marker(event.latlng, marker_options).addTo(map));

      // get current url
      var markerUrl = getCurrentURL(point);
      var popup_options = {
        maxWidth: 750,
      }

      ndv.markers[ndv.markers.length - 1].bindPopup('<a href="' + markerUrl + '">' + markerUrl + '</a>', popup_options);

      // markerOnClick causes lots of problems with controls, so only one marker per click!
      map.off('click', markerOnClick);
      // change button
      $( "#markerModeClick" ).removeClass('btn-success');
      $( "#markerModeClick" ).addClass('btn-primary');
      // turn query tool back on
      setTimeout(reEnableQueryTool, 300);
    };

    var reEnableQueryTool = function() {
      map.on('click', onMapClick);
    }

    function getCurrentURL(point) {
      var script_name = '{{ request.META.SCRIPT_NAME }}';
      var pathRaw = '{{ request.path }}';
{'fraction_non_alphanumeric': 0.1777967804109998, 'fraction_numerical': 0.0063115888916035505, 'mean_word_length': 2.5636548321563315, 'pattern_counts': {'":': 0, '<': 659, '<?xml version=': 0, '>': 667, 'https://': 6, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1972779', 'n_tokens_mistral': 23528, 'n_tokens_neox': 22307, 'n_words': 5036}