From 1971ddafaa5697ea41462c0965ade50626a57fea Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 24 Mar 2017 13:21:47 -0400 Subject: [PATCH 1/6] Support more line features from geojson properties. --- src/jsonReader.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/jsonReader.js b/src/jsonReader.js index 8a9a5249f1..80eedc6a4b 100644 --- a/src/jsonReader.js +++ b/src/jsonReader.js @@ -257,7 +257,11 @@ var jsonReader = function (arg) { .style({ strokeColor: m_this._style('strokeColor', '#ff7800', lines, convertColor), strokeWidth: m_this._style('strokeWidth', 4, lines), - strokeOpacity: m_this._style('strokeOpacity', 0.5, lines) + strokeOpacity: m_this._style('strokeOpacity', 0.5, lines), + strokeOffset: m_this._style('strokeOffset', 0, lines), + lineCap: m_this._style('lineCap', 'butt', lines), + lineJoin: m_this._style('lineCap', 'miter', lines), + closed: m_this._style('closed', false, lines) }) ); } From e52cbf7a599fc20fbbb6361aee64172ce4be80bd Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 28 Mar 2017 13:12:22 -0400 Subject: [PATCH 2/6] Add a visible() function to layers. Note that, much like feature level visibility, you might need to call layer.draw() after making the layer visible to ensure an update. For feature layers, visibility cascades down to individual features (which disables their interaction). --- src/feature.js | 23 ++++++++++++++----- src/featureLayer.js | 45 ++++++++++++++++++++++++++++++++----- src/gl/quadFeature.js | 4 ++-- src/layer.js | 23 +++++++++++++++++++ src/pixelmapFeature.js | 11 ++++----- src/pointFeature.js | 4 ---- src/polygonFeature.js | 2 +- src/tileLayer.js | 38 ++++++++++++++++++++++++++----- tests/cases/feature.js | 14 ++++++++++++ tests/cases/featureLayer.js | 13 +++++++++++ tests/cases/tileLayer.js | 25 +++++++++++++++++++++ 11 files changed, 174 insertions(+), 28 deletions(-) diff --git a/src/feature.js b/src/feature.js index a0f48ef41a..63ead89bc4 100644 --- a/src/feature.js +++ b/src/feature.js @@ -413,24 +413,37 @@ var feature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** * Get/Set visibility of the feature + * + * @param {boolean|undefined} val: undefined to return the visibility, a + * boolean to change the visibility. + * @param {boolean} direct: if true, when getting the visibility, disregard + * the visibility of the parent layer, and when setting, refresh the state + * regardless of whether it has changed or not. + * @return {boolean|object} either the visibility (if getting) or the feature + * (if setting). */ //////////////////////////////////////////////////////////////////////////// - this.visible = function (val) { + this.visible = function (val, direct) { if (val === undefined) { + if (!direct && m_layer && m_layer.visible && !m_layer.visible()) { + return false; + } return m_visible; } - if (m_visible !== val) { + if (m_visible !== val || direct) { m_visible = val; m_this.modified(); - + if (m_layer && m_layer.visible && !m_layer.visible()) { + val = false; + } // bind or unbind mouse handlers on visibility change - if (m_visible) { + if (val) { m_this._bindMouseHandlers(); } else { m_this._unbindMouseHandlers(); } for (var i = 0; i < m_dependentFeatures.length; i += 1) { - m_dependentFeatures[i].visible(val); + m_dependentFeatures[i].visible(m_visible, direct); } } return m_this; diff --git a/src/featureLayer.js b/src/featureLayer.js index 1f2fa62dc9..84b6d9f899 100644 --- a/src/featureLayer.js +++ b/src/featureLayer.js @@ -30,6 +30,7 @@ var featureLayer = function (arg) { s_init = this._init, s_exit = this._exit, s_update = this._update, + s_visible = this.visible, s_draw = this.draw; //////////////////////////////////////////////////////////////////////////// @@ -231,13 +232,45 @@ var featureLayer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this.draw = function () { - // Call sceneObject.draw, which calls draw on all child objects. - s_draw(); + if (m_this.visible()) { + // Call sceneObject.draw, which calls draw on all child objects. + s_draw(); - // Now call render on the renderer. In certain cases it may not do - // anything if the if the child objects are drawn on the screen already. - if (m_this.renderer()) { - m_this.renderer()._render(); + // Now call render on the renderer. In certain cases it may not do + // anything if the child objects are drawn on the screen already. + if (m_this.renderer()) { + m_this.renderer()._render(); + } + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set visibility of the layer + * + * @param {boolean|undefined} val: undefined to return the visibility, a + * boolean to change the visibility. + * @return {boolean|object} either the visibility (if getting) or the layer + * (if setting). + */ + //////////////////////////////////////////////////////////////////////////// + this.visible = function (val) { + if (val === undefined) { + return s_visible(); + } + if (m_this.visible() !== val) { + s_visible(val); + + // take a copy of the features; changing visible could mutate them. + var features = m_features.slice(), i; + + for (i = 0; i < features.length; i += 1) { + features[i].visible(features[i].visible(undefined, true), true); + } + if (val) { + m_this.draw(); + } } return m_this; }; diff --git a/src/gl/quadFeature.js b/src/gl/quadFeature.js index 6be4e59c40..55fe0a8f52 100644 --- a/src/gl/quadFeature.js +++ b/src/gl/quadFeature.js @@ -373,11 +373,11 @@ var gl_quadFeature = function (arg) { m_this._build(); } if (m_actor_color) { - m_actor_color.setVisible(m_this.visible()); + m_actor_color.setVisible(m_this.visible(undefined, true)); m_actor_color.material().setBinNumber(m_this.bin()); } if (m_actor_image) { - m_actor_image.setVisible(m_this.visible()); + m_actor_image.setVisible(m_this.visible(undefined, true)); m_actor_image.material().setBinNumber(m_this.bin()); } m_this.updateTime().modified(); diff --git a/src/layer.js b/src/layer.js index 74cf53a6ae..46f55c4a56 100644 --- a/src/layer.js +++ b/src/layer.js @@ -55,6 +55,7 @@ var layer = function (arg) { m_active = arg.active === undefined ? true : arg.active, m_opacity = arg.opacity === undefined ? 1 : arg.opacity, m_attribution = arg.attribution || null, + m_visible = arg.visible === undefined ? true : arg.visible, m_zIndex; m_rendererName = checkRenderer(m_rendererName); @@ -352,6 +353,28 @@ var layer = function (arg) { return m_attribution; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set visibility of the layer + * + * @param {boolean|undefined} val: undefined to return the visibility, a + * boolean to change the visibility. + * @return {boolean|object} either the visibility (if getting) or the layer + * (if setting). + */ + //////////////////////////////////////////////////////////////////////////// + this.visible = function (val) { + if (val === undefined) { + return m_visible; + } + if (m_visible !== val) { + m_visible = val; + m_node.css('display', m_visible ? '' : 'none'); + m_this.modified(); + } + return m_this; + }; + //////////////////////////////////////////////////////////////////////////// /** * Init layer diff --git a/src/pixelmapFeature.js b/src/pixelmapFeature.js index 43c5edb237..facb46bc5d 100644 --- a/src/pixelmapFeature.js +++ b/src/pixelmapFeature.js @@ -339,13 +339,14 @@ var pixelmapFeature = function (arg) { m_quadFeature = m_this.layer().createFeature('quad', { selectionAPI: false, gcs: m_this.gcs(), - visible: m_this.visible() + visible: m_this.visible(undefined, true) }); m_this.dependentFeatures([m_quadFeature]); - m_quadFeature.style({image: m_info.canvas, - position: m_this.style.get('position')}) - .data([{}]) - .draw(); + m_quadFeature.style({ + image: m_info.canvas, + position: m_this.style.get('position')}) + .data([{}]) + .draw(); } /* If we prepared the pixelmap and rendered it, send a prepared event */ if (prepared) { diff --git a/src/pointFeature.js b/src/pointFeature.js index ca557802d0..7d41f6c65e 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -224,10 +224,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 { diff --git a/src/polygonFeature.js b/src/polygonFeature.js index f745177bf4..3b5bc753af 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -271,7 +271,7 @@ var polygonFeature = function (arg) { m_lineFeature = m_this.layer().createFeature('line', { selectionAPI: false, gcs: m_this.gcs(), - visible: m_this.visible() + visible: m_this.visible(undefined, true) }); m_this.dependentFeatures([m_lineFeature]); } diff --git a/src/tileLayer.js b/src/tileLayer.js index ac39d2a963..efab5ab1c8 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -138,6 +138,11 @@ module.exports = (function () { */ ////////////////////////////////////////////////////////////////////////////// var tileLayer = function (options) { + 'use strict'; + if (!(this instanceof tileLayer)) { + return new tileLayer(options); + } + featureLayer.call(this, options); var $ = require('jquery'); var geo_event = require('./event'); @@ -147,11 +152,6 @@ module.exports = (function () { var adjustLayerForRenderer = require('./registry').adjustLayerForRenderer; var Tile = require('./tile'); - if (!(this instanceof tileLayer)) { - return new tileLayer(options); - } - featureLayer.call(this, options); - options = $.extend(true, {}, this.constructor.defaults, options || {}); if (!options.cacheSize) { // this size should be sufficient for a 4k display @@ -177,6 +177,7 @@ module.exports = (function () { var s_init = this._init, s_exit = this._exit, + s_visible = this.visible, m_lastTileSet = [], m_maxBounds = [], m_exited; @@ -1063,6 +1064,9 @@ module.exports = (function () { evt.event.event === geo_event.rotate)) { return; } + if (!this.visible()) { + return; + } var map = this.map(), bounds = map.bounds(undefined, null), mapZoom = map.zoom(), @@ -1430,6 +1434,30 @@ module.exports = (function () { return m_tileOffsetValues[level]; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set visibility of the layer + * + * @param {boolean|undefined} val: undefined to return the visibility, a + * boolean to change the visibility. + * @return {boolean|object} either the visibility (if getting) or the layer + * (if setting). + */ + //////////////////////////////////////////////////////////////////////////// + this.visible = function (val) { + if (val === undefined) { + return s_visible(); + } + if (this.visible() !== val) { + s_visible(val); + + if (val) { + this._update(); + } + } + return this; + }; + /** * Initialize after the layer is added to the map. */ diff --git a/tests/cases/feature.js b/tests/cases/feature.js index 1360d89205..2a8ed5c5a6 100644 --- a/tests/cases/feature.js +++ b/tests/cases/feature.js @@ -198,6 +198,20 @@ describe('geo.feature', function () { feat.dependentFeatures([]); expect(feat.visible(true)).toBe(feat); expect(depFeat.visible()).toBe(false); + + // the layer can control the visibility + expect(feat.visible()).toBe(true); + expect(feat.visible(undefined, true)).toBe(true); + layer.visible(false); + expect(feat.visible()).toBe(false); + expect(feat.visible(undefined, true)).toBe(true); + expect(feat.visible(false, true)).toBe(feat); + expect(feat.visible()).toBe(false); + expect(feat.visible(undefined, true)).toBe(false); + layer.visible(true); + expect(feat.visible()).toBe(false); + expect(feat.visible(true, true)).toBe(feat); + expect(feat.visible()).toBe(true); }); }); describe('Check class accessors', function () { diff --git a/tests/cases/featureLayer.js b/tests/cases/featureLayer.js index ec4ba6648c..0e8ab96608 100644 --- a/tests/cases/featureLayer.js +++ b/tests/cases/featureLayer.js @@ -127,6 +127,19 @@ describe('geo.featureLayer', function () { expect(layer.features().length).toBe(2); expect(layer.features()).toEqual([feat2, feat1]); }); + it('visible', function () { + expect(layer.visible()).toBe(true); + expect(feat1.visible()).toBe(true); + expect(feat1.visible(undefined, true)).toBe(true); + expect(layer.visible(false)).toBe(layer); + expect(layer.visible()).toBe(false); + expect(feat1.visible()).toBe(false); + expect(feat1.visible(undefined, true)).toBe(true); + expect(layer.visible(true)).toBe(layer); + expect(layer.visible()).toBe(true); + expect(feat1.visible()).toBe(true); + expect(feat1.visible(undefined, true)).toBe(true); + }); it('draw', function () { sinon.stub(feat1, 'draw', function () {}); expect(layer.draw()).toBe(layer); diff --git a/tests/cases/tileLayer.js b/tests/cases/tileLayer.js index 6a81bbe8e2..428374b290 100644 --- a/tests/cases/tileLayer.js +++ b/tests/cases/tileLayer.js @@ -96,6 +96,8 @@ describe('geo.tileLayer', function () { }, updateAttribution: function () { }, + bounds: function () { + }, node: get_set('node'), children: function () { return []; @@ -415,6 +417,29 @@ describe('geo.tileLayer', function () { expect(l.tilesAtZoom(2)).toEqual({x: 4, y: 3}); expect(l.tilesAtZoom(3)).toEqual({x: 8, y: 6}); }); + it('visible', function () { + var m = map(), layer, count = 0; + opts.map = m; + layer = geo.tileLayer(opts); + // check if we are updating by doing the least possible and tracking it + layer._getTiles = function () { + count += 1; + }; + layer._updateSubLayers = undefined; + + expect(layer.visible()).toBe(true); + layer._update(); + expect(count).toBe(1); + expect(layer.visible(false)).toBe(layer); + expect(layer.visible()).toBe(false); + layer._update(); + expect(count).toBe(1); + expect(layer.visible(true)).toBe(layer); + expect(layer.visible()).toBe(true); + expect(count).toBe(2); + layer._update(); + expect(count).toBe(3); + }); }); describe('Public utility methods', function () { describe('isValid', function () { From 3dad631dc00e3df1eb2b6740585ffaa01721f041 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 28 Mar 2017 16:31:20 -0400 Subject: [PATCH 3/6] Add a layer selectionAPI property. The active property can now be changed. --- src/feature.js | 20 ++++++++++++++----- src/featureLayer.js | 28 +++++++++++++++++++++++++++ src/layer.js | 38 ++++++++++++++++++++++++++++++++----- tests/cases/feature.js | 15 +++++++++++++++ tests/cases/featureLayer.js | 24 +++++++++++++++++++++++ 5 files changed, 115 insertions(+), 10 deletions(-) diff --git a/src/feature.js b/src/feature.js index 63ead89bc4..9f9941cadc 100644 --- a/src/feature.js +++ b/src/feature.js @@ -53,7 +53,7 @@ var feature = function (arg) { // Don't bind handlers for improved performance on features that don't // require it. - if (!m_selectionAPI) { + if (!this.selectionAPI()) { return; } @@ -545,16 +545,26 @@ var feature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** - * Query or set if the selection API is enabled for this feature. - * @returns {bool} + * Get/Set if the selection API is enabled for this feature. + * + * @param {boolean|undefined} val: undefined to return the selectionAPI + * state, or a boolean to change the state. + * @param {boolean} direct: if true, when getting the selectionAPI state, + * disregard the state of the parent layer, and when setting, refresh the + * state regardless of whether it has changed or not. + * @return {boolean|object} either the selectionAPI state (if getting) or the + * feature (if setting). */ //////////////////////////////////////////////////////////////////////////// - this.selectionAPI = function (arg) { + this.selectionAPI = function (arg, direct) { if (arg === undefined) { + if (!direct && m_layer && m_layer.selectionAPI && !m_layer.selectionAPI()) { + return false; + } return m_selectionAPI; } arg = !!arg; - if (arg !== m_selectionAPI) { + if (arg !== m_selectionAPI || direct) { m_selectionAPI = arg; this._unbindMouseHandlers(); this._bindMouseHandlers(); diff --git a/src/featureLayer.js b/src/featureLayer.js index 84b6d9f899..7ca8d0ab95 100644 --- a/src/featureLayer.js +++ b/src/featureLayer.js @@ -31,6 +31,7 @@ var featureLayer = function (arg) { s_exit = this._exit, s_update = this._update, s_visible = this.visible, + s_selectionAPI = this.selectionAPI, s_draw = this.draw; //////////////////////////////////////////////////////////////////////////// @@ -275,6 +276,33 @@ var featureLayer = function (arg) { return m_this; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set selectionAPI of the layer + * + * @param {boolean|undefined} val: undefined to return the selectionAPI + * state, or a boolean to change it. + * @return {boolean|object} either the selectionAPI state (if getting) or the + * layer (if setting). + */ + //////////////////////////////////////////////////////////////////////////// + this.selectionAPI = function (val) { + if (val === undefined) { + return s_selectionAPI(); + } + if (m_this.selectionAPI() !== val) { + s_selectionAPI(val); + + // take a copy of the features; changing selectionAPI could mutate them. + var features = m_features.slice(), i; + + for (i = 0; i < features.length; i += 1) { + features[i].selectionAPI(features[i].selectionAPI(undefined, true), true); + } + } + return m_this; + }; + //////////////////////////////////////////////////////////////////////////// /** * Clear all features in layer diff --git a/src/layer.js b/src/layer.js index 46f55c4a56..da5676b7d2 100644 --- a/src/layer.js +++ b/src/layer.js @@ -56,6 +56,7 @@ var layer = function (arg) { m_opacity = arg.opacity === undefined ? 1 : arg.opacity, m_attribution = arg.attribution || null, m_visible = arg.visible === undefined ? true : arg.visible, + m_selectionAPI = arg.selectionAPI === undefined ? true : arg.selectionAPI, m_zIndex; m_rendererName = checkRenderer(m_rendererName); @@ -193,20 +194,27 @@ var layer = function (arg) { //////////////////////////////////////////////////////////////////////////// /** - * Get whether or not the layer is active. An active layer will receive + * Get/Set whether or not the layer is active. An active layer will receive * native mouse when the layer is on top. Non-active layers will never * receive native mouse events. * - * @returns {Boolean} + * @returns {Boolean|object} */ //////////////////////////////////////////////////////////////////////////// - this.active = function () { - return m_active; + this.active = function (arg) { + if (arg === undefined) { + return m_active; + } + if (m_active !== arg) { + m_active = arg; + m_node.toggleClass('active', m_active); + } + return this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set root node of the layer + * Get root node of the layer * * @returns {div} */ @@ -375,6 +383,26 @@ var layer = function (arg) { return m_this; }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set selectionAPI of the layer + * + * @param {boolean|undefined} val: undefined to return the selectionAPI + * state, or a boolean to change it. + * @return {boolean|object} either the selectionAPI state (if getting) or the + * layer (if setting). + */ + //////////////////////////////////////////////////////////////////////////// + this.selectionAPI = function (val) { + if (val === undefined) { + return m_selectionAPI; + } + if (m_selectionAPI !== val) { + m_selectionAPI = val; + } + return m_this; + }; + //////////////////////////////////////////////////////////////////////////// /** * Init layer diff --git a/tests/cases/feature.js b/tests/cases/feature.js index 2a8ed5c5a6..304f9974bc 100644 --- a/tests/cases/feature.js +++ b/tests/cases/feature.js @@ -295,6 +295,21 @@ describe('geo.feature', function () { expect(feat.selectionAPI()).toBe(true); expect(feat.selectionAPI(0)).toBe(feat); expect(feat.selectionAPI()).toBe(false); + + // the layer can control the visibility + feat.selectionAPI(true); + expect(feat.selectionAPI()).toBe(true); + expect(feat.selectionAPI(undefined, true)).toBe(true); + layer.selectionAPI(false); + expect(feat.selectionAPI()).toBe(false); + expect(feat.selectionAPI(undefined, true)).toBe(true); + expect(feat.selectionAPI(false, true)).toBe(feat); + expect(feat.selectionAPI()).toBe(false); + expect(feat.selectionAPI(undefined, true)).toBe(false); + layer.selectionAPI(true); + expect(feat.selectionAPI()).toBe(false); + expect(feat.selectionAPI(true, true)).toBe(feat); + expect(feat.selectionAPI()).toBe(true); }); }); }); diff --git a/tests/cases/featureLayer.js b/tests/cases/featureLayer.js index 0e8ab96608..9dcfe3f239 100644 --- a/tests/cases/featureLayer.js +++ b/tests/cases/featureLayer.js @@ -140,6 +140,30 @@ describe('geo.featureLayer', function () { expect(feat1.visible()).toBe(true); expect(feat1.visible(undefined, true)).toBe(true); }); + it('selectionAPI', function () { + feat1.selectionAPI(true); + expect(layer.selectionAPI()).toBe(true); + expect(feat1.selectionAPI()).toBe(true); + expect(feat1.selectionAPI(undefined, true)).toBe(true); + expect(layer.selectionAPI(false)).toBe(layer); + expect(layer.selectionAPI()).toBe(false); + expect(feat1.selectionAPI()).toBe(false); + expect(feat1.selectionAPI(undefined, true)).toBe(true); + expect(layer.selectionAPI(true)).toBe(layer); + expect(layer.selectionAPI()).toBe(true); + expect(feat1.selectionAPI()).toBe(true); + expect(feat1.selectionAPI(undefined, true)).toBe(true); + }); + it('active', function () { + expect(layer.active()).toBe(true); + expect(layer.node().hasClass('active')).toBe(true); + expect(layer.active(false)).toBe(layer); + expect(layer.active()).toBe(false); + expect(layer.node().hasClass('active')).toBe(false); + expect(layer.active(true)).toBe(layer); + expect(layer.active()).toBe(true); + expect(layer.node().hasClass('active')).toBe(true); + }); it('draw', function () { sinon.stub(feat1, 'draw', function () {}); expect(layer.draw()).toBe(layer); From 6f1e4f2e7413323be10b2c6876c99f30f57c36d6 Mon Sep 17 00:00:00 2001 From: Jonathan Beezley Date: Thu, 30 Mar 2017 09:04:33 -0400 Subject: [PATCH 4/6] 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 5/6] 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(); + }); + }); +}); From f24bcca506d7b2bc940cdfefc0ec50e09127da1f Mon Sep 17 00:00:00 2001 From: Jonathan Beezley Date: Thu, 30 Mar 2017 14:43:45 -0400 Subject: [PATCH 6/6] Remove some unused code --- src/util/init.js | 51 ------------------------------------------------ 1 file changed, 51 deletions(-) diff --git a/src/util/init.js b/src/util/init.js index d595a660d6..ed219d59b0 100644 --- a/src/util/init.js +++ b/src/util/init.js @@ -361,57 +361,6 @@ */ radiusEarth: 6378137, - /** - * Linearly combine two "coordinate-like" objects in a uniform way. - * Coordinate like objects have ``x``, ``y``, and optionally a ``z`` - * key. The first object is mutated. - * - * a <= ca * a + cb * b - * - * @param {number} ca - * @param {object} a - * @param {number} [a.x=0] - * @param {number} [a.y=0] - * @param {number} [a.z=0] - * @param {number} cb - * @param {object} b - * @param {number} [b.x=0] - * @param {number} [b.y=0] - * @param {number} [b.z=0] - * @returns {object} ca * a + cb * b - */ - lincomb: function (ca, a, cb, b) { - a.x = ca * (a.x || 0) + cb * (b.x || 0); - a.y = ca * (a.y || 0) + cb * (b.y || 0); - a.z = ca * (a.x || 0) + cb * (b.x || 0); - return a; - }, - - /** - * Element-wise product of two coordinate-like object. Mutates - * the first object. Note the default values for ``b``, which - * are intended to used as a anisotropic scaling factors. - * - * a <= a * b^pow - * - * @param {object} a - * @param {number} [a.x=0] - * @param {number} [a.y=0] - * @param {number} [a.z=0] - * @param {object} b - * @param {number} [b.x=1] - * @param {number} [b.y=1] - * @param {number} [b.z=1] - * @param {number} [pow=1] - * @returns {object} a * b^pow - */ - scale: function (a, b, pow) { - a.x = (a.x || 0) * Math.pow(b.x || 1, pow); - a.y = (a.y || 0) * Math.pow(b.y || 1, pow); - a.z = (a.z || 0) * Math.pow(b.z || 1, pow); - return a; - }, - /** * Compare two arrays and return if their contents are equal. * @param {array} a1 first array to compare