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/feature.js b/src/feature.js index a0f48ef41a..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; } @@ -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; @@ -532,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 1f2fa62dc9..7ca8d0ab95 100644 --- a/src/featureLayer.js +++ b/src/featureLayer.js @@ -30,6 +30,8 @@ var featureLayer = function (arg) { s_init = this._init, s_exit = this._exit, s_update = this._update, + s_visible = this.visible, + s_selectionAPI = this.selectionAPI, s_draw = this.draw; //////////////////////////////////////////////////////////////////////////// @@ -231,13 +233,72 @@ 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; }; 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/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/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/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) }) ); } diff --git a/src/layer.js b/src/layer.js index 74cf53a6ae..da5676b7d2 100644 --- a/src/layer.js +++ b/src/layer.js @@ -55,6 +55,8 @@ 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_selectionAPI = arg.selectionAPI === undefined ? true : arg.selectionAPI, m_zIndex; m_rendererName = checkRenderer(m_rendererName); @@ -192,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} */ @@ -352,6 +361,48 @@ 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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/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..1ed2f7aeef 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'); //////////////////////////////////////////////////////////////////////////// /** @@ -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(); @@ -194,7 +195,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 +202,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,16 +218,12 @@ 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'), radius = m_this.style.get('radius'); - if (!m_this.selectionAPI()) { - return []; - } - data = m_this.data(); if (!data || !data.length) { return { @@ -236,6 +232,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 @@ -255,14 +255,7 @@ 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) { @@ -319,8 +312,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 +324,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 +356,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 +385,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/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/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 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 - }; -}()); diff --git a/tests/cases/feature.js b/tests/cases/feature.js index 1360d89205..304f9974bc 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 () { @@ -281,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 ec4ba6648c..9dcfe3f239 100644 --- a/tests/cases/featureLayer.js +++ b/tests/cases/featureLayer.js @@ -127,6 +127,43 @@ 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('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); 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(); + }); + }); +}); 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 () {