From 6f1e4f2e7413323be10b2c6876c99f30f57c36d6 Mon Sep 17 00:00:00 2001 From: Jonathan Beezley Date: Thu, 30 Mar 2017 09:04:33 -0400 Subject: [PATCH 1/2] Replace vendored wigglemaps with kdbush on npm --- package.json | 1 + src/index.js | 4 + src/pointFeature.js | 17 +- src/util/wigglemaps.js | 404 ----------------------------------------- 4 files changed, 10 insertions(+), 416 deletions(-) delete mode 100644 src/util/wigglemaps.js diff --git a/package.json b/package.json index 68959a7716..77cfce1035 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "karma-sinon": "^1.0.4", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.7.0", + "kdbush": "^1.0.1", "mousetrap": "^1.6.0", "nib": "^1.1.2", "node-resemble": "^1.1.3", diff --git a/src/index.js b/src/index.js index b49958d7d3..41a6191825 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,10 @@ * earcut * @copyright 2016, Mapbox * @license ISC + * + * kdbush + * @copyright 2017, Vladimir Agafonkin + * @license ISC */ var $ = require('jquery'); diff --git a/src/pointFeature.js b/src/pointFeature.js index ca557802d0..a56db93310 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -25,7 +25,7 @@ var pointFeature = function (arg) { var ClusterGroup = require('./util/clustering'); var geo_event = require('./event'); var util = require('./util'); - var wigglemaps = require('./util/wigglemaps'); + var kdbush = require('kdbush'); //////////////////////////////////////////////////////////////////////////// /** @@ -194,7 +194,6 @@ var pointFeature = function (arg) { // create an array of positions in geo coordinates pts = m_this.data().map(function (d, i) { var pt = position(d); - pt.idx = i; // store the maximum point radius m_maxRadius = Math.max( @@ -202,10 +201,10 @@ var pointFeature = function (arg) { radius(d, i) + (stroke(d, i) ? strokeWidth(d, i) : 0) ); - return pt; + return [pt.x, pt.y]; }); - m_rangeTree = new wigglemaps.RangeTree(pts); + m_rangeTree = kdbush(pts); m_rangeTreeTime.modified(); }; @@ -218,7 +217,7 @@ var pointFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this.pointSearch = function (p) { - var min, max, data, idx = [], box, found = [], ifound = [], map, pt, + var min, max, data, idx = [], found = [], ifound = [], map, pt, corners, stroke = m_this.style.get('stroke'), strokeWidth = m_this.style.get('strokeWidth'), @@ -255,14 +254,8 @@ var pointFeature = function (arg) { }; // Find points inside the bounding box - box = new wigglemaps.Box( - wigglemaps.vect(min.x, min.y), - wigglemaps.vect(max.x, max.y) - ); m_this._updateRangeTree(); - m_rangeTree.search(box).forEach(function (q) { - idx.push(q.idx); - }); + idx = m_rangeTree.range(min.x, min.y, max.x, max.y); // Filter by circular region idx.forEach(function (i) { diff --git a/src/util/wigglemaps.js b/src/util/wigglemaps.js deleted file mode 100644 index 4c5278bb89..0000000000 --- a/src/util/wigglemaps.js +++ /dev/null @@ -1,404 +0,0 @@ -////////////////////////////////////////////////////////////////////////////// -/** - * @license - * Includes several support classes adapted from wigglemaps. - * - * https://github.com/dotskapes/wigglemaps - * - * Copyright 2013 Preston and Krejci (dotSkapes Virtual Lab) - * - * 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. - */ -////////////////////////////////////////////////////////////////////////////// - -(function () { - 'use strict'; - - var RangeNode = function (elem, start, end, current) { - this.data = elem[current]; - this.left = null; - this.right = null; - if (start !== current) - this.left = new RangeNode(elem, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); - if (end !== current) - this.right = new RangeNode(elem, current + 1, end, parseInt((end + (current + 1)) / 2, 10)); - this.elem = elem; - this.start = start; - this.end = end; - this.subtree = null; /* This is populated as needed */ - this.search = rangeNodeSearch; - }; - - var rangeNodeSearch = function (result, box) { - var m_this = this; - - var xrange = function (b) { - return (b.x_in(m_this.elem[m_this.start]) && - b.x_in(m_this.elem[m_this.end])); - }; - - var yrange = function (b, start, end) { - return (b.y_in(m_this.subtree[start]) && - b.y_in(m_this.subtree[end])); - }; - - var subquery = function (result, box, start, end, current) { - if (yrange(box, start, end)) { - for (var i = start; i <= end; i ++) { - result.push(m_this.subtree[i]); - } - return; - } - if (box.y_in(m_this.subtree[current])) - result.push(m_this.subtree[current]); - if (box.y_left(m_this.subtree[current])){ - if (current !== end) - subquery(result, box, current + 1, end, parseInt((end + (current + 1)) / 2, 10)); - } else if (box.x_right(m_this.subtree[current])) { - if (current !== start) - subquery(result, box, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); - } else { - if (current !== end) - subquery(result, box, current + 1, end, parseInt((end + (current + 1)) / 2, 10)); - if (current !== start) - subquery(result, box, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); - } - }; - - if (xrange(box)) { - if (!this.subtree) { - this.subtree = this.elem.slice(this.start, this.end + 1); - this.subtree.sort(function (a, b) { - return a.y - b.y; - }); - } - subquery(result, box, 0, this.subtree.length - 1, parseInt((this.subtree.length - 1) / 2, 10)); - return; - } else { - if (box.contains(this.data)) - result.push(this.data); - if (box.x_left(this.data)) { - if (this.right) - this.right.search(result, box); - } else if (box.x_right(this.data)) { - if (this.left) - this.left.search(result, box); - } else { - if (this.left) - this.left.search(result, box); - if (this.right) - this.right.search(result, box); - } - } - }; - - var RangeTree = function (elem) { - elem.sort(function (a, b) { - return a.x - b.x; - }); - if (elem.length > 0) - this.root = new RangeNode(elem, 0, elem.length - 1, parseInt((elem.length - 1) / 2, 10)); - else - this.root = null; - - this.search = function (_box) { - if (!this.root) - return []; - //var box = new Box (min, max); - var box = _box.clone (); - var result = []; - this.root.search (result, box); - return result; - }; - }; - - var Box = function (v1, v2) { - this.min = v1.clone (); - this.max = v2.clone (); - this.contains = function (p) { - return (v1.x <= p.x) && (v2.x >= p.x) && (v1.y <= p.y) && (v2.y >= p.y); - }; - - this.x_in = function (p) { - return (v1.x <= p.x) && (v2.x >= p.x); - }; - - this.x_left = function (p) { - return (v1.x >= p.x); - }; - - this.x_right = function (p) { - return (v2.x <= p.x); - }; - - this.y_in = function (p) { - return (v1.y <= p.y) && (v2.y >= p.y); - }; - - this.y_left = function (p) { - return (v1.y >= p.y); - }; - - this.y_right = function (p) { - return (v2.y <= p.y); - }; - - this.area = function () { - return (this.max.x - this.min.x) * (this.max.y - this.min.y); - }; - - this.height = function () { - return this.max.y - this.min.y; - }; - - this.width = function () { - return this.max.x - this.min.x; - }; - - this.vertex = function (index) { - switch (index) { - case 0: - return this.min.clone (); - case 1: - return new vect (this.max.x, this.min.y); - case 2: - return this.max.clone (); - case 3: - return new vect (this.min.x, this.max.y); - default: - throw "Index out of bounds: " + index ; - } - }; - - this.intersects = function (box) { - for (var i = 0; i < 4; i ++) { - for (var j = 0; j < 4; j ++) { - if (vect.intersects (this.vertex (i), this.vertex ((i + 1) % 4), - box.vertex (j), box.vertex ((j + 1) % 4))) - return true; - } - } - if (this.contains (box.min) && - this.contains (box.max) && - this.contains (new vect (box.min.x, box.max.y)) && - this.contains (new vect (box.max.x, box.min.y))) - return true; - if (box.contains (this.min) && - box.contains (this.max) && - box.contains (new vect (this.min.x, this.max.y)) && - box.contains (new vect (this.max.x, this.min.y))) - return true; - return false; - }; - - this.union = function (b) { - this.min.x = Math.min (this.min.x, b.min.x); - this.min.y = Math.min (this.min.y, b.min.y); - - this.max.x = Math.max (this.max.x, b.max.x); - this.max.y = Math.max (this.max.y, b.max.y); - }; - - this.centroid = function () { - return new vect ((this.max.x + this.min.x) / 2, (this.max.y + this.min.y) / 2); - }; - - this.clone = function () { - return new Box (v1, v2); - }; - }; - - // A basic vector type. Supports standard 2D vector operations - var Vector2D = function (x, y) { - this.x = x; - this.y = y; - - this.add = function (v) { - this.x += v.x; - this.y += v.y; - return this; - }; - this.sub = function (v) { - this.x -= v.x; - this.y -= v.y; - return this; - }; - this.scale = function (s) { - this.x *= s; - this.y *= s; - return this; - }; - this.length = function () { - return Math.sqrt (this.x * this.x + this.y * this.y); - }; - this.normalize = function () { - var scale = this.length (); - if (scale === 0) - return this; - this.x /= scale; - this.y /= scale; - return this; - }; - this.div = function (v) { - this.x /= v.x; - this.y /= v.y; - return this; - }; - this.floor = function () { - this.x = Math.floor (this.x); - this.y = Math.floor (this.y); - return this; - }; - this.zero = function (tol) { - tol = tol || 0; - return (this.length() <= tol); - }; - this.dot = function (v) { - return (this.x * v.x) + (this.y * v.y); - }; - this.cross = function (v) { - return (this.x * v.y) - (this.y * v.x); - }; - this.rotate = function (omega) { - var cos = Math.cos (omega); - var sin = Math.sin (omega); - xp = cos * this.x - sin * this.y; - yp = sin * this.x + cos * this.y; - this.x = xp; - this.y = yp; - return this; - }; - this.clone = function () { - return new Vector2D (this.x, this.y); - }; - - this.array = function () { - return [this.x, this.y]; - }; - }; - - // A shortcut for the vector constructor - function vect (x, y) { - return new Vector2D (x, y); - } - - // Shorthand operations for vectors for operations that make new vectors - - vect.scale = function (v, s) { - return v.clone ().scale (s); - }; - - vect.add = function (v1, v2) { - return v1.clone ().add (v2); - }; - - vect.sub = function (v1, v2) { - return v1.clone ().sub (v2); - }; - - vect.dist = function (v1, v2) { - return v1.clone ().sub (v2).length (); - }; - - vect.dir = function (v1, v2) { - return v1.clone ().sub (v2).normalize (); - }; - - vect.dot = function (v1, v2) { - return (v1.x * v2.x) + (v1.y * v2.y); - }; - - vect.cross = function (v1, v2) { - return (v1.x * v2.y) - (v1.y * v2.x); - }; - - vect.left = function (a, b, c, tol) { - if (!tol) - tol = 0; - var v1 = vect.sub (b, a); - var v2 = vect.sub (c, a); - return (vect.cross (v1, v2) >= -tol); - }; - - vect.intersects = function (a, b, c, d, tol) { - if (!tol) - tol = 0; - return (vect.left (a, b, c, tol) != vect.left (a, b, d, tol) && - vect.left (c, d, b, tol) != vect.left (c, d, a, tol)); - }; - - vect.intersect2dt = function (a, b, c, d) { - var denom = a.x * (d.y - c.y) + - b.x * (c.y - d.y) + - d.x * (b.y - a.y) + - c.x * (a.y - b.y); - - if (denom === 0) - return Infinity; - - var num_s = a.x * (d.y - c.y) + - c.x * (a.y - d.y) + - d.x * (c.y - a.y); - var s = num_s / denom; - - var num_t = -(a.x * (c.y - b.y) + - b.x * (a.y - c.y) + - c.x * (b.y - a.y)); - var t = num_t / denom; - - return t; - }; - - vect.intersect2dpos = function (a, b, c, d) { - var denom = a.x * (d.y - c.y) + - b.x * (c.y - d.y) + - d.x * (b.y - a.y) + - c.x * (a.y - b.y); - - if (denom === 0) - return Infinity; - - var num_s = a.x * (d.y - c.y) + - c.x * (a.y - d.y) + - d.x * (c.y - a.y); - var s = num_s / denom; - - /*var num_t = -(a.x * (c.y - b.y) + - b.x * (a.y - c.y) + - c.x * (b.y - a.y)); - var t = num_t / denom;*/ - - var dir = vect.sub (b, a); - dir.scale (s); - return vect.add (a, dir); - }; - - vect.rotate = function (v, omega) { - var cos = Math.cos (omega); - var sin = Math.sin (omega); - xp = cos * v.x - sin * v.y; - yp = sin * v.x + cos * v.y; - var c = new vect (xp, yp); - return c; - }; - - vect.normalize = function (v) { - return v.clone ().normalize (); - }; - - module.exports = { - Box: Box, - vect: vect, - RangeTree: RangeTree - }; -}()); From 03fea0f4821983dd61eb82a9558d7f662a88fec8 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 29 Mar 2017 13:14:23 -0400 Subject: [PATCH 2/2] Add unit tests for the point feature. Fixed some issues with the pointFeature: - Setting the position on construction resulted in different behavior than setting it afterwards. - Setting the clustering on construction allowed non-default values, but during the setter would only allow defaults. - If set, the clustering could not be changed except by unsetting it and resetting it. - The maximum radius of any point is used when deciding where to search for points, but it was potentially not calculated until after it was used. Made some improvements: - The position function wraps the position in another function to handle clustering. If the position is not a function, then only one function call is made rather than two. - Removed the unused internal _boundingBox function from the pointFeature. - Added some comments about when you would want to use a different primitiveShape with the gl point feature. --- src/gl/pointFeature.js | 6 + src/pointFeature.js | 74 +++------ tests/cases/pointFeature.js | 302 ++++++++++++++++++++++++++++++++++++ 3 files changed, 327 insertions(+), 55 deletions(-) create mode 100644 tests/cases/pointFeature.js diff --git a/src/gl/pointFeature.js b/src/gl/pointFeature.js index 9bd195cc57..e7e9daf81c 100644 --- a/src/gl/pointFeature.js +++ b/src/gl/pointFeature.js @@ -38,6 +38,11 @@ var gl_pointFeature = function (arg) { m_pixelWidthUniform = null, m_aspectUniform = null, m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, + /* If you are drawing very large points, you will often get better + * performance using a different primitiveShape. The 'sprite' shape uses + * the least memory, but has hardware-specific limitations to its size. + * 'triangle' seems to be fastest on low-powered hardware, but 'square' + * visits fewer fragments. */ m_primitiveShape = 'sprite', // arg can change this, below s_init = this._init, s_update = this._update, @@ -529,6 +534,7 @@ var gl_pointFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this._exit = function () { m_this.renderer().contextRenderer().removeActor(m_actor); + m_actor = null; s_exit(); }; diff --git a/src/pointFeature.js b/src/pointFeature.js index ca557802d0..44d1c706cc 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -62,10 +62,9 @@ var pointFeature = function (arg) { m_clusterTree = null; m_clustering = false; s_data(m_allData); - m_allData = null; - } else if (!m_clustering && val) { + } else if (val && m_clustering !== val) { // Generate the cluster tree - m_clustering = true; + m_clustering = val; m_this._clusterData(); } return m_this; @@ -158,12 +157,14 @@ var pointFeature = function (arg) { if (val === undefined) { return m_this.style('position'); } else { - val = util.ensureFunction(val); + var isFunc = util.isFunction(val); m_this.style('position', function (d, i) { if (d.__cluster) { return d; - } else { + } else if (isFunc) { return val(d, i); + } else { + return val; } }); m_this.dataTime().modified(); @@ -224,10 +225,6 @@ var pointFeature = function (arg) { strokeWidth = m_this.style.get('strokeWidth'), radius = m_this.style.get('radius'); - if (!m_this.selectionAPI()) { - return []; - } - data = m_this.data(); if (!data || !data.length) { return { @@ -236,6 +233,10 @@ var pointFeature = function (arg) { }; } + // We need to do this before we find corners, since the max radius is + // determined then + m_this._updateRangeTree(); + map = m_this.layer().map(); pt = map.gcsToDisplay(p); // check all corners to make sure we handle rotations @@ -259,7 +260,6 @@ var pointFeature = function (arg) { wigglemaps.vect(min.x, min.y), wigglemaps.vect(max.x, max.y) ); - m_this._updateRangeTree(); m_rangeTree.search(box).forEach(function (q) { idx.push(q.idx); }); @@ -319,8 +319,10 @@ var pointFeature = function (arg) { if (data === undefined) { return s_data(); } - if (m_clustering && !m_ignoreData) { + if (!m_ignoreData) { m_allData = data; + } + if (m_clustering && !m_ignoreData) { m_this._clusterData(); } else { s_data(data); @@ -329,55 +331,13 @@ var pointFeature = function (arg) { return m_this; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Returns the bounding box for a given datum in screen coordinates as an - * object: :: - * - * { - * min: { - * x: value, - * y: value - * }, - * max: { - * x: value, - * y: value - * } - * } - * - * @returns {object} - */ - //////////////////////////////////////////////////////////////////////////// - this._boundingBox = function (d) { - var pt, radius; - - // get the position in geo coordinates - pt = m_this.position()(d); - - // convert to screen coordinates - pt = m_this.layer().map().gcsToDisplay(pt); - - // get the radius of the points (should we add stroke width?) - radius = m_this.style().radius(d); - - return { - min: { - x: pt.x - radius, - y: pt.y - radius - }, - max: { - x: pt.x + radius, - y: pt.y + radius - } - }; - }; - //////////////////////////////////////////////////////////////////////////// /** * Initialize */ //////////////////////////////////////////////////////////////////////////// this._init = function (arg) { + arg = arg || {}; s_init.call(m_this, arg); var defaultStyle = $.extend( @@ -403,6 +363,9 @@ var pointFeature = function (arg) { } m_this.style(defaultStyle); + if (defaultStyle.position) { + m_this.position(defaultStyle.position); + } m_this.dataTime().modified(); // bind to the zoom handler for point clustering @@ -429,9 +392,10 @@ var pointFeature = function (arg) { * @param {geo.pointFeature.spec} spec The object specification * @returns {geo.pointFeature|null} */ -pointFeature.create = function (layer, renderer, spec) { +pointFeature.create = function (layer, spec) { 'use strict'; + spec = spec || {}; spec.type = 'point'; return feature.create(layer, spec); }; diff --git a/tests/cases/pointFeature.js b/tests/cases/pointFeature.js new file mode 100644 index 0000000000..52ea343559 --- /dev/null +++ b/tests/cases/pointFeature.js @@ -0,0 +1,302 @@ +// Test geo.pointFeature, geo.d3.pointFeature, and geo.gl.pointFeature + +var geo = require('../test-utils').geo; +var $ = require('jquery'); +var mockAnimationFrame = require('../test-utils').mockAnimationFrame; +var stepAnimationFrame = require('../test-utils').stepAnimationFrame; +var unmockAnimationFrame = require('../test-utils').unmockAnimationFrame; +var vgl = require('vgl'); +var mockVGLRenderer = require('../test-utils').mockVGLRenderer; +var restoreVGLRenderer = require('../test-utils').restoreVGLRenderer; +var waitForIt = require('../test-utils').waitForIt; + +describe('geo.pointFeature', function () { + 'use strict'; + + var testPoints = [ + {x: 20, y: 10}, {x: 25, y: 10}, {x: 30, y: 10}, {x: 35, y: 12}, + {x: 32, y: 15}, {x: 30, y: 20}, {x: 35, y: 22}, {x: 32, y: 25}, + {x: 30, y: 30}, {x: 35, y: 32}, {x: 32, y: 35}, {x: 30, y: 30}, + {x: 40, y: 20, radius: 10}, {x: 42, y: 20, radius: 5}, + {x: 44, y: 20, radius: 2}, {x: 46, y: 20, radius: 2}, + {x: 50, y: 10}, {x: 50, y: 10}, {x: 60, y: 10} + ]; + + function create_map(opts) { + var node = $('
').css({width: '640px', height: '360px'}); + $('#map').remove(); + $('body').append(node); + opts = $.extend({}, opts); + opts.node = node; + return geo.map(opts); + } + + describe('create', function () { + it('create function', function () { + var map, layer, point; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + point = geo.pointFeature.create(layer); + expect(point instanceof geo.pointFeature).toBe(true); + }); + }); + + describe('Check class accessors', function () { + var map, layer, point; + var pos = [[0, 0], [10, 5], [5, 10]]; + it('position', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + point = geo.pointFeature({layer: layer}); + point._init(); + expect(point.position()('a')).toBe('a'); + point.position(pos); + expect(point.position()('a')).toEqual(pos); + point.position(function () { return pos; }); + expect(point.position()('a')).toEqual(pos); + point.position(function () { return 'b'; }); + expect(point.position()('a')).toEqual('b'); + + point = geo.pointFeature({layer: layer, position: pos}); + point._init({position: pos}); + expect(point.position()('a')).toEqual(pos); + }); + + it('data', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + point = geo.pointFeature({layer: layer}); + point._init(); + expect(point.data()).toEqual([]); + expect(point.data(pos)).toBe(point); + expect(point.data()).toEqual(pos); + }); + + it('clustering', function () { + var count = 0; + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + point = geo.pointFeature({layer: layer}); + point._init(); + point.data(pos); + point._handleZoom = function () { + count += 1; + }; + expect(point.clustering()).toBe(undefined); + expect(point.clustering(true)).toBe(point); + expect(point.clustering()).toBe(true); + expect(count).toBe(1); + expect(point.clustering(true)).toBe(point); + expect(count).toBe(1); + expect(point.clustering({radius: 1})).toBe(point); + expect(point.clustering()).toEqual({radius: 1}); + expect(count).toBe(2); + expect(point.clustering(false)).toBe(point); + expect(point.clustering()).toBe(false); + expect(count).toBe(2); + }); + }); + + describe('Public utility methods', function () { + it('pointSearch', function () { + var map, layer, point, pt, p, data = testPoints; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + point = layer.createFeature('point', {selectionAPI: true}); + point.data(data) + .style({ + strokeWidth: 2, + radius: function (d) { + return d.radius ? d.radius : 5; + } + }); + pt = point.pointSearch({x: 20, y: 10}); + expect(pt.index).toEqual([0]); + expect(pt.found.length).toBe(1); + expect(pt.found[0]).toEqual(data[0]); + /* We should land on the point if we are near the specified radius */ + p = point.featureGcsToDisplay({x: 25, y: 10}); + pt = point.pointSearch(map.displayToGcs({x: p.x, y: p.y})); + expect(pt.found.length).toBe(1); + pt = point.pointSearch(map.displayToGcs({x: p.x, y: p.y + 6.95})); + expect(pt.found.length).toBe(1); + pt = point.pointSearch(map.displayToGcs({x: p.x, y: p.y + 7.05})); + expect(pt.found.length).toBe(0); + /* Variable radius should be handled */ + p = point.featureGcsToDisplay({x: 40, y: 20}); + pt = point.pointSearch(map.displayToGcs({x: p.x, y: p.y + 11.95})); + expect(pt.found.length).toBe(1); + pt = point.pointSearch(map.displayToGcs({x: p.x, y: p.y + 12.05})); + expect(pt.found.length).toBe(0); + p = point.featureGcsToDisplay({x: 46, y: 20}); + pt = point.pointSearch(map.displayToGcs({x: p.x, y: p.y + 3.95})); + expect(pt.found.length).toBe(1); + pt = point.pointSearch(map.displayToGcs({x: p.x, y: p.y + 4.05})); + expect(pt.found.length).toBe(0); + /* We should match two coincident pointss */ + pt = point.pointSearch({x: 50, y: 10}); + expect(pt.found.length).toBe(2); + /* If we have zero-length data, we get no matches */ + point.data([]); + pt = point.pointSearch({x: 22, y: 10}); + expect(pt.found.length).toBe(0); + /* Exceptions will be returned properly */ + point.data(data).style('strokeWidth', function (d, idx) { + throw new Error('no width'); + }); + expect(function () { + point.pointSearch({x: 20, y: 10}); + }).toThrow(new Error('no width')); + /* Stop throwing the exception */ + point.style('strokeWidth', 2); + }); + it('boxSearch', function () { + var map, layer, point, data = testPoints, idx; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + point = layer.createFeature('point', {selectionAPI: true}); + point.data(data); + idx = point.boxSearch({x: 19, y: 9}, {x: 26, y: 11}); + expect(idx).toEqual([0, 1]); + idx = point.boxSearch({x: 19, y: 9}, {x: 24, y: 11}); + expect(idx).toEqual([0]); + idx = point.boxSearch({x: 19, y: 9}, {x: 18, y: 11}); + expect(idx.length).toBe(0); + }); + }); + + describe('Private utility methods', function () { + it('_clusterData', function () { + var map, layer, point, data = testPoints, count = 0; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + point = layer.createFeature('point'); + point.data(data); + var s_handleZoom = point._handleZoom; + point._handleZoom = function () { + count += 1; + return s_handleZoom.apply(point, arguments); + }; + point._clusterData(); + expect(count).toBe(0); + expect(point.data().length).toBe(data.length); + point.clustering(true); + point._clusterData(); + expect(count).toBeGreaterThan(1); + var dataLen = point.data().length; + expect(dataLen).toBeLessThan(data.length); + map.zoom(0); + expect(point.data().length).toBeLessThan(dataLen); + }); + it('_handleZoom', function () { + var map, layer, point, data = testPoints; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + point = layer.createFeature('point'); + point.data(data); + expect(point.data().length).toBe(data.length); + point._handleZoom(4); + expect(point.data().length).toBe(data.length); + point.clustering(true); + var dataLen = point.data().length; + expect(dataLen).toBeLessThan(data.length); + point._handleZoom(0); + expect(point.data().length).toBeLessThan(dataLen); + }); + it('_updateRangeTree', function () { + var map, layer, point, data = testPoints.slice(); + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + point = layer.createFeature('point'); + point.data(data); + expect(point.pointSearch({x: 20, y: 10}).index.length).toBe(1); + expect(point.pointSearch({x: -20, y: 10}).index.length).toBe(0); + data[0] = {x: -20, y: 10}; + // now we can't find the point at either locations + expect(point.pointSearch({x: 20, y: 10}).index.length).toBe(0); + expect(point.pointSearch({x: -20, y: 10}).index.length).toBe(0); + // this won't do anything, since we dont think the data is modified + point._updateRangeTree(); + expect(point.pointSearch({x: 20, y: 10}).index.length).toBe(0); + expect(point.pointSearch({x: -20, y: 10}).index.length).toBe(0); + // now we should find the point in the new location + point.dataTime().modified(); + point._updateRangeTree(); + expect(point.pointSearch({x: 20, y: 10}).index.length).toBe(0); + expect(point.pointSearch({x: -20, y: 10}).index.length).toBe(1); + }); + }); + + /* This is a basic integration test of geo.d3.pointFeature. */ + describe('geo.d3.pointFeature', function () { + var map, layer, point; + it('basic usage', function () { + mockAnimationFrame(); + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + point = layer.createFeature('point', { + style: { + strokeWidth: 2, + radius: function (d) { + return d.radius ? d.radius : 5; + } + } + }).data(testPoints); + point.draw(); + stepAnimationFrame(); + var circles = layer.node().find('circle'); + expect(circles.length).toBe(19); + expect(circles.eq(0).attr('r')).toBe('5'); + expect(circles.eq(12).attr('r')).toBe('10'); + unmockAnimationFrame(); + }); + }); + + /* This is a basic integration test of geo.gl.pointFeature. */ + describe('geo.gl.pointFeature', function () { + var map, layer, point, point2, glCounts; + it('basic usage', function () { + mockVGLRenderer(); + map = create_map(); + layer = map.createLayer('feature', {renderer: 'vgl'}); + point = layer.createFeature('point', { + style: { + strokeWidth: 2, + radius: function (d) { + return d.radius ? d.radius : 5; + } + } + }).data(testPoints); + glCounts = $.extend({}, vgl.mockCounts()); + point.draw(); + expect(point.verticesPerFeature()).toBe(1); + }); + waitForIt('next render gl A', function () { + return vgl.mockCounts().createProgram >= (glCounts.createProgram || 0) + 1; + }); + it('other primitive shapes', function () { + point2 = layer.createFeature('point', { + primitiveShape: 'triangle' + }).data(testPoints); + expect(point2.verticesPerFeature()).toBe(3); + layer.deleteFeature(point2); + point2 = layer.createFeature('point', { + primitiveShape: 'square' + }).data(testPoints); + expect(point2.verticesPerFeature()).toBe(6); + glCounts = $.extend({}, vgl.mockCounts()); + point2.draw(); + }); + waitForIt('next render gl B', function () { + return vgl.mockCounts().drawArrays >= (glCounts.drawArrays || 0) + 1; + }); + it('_exit', function () { + expect(point.actors().length).toBe(1); + layer.deleteFeature(point); + expect(point.actors().length).toBe(0); + point.data(testPoints); + map.draw(); + restoreVGLRenderer(); + }); + }); +});