diff --git a/examples/common/js/examples.js b/examples/common/js/examples.js index e69de29bb2..32aad8d40c 100644 --- a/examples/common/js/examples.js +++ b/examples/common/js/examples.js @@ -0,0 +1,19 @@ +var exampleUtils = { + /* Decode query components into a dictionary of values. + * + * @returns {object}: the query parameters as a dictionary. + */ + getQuery: function () { + var query = document.location.search.replace(/(^\?)/, '').split( + '&').map(function (n) { + n = n.split('='); + if (n[0]) { + this[decodeURIComponent(n[0])] = decodeURIComponent(n[1]); + } + return this; + }.bind({}))[0]; + return query; + } +}; + +window.utils = exampleUtils; diff --git a/examples/dynamicData/main.js b/examples/dynamicData/main.js index 0ebff4ee19..d69e9d8f29 100644 --- a/examples/dynamicData/main.js +++ b/examples/dynamicData/main.js @@ -61,5 +61,4 @@ $(function () { .draw(); }); - map.draw(); }); diff --git a/examples/quads/main.js b/examples/quads/main.js index bd894b59b6..db2ac4440d 100644 --- a/examples/quads/main.js +++ b/examples/quads/main.js @@ -1,9 +1,12 @@ +/* globals $, geo, utils */ + var quadDebug = {}; // Run after the DOM loads $(function () { 'use strict'; + var query = utils.getQuery(); var map = geo.map({ node: '#map', center: { @@ -12,7 +15,9 @@ $(function () { }, zoom: 4 }); - var layer = map.createLayer('feature', {renderer: 'vgl'}); + var layer = map.createLayer('feature', { + renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : 'vgl' + }); var quads = layer.createFeature('quad', {selectionAPI: true}); var previewImage = new Image(); previewImage.onload = function () { diff --git a/examples/transitions/main.js b/examples/transitions/main.js index 66d8d23b91..3762a1efbf 100644 --- a/examples/transitions/main.js +++ b/examples/transitions/main.js @@ -1,3 +1,5 @@ +/* globals $, d3, geo, utils */ + // Run after the DOM loads $(function () { 'use strict'; @@ -9,15 +11,7 @@ $(function () { center: {x: 28.9550, y: 41.0136} }); - // Parse query parameters into an object for ease of access - var query = document.location.search.replace(/(^\?)/, '').split( - '&').map(function (n) { - n = n.split('='); - if (n[0]) { - this[decodeURIComponent(n[0])] = decodeURIComponent(n[1]); - } - return this; - }.bind({}))[0]; + var query = utils.getQuery(); if (query.test) { $('#test').removeClass('hidden'); diff --git a/jsdoc.conf.json b/jsdoc.conf.json new file mode 100644 index 0000000000..85768428de --- /dev/null +++ b/jsdoc.conf.json @@ -0,0 +1,7 @@ +{ + "templates": { + "default": { + "useLongnameInNav": true + } + } +} diff --git a/package.json b/package.json index 426452adfb..3f3876764a 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ }, "scripts": { "build": "webpack --config webpack.config.js && webpack --config external.config.js", + "build-examples": "webpack --config webpack-examples.config.js", "lint": "eslint --cache .", "test": "karma start karma-cov.conf.js --single-run", "start": "karma start karma.conf.js", @@ -78,7 +79,7 @@ "examples": "webpack-dev-server --config webpack-examples.config.js --port 8082 --content-base dist/", "start-test": "node examples/build.js; forever start ./testing/test-runners/server.js", "stop-test": "forever stop ./testing/test-runners/server.js", - "docs": "jsdoc --pedantic -d dist/apidocs -r src" + "docs": "jsdoc --pedantic -d dist/apidocs -r src -c jsdoc.conf.json" }, "keywords": [ "map", diff --git a/src/canvas/tileLayer.js b/src/canvas/tileLayer.js index 5149a4e89f..992502fff3 100644 --- a/src/canvas/tileLayer.js +++ b/src/canvas/tileLayer.js @@ -83,7 +83,7 @@ var canvas_tileLayer = function () { /* These functions don't need to do anything. */ this._getSubLayer = function () {}; - this._updateSubLayer = undefined; + this._updateSubLayers = undefined; }; registerLayerAdjustment('canvas', 'tile', canvas_tileLayer); diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index 79d05c02b9..ad70802fe0 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -40,6 +40,9 @@ var d3Renderer = function (arg) { m_diagonal = null, m_scale = 1, m_transform = {dx: 0, dy: 0, rx: 0, ry: 0, rotation: 0}, + m_renderAnimFrameRef = null, + m_renderIds = {}, + m_removeIds = {}, m_svg = null, m_defs = null; @@ -81,13 +84,6 @@ var d3Renderer = function (arg) { }; }; - this._convertPosition = function (f) { - f = util.ensureFunction(f); - return function () { - return m_this.layer().map().worldToDisplay(f.apply(m_this, arguments)); - }; - }; - this._convertScale = function (f) { f = util.ensureFunction(f); return function () { @@ -207,37 +203,30 @@ var d3Renderer = function (arg) { return; } - var layer = m_this.layer(), - map = layer.map(), + var layer = m_this.layer(); + + var map = layer.map(), upperLeft = map.gcsToDisplay(m_corners.upperLeft, null), lowerRight = map.gcsToDisplay(m_corners.lowerRight, null), center = map.gcsToDisplay(m_corners.center, null), group = getGroup(), - canvas = m_this.canvas(), dx, dy, scale, rotation, rx, ry; - if (canvas.attr('scale') !== null) { - scale = parseFloat(canvas.attr('scale') || 1); - rx = (parseFloat(canvas.attr('dx') || 0) + - parseFloat(canvas.attr('offsetx') || 0)); - ry = (parseFloat(canvas.attr('dy') || 0) + - parseFloat(canvas.attr('offsety') || 0)); - rotation = parseFloat(canvas.attr('rotation') || 0); - dx = scale * rx + map.size().width / 2; - dy = scale * ry + map.size().height / 2; - } else { - scale = Math.sqrt( - Math.pow(lowerRight.y - upperLeft.y, 2) + - Math.pow(lowerRight.x - upperLeft.x, 2)) / m_diagonal; - // calculate the translation - rotation = map.rotation(); - rx = -m_width / 2; - ry = -m_height / 2; - dx = scale * rx + center.x; - dy = scale * ry + center.y; - } + scale = Math.sqrt( + Math.pow(lowerRight.y - upperLeft.y, 2) + + Math.pow(lowerRight.x - upperLeft.x, 2)) / m_diagonal; + // calculate the translation + rotation = map.rotation(); + rx = -m_width / 2; + ry = -m_height / 2; + dx = scale * rx + center.x; + dy = scale * ry + center.y; // set the group transform property + if (!rotation) { + dx = Math.round(dx); + dy = Math.round(dy); + } var transform = 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'; if (rotation) { transform += ' rotate(' + [ @@ -443,6 +432,8 @@ var d3Renderer = function (arg) { m_svg = undefined; m_defs.remove(); m_defs = undefined; + m_renderIds = {}; + m_removeIds = {}; s_exit(); }; @@ -465,11 +456,19 @@ var d3Renderer = function (arg) { * { * id: A unique string identifying the feature. * data: Array of data objects used in a d3 data method. - * index: A function that returns a unique id for each data element. + * dataIndex: A function that returns a unique id for each data element. + * defs: If set, a dictionary with values to render in the defs + * section. This can contain data, index, append, attributes, + * classes, style, and enter. enter is a function that is + * called on new elements. * style: An object containing element CSS styles. * attributes: An object containing element attributes. * classes: An array of classes to add to the elements. * append: The element type as used in d3 append methods. + * onlyRenderNew: a boolean. If true, features only get attributes and + * styles set when new. If false, features always have + * attributes and styles updated. + * sortByZ: a boolean. If true, sort features by the d.zIndex. * parentId: If set, the group ID of the parent element. * } */ @@ -482,6 +481,8 @@ var d3Renderer = function (arg) { attributes: arg.attributes, classes: arg.classes, append: arg.append, + onlyRenderNew: arg.onlyRenderNew, + sortByZ: arg.sortByZ, parentId: arg.parentId }; return m_this.__render(arg.id, arg.parentId); @@ -503,18 +504,56 @@ var d3Renderer = function (arg) { } return m_this; } + if (parentId) { + m_this._renderFeature(id, parentId); + } else { + m_renderIds[id] = true; + if (m_renderAnimFrameRef === null) { + m_renderAnimFrameRef = window.requestAnimationFrame(m_this._renderFrame); + } + } + }; + + this._renderFrame = function () { + var id; + for (id in m_removeIds) { + m_this.select(id).remove(); + m_defs.selectAll('.' + id).remove(); + } + m_removeIds = {}; + var ids = m_renderIds; + m_renderIds = {}; + m_renderAnimFrameRef = null; + for (id in ids) { + if (ids.hasOwnProperty(id)) { + m_this._renderFeature(id); + } + } + }; + + this._renderFeature = function (id, parentId) { + if (!m_features[id]) { + return; + } var data = m_features[id].data, index = m_features[id].index, style = m_features[id].style, attributes = m_features[id].attributes, classes = m_features[id].classes, append = m_features[id].append, - selection = m_this.select(id, parentId).data(data, index); - selection.enter().append(append); + selection = m_this.select(id, parentId).data(data, index), + entries, rendersel; + entries = selection.enter().append(append); selection.exit().remove(); - setAttrs(selection, attributes); - selection.attr('class', classes.concat([id]).join(' ')); - setStyles(selection, style); + rendersel = m_features[id].onlyRenderNew ? entries : selection; + setAttrs(rendersel, attributes); + rendersel.attr('class', classes.concat([id]).join(' ')); + setStyles(rendersel, style); + if (entries.size() && m_features[id].sortByZ) { + selection.sort(function (a, b) { + return (a.zIndex || 0) - (b.zIndex || 0); + }); + } return m_this; }; @@ -533,8 +572,14 @@ var d3Renderer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._removeFeature = function (id) { - m_this.select(id).remove(); + m_removeIds[id] = true; + if (m_renderAnimFrameRef === null) { + m_renderAnimFrameRef = window.requestAnimationFrame(m_this._renderFrame); + } delete m_features[id]; + if (m_renderIds[id]) { + delete m_renderIds[id]; + } return m_this; }; diff --git a/src/d3/index.js b/src/d3/index.js index d6030b2325..f614f615cf 100644 --- a/src/d3/index.js +++ b/src/d3/index.js @@ -13,6 +13,7 @@ module.exports = { pathFeature: require('./pathFeature'), planeFeature: require('./planeFeature'), pointFeature: require('./pointFeature'), + quadFeature: require('./quadFeature'), renderer: require('./d3Renderer'), tileLayer: require('./tileLayer'), uniqueID: require('./uniqueID'), diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js new file mode 100644 index 0000000000..9ec227e1f8 --- /dev/null +++ b/src/d3/quadFeature.js @@ -0,0 +1,232 @@ +var inherit = require('../inherit'); +var registerFeature = require('../registry').registerFeature; +var quadFeature = require('../quadFeature'); + +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of class quadFeature + * + * @class geo.d3.quadFeature + * @param {Object} arg Options object + * @extends geo.quadFeature + * @returns {geo.d3.quadFeature} + */ +////////////////////////////////////////////////////////////////////////////// +var d3_quadFeature = function (arg) { + 'use strict'; + if (!(this instanceof d3_quadFeature)) { + return new d3_quadFeature(arg); + } + + var $ = require('jquery'); + var d3 = require('d3'); + var object = require('./object'); + + quadFeature.call(this, arg); + object.call(this); + + var m_this = this, + s_exit = this._exit, + s_init = this._init, + s_update = this._update, + m_quads; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build this feature + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + if (!this.position()) { + return; + } + var renderer = this.renderer(), + map = renderer.layer().map(); + + m_quads = this._generateQuads(); + + var data = []; + $.each(m_quads.clrQuads, function (idx, quad) { + data.push({type: 'clr', quad: quad, zIndex: quad.pos[2]}); + }); + $.each(m_quads.imgQuads, function (idx, quad) { + if (quad.image) { + data.push({type: 'img', quad: quad, zIndex: quad.pos[2]}); + } + }); + + var feature = { + id: this._d3id(), + data: data, + dataIndex: function (d) { + return d.quad.quadId; + }, + append: function (d) { + var ns = this.namespaceURI, + element = d.type === 'clr' ? 'polygon' : 'image'; + return (ns ? document.createElementNS(ns, element) : + document.createElement(element)); + }, + attributes: { + fill: function (d) { + if (d.type === 'clr') { + return d3.rgb(255 * d.quad.color.r, 255 * d.quad.color.g, + 255 * d.quad.color.b); + } + /* set some styles here */ + if (d.quad.opacity !== 1) { + d3.select(this).style('opacity', d.quad.opacity); + } + }, + height: function (d) { + return d.type === 'clr' ? undefined : 1; + }, + points: function (d) { + if (d.type === 'clr' && !d.points) { + var points = [], i; + for (i = 0; i < d.quad.pos.length; i += 3) { + var p = { + x: d.quad.pos[i], + y: d.quad.pos[i + 1], + z: d.quad.pos[i + 2] + }; + /* We don't use 'p = m_this.featureGcsToDisplay(p);' because the + * quads have already been converted to the map's gcs (no longer + * the feature's gcs or map's ingcs). */ + p = map.gcsToDisplay(p, null); + p = renderer.baseToLocal(p); + points.push('' + p.x + ',' + p.y); + } + d.points = (points[0] + ' ' + points[1] + ' ' + points[3] + ' ' + + points[2]); + } + return d.type === 'clr' ? d.points : undefined; + }, + preserveAspectRatio: function (d) { + return d.type === 'clr' ? undefined : 'none'; + }, + reference: function (d) { + return d.quad.reference; + }, + stroke: false, + transform: function (d) { + if (d.type === 'img' && d.quad.image && !d.svgTransform) { + var pos = [], area, maxarea = -1, maxv, i, imgscale, + imgw = d.quad.image.width, imgh = d.quad.image.height; + for (i = 0; i < d.quad.pos.length; i += 3) { + var p = { + x: d.quad.pos[i], + y: d.quad.pos[i + 1], + z: d.quad.pos[i + 2] + }; + /* We don't use 'p = m_this.featureGcsToDisplay(p);' because the + * quads have already been converted to the map's gcs (no longer + * the feature's gcs or map's ingcs). */ + p = map.gcsToDisplay(p, null); + p = renderer.baseToLocal(p); + pos.push(p); + } + /* We can only fit three corners of the quad to the image, but we + * get to pick which three. We choose to always include the + * largest of the triangles formed by a set of three vertices. The + * image is always rendered as a parallelogram, so it may be larger + * than desired, and, for convex quads, miss some of the intended + * area. */ + for (i = 0; i < 4; i += 1) { + area = Math.abs( + pos[(i + 1) % 4].x * (pos[(i + 2) % 4].y - pos[(i + 3) % 4].y) + + pos[(i + 2) % 4].x * (pos[(i + 3) % 4].y - pos[(i + 1) % 4].y) + + pos[(i + 3) % 4].x * (pos[(i + 1) % 4].y - pos[(i + 2) % 4].y)) / 2; + if (area > maxarea) { + maxarea = area; + maxv = i; + } + } + d.svgTransform = [ + maxv === 3 || maxv === 2 ? pos[1].x - pos[0].x : pos[3].x - pos[2].x, + maxv === 3 || maxv === 2 ? pos[1].y - pos[0].y : pos[3].y - pos[2].y, + maxv === 0 || maxv === 2 ? pos[1].x - pos[3].x : pos[0].x - pos[2].x, + maxv === 0 || maxv === 2 ? pos[1].y - pos[3].y : pos[0].y - pos[2].y, + maxv === 2 ? pos[3].x + pos[0].x - pos[1].x : pos[2].x, + maxv === 2 ? pos[3].y + pos[0].y - pos[1].y : pos[2].y + ]; + if (Math.abs(d.svgTransform[1] / imgw) < 1e-6 && + Math.abs(d.svgTransform[2] / imgh) < 1e-6) { + imgscale = d.svgTransform[0] / imgw; + d.svgTransform[4] = Math.round(d.svgTransform[4] / imgscale) * imgscale; + imgscale = d.svgTransform[3] / imgh; + d.svgTransform[5] = Math.round(d.svgTransform[5] / imgscale) * imgscale; + } + } + return ((d.type !== 'img' || !d.quad.image) ? undefined : + 'matrix(' + d.svgTransform.join(' ') + ')'); + }, + width: function (d) { + return d.type === 'clr' ? undefined : 1; + }, + x: function (d) { + return d.type === 'clr' ? undefined : 0; + }, + 'xlink:href': function (d) { + return ((d.type === 'clr' || !d.quad.image) ? undefined : + d.quad.image.src); + }, + y: function (d) { + return d.type === 'clr' ? undefined : 0; + } + }, + style: { + fillOpacity: function (d) { + return d.type === 'clr' ? d.quad.opacity : undefined; + } + }, + onlyRenderNew: !this.style('previewColor') && !this.style('previewImage'), + sortByZ: true, + classes: ['d3QuadFeature'] + }; + renderer._drawFeatures(feature); + + this.buildTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + s_update.call(m_this); + if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime() || + m_this.buildTime().getMTime() < m_this.getMTime()) { + m_this._build(); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + s_init.call(m_this, arg); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + s_exit.call(m_this); + }; + + m_this._init(arg); + return this; +}; + +inherit(d3_quadFeature, quadFeature); + +// Now register it +registerFeature('d3', 'quad', d3_quadFeature); +module.exports = d3_quadFeature; diff --git a/src/d3/tileLayer.js b/src/d3/tileLayer.js index 00ed4ae7e0..bb0f25d41c 100644 --- a/src/d3/tileLayer.js +++ b/src/d3/tileLayer.js @@ -3,134 +3,83 @@ var registerLayerAdjustment = require('../registry').registerLayerAdjustment; var d3_tileLayer = function () { 'use strict'; var m_this = this, - s_update = this._update, s_init = this._init, - $ = require('jquery'), - uniqueID = require('./uniqueID'); + s_exit = this._exit, + m_quadFeature, + m_nextTileId = 0, + m_tiles = []; this._drawTile = function (tile) { var bounds = m_this._tileBounds(tile), - parentNode = m_this._getSubLayer(tile.index.level), - offsetx = parseInt(parentNode.attr('offsetx') || 0, 10), - offsety = parseInt(parentNode.attr('offsety') || 0, 10); - tile.feature = m_this.createFeature( - 'plane', {drawOnAsyncResourceLoad: true}) - .origin([bounds.left - offsetx, bounds.top - offsety]) - .upperLeft([bounds.left - offsetx, bounds.top - offsety]) - .lowerRight([bounds.right - offsetx, bounds.bottom - offsety]) - .style({ - image: tile._url, - opacity: 1, - reference: tile.toString(), - parentId: parentNode.attr('data-tile-layer-id') - }); - /* Don't respond to geo events */ - tile.feature.geoTrigger = undefined; - tile.feature._update(); + level = tile.index.level || 0, + to = this._tileOffset(level), + quad = {}; + quad.ul = this.fromLocal(this.fromLevel({ + x: bounds.left - to.x, y: bounds.top - to.y + }, level), 0); + quad.ll = this.fromLocal(this.fromLevel({ + x: bounds.left - to.x, y: bounds.bottom - to.y + }, level), 0); + quad.ur = this.fromLocal(this.fromLevel({ + x: bounds.right - to.x, y: bounds.top - to.y + }, level), 0); + quad.lr = this.fromLocal(this.fromLevel({ + x: bounds.right - to.x, y: bounds.bottom - to.y + }, level), 0); + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + m_nextTileId += 1; + quad.id = m_nextTileId; + tile.quadId = quad.id; + quad.image = tile.image; + quad.reference = tile.toString(); + m_tiles.push(quad); + m_quadFeature.data(m_tiles); + m_quadFeature._update(); m_this.draw(); }; - /** - * Return the DOM element containing a level specific - * layer. This will create the element if it doesn't - * already exist. - * @param {number} level The zoom level of the layer to fetch - * @return {DOM} - */ - this._getSubLayer = function (level) { - var node = m_this.canvas().select( - 'g[data-tile-layer="' + level.toFixed() + '"]'); - if (node.empty()) { - node = m_this.canvas().append('g'); - var id = uniqueID(); - node.classed('group-' + id, true); - node.classed('geo-tile-layer', true); - node.attr('data-tile-layer', level.toFixed()); - node.attr('data-tile-layer-id', id); + /* Remove the tile feature. */ + this._remove = function (tile) { + if (tile.quadId !== undefined && m_quadFeature) { + for (var i = 0; i < m_tiles.length; i += 1) { + if (m_tiles[i].id === tile.quadId) { + m_tiles.splice(i, 1); + break; + } + } + m_quadFeature.data(m_tiles); + m_quadFeature._update(); + m_this.draw(); } - return node; }; /** - * Set sublayer transforms to align them with the given zoom level. - * @param {number} level The target zoom level - * @param {object} view The view bounds. The top and left are used to - * adjust the offset of tile layers. - * @return {object} the x and y offsets for the current level. + * Clean up the layer. */ - this._updateSubLayers = function (level, view) { - var canvas = m_this.canvas(), - lastlevel = parseInt(canvas.attr('lastlevel'), 10), - lastx = parseInt(canvas.attr('lastoffsetx') || 0, 10), - lasty = parseInt(canvas.attr('lastoffsety') || 0, 10); - if (lastlevel === level && Math.abs(lastx - view.left) < 65536 && - Math.abs(lasty - view.top) < 65536) { - return {x: lastx, y: lasty}; - } - var to = this._tileOffset(level), - x = parseInt(view.left, 10) + to.x, - y = parseInt(view.top, 10) + to.y; - var tileCache = m_this.cache._cache; - $.each(canvas.selectAll('.geo-tile-layer')[0], function (idx, el) { - var layer = parseInt($(el).attr('data-tile-layer'), 10), - scale = Math.pow(2, level - layer); - el = m_this._getSubLayer(layer); - el.attr('transform', 'matrix(' + [scale, 0, 0, scale, 0, 0].join() + ')'); - /* x and y are the upper left of our view. This is the zero-point for - * offsets at the current level. Other tile layers' offsets are scaled - * by appropriate factors of 2. We need to shift the tiles of each - * layer by the appropriate amount (computed as dx and dy). */ - var layerx = parseInt(x / Math.pow(2, level - layer), 10), - layery = parseInt(y / Math.pow(2, level - layer), 10), - dx = layerx - parseInt(el.attr('offsetx') || 0, 10), - dy = layery - parseInt(el.attr('offsety') || 0, 10); - el.attr({offsetx: layerx, offsety: layery}); - /* We have to update the values stored in the tile features, too, - * otherwise when d3 regenerates these features, the offsets will be - * wrong. */ - $.each(tileCache, function (idx, tile) { - if (tile._index.level === layer && tile.feature) { - var f = tile.feature, - o = f.origin(), ul = f.upperLeft(), lr = f.lowerRight(); - f.origin([o[0] - dx, o[1] - dy, o[2]]); - f.upperLeft([ul[0] - dx, ul[1] - dy, ul[2]]); - f.lowerRight([lr[0] - dx, lr[1] - dy, lr[2]]); - f._update(); - } - }); - }); - canvas.attr({lastoffsetx: x, lastoffsety: y, lastlevel: level}); - return {x: x, y: y}; + this._exit = function () { + m_this.deleteFeature(m_quadFeature); + m_quadFeature = null; + m_tiles = []; + s_exit.apply(m_this, arguments); }; /* Initialize the tile layer. This creates a series of sublayers so that * the different layers will stack in the proper order. */ this._init = function () { - var sublayer; - s_init.apply(m_this, arguments); - for (sublayer = 0; sublayer <= m_this._options.maxLevel; sublayer += 1) { - m_this._getSubLayer(sublayer); - } - }; - - /* When update is called, apply the transform to our renderer. */ - this._update = function () { - s_update.apply(m_this, arguments); - m_this.renderer()._setTransform(); + m_quadFeature = this.createFeature('quad', { + previewColor: m_this._options.previewColor, + previewImage: m_this._options.previewImage + }); + m_quadFeature.geoTrigger = undefined; + m_quadFeature.gcs(m_this._options.gcs || m_this.map().gcs()); + m_quadFeature.data(m_tiles); + m_quadFeature._update(); }; - /* Remove both the tile feature and an internal image element. */ - this._remove = function (tile) { - if (tile.feature) { - m_this.deleteFeature(tile.feature); - tile.feature = null; - } - if (tile.image) { - $(tile.image).remove(); - } - }; + this._getSubLayer = function () {}; + this._updateSubLayers = undefined; }; registerLayerAdjustment('d3', 'tile', d3_tileLayer); diff --git a/src/gl/tileLayer.js b/src/gl/tileLayer.js index b6db5770cd..88685989f8 100644 --- a/src/gl/tileLayer.js +++ b/src/gl/tileLayer.js @@ -83,7 +83,7 @@ var gl_tileLayer = function () { /* These functions don't need to do anything. */ this._getSubLayer = function () {}; - this._updateSubLayer = undefined; + this._updateSubLayers = undefined; }; registerLayerAdjustment('vgl', 'tile', gl_tileLayer); diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index fa1d7aac7b..4c8d71b176 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -7,7 +7,7 @@ var transform = require('./transform'); /** * Create a new instance of class heatmapFeature * - * @class + * @class geo.heatmapFeature * @param {Object} arg Options object * @extends geo.feature * @param {Object|Function} [position] Position of the data. Default is diff --git a/src/quadFeature.js b/src/quadFeature.js index 41fe4eeac8..fddb10d4d2 100644 --- a/src/quadFeature.js +++ b/src/quadFeature.js @@ -63,6 +63,7 @@ var quadFeature = function (arg) { var m_this = this, s_init = this._init, m_cacheQuads, + m_nextQuadId = 0, m_images = [], m_quads; @@ -100,7 +101,7 @@ var quadFeature = function (arg) { /** * Add a new object to a list of object->object mappings. The key object - * should not exist, or this will create a duplicated. The new entry is + * should not exist, or this will create a duplicate. The new entry is * marked as being in use. * * @param {array} list the list of mappings. @@ -128,8 +129,12 @@ var quadFeature = function (arg) { /** * Point search method for selection api. Returns markers containing the * given point. - * @argument {Object} coordinate - * @returns {Object} + * + * @memberof geo.quadFeature + * @param {Object} coordinate coordinate in input gcs to check if it is + * located in any quad. + * @returns {Object} an object with 'index': a list of quad indices, and + * 'found': a list of quads that contain the specified coordinate. */ //////////////////////////////////////////////////////////////////////////// this.pointSearch = function (coordinate) { @@ -169,6 +174,9 @@ var quadFeature = function (arg) { /** * Get/Set position * + * @memberof geo.quadFeature + * @param {object|function} [position] object or function that returns the + * position of each quad. * @returns {geo.quadFeature} */ //////////////////////////////////////////////////////////////////////////// @@ -185,7 +193,7 @@ var quadFeature = function (arg) { /** * Given a data item and its index, fetch its position and ensure we have - * compelte information for the quad. This generates missing corners and z + * complete information for the quad. This generates missing corners and z * values. * * @param {function} posFunc a function to call to get the position of a data @@ -218,7 +226,7 @@ var quadFeature = function (arg) { if (pos[key][2] === undefined) { pos[key][2] = depthFunc.call(m_this, d, i); } - if (gcs !== map_gcs) { + if (gcs !== map_gcs && gcs !== false) { pos[key] = transform.transformCoordinates( gcs, map_gcs, pos[key]); } @@ -313,7 +321,8 @@ var quadFeature = function (arg) { idx: i, pos: pos, opacity: opacity, - color: util.convertColor(colorFunc.call(m_this, d, i)) + color: util.convertColor(colorFunc.call(m_this, d, i)), + reference: d.reference }; clrQuads.push(quad); quadinfo.clrquad = quad; @@ -331,7 +340,8 @@ var quadFeature = function (arg) { quad = { idx: i, pos: pos, - opacity: opacity + opacity: opacity, + reference: d.reference }; if (image.complete && image.naturalWidth && image.naturalHeight) { quad.image = image; @@ -379,6 +389,14 @@ var quadFeature = function (arg) { quadinfo.imgquad = quad; } if (m_cacheQuads !== false && quadinfo.keep !== false) { + if (quadinfo.clrquad) { + m_nextQuadId += 1; + quadinfo.clrquad.quadId = m_nextQuadId; + } + if (quadinfo.imgquad) { + m_nextQuadId += 1; + quadinfo.imgquad.quadId = m_nextQuadId; + } d._cachedQuad = quadinfo; } }); @@ -433,6 +451,7 @@ var quadFeature = function (arg) { /** * Create a quadFeature from an object. + * * @see {@link geo.feature.create} * @param {geo.layer} layer The layer to add the feature to * @param {geo.quadFeature.spec} spec The object specification diff --git a/src/tileLayer.js b/src/tileLayer.js index 5ca7731ab2..141ba26e61 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -1010,12 +1010,11 @@ module.exports = (function () { } var map = this.map(), bounds = map.bounds(undefined, null), + mapZoom = map.zoom(), + zoom = this._options.tileRounding(mapZoom), tiles; - if (this._updateSubLayers) { - var mapZoom = map.zoom(), - zoom = this._options.tileRounding(mapZoom), - view = this._getViewBounds(); + var view = this._getViewBounds(); // Update the transform for the local layer coordinates var offset = this._updateSubLayers(zoom, view) || {x: 0, y: 0}; diff --git a/src/vectorFeature.js b/src/vectorFeature.js index a8350bded2..a9792b3090 100644 --- a/src/vectorFeature.js +++ b/src/vectorFeature.js @@ -5,7 +5,7 @@ var feature = require('./feature'); /** * Create a new instance of class vectorFeature * - * @class + * @class geo.vectorFeature * @extends geo.feature * @returns {geo.vectorFeature} */ diff --git a/tests/cases/d3GraphFeature.js b/tests/cases/d3GraphFeature.js index bfa6fad56c..707e7ec8a9 100644 --- a/tests/cases/d3GraphFeature.js +++ b/tests/cases/d3GraphFeature.js @@ -1,5 +1,8 @@ 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; beforeEach(function () { $('
').appendTo('body') @@ -21,6 +24,7 @@ describe('d3 graph feature', function () { }); it('Add features to a layer', function () { + mockAnimationFrame(); var selection, nodes; nodes = [ @@ -36,6 +40,7 @@ describe('d3 graph feature', function () { feature = layer.createFeature('graph') .data(nodes) .draw(); + stepAnimationFrame(); selection = layer.canvas().selectAll('circle'); expect(selection[0].length).toBe(4); @@ -48,11 +53,13 @@ describe('d3 graph feature', function () { var selection; layer.deleteFeature(feature).draw(); + stepAnimationFrame(); selection = layer.canvas().selectAll('circle'); expect(selection[0].length).toBe(0); selection = layer.canvas().selectAll('path'); expect(selection[0].length).toBe(0); + unmockAnimationFrame(); }); }); diff --git a/tests/cases/d3PointFeature.js b/tests/cases/d3PointFeature.js index b31cb3003b..4fc593e027 100644 --- a/tests/cases/d3PointFeature.js +++ b/tests/cases/d3PointFeature.js @@ -1,5 +1,8 @@ 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; beforeEach(function () { $('
').appendTo('body') @@ -23,10 +26,12 @@ describe('d3 point feature', function () { }); it('Add features to a layer', function () { + mockAnimationFrame(); var selection; feature1 = layer.createFeature('point', {selectionAPI: true}) .data([{y: 0, x: 0}, {y: 10, x: 0}, {y: 0, x: 10}]) .draw(); + stepAnimationFrame(); selection = layer.node().find('circle'); expect(selection.length).toBe(3); @@ -34,6 +39,7 @@ describe('d3 point feature', function () { feature2 = layer.createFeature('point') .data([{y: -10, x: -10}, {y: 10, x: -10}]) .draw(); + stepAnimationFrame(); selection = layer.node().find('circle'); expect(selection.length).toBe(5); @@ -41,6 +47,7 @@ describe('d3 point feature', function () { layer.createFeature('point') .data([{y: -10, x: 10}]) .draw(); + stepAnimationFrame(); selection = layer.node().find('circle'); expect(selection.length).toBe(6); @@ -55,6 +62,7 @@ describe('d3 point feature', function () { var selection; layer.deleteFeature(feature2).draw(); + stepAnimationFrame(); selection = layer.node().find('circle'); expect(selection.length).toBe(4); @@ -64,8 +72,10 @@ describe('d3 point feature', function () { layer.clear().draw(); map.draw(); + stepAnimationFrame(); selection = layer.node().find('circle'); expect(selection.length).toBe(0); + unmockAnimationFrame(); }); }); diff --git a/tests/cases/d3VectorFeature.js b/tests/cases/d3VectorFeature.js index 63396f95c1..aa3f65e500 100644 --- a/tests/cases/d3VectorFeature.js +++ b/tests/cases/d3VectorFeature.js @@ -1,5 +1,8 @@ var geo = require('../test-utils').geo; var d3 = require('d3'); +var mockAnimationFrame = require('../test-utils').mockAnimationFrame; +var stepAnimationFrame = require('../test-utils').stepAnimationFrame; +var unmockAnimationFrame = require('../test-utils').unmockAnimationFrame; describe('d3 vector feature', function () { 'use strict'; @@ -33,6 +36,7 @@ describe('d3 vector feature', function () { }); it('Add features to a layer', function () { + mockAnimationFrame(); var vectorLines, featureGroup, markers; feature1 = layer.createFeature('vector') .data([{y: 0, x: 0}, {y: 10, x: 0}, {y: 0, x: 10}]) @@ -54,6 +58,7 @@ describe('d3 vector feature', function () { endStyle: 'arrow' }) .draw(); + stepAnimationFrame(); vectorLines = d3.select('#map-d3-vector svg').selectAll('line'); expect(vectorLines.size()).toBe(3); @@ -77,6 +82,7 @@ describe('d3 vector feature', function () { var selection, markers; layer.deleteFeature(feature1).draw(); + stepAnimationFrame(); selection = d3.select('#map-d3-vector svg').selectAll('line'); expect(selection.size()).toBe(0); @@ -111,6 +117,7 @@ describe('d3 vector feature', function () { endStyle: 'arrow' }) .draw(); + stepAnimationFrame(); vectorLines = d3.select('#map-d3-vector svg').selectAll('line'); expect(vectorLines.size()).toBe(3); @@ -134,5 +141,6 @@ describe('d3 vector feature', function () { it('Delete the map', function () { map.exit(); d3.select('#map-d3-vector').remove(); + unmockAnimationFrame(); }); }); diff --git a/tests/cases/osmLayer.js b/tests/cases/osmLayer.js index f11ac5ae12..1b31458b34 100644 --- a/tests/cases/osmLayer.js +++ b/tests/cases/osmLayer.js @@ -1,6 +1,9 @@ // Test geo.core.osmLayer 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; describe('geo.core.osmLayer', function () { 'use strict'; @@ -142,37 +145,39 @@ describe('geo.core.osmLayer', function () { /* The follow is a test of tileLayer as attached to a map. We don't * currently expose the tileLayer class directly to the createLayer * function, so some testing is done here */ - - // This test is currently disabled because it contains an unknown race condition. - xit('_update', function () { - var transform = layer.canvas().css('transform'); + it('_update', function () { + var lastlevel = layer.canvas().attr('lastlevel'); layer._update(); - expect(layer.canvas().css('transform')).toBe(transform); + expect(layer.canvas().attr('lastlevel')).toBe(lastlevel); map.zoom(1.5); - expect(layer.canvas().css('transform')).not.toBe(transform); + expect(layer.canvas().attr('lastlevel')).not.toBe(lastlevel); }); it('destroy', destroy_map); }); describe('d3', function () { - var layer, lastlevel; + var layer; it('creation', function () { map = create_map(); layer = map.createLayer('osm', {renderer: 'd3', url: '/data/white.jpg'}); - expect(map.node().find('[data-tile-layer="0"]').length).toBe(1); }); - waitForIt('.d3PlaneFeature', function () { - return map.node().find('.d3PlaneFeature').length > 0; + waitForIt('.d3QuadFeature', function () { + return map.node().find('.d3QuadFeature').length > 0; }); it('check for tiles', function () { - expect(map.node().find('.d3PlaneFeature').length).toBeGreaterThan(0); + expect(map.node().find('.d3QuadFeature').length).toBeGreaterThan(0); }); /* The following is a test of d3.tileLayer as attached to a map. */ it('_update', function () { - lastlevel = layer.canvas().attr('lastlevel'); + var elem = $('.d3QuadFeature').closest('g'); + var transform = elem.attr('transform'); + mockAnimationFrame(); layer._update(); - expect(layer.canvas().attr('lastlevel')).toBe(lastlevel); + stepAnimationFrame(); + expect(elem.attr('transform')).toBe(transform); map.zoom(1); - expect(layer.canvas().attr('lastlevel')).not.toBe(lastlevel); + stepAnimationFrame(); + expect(elem.attr('transform')).not.toBe(transform); + unmockAnimationFrame(); }); it('destroy', destroy_map); }); @@ -202,17 +207,15 @@ describe('geo.core.osmLayer', function () { expect(map.node().find('[data-tile-layer="0"]').is('div')).toBe(true); map.deleteLayer(layer); layer = map.createLayer('osm', {renderer: 'd3', url: '/data/white.jpg'}); - expect(map.node().find('[data-tile-layer="0"]').is('div')).toBe(false); - expect(map.node().find('[data-tile-layer="0"]').length).toBe(1); + expect(map.node().find('[data-tile-layer="0"]').length).toBe(0); }); - waitForIt('.d3PlaneFeature', function () { - return map.node().find('.d3PlaneFeature').length > 0; + waitForIt('.d3QuadFeature', function () { + return map.node().find('.d3QuadFeature').length > 0; }); it('d3 to canvas', function () { - expect(map.node().find('[data-tile-layer="0"]').is('g')).toBe(true); map.deleteLayer(layer); layer = map.createLayer('osm', {renderer: 'canvas', url: '/data/white.jpg'}); - expect(map.node().find('[data-tile-layer="0"]').is('g')).toBe(false); + expect(map.node().find('.d3QuadFature').length).toBe(0); expect(map.node().find('.canvas-canvas').length).toBe(1); }); it('canvas to vgl', function () { @@ -255,8 +258,7 @@ describe('geo.core.osmLayer', function () { }); map.deleteLayer(layer); layer = map.createLayer('osm', {renderer: 'd3', url: '/data/white.jpg'}); - expect(map.node().find('[data-tile-layer="0"]').is('div')).toBe(false); - expect(map.node().find('[data-tile-layer="0"]').length).toBe(1); + expect(map.node().find('[data-tile-layer="0"]').length).toBe(0); }); waitForIt('d3 tiles to load', function () { return $('image[reference]').length === numTiles; @@ -264,9 +266,12 @@ describe('geo.core.osmLayer', function () { it('compare tile offsets at angle ' + angle, function () { $.each($('image[reference]'), function () { var ref = $(this).attr('reference'); - var offset = $(this)[0].getBoundingClientRect(); - /* Allow around 1 pixel of difference */ - expect(closeToEqual(offset, positions[ref], -0.4)).toBe(true); + /* Only check the top level */ + if (ref.indexOf('4_') === 0) { + var offset = $(this)[0].getBoundingClientRect(); + /* Allow around 1 pixel of difference */ + expect(closeToEqual(offset, positions[ref], -0.4)).toBe(true); + } }); }); it('destroy', destroy_map); @@ -274,6 +279,29 @@ describe('geo.core.osmLayer', function () { }); }); + describe('geo.d3.osmLayer', function () { + var layer, mapinfo = {}; + it('test that tiles are created', function () { + map = create_map(); + mapinfo.map = map; + layer = map.createLayer('osm', { + renderer: 'd3', + url: '/data/white.jpg' + }); + }); + waitForIt('tiles to load', function () { + return Object.keys(layer.activeTiles).length === 21; + }); + it('zoom out', function () { + map.zoom(3); + }); + /* This checks to make sure tiles are removed */ + waitForIt('tiles to load', function () { + return Object.keys(layer.activeTiles).length === 17; + }); + it('destroy', destroy_map); + }); + describe('geo.canvas.osmLayer', function () { var layer, mapinfo = {}; it('test that tiles are created', function () { diff --git a/tests/cases/quadFeature.js b/tests/cases/quadFeature.js new file mode 100644 index 0000000000..c395064535 --- /dev/null +++ b/tests/cases/quadFeature.js @@ -0,0 +1,639 @@ +// Test geo.quadFeature and geo.gl.quadFeature + +/* globals Image */ + +var geo = require('../test-utils').geo; +var $ = require('jquery'); +var vgl = require('vgl'); +var mockVGLRenderer = require('../test-utils').mockVGLRenderer; +var restoreVGLRenderer = require('../test-utils').restoreVGLRenderer; +var waitForIt = require('../test-utils').waitForIt; +var closeToArray = require('../test-utils').closeToArray; +var logCanvas2D = require('../test-utils').logCanvas2D; + +describe('geo.quadFeature', function () { + 'use strict'; + + var previewImage = new Image(); + var preloadImage = new Image(); + preloadImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12P4zcAAAAL1APz9mbnSAAAAAElFTkSuQmCC'; // red 1x1 + var postloadImage = new Image(); + var testQuads = [{ + ll: {x: -108, y: 29}, + ur: {x: -88, y: 49}, + image: postloadImage + }, { + ll: {x: -88, y: 29}, + ur: {x: -58, y: 49}, + image: postloadImage, + opacity: 0.75 + }, { + ul: {x: -108, y: 29}, + ur: {x: -58, y: 29}, + ll: {x: -98, y: 9}, + lr: {x: -68, y: 9}, + previewImage: null, + image: postloadImage + }, { + lr: {x: -58, y: 29}, + ur: {x: -58, y: 49}, + ul: {x: -38, y: 54}, + ll: {x: -33, y: 34}, + image: preloadImage, + opacity: 0.15 + }, { + ll: {x: -33, y: 34}, + lr: {x: -33, y: 9}, + ur: {x: -68, y: 9}, + ul: {x: -58, y: 29}, + reload: false, + image: preloadImage + }, { + ll: {x: -128, y: 29}, + ur: {x: -108, y: 49}, + image: 'nosuchimage.png' + }, { + ul: {x: -128, y: 29}, + ur: {x: -108, y: 29}, + ll: {x: -123, y: 9}, + lr: {x: -98, y: 9}, + previewImage: null, + image: 'nosuchimage.png' + }, { + ul: {x: -148, y: 29}, + ur: {x: -128, y: 29}, + ll: {x: -148, y: 9}, + lr: {x: -123, y: 9}, + previewImage: previewImage, + image: 'nosuchimage.png' + }, { + ll: {x: -138, y: 29}, + ur: {x: -128, y: 39}, + color: '#FF0000' + }, { + ll: [-148, 39], + ur: [-138, 49], + color: '#FF0000' + }, { + ll: {x: -138, y: 39}, + ur: {x: -128, y: 49}, + color: '#00FFFF' + }, { + ll: {x: -148, y: 29}, + ur: {x: -138, y: 39}, + opacity: 0.25, + color: '#0000FF' + }, { + ll: {x: -108, y: 49}, + lr: {x: -88, y: 49}, + ur: {x: -108, y: 59}, + ul: {x: -88, y: 59}, + image: postloadImage + }, { + ll: {x: -88, y: 49}, + ur: {x: -68, y: 49}, + ul: {x: -88, y: 59}, + lr: {x: -68, y: 59}, + image: postloadImage + }, { + image: 'noposition.png' + }, { + ll: {x: -118, y: 49}, + ur: {x: -108, y: 59}, + previewImage: null, + previewColor: null, + image: postloadImage + }, { + ll: {x: -128, y: 49}, + ur: {x: -118, y: 59}, + previewImage: null, + previewColor: null, + image: 'nosuchimage.png' + }, { + ll: {x: -138, y: 49}, + ur: {x: -128, y: 59}, + previewImage: null, + previewColor: null, + reload: false, + image: postloadImage + }]; + var testStyle = { + opacity: function (d) { + return d.opacity !== undefined ? d.opacity : 1; + }, + color: function (d) { + return d.color; + }, + previewColor: function (d) { + return d.previewColor !== undefined ? d.previewColor : + {r: 1, g: 0.75, b: 0.75}; + }, + previewImage: function (d) { + return d.previewImage !== undefined ? d.previewImage : + previewImage; + }, + drawOnAsyncResourceLoaded: function (d) { + return d.reload !== undefined ? d.reload : true; + } + }; + + 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); + } + + function load_preview_image(done) { + if (!previewImage.src) { + previewImage.onload = function () { + done(); + }; + previewImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12P4DwABAQEAG7buVgAAAABJRU5ErkJggg=='; // white 1x1 + } else { + done(); + } + } + + describe('create', function () { + it('create function', function () { + mockVGLRenderer(); + var map, layer, quad; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'vgl'}); + quad = geo.quadFeature.create(layer); + expect(quad instanceof geo.quadFeature).toBe(true); + restoreVGLRenderer(); + }); + }); + + describe('Check class accessors', function () { + var map, layer; + it('position', function () { + var pos = {ll: {x: 0, y: 0}, ur: {x: 1, y: 1}}, quad; + + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + quad = geo.quadFeature({layer: layer}); + quad._init(); + expect(quad.position()('a')).toBe('a'); + quad.position(pos); + expect(quad.position()('a')).toEqual(pos); + expect(quad.style('position')('a')).toEqual(pos); + quad.position(function () { return 'b'; }); + expect(quad.position()('a')).toEqual('b'); + }); + }); + + describe('Public utility methods', function () { + describe('pointSearch', function () { + it('basic usage', function () { + var map, layer, quad, data, pt; + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + quad = geo.quadFeature({layer: layer}); + quad._init(); + data = [{ + ll: [-60, 10], ur: [-40, 30], image: preloadImage + }, { + ll: [-80, 10], lr: [-50, 10], ur: [-70, 30], image: preloadImage + }]; + quad.data(data); + pt = quad.pointSearch({x: -45, y: 11}); + expect(pt.index).toEqual([0]); + expect(pt.found.length).toBe(1); + expect(pt.found[0].ll).toEqual(data[0].ll); + pt = quad.pointSearch({x: -55, y: 11}); + expect(pt.index).toEqual([0, 1]); + expect(pt.found.length).toBe(2); + pt = quad.pointSearch({x: -35, y: 11}); + expect(pt.index).toEqual([]); + expect(pt.found.length).toBe(0); + }); + }); + }); + + describe('Private utility methods', function () { + describe('_object_list methods', function () { + var map, layer, quad, olist = []; + it('_objectListStart', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + quad = geo.quadFeature({layer: layer}); + quad._objectListStart(olist); + expect(olist).toEqual([]); + olist.push({entry: 1, value: 'a'}); + quad._objectListStart(olist); + expect(olist).toEqual([{entry: 1, value: 'a', used: false}]); + olist[0].used = true; + quad._objectListStart(olist); + expect(olist).toEqual([{entry: 1, value: 'a', used: false}]); + }); + it('_objectListGet', function () { + quad._objectListStart(olist); + expect(quad._objectListGet(olist, 1)).toEqual('a'); + expect(olist).toEqual([{entry: 1, value: 'a', used: true}]); + expect(quad._objectListGet(olist, 2)).toBe(undefined); + }); + it('_objectListAdd', function () { + expect(quad._objectListGet(olist, 2)).toBe(undefined); + quad._objectListAdd(olist, 2, 'b'); + expect(olist).toEqual([ + {entry: 1, value: 'a', used: true}, + {entry: 2, value: 'b', used: true}]); + expect(quad._objectListGet(olist, 2)).toEqual('b'); + }); + it('_objectListEnd', function () { + quad._objectListEnd(olist); + expect(olist).toEqual([ + {entry: 1, value: 'a', used: true}, + {entry: 2, value: 'b', used: true}]); + quad._objectListStart(olist); + expect(quad._objectListGet(olist, 1)).toEqual('a'); + expect(olist).toEqual([ + {entry: 1, value: 'a', used: true}, + {entry: 2, value: 'b', used: false}]); + quad._objectListEnd(olist); + expect(olist).toEqual([{entry: 1, value: 'a', used: true}]); + }); + }); + + describe('_init', function () { + var map, layer; + it('arg gets added to style', function () { + var pos = {ll: {x: 0, y: 0}, ur: {x: 1, y: 1}}, quad; + + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + quad = geo.quadFeature({layer: layer}); + /* init is not automatically called on the geo.quadFeature (it is on + * geo.gl.quadFeature). */ + quad._init({ + style: {color: '#FFFFFF'}, + position: pos + }); + expect(quad.style('color')).toBe('#FFFFFF'); + expect(quad.style('position')()).toEqual(pos); + expect(quad.position()()).toEqual(pos); + }); + }); + + describe('_generateQuads', function () { + /* This implicitly tests _positionToQuad, and the testQuads are designed + * to exercise that thoroughly. It still might be good to have an + * explicit unit test of _positionToQuad. */ + var expectedClrQuads = [{ + idx: 2, + pos: [-98, 9, 0, -68, 9, 0, -108, 29, 0, -58, 29, 0], + opacity: 1, + color: {r: 1, g: 0.75, b: 0.75} + }, { + idx: 6, + pos: [-123, 9, 0, -98, 9, 0, -128, 29, 0, -108, 29, 0], + opacity: 1, + color: {r: 1, g: 0.75, b: 0.75} + }, { + idx: 8, + pos: [-138, 29, 0, -128, 29, 0, -138, 39, 0, -128, 39, 0], + opacity: 1, + color: {r: 1, g: 0, b: 0} + }, { + idx: 9, + pos: [-148, 39, 0, -138, 39, 0, -148, 49, 0, -138, 49, 0], + opacity: 1, + color: {r: 1, g: 0, b: 0} + }, { + idx: 10, + pos: [-138, 39, 0, -128, 39, 0, -138, 49, 0, -128, 49, 0], + opacity: 1, + color: {r: 0, g: 1, b: 1} + }, { + idx: 11, + pos: [-148, 29, 0, -138, 29, 0, -148, 39, 0, -138, 39, 0], + opacity: 0.25, + color: {r: 0, g: 0, b: 1} + }]; + var expectedImgQuads = [{ + idx: 0, + pos: [-108, 29, 0, -88, 29, 0, -108, 49, 0, -88, 49, 0], + opacity: 1, + image: previewImage, + postimage: postloadImage + }, { + idx: 1, + pos: [-88, 29, 0, -58, 29, 0, -88, 49, 0, -58, 49, 0], + opacity: 0.75, + image: previewImage, + postimage: postloadImage + }, { + idx: 2, + pos: [-98, 9, 0, -68, 9, 0, -108, 29, 0, -58, 29, 0], + opacity: 1, + postimage: postloadImage + }, { + idx: 3, + pos: [-33, 34, 0, -58, 29, 0, -38, 54, 0, -58, 49, 0], + opacity: 0.15, + image: preloadImage + }, { + idx: 4, + pos: [-33, 34, 0, -33, 9, 0, -58, 29, 0, -68, 9, 0], + opacity: 1, + image: preloadImage + }, { + idx: 5, + pos: [-128, 29, 0, -108, 29, 0, -128, 49, 0, -108, 49, 0], + opacity: 1, + image: previewImage + }, { + idx: 6, + pos: [-123, 9, 0, -98, 9, 0, -128, 29, 0, -108, 29, 0], + opacity: 1 + }, { + idx: 7, + pos: [-148, 9, 0, -123, 9, 0, -148, 29, 0, -128, 29, 0], + opacity: 1, + image: previewImage + }, { + idx: 12, + pos: [-108, 49, 0, -88, 49, 0, -88, 59, 0, -108, 59, 0], + opacity: 1, + image: previewImage, + postimage: postloadImage + }, { + idx: 13, + pos: [-88, 49, 0, -68, 59, 0, -88, 59, 0, -68, 49, 0], + opacity: 1, + image: previewImage, + postimage: postloadImage + }, { + idx: 15, + pos: [-118, 49, 0, -108, 49, 0, -118, 59, 0, -108, 59, 0], + opacity: 1, + postimage: postloadImage + }, { + idx: 16, + pos: [-128, 49, 0, -118, 49, 0, -128, 59, 0, -118, 59, 0], + opacity: 1 + }]; + var map, layer, quad, gen; + + it('load preview image', load_preview_image); + it('overall generation', function () { + map = create_map({gcs: 'EPSG:4326'}); + layer = map.createLayer('feature', {renderer: null}); + quad = geo.quadFeature({layer: layer}); + quad._init({style: testStyle}); + quad.data(testQuads); + gen = quad._generateQuads(); + expect(gen.clrQuads.length).toBe(6); + expect(gen.imgQuads.length).toBe(12); + $.each(expectedClrQuads, function (idx, exq) { + expect(gen.clrQuads[idx].idx).toBe(exq.idx); + expect(closeToArray(gen.clrQuads[idx].pos, exq.pos)).toBe(true); + expect(gen.clrQuads[idx].opacity).toEqual(exq.opacity); + expect(gen.clrQuads[idx].color).toEqual(exq.color); + }); + $.each(expectedImgQuads, function (idx, exq) { + expect(gen.imgQuads[idx].idx).toBe(exq.idx); + expect(closeToArray(gen.imgQuads[idx].pos, exq.pos)).toBe(true); + expect(gen.imgQuads[idx].opacity).toEqual(exq.opacity); + if (exq.image) { + expect(gen.imgQuads[idx].image.src).toBe(exq.image.src); + } else { + expect(gen.imgQuads[idx].image).toBe(undefined); + } + }); + }); + it('load postload image', function (done) { + var oldload = postloadImage.onload; + postloadImage.onload = function () { + if (oldload) { + oldload.apply(this, arguments); + } + done(); + }; + postloadImage.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVQI12Pg+M4AAAIKAQBkG8RkAAAAAElFTkSuQmCC'; // green 1x1 + }); + it('after loading images', function () { + expect(gen.clrQuads.length).toBe(5); + expect(gen.imgQuads.length).toBe(12); + expect(gen.clrQuads[0].idx).toBe(6); + $.each(expectedImgQuads, function (idx, exq) { + if (exq.postimage) { + expect(gen.imgQuads[idx].image.src).toBe(exq.postimage.src); + } else if (exq.image) { + expect(gen.imgQuads[idx].image.src).toBe(exq.image.src); + } else { + expect(gen.imgQuads[idx].image).toBe(undefined); + } + }); + }); + it('regenerate', function () { + gen = quad._generateQuads(); + expect(gen.clrQuads.length).toBe(5); + expect(gen.imgQuads.length).toBe(13); + }); + }); + }); + + /* This is a basic integration test of geo.gl.quadFeature. */ + describe('geo.gl.quadFeature', function () { + var map, layer, quads, glCounts; + it('load preview image', load_preview_image); + it('basic usage', function () { + var buildTime; + + $.each(testQuads, function (idx, quad) { + delete quad._cachedQuad; + }); + mockVGLRenderer(); + map = create_map(); + layer = map.createLayer('feature'); + quads = layer.createFeature('quad', {style: testStyle, data: testQuads}); + buildTime = quads.buildTime().getMTime(); + /* Trigger rerendering */ + quads.data(testQuads); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + glCounts = $.extend({}, vgl.mockCounts()); + }); + waitForIt('next render gl A', function () { + return vgl.mockCounts().createProgram === (glCounts.createProgram || 0) + 2; + }); + it('only img quad', function () { + glCounts = $.extend({}, vgl.mockCounts()); + var buildTime = quads.buildTime().getMTime(); + quads.data([testQuads[0], testQuads[1]]); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + }); + waitForIt('next render gl B', function () { + return vgl.mockCounts().activeTexture >= glCounts.activeTexture + 2 && + vgl.mockCounts().uniform3fv >= glCounts.uniform3fv + 1 && + vgl.mockCounts().bufferSubData >= (glCounts.bufferSubData || 0) + 1; + }); + it('only clr quad', function () { + glCounts = $.extend({}, vgl.mockCounts()); + var buildTime = quads.buildTime().getMTime(); + quads.data([testQuads[8], testQuads[9]]); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + }); + waitForIt('next render gl C', function () { + return vgl.mockCounts().activeTexture === glCounts.activeTexture && + vgl.mockCounts().uniform3fv === glCounts.uniform3fv + 2 && + vgl.mockCounts().bufferSubData === glCounts.bufferSubData + 1; + }); + it('many quads', function () { + glCounts = $.extend({}, vgl.mockCounts()); + var data = []; + for (var i = 0; i < 200; i += 1) { + data.push({ll: [i - 100, 0], ur: [i - 99, 10], color: '#0000FF'}); + data.push({ll: [i - 100, 10], ur: [i - 99, 20], image: preloadImage}); + } + quads.data(data); + map.draw(); + }); + waitForIt('next render gl D', function () { + return vgl.mockCounts().deleteBuffer === (glCounts.deleteBuffer || 0) + 2 && + vgl.mockCounts().uniform3fv === glCounts.uniform3fv + 2 && + vgl.mockCounts().bufferSubData === glCounts.bufferSubData; + }); + it('_exit', function () { + var buildTime = quads.buildTime().getMTime(); + layer.deleteFeature(quads); + quads.data(testQuads); + map.draw(); + expect(buildTime).toEqual(quads.buildTime().getMTime()); + restoreVGLRenderer(); + }); + }); + + /* This is a basic integration test of geo.canvas.quadFeature. */ + describe('geo.canvas.quadFeature', function () { + var map, layer, quads, counts; + it('load preview image', load_preview_image); + it('basic usage', function () { + var buildTime; + + $.each(testQuads, function (idx, quad) { + delete quad._cachedQuad; + }); + logCanvas2D(); + map = create_map(); + layer = map.createLayer('feature', {renderer: 'canvas'}); + quads = layer.createFeature('quad', {style: testStyle, data: testQuads}); + buildTime = quads.buildTime().getMTime(); + /* Trigger rerendering */ + quads.data(testQuads); + counts = $.extend({}, window._canvasLog.counts); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + }); + waitForIt('next render canvas A', function () { + return window._canvasLog.counts.clearRect >= (counts.clearRect || 0) + 1; + }); + it('only img quad', function () { + counts = $.extend({}, window._canvasLog.counts); + var buildTime = quads.buildTime().getMTime(); + quads.data([testQuads[0], testQuads[1]]); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + }); + waitForIt('next render canvas B', function () { + return window._canvasLog.counts.drawImage === counts.drawImage + 2 && + window._canvasLog.counts.clearRect === counts.clearRect + 1; + }); + /* Add a test for color quads here when they are implemented */ + it('many quads', function () { + counts = $.extend({}, window._canvasLog.counts); + var data = []; + for (var i = 0; i < 200; i += 1) { + /* Add color quads when implemented */ + data.push({ll: [i - 100, 10], ur: [i - 99, 20], image: preloadImage}); + } + quads.data(data); + map.draw(); + }); + waitForIt('next render canvas C', function () { + return window._canvasLog.counts.drawImage === counts.drawImage + 200 && + window._canvasLog.counts.clearRect === counts.clearRect + 1; + }); + it('_exit', function () { + var buildTime = quads.buildTime().getMTime(); + layer.deleteFeature(quads); + quads.data(testQuads); + map.draw(); + expect(buildTime).toEqual(quads.buildTime().getMTime()); + }); + }); + + /* This is a basic integration test of geo.d3.quadFeature. */ + describe('geo.d3.quadFeature', function () { + var map, layer, quads; + it('load preview image', load_preview_image); + it('basic usage', function () { + var buildTime; + + $.each(testQuads, function (idx, quad) { + delete quad._cachedQuad; + }); + logCanvas2D(); + map = create_map(); + layer = map.createLayer('feature', {renderer: 'd3'}); + quads = layer.createFeature('quad', {style: testStyle, data: testQuads}); + buildTime = quads.buildTime().getMTime(); + /* Trigger rerendering */ + quads.data(testQuads); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + /* Force the quads to render synchronously. */ + layer.renderer()._renderFrame(); + expect($('svg image').length).toBe(11); + expect($('svg polygon').length).toBe(5); + }); + it('only img quad', function () { + var buildTime = quads.buildTime().getMTime(); + quads.data([testQuads[0], testQuads[1]]); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + /* Force the quads to render synchronously. */ + layer.renderer()._renderFrame(); + expect($('svg image').length).toBe(2); + expect($('svg polygon').length).toBe(0); + }); + it('only clr quad', function () { + var buildTime = quads.buildTime().getMTime(); + quads.data([testQuads[8], testQuads[9]]); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + /* Force the quads to render synchronously. */ + layer.renderer()._renderFrame(); + expect($('svg image').length).toBe(0); + expect($('svg polygon').length).toBe(2); + }); + it('many quads', function () { + var data = []; + for (var i = 0; i < 200; i += 1) { + data.push({ll: [i - 100, 0], ur: [i - 99, 10], color: '#0000FF', reference: 'clr' + i}); + data.push({ll: [i - 100, 10], ur: [i - 99, 20], image: preloadImage, reference: 'img' + i}); + } + quads.data(data); + map.draw(); + /* Force the quads to render synchronously. */ + layer.renderer()._renderFrame(); + expect($('svg image').length).toBe(200); + expect($('svg polygon').length).toBe(200); + }); + it('_exit', function () { + var buildTime = quads.buildTime().getMTime(); + layer.deleteFeature(quads); + quads.data(testQuads); + map.draw(); + expect(buildTime).toEqual(quads.buildTime().getMTime()); + }); + }); +}); diff --git a/tests/test-utils.js b/tests/test-utils.js index 651ccd7630..c48d031ab6 100644 --- a/tests/test-utils.js +++ b/tests/test-utils.js @@ -309,6 +309,9 @@ module.exports.unmockAnimationFrame = function () { * @param {float} time float milliseconds. */ module.exports.stepAnimationFrame = function (time) { + if (time === undefined) { + time = new Date().getTime(); + } var callbacks = animFrameCallbacks, action; animFrameCallbacks = []; while (callbacks.length > 0) {