Add updateStyleFromArray optimizations to marker features.
MODIFY
CHANGELOG.md
CHANGELOG.md
### Improvements
- The pointInPolygon2D function is faster (#1052)
+- On markers, updateStyleFromArray optimizes certain updates and exposes a faster way to change symbol and symbol value (#1049)
## Version 0.19.8

MODIFY
src/webgl/markerFeature.js
src/webgl/markerFeature.js
+var $ = require('jquery');
var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var markerFeature = require('../markerFeature');
@@ -44,7 +45,8 @@ var webgl_markerFeature = function (arg) {
m_modelViewUniform,
m_origin,
s_init = this._init,
- s_update = this._update;
+ s_update = this._update,
+ s_updateStyleFromArray = this.updateStyleFromArray;
pointUtil(m_this, arg);
@@ -251,6 +253,111 @@ var webgl_markerFeature = function (arg) {
return [m_actor];
};
+ /**
+ * Set style(s) from array(s). For each style, the array should have one
+ * value per data item. The values are not converted or validated. Color
+ * values are {@link geo.geoColorObject} objects. If invalid values are
+ * given the behavior is undefined.
+ * For some feature styles, if the first entry of an array is itself an
+ * array, then each entry of the array is expected to be an array, and values
+ * are used from these subarrays. This allows a style to apply, for
+ * instance, per vertex of a data item rather than per data item.
+ *
+ * For markers, there are two special keys: `symbolComputed` and
+ * `symbolValueComputed`. If these keys are used, they are assumed to be
+ * processed values that can be set in the webgl buffer directly. The style
+ * is NOT updated with these values, as they may not be directly applicable.
+ * Use `symbol` and `symbolValue` for a more expected behavior.
+ *
+ * @param {string|object} keyOrObject Either the name of a single style or
+ * an object where the keys are the names of styles and the values are
+ * each arrays.
+ * @param {array} styleArray If keyOrObject is a string, an array of values
+ * for the style. If keyOrObject is an object, this parameter is ignored.
+ * @param {boolean} [refresh=false] `true` to redraw the feature when it has
+ * been updated. If an object with styles is passed, the redraw is only
+ * done once.
+ * @returns {this}
+ */
+ this.updateStyleFromArray = function (keyOrObject, styleArray, refresh) {
+ var bufferedKeys = {
+ fillColor: 3,
+ fillOpacity: 1,
+ radius: 1,
+ strokeColor: 3,
+ strokeOpacity: 1,
+ strokeWidth: 1,
+ symbolComputed: 1,
+ symbolValueComputed: 1
+ };
+ var needsRefresh, needsRender;
+ if (typeof keyOrObject === 'string') {
+ var obj = {};
+ obj[keyOrObject] = styleArray;
+ keyOrObject = obj;
+ }
+ $.each(keyOrObject, function (key, styleArray) {
+ if (m_this.visible() && m_actor && bufferedKeys[key] && !needsRefresh && !m_this.clustering()) {
+ var vpf, mapper, buffer, numPts, value, i, j, v, bpv, sbkey;
+ bpv = bufferedKeys[key];
+ numPts = m_this.data().length;
+ mapper = m_actor.mapper();
+ sbkey = key === 'symbolComputed' ? 'symbol' : key === 'symbolValueComputed' ? 'symbolValue' : key;
+ buffer = mapper.getSourceBuffer(sbkey);
+ vpf = m_this.verticesPerFeature();
+ if (!buffer || !numPts || numPts * vpf * bpv !== buffer.length) {
+ needsRefresh = true;
+ } else {
+ switch (bufferedKeys[key]) {
+ case 1:
+ for (i = 0, v = 0; i < numPts; i += 1) {
+ value = styleArray[i];
+ for (j = 0; j < vpf; j += 1, v += 1) {
+ buffer[v] = value;
+ }
+ }
+ break;
+ case 3:
+ for (i = 0, v = 0; i < numPts; i += 1) {
+ value = styleArray[i];
+ for (j = 0; j < vpf; j += 1, v += 3) {
+ buffer[v] = value.r;
+ buffer[v + 1] = value.g;
+ buffer[v + 2] = value.b;
+ }
+ }
+ break;
+ }
+ mapper.updateSourceBuffer(sbkey);
+ /* This could probably be even faster than calling _render after
+ * updating the buffer, if the context's buffer was bound and
+ * updated. This would requiring knowing the webgl context and
+ * probably the source to buffer mapping. */
+ needsRender = true;
+ }
+ } else {
+ needsRefresh = true;
+ }
+ if (key === sbkey) {
+ let mod = m_this.modified;
+ if (!needsRefresh) {
+ // don't allow modified to be adjusted if we don't need to refresh
+ m_this.modified = () => {};
+ }
+ s_updateStyleFromArray(key, styleArray, false);
+ m_this.modified = mod;
+ }
+ });
+ if (refresh) {
+ if (m_this.visible() && needsRefresh) {
+ m_this.draw();
+ } else if (needsRender) {
+ m_this.renderer()._render();
+ }
+ }
+ return m_this;
+ };
+
/**
* Handle zoom events for automatic primive shape adjustment.
*

MODIFY
src/webgl/pointFeature.js
src/webgl/pointFeature.js
@@ -321,7 +321,13 @@ var webgl_pointFeature = function (arg) {
} else {
needsRefresh = true;
}
+ let mod = m_this.modified;
+ if (!needsRefresh) {
+ // don't allow modified to be adjusted if we don't need to refresh
+ m_this.modified = () => {};
+ }
s_updateStyleFromArray(key, styleArray, false);
+ m_this.modified = mod;
});
if (refresh) {
if (m_this.visible() && needsRefresh) {

MODIFY
tests/cases/markerFeature.js
tests/cases/markerFeature.js
@@ -162,7 +162,15 @@ describe('geo.markerFeature', function () {
/* This is a basic integration test of geo.webgl.markerFeature. */
describe('geo.webgl.markerFeature', function () {
- var map, layer, marker, marker2, glCounts;
+ var map, layer, marker, marker2, glCounts, i, count = 0;
+ var array1 = new Array(testMarkers.length),
+ array2 = new Array(testMarkers.length),
+ array3 = new Array(testMarkers.length);
+ for (i = 0; i < testMarkers.length; i += 1) {
+ array1[i] = i + 1;
+ array2[i] = i % 2 ? true : false;
+ array3[i] = {r: 0.5, g: 0.5, b: 0.5};
+ }
it('basic usage', function () {
mockWebglRenderer();
map = createMap();
@@ -219,6 +227,45 @@ describe('geo.markerFeature', function () {
expect(marker2.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.triangle);
expect(marker2.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.triangle);
});
+ it('updateStyleFromArray single', function () {
+ marker.draw = function () {
+ count += 1;
+ };
+ glCounts = $.extend({}, vgl.mockCounts());
+ var t = marker.timestamp();
+ marker.updateStyleFromArray('radius', array1, true);
+ expect(count).toBe(0);
+ expect(marker.timestamp()).toBe(t);
+ });
+ waitForIt('next render gl C', function () {
+ return vgl.mockCounts().drawArrays >= (glCounts.drawArrays || 0) + 1;
+ });
+ it('updateStyleFromArray multiple', function () {
+ glCounts = $.extend({}, vgl.mockCounts());
+ marker.updateStyleFromArray({symbolComputed: array2, strokeColor: array3}, null, true);
+ expect(count).toBe(0);
+ });
+ waitForIt('next render gl D', function () {
+ return vgl.mockCounts().drawArrays >= (glCounts.drawArrays || 0) + 1;
+ });
+ it('updateStyleFromArray non-optimized', function () {
+ marker.updateStyleFromArray('unknown', array1, true);
+ expect(count).toBe(1);
+ // a different length array will trigger a slow draw, too.
+ marker.data(testMarkers.slice(0, 2));
+ var t = marker.timestamp();
+ marker.updateStyleFromArray('radius', array1, true);
+ expect(count).toBe(2);
+ expect(marker.timestamp()).toBeGreaterThan(t);
+ });
+ it('modify and refresh', function () {
+ glCounts = $.extend({}, vgl.mockCounts());
+ marker2.modified().draw();
+ expect(count).toBe(2);
+ });
+ waitForIt('next render gl E', function () {
+ return vgl.mockCounts().bufferSubData >= (glCounts.bufferSubData || 0) + 8;
+ });
it('_exit', function () {
expect(marker.actors().length).toBe(1);
layer.deleteFeature(marker);

MODIFY
tests/cases/pointFeature.js
tests/cases/pointFeature.js
@@ -371,8 +371,10 @@ describe('geo.pointFeature', function () {
count += 1;
};
glCounts = $.extend({}, vgl.mockCounts());
+ var t = point.timestamp();
point.updateStyleFromArray('radius', array1, true);
expect(count).toBe(0);
+ expect(point.timestamp()).toBe(t);
});
waitForIt('next render gl C', function () {
return vgl.mockCounts().drawArrays >= (glCounts.drawArrays || 0) + 1;
@@ -390,8 +392,10 @@ describe('geo.pointFeature', function () {
expect(count).toBe(1);
// a different length array will trigger a slow draw, too.
point.data(testPoints.slice(0, 18));
+ var t = point.timestamp();
point.updateStyleFromArray('radius', array1, true);
expect(count).toBe(2);
+ expect(point.timestamp()).toBeGreaterThan(t);
});
it('modify and refresh', function () {
glCounts = $.extend({}, vgl.mockCounts());

Add updateStyleFromArray optimizations to marker features.