From 263b37b38542943180594d2664896dc15bdc99a6 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 19 May 2016 16:09:06 -0400 Subject: [PATCH 01/11] Add a d3 quadFeature. This will render nearly identically to the gl quadFeature, with the exception that when an image quad is not a parallelogram. When the quad is not a parallelogram, the gl renderer will skew the image using two different transforms making the quad contain the full image, but with a transform discontinuity along one of the diagonals. The d3 render will skew the image with a single transform, and part of the image will not be visible. For not convex quads, the d3 renderer could display part of the image multiple times. --- src/d3/d3Renderer.js | 16 +++- src/d3/index.js | 1 + src/d3/quadFeature.js | 197 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/d3/quadFeature.js diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index 79d05c02b9..3a5148be5d 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -465,7 +465,7 @@ 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. * style: An object containing element CSS styles. * attributes: An object containing element attributes. * classes: An array of classes to add to the elements. @@ -478,6 +478,7 @@ var d3Renderer = function (arg) { m_features[arg.id] = { data: arg.data, index: arg.dataIndex, + defs: arg.defs, style: arg.style, attributes: arg.attributes, classes: arg.classes, @@ -509,12 +510,24 @@ var d3Renderer = function (arg) { attributes = m_features[id].attributes, classes = m_features[id].classes, append = m_features[id].append, + defs = m_features[id].defs, selection = m_this.select(id, parentId).data(data, index); selection.enter().append(append); selection.exit().remove(); setAttrs(selection, attributes); selection.attr('class', classes.concat([id]).join(' ')); setStyles(selection, style); + if (defs) { + selection = m_defs.selectAll('.' + id).data(defs.data, index); + var entries = selection.enter().append(defs.append); + if (defs.enter) { + defs.enter(entries); + } + selection.exit().remove(); + setAttrs(selection, defs.attributes); + selection.attr('class', (defs.classes || []).concat([id]).join(' ')); + setStyles(selection, defs.style); + } return m_this; }; @@ -534,6 +547,7 @@ var d3Renderer = function (arg) { //////////////////////////////////////////////////////////////////////////// this._removeFeature = function (id) { m_this.select(id).remove(); + m_defs.selectAll('.' + id).remove(); delete m_features[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..c01b734675 --- /dev/null +++ b/src/d3/quadFeature.js @@ -0,0 +1,197 @@ +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}); + }); + $.each(m_quads.imgQuads, function (idx, quad) { + if (quad.image) { + data.push({type: 'img', quad: quad}); + } + }); + $.each(data, function (idx, d) { + var points = [], pos = [], area, maxarea = -1, maxv, i; + for (i = 0; i < d.quad.pos.length; i += 3) { + var p = map.gcsToDisplay({x: d.quad.pos[i], y: d.quad.pos[i + 1], z: d.quad.pos[i + 2]}, null); + pos.push(p); + points.push('' + p.x + ',' + p.y); + } + d.points = points[0] + ' ' + points[1] + ' ' + points[3] + ' ' + points[2]; + /* 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. This can result in some + * of the image not being visible, or, in twisted or concave quads, some + * of the image being duplicated. */ + 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.quad.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 + ]; + }); + var id = this._d3id(); + var feature = { + id: id, + data: data, + append: 'polygon', + attributes: { + points: function (d) { + return d.points; + }, + 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); + } + if (!d.quad.image) { + return 'none'; + } + /* Our method for setting style for fill prevents doing this in the + * style object, so do it here. */ + d3.select(this).style('fill', 'url(#' + id + '-img-' + d.quad.idx + ')'); + }, + stroke: false + }, + style: { + fillOpacity: function (d) { + return d.quad.opacity; + } + }, + classes: ['d3QuadFeature'], + defs: { + data: m_quads.imgQuads, + append: 'pattern', + attributes: { + id: function (d) { + return id + '-img-' + d.idx; + }, + x: 0, + y: 0, + patternTransform: function (d) { + return 'matrix(' + d.svgTransform.join(' ') + ')'; + }, + patternUnits: 'userSpaceOnUse', + width: 1, + height: 1, + enter: function (d) { + var node = d3.select(this), + imageElem = node.selectAll('.d3QuadFeatureImage'); + if (d.image && d.image.src) { + if (!imageElem.size()) { + imageElem = node.append('image').attr({ + 'class': 'd3QuadFeatureImage', x: 0, y: 0}); + } + imageElem.attr({ + width: 1, + height: 1, + preserveAspectRatio: 'none', + 'xlink:href': d.image.src + }); + } else { + imageElem.remove(); + } + } + } + } + }; + 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; From 70d558dd71a71a000f1a2e62f396597f01f69d29 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 20 May 2016 14:18:02 -0400 Subject: [PATCH 02/11] Polygons are very slow. Switch to using svg image elements. These are always parallelograms, so quads that are convex may not have their full areas covered, and quads that are not parallelograms will have extra areas rendered. --- src/d3/d3Renderer.js | 13 ------ src/d3/quadFeature.js | 98 ++++++++++++++++++++----------------------- src/quadFeature.js | 11 ++++- 3 files changed, 55 insertions(+), 67 deletions(-) diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index 3a5148be5d..6ca5d20af5 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -478,7 +478,6 @@ var d3Renderer = function (arg) { m_features[arg.id] = { data: arg.data, index: arg.dataIndex, - defs: arg.defs, style: arg.style, attributes: arg.attributes, classes: arg.classes, @@ -510,24 +509,12 @@ var d3Renderer = function (arg) { attributes = m_features[id].attributes, classes = m_features[id].classes, append = m_features[id].append, - defs = m_features[id].defs, selection = m_this.select(id, parentId).data(data, index); selection.enter().append(append); selection.exit().remove(); setAttrs(selection, attributes); selection.attr('class', classes.concat([id]).join(' ')); setStyles(selection, style); - if (defs) { - selection = m_defs.selectAll('.' + id).data(defs.data, index); - var entries = selection.enter().append(defs.append); - if (defs.enter) { - defs.enter(entries); - } - selection.exit().remove(); - setAttrs(selection, defs.attributes); - selection.attr('class', (defs.classes || []).concat([id]).join(' ')); - setStyles(selection, defs.style); - } return m_this; }; diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js index c01b734675..78520d73d2 100644 --- a/src/d3/quadFeature.js +++ b/src/d3/quadFeature.js @@ -63,9 +63,9 @@ var d3_quadFeature = function (arg) { d.points = points[0] + ' ' + points[1] + ' ' + points[3] + ' ' + points[2]; /* 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. This can result in some - * of the image not being visible, or, in twisted or concave quads, some - * of the image being duplicated. */ + * 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) + @@ -76,7 +76,7 @@ var d3_quadFeature = function (arg) { maxv = i; } } - d.quad.svgTransform = [ + 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, @@ -89,65 +89,57 @@ var d3_quadFeature = function (arg) { var feature = { id: id, data: data, - append: 'polygon', + 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: { - points: function (d) { - return d.points; - }, 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); - } - if (!d.quad.image) { - return 'none'; + return d3.rgb(255 * d.quad.color.r, 255 * d.quad.color.g, + 255 * d.quad.color.b); } - /* Our method for setting style for fill prevents doing this in the - * style object, so do it here. */ - d3.select(this).style('fill', 'url(#' + id + '-img-' + d.quad.idx + ')'); + d3.select(this).style('opacity', d.quad.opacity); + }, + height: function (d) { + return d.type === 'clr' ? undefined : 1; + }, + points: function (d) { + return d.type === 'clr' ? d.points : undefined; + }, + preserveAspectRatio: function (d) { + return d.type === 'clr' ? undefined : 'none'; }, - stroke: false + stroke: false, + transform: function (d) { + return ((d.type === 'clr' || !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.quad.opacity; + return d.type === 'clr' ? d.quad.opacity : undefined; } }, - classes: ['d3QuadFeature'], - defs: { - data: m_quads.imgQuads, - append: 'pattern', - attributes: { - id: function (d) { - return id + '-img-' + d.idx; - }, - x: 0, - y: 0, - patternTransform: function (d) { - return 'matrix(' + d.svgTransform.join(' ') + ')'; - }, - patternUnits: 'userSpaceOnUse', - width: 1, - height: 1, - enter: function (d) { - var node = d3.select(this), - imageElem = node.selectAll('.d3QuadFeatureImage'); - if (d.image && d.image.src) { - if (!imageElem.size()) { - imageElem = node.append('image').attr({ - 'class': 'd3QuadFeatureImage', x: 0, y: 0}); - } - imageElem.attr({ - width: 1, - height: 1, - preserveAspectRatio: 'none', - 'xlink:href': d.image.src - }); - } else { - imageElem.remove(); - } - } - } - } + classes: ['d3QuadFeature'] }; renderer._drawFeatures(feature); diff --git a/src/quadFeature.js b/src/quadFeature.js index 41fe4eeac8..12100dd6b1 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; @@ -185,7 +186,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 @@ -379,6 +380,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; } }); From 3993e84e8a4c756c74998d491d8bcfbf639c7f96 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 20 May 2016 13:24:25 -0400 Subject: [PATCH 03/11] Use the d3.quadFeature for the tileLayer. --- examples/common/js/examples.js | 19 ++++ examples/quads/main.js | 7 +- examples/transitions/main.js | 12 +-- src/canvas/tileLayer.js | 2 +- src/d3/d3Renderer.js | 62 +++++++++++-- src/d3/quadFeature.js | 106 ++++++++++++++------- src/d3/tileLayer.js | 162 +++++++++++---------------------- src/gl/tileLayer.js | 2 +- src/quadFeature.js | 2 +- src/tileLayer.js | 7 +- 10 files changed, 216 insertions(+), 165 deletions(-) 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/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/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 6ca5d20af5..aec926bb60 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -40,6 +40,8 @@ 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_svg = null, m_defs = null; @@ -207,8 +209,9 @@ 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), @@ -466,10 +469,18 @@ var d3Renderer = function (arg) { * id: A unique string identifying the feature. * data: Array of data objects used in a d3 data method. * 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 +493,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 +516,50 @@ 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 ids = m_renderIds; + m_renderIds = {}; + m_renderAnimFrameRef = null; + for (var 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; }; @@ -536,6 +581,9 @@ var d3Renderer = function (arg) { m_this.select(id).remove(); m_defs.selectAll('.' + id).remove(); delete m_features[id]; + if (m_renderIds[id]) { + delete m_renderIds[id]; + } return m_this; }; diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js index 78520d73d2..d7768a6990 100644 --- a/src/d3/quadFeature.js +++ b/src/d3/quadFeature.js @@ -44,6 +44,7 @@ var d3_quadFeature = function (arg) { map = renderer.layer().map(); m_quads = this._generateQuads(); + var data = []; $.each(m_quads.clrQuads, function (idx, quad) { data.push({type: 'clr', quad: quad}); @@ -54,40 +55,12 @@ var d3_quadFeature = function (arg) { } }); $.each(data, function (idx, d) { - var points = [], pos = [], area, maxarea = -1, maxv, i; - for (i = 0; i < d.quad.pos.length; i += 3) { - var p = map.gcsToDisplay({x: d.quad.pos[i], y: d.quad.pos[i + 1], z: d.quad.pos[i + 2]}, null); - pos.push(p); - points.push('' + p.x + ',' + p.y); - } - d.points = points[0] + ' ' + points[1] + ' ' + points[3] + ' ' + points[2]; - /* 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 - ]; + d.points = d.svgTransform = null; + d.zIndex = d.quad.pos[2]; }); - var id = this._d3id(); + var feature = { - id: id, + id: this._d3id(), data: data, dataIndex: function (d) { return d.quad.quadId; @@ -104,12 +77,33 @@ var d3_quadFeature = function (arg) { return d3.rgb(255 * d.quad.color.r, 255 * d.quad.color.g, 255 * d.quad.color.b); } - d3.select(this).style('opacity', d.quad.opacity); + /* 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) { @@ -117,7 +111,47 @@ var d3_quadFeature = function (arg) { }, stroke: false, transform: function (d) { - return ((d.type === 'clr' || !d.quad.image) ? undefined : + if (d.type === 'img' && d.quad.image && !d.svgTransform) { + var pos = [], area, maxarea = -1, maxv, 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); + 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 + ]; + } + return ((d.type !== 'img' || !d.quad.image) ? undefined : 'matrix(' + d.svgTransform.join(' ') + ')'); }, width: function (d) { @@ -139,6 +173,8 @@ var d3_quadFeature = function (arg) { return d.type === 'clr' ? d.quad.opacity : undefined; } }, + onlyRenderNew: !this.style('previewColor') && !this.style('previewImage'), + sortByZ: true, classes: ['d3QuadFeature'] }; renderer._drawFeatures(feature); @@ -155,6 +191,8 @@ var d3_quadFeature = function (arg) { s_update.call(m_this); if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime() || m_this.buildTime().getMTime() < m_this.getMTime()) { + m_this.buildCount = (m_this.buildCount || 0) + 1; // DWM:: + window.buildCount = m_this.buildCount; // DWM:: m_this._build(); } return m_this; diff --git a/src/d3/tileLayer.js b/src/d3/tileLayer.js index 00ed4ae7e0..5c2e4db3ca 100644 --- a/src/d3/tileLayer.js +++ b/src/d3/tileLayer.js @@ -3,134 +3,82 @@ 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; + 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/quadFeature.js b/src/quadFeature.js index 12100dd6b1..d05952f6f0 100644 --- a/src/quadFeature.js +++ b/src/quadFeature.js @@ -219,7 +219,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]); } 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}; From 0f04b9cd805a649292eeddb6f0931533f4bf3a9e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 31 May 2016 13:59:14 -0400 Subject: [PATCH 04/11] Update tests to work with the quad-based d3 tile layer. Percolate a reference for each tile to the canvas element to make it easier to debug and test. Use floats for the null renderer so that its tiles will be located at close to the same position as svg tiles. Reenable the one disabled test for null renderer tiles. --- src/d3/quadFeature.js | 3 +++ src/d3/tileLayer.js | 1 + src/quadFeature.js | 6 +++-- src/tileLayer.js | 20 +++++++-------- tests/cases/d3GraphFeature.js | 7 ++++++ tests/cases/d3PointFeature.js | 8 ++++++ tests/cases/d3VectorFeature.js | 8 ++++++ tests/cases/osmLayer.js | 46 ++++++++++++++++++---------------- tests/test-utils.js | 3 +++ 9 files changed, 68 insertions(+), 34 deletions(-) diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js index d7768a6990..224e44e30a 100644 --- a/src/d3/quadFeature.js +++ b/src/d3/quadFeature.js @@ -109,6 +109,9 @@ var d3_quadFeature = function (arg) { 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) { diff --git a/src/d3/tileLayer.js b/src/d3/tileLayer.js index 5c2e4db3ca..bb0f25d41c 100644 --- a/src/d3/tileLayer.js +++ b/src/d3/tileLayer.js @@ -31,6 +31,7 @@ var d3_tileLayer = function () { 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(); diff --git a/src/quadFeature.js b/src/quadFeature.js index d05952f6f0..bfec3d1037 100644 --- a/src/quadFeature.js +++ b/src/quadFeature.js @@ -314,7 +314,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; @@ -332,7 +333,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; diff --git a/src/tileLayer.js b/src/tileLayer.js index 141ba26e61..7bc17504f4 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -728,8 +728,8 @@ module.exports = (function () { container.append(tile.image); container.css({ position: 'absolute', - left: (bounds.left - parseInt(div.attr('offsetx') || 0, 10)) + 'px', - top: (bounds.top - parseInt(div.attr('offsety') || 0, 10)) + 'px' + left: (bounds.left - parseFloat(div.attr('offsetx') || 0)) + 'px', + top: (bounds.top - parseFloat(div.attr('offsety') || 0)) + 'px' }); // apply fade in animation @@ -964,8 +964,8 @@ module.exports = (function () { this._updateSubLayers = function (level, view) { var canvas = this.canvas(), lastlevel = parseInt(canvas.attr('lastlevel'), 10), - lastx = parseInt(canvas.attr('lastoffsetx') || 0, 10), - lasty = parseInt(canvas.attr('lastoffsety') || 0, 10); + lastx = parseFloat(canvas.attr('lastoffsetx') || 0), + lasty = parseFloat(canvas.attr('lastoffsety') || 0); if (lastlevel === level && Math.abs(lastx - view.left) < 65536 && Math.abs(lasty - view.top) < 65536) { return {x: lastx, y: lasty}; @@ -981,15 +981,15 @@ module.exports = (function () { 'transform', 'scale(' + Math.pow(2, level - layer) + ')' ); - 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); + var layerx = parseFloat(x / Math.pow(2, level - layer)), + layery = parseFloat(y / Math.pow(2, level - layer)), + dx = layerx - parseFloat($el.attr('offsetx') || 0), + dy = layery - parseFloat($el.attr('offsety') || 0); $el.attr({offsetx: layerx, offsety: layery}); $el.find('.geo-tile-container').each(function (tileidx, tileel) { $(tileel).css({ - left: (parseInt($(tileel).css('left'), 10) - dx) + 'px', - top: (parseInt($(tileel).css('top'), 10) - dy) + 'px' + left: (parseFloat($(tileel).css('left')) - dx) + 'px', + top: (parseFloat($(tileel).css('top')) - dy) + 'px' }); }); }); 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..d4f65dcccf 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); @@ -67,5 +74,6 @@ describe('d3 point feature', function () { 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..5f40bdb4f8 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; 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) { From acf736a11197053ca8206aa768d5a9f3d0769a56 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 31 May 2016 14:59:51 -0400 Subject: [PATCH 05/11] Replace the quad feature unit tests. --- tests/cases/quadFeature.js | 567 +++++++++++++++++++++++++++++++++++++ 1 file changed, 567 insertions(+) create mode 100644 tests/cases/quadFeature.js diff --git a/tests/cases/quadFeature.js b/tests/cases/quadFeature.js new file mode 100644 index 0000000000..dd937274e8 --- /dev/null +++ b/tests/cases/quadFeature.js @@ -0,0 +1,567 @@ +// 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; + + 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', 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', 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', 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: [0, i - 100], ur: [10, i - 99], color: '#0000FF'}); + data.push({ll: [10, i - 100], ur: [10, i - 99], image: preloadImage}); + } + quads.data(data); + map.draw(); + }); + waitForIt('next render', 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; + + 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); + map.draw(); + expect(buildTime).not.toEqual(quads.buildTime().getMTime()); + counts = $.extend({}, window._canvasLog.counts); + }); + waitForIt('next render', 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', 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: [10, i - 100], ur: [10, i - 99], image: preloadImage}); + } + quads.data(data); + map.draw(); + }); + waitForIt('next render', 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()); + }); + }); +}); From 66707ce48c6fceaea92a56cba7fa0f262659a337 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Tue, 31 May 2016 16:40:23 -0400 Subject: [PATCH 06/11] Improve d3 quad tests. Remove some code that is no longer used. --- src/d3/d3Renderer.js | 37 +++++------------- src/d3/quadFeature.js | 10 +---- src/quadFeature.js | 2 +- tests/cases/osmLayer.js | 23 +++++++++++ tests/cases/quadFeature.js | 78 ++++++++++++++++++++++++++++++++++++-- 5 files changed, 110 insertions(+), 40 deletions(-) diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index aec926bb60..c083d1b86c 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -83,13 +83,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 () { @@ -216,29 +209,17 @@ var d3Renderer = function (arg) { 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 var transform = 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'; diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js index 224e44e30a..b35f01f378 100644 --- a/src/d3/quadFeature.js +++ b/src/d3/quadFeature.js @@ -47,17 +47,13 @@ var d3_quadFeature = function (arg) { var data = []; $.each(m_quads.clrQuads, function (idx, quad) { - data.push({type: 'clr', quad: 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}); + data.push({type: 'img', quad: quad, zIndex: quad.pos[2]}); } }); - $.each(data, function (idx, d) { - d.points = d.svgTransform = null; - d.zIndex = d.quad.pos[2]; - }); var feature = { id: this._d3id(), @@ -194,8 +190,6 @@ var d3_quadFeature = function (arg) { s_update.call(m_this); if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime() || m_this.buildTime().getMTime() < m_this.getMTime()) { - m_this.buildCount = (m_this.buildCount || 0) + 1; // DWM:: - window.buildCount = m_this.buildCount; // DWM:: m_this._build(); } return m_this; diff --git a/src/quadFeature.js b/src/quadFeature.js index bfec3d1037..4d63464320 100644 --- a/src/quadFeature.js +++ b/src/quadFeature.js @@ -101,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. diff --git a/tests/cases/osmLayer.js b/tests/cases/osmLayer.js index 5f40bdb4f8..64f5c93038 100644 --- a/tests/cases/osmLayer.js +++ b/tests/cases/osmLayer.js @@ -276,6 +276,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 index dd937274e8..1916f93555 100644 --- a/tests/cases/quadFeature.js +++ b/tests/cases/quadFeature.js @@ -446,6 +446,9 @@ describe('geo.quadFeature', function () { it('basic usage', function () { var buildTime; + $.each(testQuads, function (idx, quad) { + delete quad._cachedQuad; + }); mockVGLRenderer(); map = create_map(); layer = map.createLayer('feature'); @@ -488,8 +491,8 @@ describe('geo.quadFeature', function () { glCounts = $.extend({}, vgl.mockCounts()); var data = []; for (var i = 0; i < 200; i += 1) { - data.push({ll: [0, i - 100], ur: [10, i - 99], color: '#0000FF'}); - data.push({ll: [10, i - 100], ur: [10, i - 99], image: preloadImage}); + 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(); @@ -516,6 +519,9 @@ describe('geo.quadFeature', function () { it('basic usage', function () { var buildTime; + $.each(testQuads, function (idx, quad) { + delete quad._cachedQuad; + }); logCanvas2D(); map = create_map(); layer = map.createLayer('feature', {renderer: 'canvas'}); @@ -547,7 +553,7 @@ describe('geo.quadFeature', function () { var data = []; for (var i = 0; i < 200; i += 1) { /* Add color quads when implemented */ - data.push({ll: [10, i - 100], ur: [10, i - 99], image: preloadImage}); + data.push({ll: [i - 100, 10], ur: [i - 99, 20], image: preloadImage}); } quads.data(data); map.draw(); @@ -564,4 +570,70 @@ describe('geo.quadFeature', function () { 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()); + }); + }); }); From 82e023d5ffa112963c114a951818f24865990437 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 1 Jun 2016 12:04:34 -0400 Subject: [PATCH 07/11] Fix some of the jsdocs. I've added a config file for jsdoc so that the class names show the long name. This makes it so instead of seeing four quadFeature in the table of contents, you now see geo.quadFeature, geo.gl.quadFeature, geo.canvas.quadFeature, and geo.d3.quadFeature. I fixed a few other minor jsdoc issues. I also added a build-examples script to package.json. --- jsdoc.conf.json | 7 +++++++ package.json | 3 ++- src/heatmapFeature.js | 2 +- src/quadFeature.js | 12 ++++++++++-- src/vectorFeature.js | 2 +- 5 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 jsdoc.conf.json 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/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 4d63464320..fddb10d4d2 100644 --- a/src/quadFeature.js +++ b/src/quadFeature.js @@ -129,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) { @@ -170,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} */ //////////////////////////////////////////////////////////////////////////// @@ -444,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/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} */ From 8d04f19ae74a5dca84b8864f9167cdfb6d078aa5 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Wed, 1 Jun 2016 13:46:54 -0400 Subject: [PATCH 08/11] Harden the quadFeature testing. I had one of the tests fail twice on my system and this fixes it. --- tests/cases/quadFeature.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/cases/quadFeature.js b/tests/cases/quadFeature.js index 1916f93555..c395064535 100644 --- a/tests/cases/quadFeature.js +++ b/tests/cases/quadFeature.js @@ -460,7 +460,7 @@ describe('geo.quadFeature', function () { expect(buildTime).not.toEqual(quads.buildTime().getMTime()); glCounts = $.extend({}, vgl.mockCounts()); }); - waitForIt('next render', function () { + waitForIt('next render gl A', function () { return vgl.mockCounts().createProgram === (glCounts.createProgram || 0) + 2; }); it('only img quad', function () { @@ -470,7 +470,7 @@ describe('geo.quadFeature', function () { map.draw(); expect(buildTime).not.toEqual(quads.buildTime().getMTime()); }); - waitForIt('next render', function () { + 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; @@ -482,7 +482,7 @@ describe('geo.quadFeature', function () { map.draw(); expect(buildTime).not.toEqual(quads.buildTime().getMTime()); }); - waitForIt('next render', function () { + waitForIt('next render gl C', function () { return vgl.mockCounts().activeTexture === glCounts.activeTexture && vgl.mockCounts().uniform3fv === glCounts.uniform3fv + 2 && vgl.mockCounts().bufferSubData === glCounts.bufferSubData + 1; @@ -497,7 +497,7 @@ describe('geo.quadFeature', function () { quads.data(data); map.draw(); }); - waitForIt('next render', function () { + 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; @@ -529,12 +529,12 @@ describe('geo.quadFeature', function () { buildTime = quads.buildTime().getMTime(); /* Trigger rerendering */ quads.data(testQuads); + counts = $.extend({}, window._canvasLog.counts); map.draw(); expect(buildTime).not.toEqual(quads.buildTime().getMTime()); - counts = $.extend({}, window._canvasLog.counts); }); - waitForIt('next render', function () { - return window._canvasLog.counts.clearRect === (counts.clearRect || 0) + 1; + 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); @@ -543,7 +543,7 @@ describe('geo.quadFeature', function () { map.draw(); expect(buildTime).not.toEqual(quads.buildTime().getMTime()); }); - waitForIt('next render', function () { + waitForIt('next render canvas B', function () { return window._canvasLog.counts.drawImage === counts.drawImage + 2 && window._canvasLog.counts.clearRect === counts.clearRect + 1; }); @@ -558,7 +558,7 @@ describe('geo.quadFeature', function () { quads.data(data); map.draw(); }); - waitForIt('next render', function () { + waitForIt('next render canvas C', function () { return window._canvasLog.counts.drawImage === counts.drawImage + 200 && window._canvasLog.counts.clearRect === counts.clearRect + 1; }); From 6946f8b9076fa8ef6649e811813300b6676f341b Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 3 Jun 2016 09:09:42 -0400 Subject: [PATCH 09/11] Switch back to integer transforms for the html tile layer. Also, use integer transforms for the svg tile layer where appropriate. Only compare the positions of the top-most level, as lower levels are increasingly offset. --- src/d3/d3Renderer.js | 4 ++++ src/d3/quadFeature.js | 10 +++++++++- src/tileLayer.js | 20 ++++++++++---------- tests/cases/osmLayer.js | 9 ++++++--- 4 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index c083d1b86c..c486298a85 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -222,6 +222,10 @@ var d3Renderer = function (arg) { dy = scale * ry + center.y; // set the group transform property + if (!rotation) { + dx = parseInt(dx, 10); + dy = parseInt(dy, 10); + } var transform = 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'; if (rotation) { transform += ' rotate(' + [ diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js index b35f01f378..e5d1b4d09e 100644 --- a/src/d3/quadFeature.js +++ b/src/d3/quadFeature.js @@ -111,7 +111,8 @@ var d3_quadFeature = function (arg) { stroke: false, transform: function (d) { if (d.type === 'img' && d.quad.image && !d.svgTransform) { - var pos = [], area, maxarea = -1, maxv, i; + 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], @@ -149,6 +150,13 @@ var d3_quadFeature = function (arg) { 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] = parseInt(d.svgTransform[4] / imgscale) * imgscale; + imgscale = d.svgTransform[3] / imgh; + d.svgTransform[5] = parseInt(d.svgTransform[5] / imgscale) * imgscale; + } } return ((d.type !== 'img' || !d.quad.image) ? undefined : 'matrix(' + d.svgTransform.join(' ') + ')'); diff --git a/src/tileLayer.js b/src/tileLayer.js index 7bc17504f4..141ba26e61 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -728,8 +728,8 @@ module.exports = (function () { container.append(tile.image); container.css({ position: 'absolute', - left: (bounds.left - parseFloat(div.attr('offsetx') || 0)) + 'px', - top: (bounds.top - parseFloat(div.attr('offsety') || 0)) + 'px' + left: (bounds.left - parseInt(div.attr('offsetx') || 0, 10)) + 'px', + top: (bounds.top - parseInt(div.attr('offsety') || 0, 10)) + 'px' }); // apply fade in animation @@ -964,8 +964,8 @@ module.exports = (function () { this._updateSubLayers = function (level, view) { var canvas = this.canvas(), lastlevel = parseInt(canvas.attr('lastlevel'), 10), - lastx = parseFloat(canvas.attr('lastoffsetx') || 0), - lasty = parseFloat(canvas.attr('lastoffsety') || 0); + 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}; @@ -981,15 +981,15 @@ module.exports = (function () { 'transform', 'scale(' + Math.pow(2, level - layer) + ')' ); - var layerx = parseFloat(x / Math.pow(2, level - layer)), - layery = parseFloat(y / Math.pow(2, level - layer)), - dx = layerx - parseFloat($el.attr('offsetx') || 0), - dy = layery - parseFloat($el.attr('offsety') || 0); + 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}); $el.find('.geo-tile-container').each(function (tileidx, tileel) { $(tileel).css({ - left: (parseFloat($(tileel).css('left')) - dx) + 'px', - top: (parseFloat($(tileel).css('top')) - dy) + 'px' + left: (parseInt($(tileel).css('left'), 10) - dx) + 'px', + top: (parseInt($(tileel).css('top'), 10) - dy) + 'px' }); }); }); diff --git a/tests/cases/osmLayer.js b/tests/cases/osmLayer.js index 64f5c93038..1b31458b34 100644 --- a/tests/cases/osmLayer.js +++ b/tests/cases/osmLayer.js @@ -266,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); From 36b80779662d03a5be3583f640e1d210de474e33 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 3 Jun 2016 09:57:56 -0400 Subject: [PATCH 10/11] Use round not parseInt. parseInt casts to a string and then converts to an integer (at least in Chrome), so values like 8e-14 convert to 8, not 0. --- src/d3/d3Renderer.js | 4 ++-- src/d3/quadFeature.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index c486298a85..6c9b8dffd8 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -223,8 +223,8 @@ var d3Renderer = function (arg) { // set the group transform property if (!rotation) { - dx = parseInt(dx, 10); - dy = parseInt(dy, 10); + dx = Math.round(dx); + dy = Math.round(dy); } var transform = 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'; if (rotation) { diff --git a/src/d3/quadFeature.js b/src/d3/quadFeature.js index e5d1b4d09e..9ec227e1f8 100644 --- a/src/d3/quadFeature.js +++ b/src/d3/quadFeature.js @@ -153,9 +153,9 @@ var d3_quadFeature = function (arg) { 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] = parseInt(d.svgTransform[4] / imgscale) * imgscale; + d.svgTransform[4] = Math.round(d.svgTransform[4] / imgscale) * imgscale; imgscale = d.svgTransform[3] / imgh; - d.svgTransform[5] = parseInt(d.svgTransform[5] / imgscale) * imgscale; + d.svgTransform[5] = Math.round(d.svgTransform[5] / imgscale) * imgscale; } } return ((d.type !== 'img' || !d.quad.image) ? undefined : From 0c00e1d1f59a00e8d8b410d3f795f6a599e2df95 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 3 Jun 2016 14:11:57 -0400 Subject: [PATCH 11/11] Remove features during the animation loop for the d3 renderer. Drawing features was moved inside a requestAnimationFrame. Removing features also needs to be there or the dynamicData example doesn't work. --- examples/dynamicData/main.js | 1 - src/d3/d3Renderer.js | 17 ++++++++++++++--- tests/cases/d3PointFeature.js | 2 ++ 3 files changed, 16 insertions(+), 4 deletions(-) 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/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index 6c9b8dffd8..ad70802fe0 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -42,6 +42,7 @@ var d3Renderer = function (arg) { 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; @@ -431,6 +432,8 @@ var d3Renderer = function (arg) { m_svg = undefined; m_defs.remove(); m_defs = undefined; + m_renderIds = {}; + m_removeIds = {}; s_exit(); }; @@ -512,10 +515,16 @@ var d3Renderer = function (arg) { }; 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 (var id in ids) { + for (id in ids) { if (ids.hasOwnProperty(id)) { m_this._renderFeature(id); } @@ -563,8 +572,10 @@ var d3Renderer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._removeFeature = function (id) { - m_this.select(id).remove(); - m_defs.selectAll('.' + 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]; diff --git a/tests/cases/d3PointFeature.js b/tests/cases/d3PointFeature.js index d4f65dcccf..4fc593e027 100644 --- a/tests/cases/d3PointFeature.js +++ b/tests/cases/d3PointFeature.js @@ -62,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); @@ -71,6 +72,7 @@ describe('d3 point feature', function () { layer.clear().draw(); map.draw(); + stepAnimationFrame(); selection = layer.node().find('circle'); expect(selection.length).toBe(0);