SpeciesFileGroup/taxonworks

View on GitHub
app/assets/javascripts/vendor/lib/google/maps.es6.erb

Summary

Maintainability
Test Coverage
/* Basic library to parse through TW returned geoJSON and draw a FeatureCollection on a Google Map.
 */
var TW = TW || {};                      // TW "namespacing" object
TW.vendor = TW.vendor || {};            // mimic directory structure in app/assets/javascripts
TW.vendor.lib = TW.vendor.lib || {};
TW.vendor.lib.google = TW.vendor.lib.google || {};
TW.vendor.lib.google.maps = TW.vendor.lib.google.maps || {};

Object.assign(TW.vendor.lib.google.maps, {               // internally referred to as 'this'; externally as TW.vendor.lib.google.maps

    mapIcons: {    // hash of defined colors and their paths
      empty: '<%= asset_path('map_icons/mm_20_empty.png') %>',
      black: '<%= asset_path('map_icons/mm_20_black.png') %>',
      brown: '<%= asset_path('map_icons/mm_20_brown.png') %>',
      red: '<%= asset_path('map_icons/mm_20_red.png') %>',
      orange: '<%= asset_path('map_icons/mm_20_orange.png') %>',
      yellow: '<%= asset_path('map_icons/mm_20_yellow.png') %>',
      green: '<%= asset_path('map_icons/mm_20_green.png') %>',
      blue: '<%= asset_path('map_icons/mm_20_blue.png') %>',
      purple: '<%= asset_path('map_icons/mm_20_purple.png') %>',
      white: '<%= asset_path('map_icons/mm_20_white.png') %>',
      gray: '<%= asset_path('map_icons/mm_20_gray.png') %>',
      shadow: '<%= asset_path('map_icons/mm_20_shadow.png') %>'
    },

    isGoogleMapsAPILoaded: false,
    isProcessing: false,

    //Load Google Maps
    loadGoogleMapsAPI: function () {
      return new Promise(function (resolve) {
        if (this.isGoogleMapsAPILoaded || (typeof window.google == 'object')) {
          resolve(true);
        }
        else {
          var that = this,
            script = document.createElement("script");

          script.type = "text/javascript";
          script.src = "//maps.google.com/maps/api/js?v=3&key=AIzaSyDKkPt458dNMlfIWbpQCbClJoVq2vP_k4E&libraries=drawing";
          if (!this.isProcessing) {
            document.head.appendChild(script);
          }
          this.isProcessing = true;
          script.onload = function () {
            that.isGoogleMapsAPILoaded = true;
            that.isProcessing = false;
            resolve(true);
          }
        }
      })
    },

    initializeMap: function (canvas, feature_collection) {

      var myOptions = {
        zoom: 1,
        center: {lat: 0, lng: 0}, //center_lat_long, set to 0,0
        mapTypeControl: true,
        mapTypeControlOptions: {style: google.maps.MapTypeControlStyle.DROPDOWN_MENU},
        navigationControl: true,
        mapTypeId: google.maps.MapTypeId.TERRAIN
      };
      var mapIcons = this.mapIcons;
      //var map = this.initialize_map(canvas, myOptions);
      var map = new google.maps.Map(document.getElementById(canvas), myOptions);
      var data = feature_collection;

      map.data.setStyle(function (feature) {
        var color = feature.getProperty('fillColor');       // if server provided a color
        if (color == undefined) {
          color = "#191919";
        }        // otherwise use gray 19 (apologies, Steely Dan
        var color_name = feature.getProperty('colorName');  // or specified a marker color
        if (color_name == undefined) {
          color_name = 'gray'
        }  // else use the same shade of gray
        var icon = mapIcons[color_name];                    // get the marker by color_name
        var fill_opacity = feature.getProperty('fillOpacity');
        if (fill_opacity == undefined) {
          fill_opacity = 0.3;
        }
        return ({
          icon: icon,
          fillColor: color,
          fillOpacity: fill_opacity,
          strokeColor: "black",
          strokeOpacity: 0.5,
          strokeWeight: 1
        });   // @type {google.maps.Data.StyleOptions}
      });

      if (data != undefined) {
        var chained = JSON.parse('{"type":"FeatureCollection","features":[]}'); // container for the distribution
        if (data["type"] == "FeatureCollection") {  // once again, only looking for feature collections, but with properties
          this.doFeatureCollection(data, null, chained);
        }   // end: if data.type == 'FeatureCollection'
        else {              // this is not a feature collection, so what is it?
          if (data["otu_ids"] != undefined) {
            for (var j = 0; j < data["otu_ids"].length; j++) {
              var this_otu_id = data["otu_ids"][j];
              var this_feature_collection = data[this_otu_id];
              this.doFeatureCollection(this_feature_collection, this_otu_id, chained);
            }
          }
          else {
            chained = data;   // and good luck with this ...
          }
        }
        map.data.addGeoJson(chained);
      }
      ;  // put data on the map if present

// bounds for calculating center point
      var width;
      var height;
      var canvas_ratio = 1.0;     // default value
      var style = document.getElementById(canvas).style;

      if (style != null) {      // null short for undefined in js
        if (style.width != undefined && style.height != undefined) {
          width = style.width.toString().split('px')[0];
          height = style.height.toString().split('px')[0];
          canvas_ratio = width / height;
        }
      }
      var bounds = {};    //xminp: xmaxp: xminm: xmaxm: ymin: ymax: -90.0, center_long: center_lat: gzoom:
      this.getData(chained, bounds);               // scan var data as feature collection with homebrew traverser, collecting bounds
      bounds.canvas_ratio = canvas_ratio;
      bounds.canvas_width = width;
      bounds.canvas_height = height;
      var center_lat_long = this.get_window_center(bounds);      // compute center_lat_long from bounds and compute zoom level as gzoom
      map.setCenter(center_lat_long);
      map.setZoom(bounds.gzoom);
      //map.fitBounds(bounds.box);  /// no better results on non 2:1 canvas ratios
      if (document.getElementById("map_coords") != undefined) {
        document.getElementById("map_coords").textContent = 'LAT: ' + center_lat_long['lat']() + ' - LNG: '
          + center_lat_long['lng']() + ' - ZOOM: ' + bounds.gzoom;
      }
      var sw = bounds.sw;       // draw the bounding box for JDT
      var ne = bounds.ne;
      var coordList = [];
      coordList.push([sw['lng'](), sw['lat']()]);   // southwest point
      coordList.push([sw['lng'](), ne['lat']()]);   // northwest point
      //if (sw['lng']() > 0 && ne['lng']() < 0) {     // are we spanning the prime meridian
      //  coordList.push([180.0, ne['lat']()]);       // point at prime meridian
      //}
      coordList.push([center_lat_long['lng'](), ne['lat']()]);       // point at center ALWAYS vs conditional at 0
      coordList.push([ne['lng'](), ne['lat']()]);   // northeast point
      coordList.push([ne['lng'](), sw['lat']()]);   // southeast point
      //if (sw['lng']() > 0 && ne['lng']() < 0) {     // are we spanning the prime meridian
      //  coordList.push([-180.0, sw['lat']()]);       // point at prime meridian
      //}
      coordList.push([center_lat_long['lng'](), sw['lat']()]);       // point at center ALWAYS vs conditional at 0
      coordList.push([sw['lng'](), sw['lat']()]);
      var temparray = [];
      temparray[0] = coordList;
      coordList = temparray;        // this is an expedient kludge to get [[[lng,lat],...]]
      var bounds_box = {
        "type": "Feature",
        "geometry": {
          "type": "multilinestring",
          "coordinates": coordList
        },
        "properties": {}
      };
      map.data.addGeoJson(bounds_box);
      return map;             // now no global map object, use this object to add listeners to THIS map
    },

    doFeatureCollection: function (featureCollection, otu_id, chained) {
      if (featureCollection.features.length > 0) {
        for (var i = 0; i < featureCollection.features.length; i++) { // this loop looks for (currently) checkboxes that
          var this_feature = featureCollection.features[i];           // indicate inclusion into the google maps features
          if (this_feature.properties) {
            var this_property_key = this_feature.properties.source_type;
            var this_control = 'check_' + this_property_key;    // + '_' + OTU_ID
            if (otu_id != null) {
              this_control += '_' + otu_id
            }
          }
          if (document.getElementById(this_control) != undefined) {   // if checkbox control exists
            if (document.getElementById(this_control).checked) {      // if checked, and only
              this_feature.properties.otu_id = otu_id;
              chained.features.push(this_feature);                    // if checked, insert this feature/properties
            }                                                         // otherwise skip this feature
          }
          else {                                    // if no corresponding control property, do not block insertion
            chained.features.push(this_feature);    // functionality for non-checkbox-connected data
          }
        }
      }
    },

    log_2: function (x) {
      return Math.log(x) / Math.LN2;  // log_2(x) = ln(x)/ln(2)
    },

    clear_map: function (map) {
      map.data.forEach(function (feature) {
        map.data.remove(feature);
      });
    },

    get_canvas_bounds: function (map) {
      // original map canvas parameters are now lost, so we have to find them again
      var bounds = bounds || {};
      let canvas = map.data.map.getDiv();
      bounds.canvas_width = canvas.style.width.toString().split('px')[0];
      bounds.canvas_height = canvas.style.height.toString().split('px')[0];
      bounds.canvas_ratio = bounds.canvas_width / bounds.canvas_height;
      return bounds
    },

    // composite function to chain common dsplay stages (used by TW.tasks.gis.asserted_distributions)
    bound_and_recenter_map: function (data, map) {
      var bounds = TW.vendor.lib.google.maps.get_canvas_bounds(map);
      TW.vendor.lib.google.maps.getData(data, bounds);
      var center_lat_long = TW.vendor.lib.google.maps.get_window_center(bounds);
      map.setCenter(center_lat_long);
      map.setZoom(bounds.gzoom);
      TW.vendor.lib.google.maps.updateCoordinates(map, center_lat_long);
    },

    updateCoordinates: function (map, mapPoint) {
      if (document.getElementById("map_coords") != undefined) {
        let nowZoom = document.getElementById("map_coords").textContent.split('ZOOM: ')[1];
        document.getElementById("map_coords").textContent = 'LAT: ' + mapPoint.lat() + ' - LNG: '
          + mapPoint.lng() + ' - ZOOM: ' + map.getZoom();
      }
    },

    get_window_center: function (bounds) {      // for use with home-brew geoJSON scanner/enumerator
      var xminp = bounds.xminp;           //
      var xmaxp = bounds.xmaxp;           //
      var xminm = bounds.xminm;           //
      var xmaxm = bounds.xmaxm;           // these elements already set
      //if (bounds.prime /*&& bounds.anti*/) {
      //  xminp = 0;
      //  xmaxm = 0;
      //}
      //if (bounds.anti) {
      //  //xmaxp = 180;
      //  if(xminm <= -180) {
      //    if ((xminm + 360) < xmaxp) {
      //      xminm = xminm + 360;
      //    }
      //
      //  }
      //}
      //
      var ymin = bounds.ymin;             //
      var ymax = bounds.ymax;             //

      var center_long = bounds.center_long;   // these elements are calculated here,
      var center_lat = bounds.center_lat;     // unless preset validly in data-map-center
      var gzoom = bounds.gzoom;               //

      if (center_long == undefined) {    //determine case of area extent
        center_long = 0.0;
        var wm = 0.0;        // western hemisphere default area width
        var wp = 0.0;        // eastern hemisphere default area width
        var emptyEast = (xminp == 180 && xmaxp == 0);
        var emptyWest = (xminm == 0 && xmaxm == -180);
        if ((xminp == 180.0) && (xmaxp == 0.0)) {    //if no points, null out the range for this hemisphere
          xminp = 0.0;                                // possible place to mark singleton point
        }
        if ((xmaxm == -180.0) && (xminm == 0.0)) {    //if no points, null out the range for this hemisphere
          xmaxm = 0.0;                                // possible place to mark singleton point
        }
        if ((xminp == 0.0) && (xmaxp == 0.0) && (xmaxm == 0.0) && (xminm == 0.0)) {   // no data (kinda)
          xminm = -180.0;     // this calculation is to reflect
          xmaxp = 180.0;      // latitude calculation below
          center_long = 0.0;
          wx = 360.;
        }
        else {        // determine distribution of features in hemispheres
          wm = xmaxm - xminm;    // width of western area, if present (incorrect if e.g., Aleutean Islands)
          wp = xmaxp - xminp;    // width of eastern area, if present (incorrect if e.g., Aleutean Islands)
          var wxp;                    // width to anti/prime meridian east
          var wxm;                    // width to anti/prime meridian west
          var wx;                               // total width of all areas
          // determine if only one hemisphere
          if (emptyEast || emptyWest) {
            if (emptyEast) {    // then center_long is in the west
              center_long = xminm + 0.5 * wm;     // midpoint of west
              wx = wm;
            }
            else {
              center_long = xminp + 0.5 * wp;     // midpoint of east
              wx = wp;
            }
          }         // simple cases treated, continue far below at latitude compensation stage
          else {    // now known data in BOTH hemispheres
            // determine whether areas span meridian or antimeridian
            var primeWidth = xmaxp - xminm;
            var antiWidth = (180 - xminp) - (-180 - xmaxm);
            // determine which meridian is spanned
            var antiSpan = (180 - xmaxp) - (-180 - xminm);
            var primeSpan = xminp - xmaxm;
            var eastWest = (primeSpan < antiSpan);
            if (eastWest) {
              wx = primeWidth;
              wxp = xmaxp;
              wxm = -xminm;
              center_long = (xmaxp + xminm) / 2;
            }
            else {
              wx = antiWidth;
              wxp = 180 - xminp;
              wxm = 180 + xmaxm;
              center_long = -180 + (xminp + xmaxm) / 2;
            }
          }
        }
      }   // END center_long == undefined
      var offset = 0;                 // scope extended to be used in zoom calculation later
      if (center_lat == undefined) {
        if ((ymax == -90) && (ymin == 90)) {      // no data, so set whole earth limits
          ymax = 90.0;
          ymin = -90.0;
          center_lat = 0.0;
        }
        var wy = ymax - ymin;
        center_lat = 0.5 * (ymax + ymin);
        if (Math.abs(center_lat) > 1.0) {           // if vertical center not very close to equator
          var cutoff = 65.0;                        // empirically determined latitude
          if ((ymax > cutoff || ymin < -cutoff)) {  // if any vertical extent beyond cutoff
            var angle = ymax - cutoff;              // calculate
            if (center_lat < 0) {                   // angular distance
              angle = ymin + cutoff;                //  beyond cutoff
            }
            offset = Math.cos((angle /*- center_lat*/) / (180.0 / 3.1415926535));
            offset = 0.1 * (ymax - ymin) / offset;    // signed result in degrees(?)
            center_lat = center_lat + offset;
          }
        }
      }
      var swX = center_long - 0.5 * wx;               // pretest for overflow of antimeridian
      if (swX < -180) {
        swX = swX + 360
      }               // and if so, offset appropriately
      var sw = new google.maps.LatLng(ymin, swX);     // correct x JRF 29JUL2015
      bounds.sw = sw;
      var neX = center_long + 0.5 * wx;               // pretest for overflow of prime meridian
      if (neX > 180) {
        neX = neX - 360
      }               // and if so, offset appropriately
      var ne = new google.maps.LatLng(ymax, neX);     // correct x
      bounds.ne = ne;
      var box = new google.maps.LatLngBounds(sw, ne);

      var xzoom;
      var yzoom;

      ///// Google Maps only shows whole earth at zoom 1 if square canvas
      ///// Google Maps cuts off latitude at ~85+/- degrees
      ///// Hence vertical degrees yield twice as many pixels per degree as horizontal ones
      ///// for a square canvas
      ///// NOTE:  Google maps alias longitudes at zoom 1 unless canvas width is 2^n
      ///// calibrate zoom to canvas by ~  Math.pow(2, Math.floor(log_2(bounds.canvas_width)))
      ///// @ zoom = 0, the world is 256 x 256 pixels

      var x_deg_per_pix = 360.0 / bounds.canvas_width;
      var y_deg_per_pix = 170.0 / bounds.canvas_height / bounds.canvas_ratio;    // ?;
      y_deg_per_pix = (170.0 / bounds.canvas_ratio) / bounds.canvas_height;    // ?;
      y_deg_per_pix = 170.0 / (bounds.canvas_ratio * bounds.canvas_height);    // ?;

      var x_pixels_per_degree = bounds.canvas_width / 360;
      var y_pixels_per_degree = bounds.canvas_height / 170 * bounds.canvas_ratio; // mercator cutoff latitude
      y_pixels_per_degree = bounds.canvas_height / (170 / bounds.canvas_ratio); // mercator cutoff latitude
      // need mercator adjustment: approximate by

      var x_pixels = wx / x_deg_per_pix;
      var y_pixels = wy / y_deg_per_pix;

      x_pixels = wx * x_pixels_per_degree;
      //y_pixels = wy * y_pixels_per_degree;
      y_pixels = wy * y_pixels_per_degree / (Math.cos((center_lat) / (180.0 / 3.1415926535)));

      var aspect_ratio = x_pixels / y_pixels;   //changed from wx/wy//
      let zoomLimit = 15;
      if (x_pixels / bounds.canvas_width > y_pixels / bounds.canvas_height) {     // wider
        // pick x-axis
        xzoom = 1.0 - this.log_2(x_pixels / bounds.canvas_width);      // empirical inversion of log result
        if (xzoom > zoomLimit || xzoom == Infinity) {
          xzoom = zoomLimit;
        }
        gzoom = Math.floor(xzoom);
        if ((x_pixels * Math.pow(2, gzoom) * 2 < bounds.canvas_width) && (y_pixels * Math.pow(2, gzoom) * 2 < bounds.canvas_height)) {   // if I zoom again will it be not too wide?
          gzoom = gzoom + 1;
        }
      }
      else {                                        // wider
        // pick y-axis
        yzoom = 1.2 - this.log_2((y_pixels / bounds.canvas_height) /*/ bounds.canvas_ratio*/);      // terms expanded for debugging purposes
        if (yzoom > zoomLimit || yzoom == Infinity) {
          yzoom = zoomLimit;
        }
        gzoom = Math.floor(yzoom);
        if ((x_pixels * Math.pow(2, gzoom) < bounds.canvas_width) && (y_pixels * Math.pow(2, gzoom - 1) < bounds.canvas_height)) {   // if I zoom again will it be not too wide?
          gzoom = gzoom + 1;
        }
      }
/////// patch in new zoom calculation
//  xzoom = Math.floor(this.log_2((bounds.canvas_width / 256) * (360 / wx) ) );
//  yzoom = Math.floor(this.log_2((bounds.canvas_height / 256) * (360 / wy) ) );
//  xzoom = -Math.floor(this.log_2(1 / ( (bounds.canvas_width / 256) * (360 / wx) ) ) );
//  yzoom = -Math.floor(this.log_2( 1 / ((bounds.canvas_height / 256) * (360 / wy) ) ) );
//  gzoom = Math.min(xzoom, yzoom, 15);
///////
      if (wx > 90.0 || wy * bounds.canvas_ratio > 45.0) {   // Band-aids to cover USA, etc.
        gzoom = 2
      }
      if (wx > 180.0) {
        gzoom = 1
      }

      if (wx == 0 && wy == 0) {   // if there is a single marker
        gzoom = 12;
      }
      bounds.center_lat = center_lat;
      bounds.center_long = center_long;
      bounds.gzoom = gzoom;
      bounds.box = box;
      return new google.maps.LatLng(center_lat, center_long);
    },

// bounds for calculating center point
// divide longitude checks by hemisphere
// variables below used by stripped-down getData to compute bounds
// used to clear previous history
// so that center is recalculated
    reset_center_and_bounds: function (bounds) {
      bounds.center_long = undefined;     // current data-elements always (wrongly) contain -map-center='POINT (0.0 0.0 0.0)'
      bounds.center_lat = undefined;      // this section should set these bounds from data element when properly present

      bounds.xminp = 180.0;       // use 0
      bounds.xmaxp = 0.0;        // to
      bounds.xminm = 0.0;       // +/-180-based
      bounds.xmaxm = -180.0;   // coordinates for longitude

      bounds.ymin = 90.0;    // +/-90 for latitude
      bounds.ymax = -90.0;

      bounds.gzoom = 1;   // default zoom to whole earth
      bounds.box = new google.maps.LatLngBounds(new google.maps.LatLng(bounds.ymax, bounds.xmaxm),
        new google.maps.LatLng(bounds.ymin, bounds.xminp));
      bounds.canvas_ratio = 1;    // overridable prior to get_window_center

      bounds.prime = false;         // marker for prime meridian crossed
      bounds.true_now = false;      // marker for this point pair crossing
      bounds.prime_cont = false;    // flag for continuation of geometry
      bounds.prime_dir = 0;         // dire
      bounds.anti = false;
      bounds.anti_now = false;
      bounds.anti_cont = false;
      bounds.anti_dir = 0;

      bounds.xxminm = -180;
      bounds.xxmaxm = 0;
      bounds.xxminp = 0;
      bounds.xxmaxp = 180;
    },

// this is the scanner version; no google objects are created
    getData: function (feature_collection_data, bounds) {
      this.reset_center_and_bounds(bounds);

      // TODO: is this doing two things? What is the code after the above doing, altering the content of feature_collection_data
      // ANSWER: this sets the bounds, including zoom level ^ & v
      // ?! does this actually affect/return anything?!
      //  get data object encoded as geoJSON (deprecated: and disseminate to google (deprecated: and leaflet arrays))
      var data = feature_collection_data;
      if (typeof (data) != "undefined") {
        var dataArray = [];
        if (data instanceof Array) {
        }       // if already an array, then do nothing
        else    // convert it to an array
        {
          dataArray[0] = data;
          data = [];
          data[0] = dataArray[0];
        }
        for (var i = 0; i < data.length; i++) {
          if (typeof (data[i].type) != "undefined") {
            if (data[i].type == "FeatureCollection") {
              for (var j = 0; j < data[i].features.length; j++) {
                //getFeature(data[i].features[j]);
                this.getFeature(data[i].features[j], bounds);  //getTypeData(data[i].features[j].geometry);
              }
            }
            if (data[i].type == "GeometryCollection") {
              for (var j = 0; j < data[i].geometries.length; j++) {
                this.getTypeData(data[i].geometries[j], bounds);
              }
            }
            else {
              this.getTypeData(data[i], bounds);
            }  //data[i].type
          }     //data[i] != undefined
        }        // for i
      }           //data != undefined
    },             //getData

    getFeature: function (thisFeature, bounds) {
      if (thisFeature.type == "FeatureCollection") {
        this.getTypeData(thisFeature, bounds);   // if it requires recursion on FC
      }
      else {
        this.getTypeData(thisFeature.geometry, bounds);
      }
    },

    getTypeData: function (thisType, bounds) {        // this version does not create google objects
      if (thisType == undefined) {      // this test if to avoid js errors from  features without geometry
        return;                         // google maps forgives features wthout geometries
      }
      var i;                                    // loop
      var j;
      var k;                                    // indices
      var l;
      // for geometry types which contain line segments, we must introduce
      // detection of prime meridian and antimeridian crossing by each segment
      var lastX;        // used to compare prior point
      var lastY;
      var thisX;        // to current point
      var thisY;
      var ltNeg180;     // flag for detection of permuted line segment
      var gtPos180;     // flag for detection of permuted line segment
      var sign;
      lastX = undefined;                        // polygon starting in western hemisphere crossing anti-meridian
      ltNeg180 = false;                         // has all negative x-coordinates
      gtPos180 = false;                         // eastern-starting polys have all positive x-coords
      sign = 0;
      if (thisType.type == "FeatureCollection") {
        for (i = 0; i < thisType.features.length; i++) {
          if (typeof (thisType.features[i].type) != "undefined") {
            this.getFeature(thisType.features[i], bounds);    //  recurse if FeatureCollection
          }     //thisType != undefined
        }        //for i
        return
      }

      if (thisType.type == "GeometryCollection") {
        for (i = 0; i < thisType.geometries.length; i++) {
          if (typeof (thisType.geometries[i].type) != "undefined") {
            this.getTypeData(thisType.geometries[i], bounds);    //  recurse if GeometryCollection
          }     //thisType != undefined
        }       //for i
      }

      if (thisType.type == "Point") {
        this.xgtlt(bounds, thisType.coordinates[0]);
        this.ygtlt(bounds, thisType.coordinates[1]); //box check
      }

      if (thisType.type == "MultiPoint") {
        for (l = 0; l < thisType.coordinates.length; l++) {
          this.xgtlt(bounds, thisType.coordinates[l][0]);
          this.ygtlt(bounds, thisType.coordinates[l][1]); //box check
        }
      }

      if (thisType.type == "LineString") {          // special segments to compensate for permutation in controller
        for (l = 0; l < thisType.coordinates.length; l++) {   // first scan polyline/linestring to detect permuted case
          thisX = thisType.coordinates[l][0];
          thisY = thisType.coordinates[l][1];
          if (lastX) {
            if (this.meridianCheck(bounds, lastX, thisX)) {   // true if crossed anti-meridian
              if (thisX < -180) {
                ltNeg180 = true
              }             // permuted case detector
            }
          }
          lastX = thisX;
          lastY = thisY;
        }

        if (ltNeg180) {                                         // if permuted case detected
          for (l = 0; l < thisType.coordinates.length; l++) {
            thisX = thisType.coordinates[l][0];
            thisType.coordinates[l][0] = 360 + thisX;           // revert/convert permuted polygon to positive polygon
          }
        }

        lastX = undefined;                                      // now scan for bounds on adjusted linestring/polyline
        for (l = 0; l < thisType.coordinates.length; l++) {
          thisX = thisType.coordinates[l][0];
          this.xgtlt(bounds, thisX);
          this.ygtlt(bounds, thisType.coordinates[l][1]); //box check
        }
      }

      if (thisType.type == "MultiLineString") {
        for (k = 0; k < thisType.coordinates.length; k++) {   //k enumerates linestrings, l enums points
          for (l = 0; l < thisType.coordinates[k].length; l++) {
            this.xgtlt(bounds, thisType.coordinates[k][l][0]);
            this.ygtlt(bounds, thisType.coordinates[k][l][1]); //box check
            if (lastX) {
              if (this.meridianCheck(bounds, lastX, thisX)) {   // true if crossed anti-meridian
              }
            }
            lastX = thisX;
            lastY = thisY;
          }
        }
      }

      if (thisType.type == "Polygon") {           // special segments to compensate for permutation in controller
///*
        for (k = 0; k < thisType.coordinates.length; k++) {   // first scan polygon to detect permuted case
          // k enumerates polygons, l enumerates points
          for (l = 0; l < thisType.coordinates[k].length; l++) {
            thisX = thisType.coordinates[k][l][0];
            if (lastX) {
              if (this.meridianCheck(bounds, lastX, thisX)) {   // true if crossed anti-meridian
                if (thisX < -180) {
                  ltNeg180 = true;
                  sign = -1;
                }             // permuted case detector
                if (thisX > 180) {
                  gtPos180 = true;
                  sign = 1;
                }             // permuted case detector
              }
              this.extendedCheck(bounds, lastX, thisX);
            }
            lastX = thisX;
            lastY = thisY;
          }
        }

        if (ltNeg180 || gtPos180) {                                         // if permuted case detected
          for (k = 0; k < thisType.coordinates.length; k++) {
            // k enumerates polygons, l enumerates points
            for (l = 0; l < thisType.coordinates[k].length; l++) {
              thisX = thisType.coordinates[k][l][0];
              thisType.coordinates[k][l][0] = sign * (360 - thisX);      // revert/convert permuted polygon to positive polygon
            }
          }
        }
//*/
        lastX = undefined;                                      // now scan for bounds on adjusted polygon
        for (k = 0; k < thisType.coordinates.length; k++) {
          // k enumerates polygons, l enumerates points
          for (l = 0; l < thisType.coordinates[k].length; l++) {
            thisX = thisType.coordinates[k][l][0];
            this.xgtlt(bounds, thisX);    // thisType.coordinates[k][l][0]);
            this.ygtlt(bounds, thisType.coordinates[k][l][1]); //box check
            if (lastX) {
              if (this.meridianCheck(bounds, lastX, thisX)) {   // true if crossed anti-meridian
                if (thisX < -180) {
                  ltNeg180 = true
                }
              }
            }
            lastX = thisX;
          }
        }
      }

      if (thisType.type == "MultiPolygon") {
        for (j = 0; j < thisType.coordinates.length; j++) {    // j iterates over multipolygons   *-
          for (k = 0; k < thisType.coordinates[j].length; k++) {  //k iterates over polygons
            for (l = 0; l < thisType.coordinates[j][k].length; l++) {
              this.xgtlt(bounds, thisType.coordinates[j][k][l][0]);
              this.ygtlt(bounds, thisType.coordinates[j][k][l][1]); //box check
            }
          }
        }
      }
    },        //getFeatureData


    xgtlt: function (bounds, xtest) {         // point-wise x-bound extender

      if (xtest <= 0) {            // if western hemisphere,        ///////// replaced < with <= to mitigate point at
        if (xtest >= bounds.xmaxm) {    // xmaxm initially -180     ///////// replaced > with >= prime meridian problem
          bounds.xmaxm = xtest;
        }
        if (xtest <= bounds.xminm) {   // xminm initially 0
          bounds.xminm = xtest;
        }
      }
      if (xtest >= 0) {                  // eastern hemisphere
        if (xtest >= bounds.xmaxp) {   // xmaxp initially 0
          bounds.xmaxp = xtest;
        }
        if (xtest <= bounds.xminp) {   // xminp initially 180
          bounds.xminp = xtest;
        }
      }
    },

    ygtlt: function (bounds, ytest) {         // point-wise y-bound extender
      if (ytest > bounds.ymax) {
        bounds.ymax = ytest;
      }
      if (ytest < bounds.ymin) {
        bounds.ymin = ytest;
      }
    },

    meridianCheck: function (bounds, lastX, thisX) {
      if (lastX) {                            // do nothing if this is the first point (i.e., lastX undefined)
        var xm; // = 0.5 * (lastX + thisX);       // midpoint of current segment

        if (lastX <= 0) {                     // if prior point is western and this point is eastern
          if (thisX >= 0) {                 // crossing a meridian, sign changed
            xm = 0.5 * (thisX - lastX);     // calculate midpoint of west to east segment
            if (Math.abs(xm) > 90) {          // gross test for closer to anti-meridian
              bounds.anti = true;
              bounds.anti_now = true;         // must be cleared on next read
              bounds.anti_cont = true;
              bounds.anti_dir = 1;           // -1 is toward west
            }
            else {                            // crossing closer to prime meridian
              bounds.prime = true;
              bounds.prime_now = true;
              bounds.prime_cont = true;
              bounds.prime_dir = -1;           // +1 is toward east
            }
          }
          if (thisX <= -180) {                  // and lastX < 0
            xm = 0.5 * (thisX + lastX);
            if (Math.abs(xm) > 90) {          // gross test for closer to anti-meridian
              bounds.anti = true;
              bounds.anti_now = true;         // must be cleared on next read
              bounds.anti_cont = true;
              bounds.anti_dir = -1;           // -1 is toward west
            }
            else {
              bounds.prime = true;
              bounds.prime_now = true;
              bounds.prime_cont = true;
              bounds.prime_dir = 1;           // +1 is toward east
            }

          }
        }
        if (lastX >= 0) {                     // if prior point is eastern and this point is western
          if (thisX <= 0) {                 // crossing a meridian, sign changed
            xm = 0.5 * (thisX - lastX);     // calculate midpoint of west to east segment
            if (Math.abs(xm) > 90) {          // gross test for closer to anti-meridian
              bounds.anti = true;
              bounds.anti_now = true;         // must be cleared on next read
              bounds.anti_cont = true;
              bounds.anti_dir = 1;           // -1 is toward west
            }
            else {                            // crossing closer to prime meridian
              bounds.prime = true;
              bounds.prime_now = true;
              bounds.prime_cont = true;
              bounds.prime_dir = -1;           // +1 is toward east
            }
          }
          if (thisX >= 180) {                  // and lastX >= 0
            xm = 0.5 * (thisX + lastX);
            if (Math.abs(xm) > 90) {          // gross test for closer to anti-meridian
              bounds.anti = true;
              bounds.anti_now = true;         // must be cleared on next read
              bounds.anti_cont = true;
              bounds.anti_dir = +1;           // -1 is toward west
            }
            else {
              bounds.prime = true;
              bounds.prime_now = true;
              bounds.prime_cont = true;
              bounds.prime_dir = -1;           // +1 is toward east
            }
          }
        }
        return undefined;
      }
    },

    extendedCheck: function (bounds, lastX, thisX) {
      // need continuation semaphores for each meridian crossing
      // if cross both merdians, we were inside the polygon
      // if cross the same meridian these are the points we want
      if (bounds.prime_cont) {          // crossing prime meridian is the simpler case
        if (bounds.prime_dir > 0) {
          if (thisX > bounds.xxmaxm) {
            bounds.xxmaxm = thisX
          }
        }
        else {
          if (thisX < bounds.xxminp) {
            bounds.xxminp = thisX
          }
        }
      }
      if (bounds.anti_cont) {
        if (bounds.anti_dir < 0) {
          if (bounds.anti_now) {
            thisX = 360 - thisX
          }
          if (thisX < bounds.xxminm) {
            bounds.xxminm = thisX
          }
        }
        else {
          if (bounds.anti_now) {
            thisX = thisX - 360
          }
          if (thisX > bounds.xxmaxp) {
            bounds.xxmaxp = thisX
          }
        }
      }
      bounds.anti_now = false;
      bounds.prime_now = false;
    }

  }
);