Zachary Tong 12 лет назад
Родитель
Сommit
7dc708d545
1 измененных файлов с 415 добавлено и 415 удалено
  1. 415 415
      panels/map2/module.js

+ 415 - 415
panels/map2/module.js

@@ -1,489 +1,489 @@
 angular.module('kibana.map2', [])
-    .controller('map2', function ($scope, eventBus, keylistener) {
-
-        // Set and populate defaults
-        var _d = {
-            query: "*",
-            map: "world",
-            colors: ['#C8EEFF', '#0071A4'],
-            size: 100,
-            exclude: [],
-            spyable: true,
-            group: "default",
-            index_limit: 0,
-            display: {
-                translate:[0, 0],
-                scale:-1,
-                data: {
-                    samples: 1000,
-                    type: "mercator"
-                },
-                geopoints: {
-                    enabled: false,
-                    enabledText: "Enabled",
-                    pointSize: 0.3,
-                    pointAlpha: 0.6
-                },
-                binning: {
-                    enabled: false,
-                    hexagonSize: 2,
-                    hexagonAlpha: 1.0,
-                    areaEncoding: true,
-                    areaEncodingField: "primary",
-                    colorEncoding: true,
-                    colorEncodingField: "primary"
-                },
-                choropleth: {
-                    enabled: false
-                },
-                bullseye: {
-                    enabled: false,
-                    coord: {
-                        lat: 0,
-                        lon: 0
-                    }
-                }
-            },
-            activeDisplayTab:"Geopoints"
-        };
-
-        _.defaults($scope.panel, _d)
-
-        $scope.init = function () {
-            eventBus.register($scope, 'time', function (event, time) {
-                set_time(time)
-            });
-            eventBus.register($scope, 'query', function (event, query) {
-                $scope.panel.query = _.isArray(query) ? query[0] : query;
-                $scope.get_data();
-            });
-            // Now that we're all setup, request the time from our group
-            eventBus.broadcast($scope.$id, $scope.panel.group, 'get_time');
-
-
-
-        };
-
-        $scope.get_data = function () {
-
-            // Make sure we have everything for the request to complete
-            if (_.isUndefined($scope.panel.index) || _.isUndefined($scope.time))
-                return
-
-            $scope.panel.loading = true;
-            var request = $scope.ejs.Request().indices($scope.panel.index);
-
-
-            var metric = 'count';
-
-            //Use a regular term facet if there is no secondary field
-            if (typeof $scope.panel.secondaryfield === "undefined") {
-                var facet = $scope.ejs.TermsFacet('map')
-                    .field($scope.panel.field)
-                    .size($scope.panel.display.data.samples)
-                    .exclude($scope.panel.exclude)
-                    .facetFilter(ejs.QueryFilter(
-                        ejs.FilteredQuery(
-                            ejs.QueryStringQuery($scope.panel.query || '*'),
-                            ejs.RangeFilter($scope.time.field)
-                                .from($scope.time.from)
-                                .to($scope.time.to))));
-
-                metric = 'count';
-            } else {
-                //otherwise, use term stats
-                //NOTE: this will break if valueField is a geo_point
-                //      need to put in checks for that
-                var facet = $scope.ejs.TermStatsFacet('map')
-                    .keyField($scope.panel.field)
-                    .valueField($scope.panel.secondaryfield)
-                    .size($scope.panel.display.data.samples)
-                    .facetFilter(ejs.QueryFilter(
-                        ejs.FilteredQuery(
-                            ejs.QueryStringQuery($scope.panel.query || '*'),
-                            ejs.RangeFilter($scope.time.field)
-                                .from($scope.time.from)
-                                .to($scope.time.to))));
-
-                metric = 'total';
-            }
-
-
-            // Then the insert into facet and make the request
-            var request = request.facet(facet).size(0);
-
-            $scope.populate_modal(request);
-
-            var results = request.doSearch();
-
-            // Populate scope when we have results
-            results.then(function (results) {
-                $scope.panel.loading = false;
-                $scope.hits = results.hits.total;
-                $scope.data = {};
-
-                _.each(results.facets.map.terms, function (v) {
-
-                    if (!_.isNumber(v.term)) {
-                        $scope.data[v.term.toUpperCase()] = v[metric];
-                    } else {
-                        $scope.data[v.term] = v[metric];
-                    }
-                });
-
-                $scope.$emit('render')
-            });
-        };
-
-        // I really don't like this function, too much dom manip. Break out into directive?
-        $scope.populate_modal = function (request) {
-            $scope.modal = {
-                title: "Inspector",
-                body: "<h5>Last Elasticsearch Query</h5><pre>" + 'curl -XGET ' + config.elasticsearch + '/' + $scope.panel.index + "/_search?pretty -d'\n" + angular.toJson(JSON.parse(request.toString()), true) + "'</pre>"
-            }
-        };
-
-        function set_time(time) {
-            $scope.time = time;
-            $scope.panel.index = _.isUndefined(time.index) ? $scope.panel.index : time.index
-            $scope.get_data();
+  .controller('map2', function ($scope, eventBus, keylistener) {
+
+    // Set and populate defaults
+    var _d = {
+      query: "*",
+      map: "world",
+      colors: ['#C8EEFF', '#0071A4'],
+      size: 100,
+      exclude: [],
+      spyable: true,
+      group: "default",
+      index_limit: 0,
+      display: {
+        translate:[0, 0],
+        scale:-1,
+        data: {
+          samples: 1000,
+          type: "mercator"
+        },
+        geopoints: {
+          enabled: false,
+          enabledText: "Enabled",
+          pointSize: 0.3,
+          pointAlpha: 0.6
+        },
+        binning: {
+          enabled: false,
+          hexagonSize: 2,
+          hexagonAlpha: 1.0,
+          areaEncoding: true,
+          areaEncodingField: "primary",
+          colorEncoding: true,
+          colorEncodingField: "primary"
+        },
+        choropleth: {
+          enabled: false
+        },
+        bullseye: {
+          enabled: false,
+          coord: {
+            lat: 0,
+            lon: 0
+          }
         }
-
-        $scope.build_search = function (field, value) {
-            $scope.panel.query = add_to_query($scope.panel.query, field, value, false)
-            $scope.get_data();
-            eventBus.broadcast($scope.$id, $scope.panel.group, 'query', $scope.panel.query);
+      },
+      activeDisplayTab:"Geopoints"
+    };
+
+    _.defaults($scope.panel, _d)
+
+    $scope.init = function () {
+      eventBus.register($scope, 'time', function (event, time) {
+        set_time(time)
+      });
+      eventBus.register($scope, 'query', function (event, query) {
+        $scope.panel.query = _.isArray(query) ? query[0] : query;
+        $scope.get_data();
+      });
+      // Now that we're all setup, request the time from our group
+      eventBus.broadcast($scope.$id, $scope.panel.group, 'get_time');
+
+
+
+    };
+
+    $scope.get_data = function () {
+
+      // Make sure we have everything for the request to complete
+      if (_.isUndefined($scope.panel.index) || _.isUndefined($scope.time))
+        return
+
+      $scope.panel.loading = true;
+      var request = $scope.ejs.Request().indices($scope.panel.index);
+
+
+      var metric = 'count';
+
+      //Use a regular term facet if there is no secondary field
+      if (typeof $scope.panel.secondaryfield === "undefined") {
+        var facet = $scope.ejs.TermsFacet('map')
+          .field($scope.panel.field)
+          .size($scope.panel.display.data.samples)
+          .exclude($scope.panel.exclude)
+          .facetFilter(ejs.QueryFilter(
+            ejs.FilteredQuery(
+              ejs.QueryStringQuery($scope.panel.query || '*'),
+              ejs.RangeFilter($scope.time.field)
+                .from($scope.time.from)
+                .to($scope.time.to))));
+
+        metric = 'count';
+      } else {
+        //otherwise, use term stats
+        //NOTE: this will break if valueField is a geo_point
+        //      need to put in checks for that
+        var facet = $scope.ejs.TermStatsFacet('map')
+          .keyField($scope.panel.field)
+          .valueField($scope.panel.secondaryfield)
+          .size($scope.panel.display.data.samples)
+          .facetFilter(ejs.QueryFilter(
+            ejs.FilteredQuery(
+              ejs.QueryStringQuery($scope.panel.query || '*'),
+              ejs.RangeFilter($scope.time.field)
+                .from($scope.time.from)
+                .to($scope.time.to))));
+
+        metric = 'total';
+      }
+
+
+      // Then the insert into facet and make the request
+      var request = request.facet(facet).size(0);
+
+      $scope.populate_modal(request);
+
+      var results = request.doSearch();
+
+      // Populate scope when we have results
+      results.then(function (results) {
+        $scope.panel.loading = false;
+        $scope.hits = results.hits.total;
+        $scope.data = {};
+
+        _.each(results.facets.map.terms, function (v) {
+
+          if (!_.isNumber(v.term)) {
+            $scope.data[v.term.toUpperCase()] = v[metric];
+          } else {
+            $scope.data[v.term] = v[metric];
+          }
+        });
+
+        $scope.$emit('render')
+      });
+    };
+
+    // I really don't like this function, too much dom manip. Break out into directive?
+    $scope.populate_modal = function (request) {
+      $scope.modal = {
+        title: "Inspector",
+        body: "<h5>Last Elasticsearch Query</h5><pre>" + 'curl -XGET ' + config.elasticsearch + '/' + $scope.panel.index + "/_search?pretty -d'\n" + angular.toJson(JSON.parse(request.toString()), true) + "'</pre>"
+      }
+    };
+
+    function set_time(time) {
+      $scope.time = time;
+      $scope.panel.index = _.isUndefined(time.index) ? $scope.panel.index : time.index
+      $scope.get_data();
+    }
+
+    $scope.build_search = function (field, value) {
+      $scope.panel.query = add_to_query($scope.panel.query, field, value, false)
+      $scope.get_data();
+      eventBus.broadcast($scope.$id, $scope.panel.group, 'query', $scope.panel.query);
+    };
+
+    $scope.isActive = function(tab) {
+      return (tab.toLowerCase() === $scope.panel.activeDisplayTab.toLowerCase());
+    }
+
+    $scope.tabClick = function(tab) {
+      $scope.panel.activeDisplayTab = tab;
+    }
+
+  })
+  .filter('enabledText', function() {
+    return function (value) {
+      if (value === true) {
+        return "Enabled";
+      } else {
+        return "Disabled";
+      }
+    }
+  })
+  .directive('map2', function () {
+    return {
+      restrict: 'A',
+      link: function (scope, elem, attrs) {
+
+
+        //elem.html('')
+        scope.initializing = false;
+        scope.worldData = null;
+        scope.worldNames = null;
+
+        scope.ctrlKey = false;
+
+        //These are various options that should not be cached in scope.panel
+        scope.options = {
+
+          data: {
+            dropdown:[
+              {
+                "text": "Mercator (Flat)",
+                id: "mercator"
+              },
+              {
+                text: "Orthographic (Sphere)",
+                id: "orthographic"
+              }
+            ]
+          }
         };
 
-        $scope.isActive = function(tab) {
-            return (tab.toLowerCase() === $scope.panel.activeDisplayTab.toLowerCase());
-        }
 
-        $scope.tabClick = function(tab) {
-            $scope.panel.activeDisplayTab = tab;
-        }
+        /**
+         * Initialize the panels if new, or render existing panels
+         */
+        scope.init_or_render = function() {
+          if (typeof scope.svg === 'undefined') {
+            console.log("init");
 
-    })
-    .filter('enabledText', function() {
-        return function (value) {
-            if (value === true) {
-                return "Enabled";
-            } else {
-                return "Disabled";
+            //prevent duplicate initialization steps, if render is called again
+            //before the svg is setup
+            if (!scope.initializing) {
+              init_panel();
             }
-        }
-    })
-    .directive('map2', function () {
-        return {
-            restrict: 'A',
-            link: function (scope, elem, attrs) {
-
+          } else {
+            console.log("render");
+            render_panel();
+          }
+        };
 
-                //elem.html('')
-                scope.initializing = false;
-                scope.worldData = null;
-                scope.worldNames = null;
-
-                scope.ctrlKey = false;
-
-                //These are various options that should not be cached in scope.panel
-                scope.options = {
-
-                    data: {
-                        dropdown:[
-                            {
-                                "text": "Mercator (Flat)",
-                                id: "mercator"
-                            },
-                            {
-                                text: "Orthographic (Sphere)",
-                                id: "orthographic"
-                            }
-                        ]
-                    }
-                };
-
-
-                /**
-                 * Initialize the panels if new, or render existing panels
-                 */
-                scope.init_or_render = function() {
-                    if (typeof scope.svg === 'undefined') {
-                        console.log("init");
-
-                        //prevent duplicate initialization steps, if render is called again
-                        //before the svg is setup
-                        if (!scope.initializing) {
-                            init_panel();
-                        }
-                    } else {
-                        console.log("render");
-                        render_panel();
-                    }
-                };
 
+        /**
+         * Receive render events
+         */
+        scope.$on('render', function () {
+          scope.init_or_render();
+        });
 
-                /**
-                 * Receive render events
-                 */
-                scope.$on('render', function () {
-                    scope.init_or_render();
-                });
+        /**
+         * On window resize, re-render the panel
+         */
+        angular.element(window).bind('resize', function () {
+          scope.init_or_render();
+        });
 
-                /**
-                 * On window resize, re-render the panel
-                 */
-                angular.element(window).bind('resize', function () {
-                    scope.init_or_render();
-                });
 
+        /**
+         * Load the various panel-specific scripts, map data, then initialize
+         * the svg and set appropriate D3 settings
+         */
+        function init_panel() {
 
-                /**
-                 * Load the various panel-specific scripts, map data, then initialize
-                 * the svg and set appropriate D3 settings
-                 */
-                function init_panel() {
+          scope.initializing = true;
+          // Using LABjs, wait until all scripts are loaded before rendering panel
+          var scripts = $LAB.script("common/lib/d3.v3.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/topojson.v1.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/node-geohash.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/d3.hexbin.v0.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/lib/queue.v1.min.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/display/binning.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/display/geopoints.js?rand="+Math.floor(Math.random()*10000))
+            .script("panels/map2/display/bullseye.js?rand="+Math.floor(Math.random()*10000));
 
-                    scope.initializing = true;
-                    // Using LABjs, wait until all scripts are loaded before rendering panel
-                    var scripts = $LAB.script("panels/map2/lib/d3.v3.min.js?rand="+Math.floor(Math.random()*10000))
-                        .script("panels/map2/lib/topojson.v1.min.js?rand="+Math.floor(Math.random()*10000))
-                        .script("panels/map2/lib/node-geohash.js?rand="+Math.floor(Math.random()*10000))
-                        .script("panels/map2/lib/d3.hexbin.v0.min.js?rand="+Math.floor(Math.random()*10000))
-                        .script("panels/map2/lib/queue.v1.min.js?rand="+Math.floor(Math.random()*10000))
-                        .script("panels/map2/display/binning.js?rand="+Math.floor(Math.random()*10000))
-                        .script("panels/map2/display/geopoints.js?rand="+Math.floor(Math.random()*10000))
-                        .script("panels/map2/display/bullseye.js?rand="+Math.floor(Math.random()*10000));
+          // Populate element. Note that jvectormap appends, does not replace.
+          scripts.wait(function () {
 
-                    // Populate element. Note that jvectormap appends, does not replace.
-                    scripts.wait(function () {
+            queue()
+              .defer(d3.json, "panels/map2/lib/world-110m.json")
+              .defer(d3.tsv, "panels/map2/lib/world-country-names.tsv")
+              .await(function(error, world, names) {
+                scope.worldData = world;
+                scope.worldNames = names;
 
-                        queue()
-                            .defer(d3.json, "panels/map2/lib/world-110m.json")
-                            .defer(d3.tsv, "panels/map2/lib/world-country-names.tsv")
-                            .await(function(error, world, names) {
-                                scope.worldData = world;
-                                scope.worldNames = names;
 
+                //Better way to get these values?  Seems kludgy to use jQuery on the div...
+                var width = $(elem[0]).width(),
+                  height = $(elem[0]).height();
 
-                                //Better way to get these values?  Seems kludgy to use jQuery on the div...
-                                var width = $(elem[0]).width(),
-                                    height = $(elem[0]).height();
 
+                //scale to whichever dimension is smaller, helps to ensure the whole map is displayed
+                scope.scale = (width > height) ? (height/5) : (width/5);
 
-                                //scale to whichever dimension is smaller, helps to ensure the whole map is displayed
-                                scope.scale = (width > height) ? (height/5) : (width/5);
 
+                scope.zoom = d3.behavior.zoom()
+                  .scaleExtent([1, 8])
+                  .on("zoom", translate_map);
 
-                                scope.zoom = d3.behavior.zoom()
-                                    .scaleExtent([1, 8])
-                                    .on("zoom", translate_map);
+                //used by choropleth
+                //@todo change domain so that it reflects the domain of the data
+                scope.quantize = d3.scale.quantize()
+                  .domain([0, 1000])
+                  .range(d3.range(9).map(function(i) { return "q" + (i+1); }));
 
-                                //used by choropleth
-                                //@todo change domain so that it reflects the domain of the data
-                                scope.quantize = d3.scale.quantize()
-                                    .domain([0, 1000])
-                                    .range(d3.range(9).map(function(i) { return "q" + (i+1); }));
 
+                //Extract name and two-letter codes for our countries
+                scope.countries = topojson.feature(scope.worldData, scope.worldData.objects.countries).features;
 
-                                //Extract name and two-letter codes for our countries
-                                scope.countries = topojson.feature(scope.worldData, scope.worldData.objects.countries).features;
+                scope.countries = scope.countries.filter(function(d) {
+                  return scope.worldNames.some(function(n) {
+                    if (d.id == n.id) {
+                      d.name = n.name;
+                      return d.short = n.short;
+                    }
+                  });
+                }).sort(function(a, b) {
+                    return a.name.localeCompare(b.name);
+                  });
 
-                                scope.countries = scope.countries.filter(function(d) {
-                                    return scope.worldNames.some(function(n) {
-                                        if (d.id == n.id) {
-                                            d.name = n.name;
-                                            return d.short = n.short;
-                                        }
-                                    });
-                                }).sort(function(a, b) {
-                                        return a.name.localeCompare(b.name);
-                                    });
 
+                //create the new svg
+                scope.svg = d3.select(elem[0]).append("svg")
+                  .attr("width", "100%")
+                  .attr("height", "100%")
+                  .attr("viewBox", "0 0 " + width + " " + height)
+                  .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
+                  .call(scope.zoom);
+                scope.g = scope.svg.append("g");
 
-                                //create the new svg
-                                scope.svg = d3.select(elem[0]).append("svg")
-                                    .attr("width", "100%")
-                                    .attr("height", "100%")
-                                    .attr("viewBox", "0 0 " + width + " " + height)
-                                    .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
-                                    .call(scope.zoom);
-                                scope.g = scope.svg.append("g");
 
+                scope.initializing = false;
+                render_panel();
+              });
+          });
 
-                                scope.initializing = false;
-                                render_panel();
-                            });
-                    });
+        }
 
-                }
 
+        /**
+         * Render updates to the SVG. Typically happens when the data changes (time, query)
+         * or when new options are selected
+         */
+        function render_panel() {
 
-                /**
-                 * Render updates to the SVG. Typically happens when the data changes (time, query)
-                 * or when new options are selected
-                 */
-                function render_panel() {
-
-                    var width = $(elem[0]).width(),
-                        height = $(elem[0]).height();
+          var width = $(elem[0]).width(),
+            height = $(elem[0]).height();
 
 
-                    //Projection is dependant on the map-type
-                    if (scope.panel.display.data.type === 'mercator') {
-                        scope.projection = d3.geo.mercator()
-                            .translate([width/2, height/2])
-                            .scale(scope.scale);
+          //Projection is dependant on the map-type
+          if (scope.panel.display.data.type === 'mercator') {
+            scope.projection = d3.geo.mercator()
+              .translate([width/2, height/2])
+              .scale(scope.scale);
 
-                    } else if (scope.panel.display.data.type === 'orthographic') {
-                        scope.projection = d3.geo.orthographic()
-                            .translate([width/2, height/2])
-                            .scale(100)
-                            .clipAngle(90);
+          } else if (scope.panel.display.data.type === 'orthographic') {
+            scope.projection = d3.geo.orthographic()
+              .translate([width/2, height/2])
+              .scale(100)
+              .clipAngle(90);
 
-                        //recenters the sphere more towards the US...not really necessary
-                        scope.projection.rotate([100 / 2, 20 / 2, scope.projection.rotate()[2]]);
+            //recenters the sphere more towards the US...not really necessary
+            scope.projection.rotate([100 / 2, 20 / 2, scope.projection.rotate()[2]]);
 
-                    }
+          }
 
-                    var path = d3.geo.path()
-                        .projection(scope.projection);
+          var path = d3.geo.path()
+            .projection(scope.projection);
 
 
-                    //Special fix for when the user changes from mercator -> orthographic
-                    //The globe won't redraw automatically, we need to force it
-                    if (scope.panel.display.data.type === 'orthographic') {
-                        scope.svg.selectAll("path").attr("d", path);
-                    }
+          //Special fix for when the user changes from mercator -> orthographic
+          //The globe won't redraw automatically, we need to force it
+          if (scope.panel.display.data.type === 'orthographic') {
+            scope.svg.selectAll("path").attr("d", path);
+          }
 
-                    console.log(scope.data);
+          console.log(scope.data);
 
-                    //Geocoded points are decoded into lonlat
-                    scope.points = _.map(scope.data, function (k, v) {
-                        //console.log(k,v);
-                        var decoded = geohash.decode(v);
-                        return [decoded.longitude, decoded.latitude];
-                    });
+          //Geocoded points are decoded into lonlat
+          scope.points = _.map(scope.data, function (k, v) {
+            //console.log(k,v);
+            var decoded = geohash.decode(v);
+            return [decoded.longitude, decoded.latitude];
+          });
 
-                    //And also projected projected to x/y.  Both sets of points are used
-                    //by different functions
-                    scope.projectedPoints = _.map(scope.points, function (coords) {
-                        return scope.projection(coords);
-                    });
+          //And also projected projected to x/y.  Both sets of points are used
+          //by different functions
+          scope.projectedPoints = _.map(scope.points, function (coords) {
+            return scope.projection(coords);
+          });
 
 
 
-                    scope.svg.select(".overlay").remove();
+          scope.svg.select(".overlay").remove();
 
-                    scope.svg.append("rect")
-                        .attr("class", "overlay")
-                        .attr("width", width)
-                        .attr("height", height)
-                        .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
+          scope.svg.append("rect")
+            .attr("class", "overlay")
+            .attr("width", width)
+            .attr("height", height)
+            .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
 
 
-                    //Draw the countries, if this is a choropleth, draw with fancy colors
-                    var countryPath = scope.g.selectAll(".land")
-                        .data(scope.countries);
+          //Draw the countries, if this is a choropleth, draw with fancy colors
+          var countryPath = scope.g.selectAll(".land")
+            .data(scope.countries);
 
-                    countryPath.enter().append("path")
-                        .attr("class", function(d) {
-                            if (scope.panel.display.choropleth.enabled) {
-                                return 'land ' + scope.quantize(scope.data[d.short]);
-                            } else {
-                                return 'land';
-                            }
-                        })
-                        .attr("d", path);
+          countryPath.enter().append("path")
+            .attr("class", function(d) {
+              if (scope.panel.display.choropleth.enabled) {
+                return 'land ' + scope.quantize(scope.data[d.short]);
+              } else {
+                return 'land';
+              }
+            })
+            .attr("d", path);
 
-                    countryPath.exit().remove();
+          countryPath.exit().remove();
 
 
-                    /*
-                    //draw boundaries
-                    scope.g.selectAll("land").append("path")
-                        .datum(topojson.mesh(scope.worldData, scope.worldData.objects.land, function(a, b) { return a !== b; }))
-                        .attr("class", "land boundary")
-                        .attr("d", path);
-                    */
+          /*
+           //draw boundaries
+           scope.g.selectAll("land").append("path")
+           .datum(topojson.mesh(scope.worldData, scope.worldData.objects.land, function(a, b) { return a !== b; }))
+           .attr("class", "land boundary")
+           .attr("d", path);
+           */
 
 
 
-                    //If this is a sphere, set up drag and keypress listeners
-                    //@todo implement a global "keypress service", since this fails if there are >1 spheres
-                    if (scope.panel.display.data.type === 'orthographic') {
+          //If this is a sphere, set up drag and keypress listeners
+          //@todo implement a global "keypress service", since this fails if there are >1 spheres
+          if (scope.panel.display.data.type === 'orthographic') {
 
-                        //scope.svg.focus();
-                        /*
-                        scope.svg.selectAll(".overlay")
-                            .on("keydown", function() {
-                                scope.ctrlKey = d3.event.ctrlKey;
-                            })
-                            .on("keyup", function() {
-                                scope.ctrlKey = d3.event.ctrlKey;
-                            });
-*/
+            //scope.svg.focus();
+            /*
+             scope.svg.selectAll(".overlay")
+             .on("keydown", function() {
+             scope.ctrlKey = d3.event.ctrlKey;
+             })
+             .on("keyup", function() {
+             scope.ctrlKey = d3.event.ctrlKey;
+             });
+             */
 
 
-                        scope.svg.style("cursor", "move")
-                            .call(d3.behavior.drag()
-                                .origin(function() { var rotate = scope.projection.rotate(); return {x: 2 * rotate[0], y: -2 * rotate[1]}; })
-                                .on("drag", function() {
-                                    if ( scope.ctrlKey) {
-                                        scope.projection.rotate([d3.event.x / 2, -d3.event.y / 2, scope.projection.rotate()[2]]);
-                                        scope.svg.selectAll("path").attr("d", path);
-                                    }
-                                }));
-                    }
+            scope.svg.style("cursor", "move")
+              .call(d3.behavior.drag()
+                .origin(function() { var rotate = scope.projection.rotate(); return {x: 2 * rotate[0], y: -2 * rotate[1]}; })
+                .on("drag", function() {
+                  if ( scope.ctrlKey) {
+                    scope.projection.rotate([d3.event.x / 2, -d3.event.y / 2, scope.projection.rotate()[2]]);
+                    scope.svg.selectAll("path").attr("d", path);
+                  }
+                }));
+          }
 
 
-                    /**
-                     * Display option rendering
-                     * Order is important to render order here!
-                     */
+          /**
+           * Display option rendering
+           * Order is important to render order here!
+           */
 
-                    //@todo fix this
-                    var dimensions = [width, height];
-                    displayBinning(scope, dimensions);
-                    displayGeopoints(scope, path);
-                    displayBullseye(scope, path);
+          //@todo fix this
+          var dimensions = [width, height];
+          displayBinning(scope, dimensions);
+          displayGeopoints(scope, path);
+          displayBullseye(scope, path);
 
 
 
 
-                    //If the panel scale is not default (e.g. the user has moved the maps around)
-                    //set the scale and position to the last saved config
-                    if (scope.panel.display.scale != -1) {
-                        scope.zoom.scale(scope.panel.display.scale).translate(scope.panel.display.translate);
-                        scope.g.style("stroke-width", 1 / scope.panel.display.scale).attr("transform", "translate(" + scope.panel.display.translate + ") scale(" + scope.panel.display.scale + ")");
+          //If the panel scale is not default (e.g. the user has moved the maps around)
+          //set the scale and position to the last saved config
+          if (scope.panel.display.scale != -1) {
+            scope.zoom.scale(scope.panel.display.scale).translate(scope.panel.display.translate);
+            scope.g.style("stroke-width", 1 / scope.panel.display.scale).attr("transform", "translate(" + scope.panel.display.translate + ") scale(" + scope.panel.display.scale + ")");
 
-                    }
+          }
 
-                }
+        }
 
 
-                /**
-                 * On D3 zoom events, pan/zoom the map
-                 * Only applies if the ctrl-key is not pressed, so it doesn't clobber
-                 * sphere dragging
-                 */
-                function translate_map() {
+        /**
+         * On D3 zoom events, pan/zoom the map
+         * Only applies if the ctrl-key is not pressed, so it doesn't clobber
+         * sphere dragging
+         */
+        function translate_map() {
 
-                    var width = $(elem[0]).width(),
-                        height = $(elem[0]).height();
+          var width = $(elem[0]).width(),
+            height = $(elem[0]).height();
 
-                    if (! scope.ctrlKey) {
-                        var t = d3.event.translate,
-                            s = d3.event.scale;
-                        t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
-                        t[1] = Math.min(height / 2 * (s - 1) + 230 * s, Math.max(height / 2 * (1 - s) - 230 * s, t[1]));
-                        scope.zoom.translate(t);
+          if (! scope.ctrlKey) {
+            var t = d3.event.translate,
+              s = d3.event.scale;
+            t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
+            t[1] = Math.min(height / 2 * (s - 1) + 230 * s, Math.max(height / 2 * (1 - s) - 230 * s, t[1]));
+            scope.zoom.translate(t);
 
-                        scope.panel.display.translate = t;
-                        scope.panel.display.scale = s;
-                        scope.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
-                    }
-                }
-            }
-        };
-    });
+            scope.panel.display.translate = t;
+            scope.panel.display.scale = s;
+            scope.g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
+          }
+        }
+      }
+    };
+  });