diff --git a/.eslintignore b/.eslintignore index 7b371a6a42..9503f20707 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,3 +5,4 @@ website/** jsdoc/template/publish.js jsdoc/template/static/** jsdoc/template/tmpl/** +docs/_build/** diff --git a/scripts/datastore.js b/scripts/datastore.js index db26118a29..40b22aaef0 100644 --- a/scripts/datastore.js +++ b/scripts/datastore.js @@ -6,7 +6,7 @@ var tar = require('tar'); var registry = { 'AdderallCities2015.csv': 'c3e984482cc6db1193a6dca2a55396a2baad8541a5c8c679f33750b76f766f40a119ec3e63abbabcd095d752c3df8ce21bf24cbe629502121f24ba90b4b0674c', - 'base-images.tgz': '346dcdcf6e88aa9bfde684a311a452431af01b055f007e73839d42aa19ce0846af20d0bc296f7e1acca0af2759896d647dbbdbf07b20428f0e10464a1764c77e', + 'base-images.tgz': '9eed90578ac35e96ba701198e2518cb03bf08447d649375cbf7729524139a9951a5b1b580fc03371532dc128a530794e061a8e05b69aa5b5869efbf98245670e', 'blue.jpg': '867b1f3c568289efc7d0dba97d827a2bc4d83a7465cebcb3b5aec7bac6a38cf70d037d1814402bc97ad1f2f6737cfb5ce97db0a4fb53a716e77fd3ba57a7ab3b', 'cities.csv': '5a665e5feda24f28e5cf4ed0801b67e73bbcf3ea781b2e50d11284214e67b25b68e6a1c48da46e5e4d4d0c54c2ec18f88d292224b4541fb279396cf7b94beac9', 'earthquakes.json': 'f098b6437411384b552419b4a36264c1bb3fed816ccfe9545145175e0b92a0b7ad5ebdcb9dddd0a12a90499143ffa471c02f6e049be5b973db607ff066892500', diff --git a/scripts/make_thumbnails.py b/scripts/make_thumbnails.py index 9d3eab3e66..79493ee453 100755 --- a/scripts/make_thumbnails.py +++ b/scripts/make_thumbnails.py @@ -55,7 +55,7 @@ def process_item(path, opts): - output = (open('/tmp/thumbnail.out', 'ab') + output = (open('/tmp/thumbnail.out', 'a') if opts.get('verbose', 0) >= 1 else open(os.devnull, 'w')) data = json.load(open(path)) if data.get('disabled') and not opts.get('all'): diff --git a/src/contourFeature.js b/src/contourFeature.js index ae5c78b26b..faf015313e 100644 --- a/src/contourFeature.js +++ b/src/contourFeature.js @@ -5,6 +5,7 @@ var meshFeature = require('./meshFeature'); * Contour feature specification. * * @typedef {geo.feature.spec} geo.contourFeature.spec + * @extends geo.feature.spec * @property {object[]} [data=[]] An array of arbitrary objects used to * construct the feature. * @property {geo.contourFeature.styleSpec} [style] An object that contains @@ -71,23 +72,8 @@ var meshFeature = require('./meshFeature'); /** * Computed contour information. * - * @typedef {geo.meshFeature.meshInfo} geo.contourFeature.contourInfo - * @property {number[]} value An array of values that have been normalized to a - * range of [0, steps]. There is one value per vertex. - * @property {number[]} opacity An array of opacities per vertex. - * @property {number} minValue the minimum value used for the contour. If - * `rangeValues` was specified, this is the first entry of that array. - * @property {number} maxValue the maximum value used for the contour. If - * `rangeValues` was specified, this is the last entry of that array. - * @property {number} factor If linear value scaling is used, this is the - * number of color values divided by the difference between the maximum and - * minimum values. It is ignored if non-linear value scaling is used. - * @property {geo.geoColorObject} minColor The color used for values below - * minValue. Includes opacity. - * @property {geo.geoColorObject} maxColor The color used for values above - * maxValue. Includes opacity. - * @property {geo.geoColorObject[]} colorMap The specified `colorRange` and - * `opacityRange` converted into objects that include opacity. + * @typedef {geo.meshFeature.meshColoredInfo} geo.contourFeature.contourInfo + * @extends geo.meshFeature.meshColoredInfo */ /** @@ -110,6 +96,7 @@ var contourFeature = function (arg) { var $ = require('jquery'); var util = require('./util'); + var meshUtil = require('./util/mesh'); arg = arg || {}; meshFeature.call(this, arg); @@ -129,82 +116,7 @@ var contourFeature = function (arg) { * information. */ this._createContours = function () { - var contour = m_this.contour, - valueFunc = m_this.style.get('value'), - usedFunc = m_this.style('used') !== undefined ? - m_this.style.get('used') : - function (d, i) { return util.isNonNullFinite(valueFunc(d, i)); }, - minmax, val, range, i, k; - var result = m_this._createMesh({ - used: usedFunc, - opacity: m_this.style.get('opacity'), - value: valueFunc - }); - if (!result.numVertices || !result.numElements) { - return result; - } - var stepped = contour.get('stepped')(result), - opacityRange = contour.get('opacityRange')(result), - rangeValues = contour.get('rangeValues')(result); - result.stepped = stepped === undefined || stepped ? true : false; - /* Create the min/max colors and the color array */ - result.colorMap = []; - result.minColor = $.extend( - {a: contour.get('minOpacity')(result) || 0}, - util.convertColor(contour.get('minColor')(result))); - result.maxColor = $.extend( - {a: contour.get('maxOpacity')(result) || 0}, - util.convertColor(contour.get('maxColor')(result))); - contour.get('colorRange')(result).forEach(function (clr, idx) { - result.colorMap.push($.extend({ - a: opacityRange && opacityRange[idx] !== undefined ? opacityRange[idx] : 1 - }, util.convertColor(clr))); - }); - /* Get min and max values */ - minmax = util.getMinMaxValues(result.value, contour.get('min')(result), contour.get('max')(result)); - result.minValue = minmax.min; - result.maxValue = minmax.max; - if (!rangeValues || !result.colorMap || - (rangeValues.length !== result.colorMap.length + 1 && ( - stepped || rangeValues.length !== result.colorMap.length))) { - rangeValues = null; - } - if (rangeValues) { /* ensure increasing monotonicity */ - for (k = 1; k < rangeValues.length; k += 1) { - if (rangeValues[k - 1] > rangeValues[k]) { - rangeValues = null; - break; - } - } - } - if (rangeValues) { - result.minValue = rangeValues[0]; - result.maxValue = rangeValues[rangeValues.length - 1]; - } - range = result.maxValue - result.minValue; - if (!range) { - result.colorMap = result.colorMap.slice(0, 1); - range = 1; - rangeValues = null; - } - result.rangeValues = rangeValues; - result.factor = result.colorMap.length / range; - /* Scale values */ - for (i = 0; i < result.numVertices; i += 1) { - val = result.value[i]; - if (rangeValues && val >= result.minValue && val <= result.maxValue) { - for (k = 1; k < rangeValues.length; k += 1) { - if (val <= rangeValues[k]) { - result.value[i] = k - 1 + (val - rangeValues[k - 1]) / - (rangeValues[k] - rangeValues[k - 1]); - break; - } - } - } else { - result.value[i] = (val - result.minValue) * result.factor; - } - } - return result; + return meshUtil.createColoredMesh(m_this, false); }; this.contour = m_this.mesh; @@ -222,8 +134,9 @@ var contourFeature = function (arg) { { opacity: 1.0, value: function (d, i) { - return m_this.position()(d, i).z; + return util.isNonNullFinite(d) ? d : m_this.position()(d, i).z; }, + position: (d) => d || {x: 0, y: 0}, origin: (p) => (p.length >= 3 ? [p[0], p[1], 0] : [0, 0, 0]) }, arg.style === undefined ? {} : arg.style diff --git a/src/gridFeature.js b/src/gridFeature.js new file mode 100644 index 0000000000..ed88aef88c --- /dev/null +++ b/src/gridFeature.js @@ -0,0 +1,175 @@ +var inherit = require('./inherit'); +var meshFeature = require('./meshFeature'); + +/** + * Grid feature specification. + * + * @typedef {geo.feature.spec} geo.gridFeature.spec + * @extends geo.feature.spec + * @property {object[]} [data=[]] An array of arbitrary objects used to + * construct the feature. + * @property {geo.gridFeature.styleSpec} [style] An object that contains + * style values for the feature. + * @property {geo.gridFeature.gridSpec} [grid] The grid specification for the + * feature. + */ + +/** + * Style specification for a grid feature. + * + * @typedef {geo.feature.styleSpec} geo.gridFeature.styleSpec + * @extends geo.feature.styleSpec + * @property {geo.geoPosition|function} [position=data] The position of each + * data element. This defaults to just using `x`, `y`, and `z` properties + * of the data element itself. The position is in the feature's gcs + * coordinates. + * @property {number|function} [value=data.z] The value of each data element. + * This defaults to the `z` property of the data elements. If the value of + * a grid point is `null` or `undefined`, the point and elements that use + * that point won't be included in the results. + * @property {number|function} [opacity=1] The opacity for the whole feature on + * a scale of 0 to 1. + * @property {number[]|function} [origin] Origin in map gcs coordinates used + * for to ensure high precision drawing in this location. When called as a + * function, this is passed the vertex positions as a single continuous array + * in map gcs coordinates. It defaults to the first vertex used in the + * grid. + */ + +/** + * Grid specification. All of these properties can be functions, which get + * passed the {@link geo.meshFeature.meshInfo} object. + * + * @typedef {geo.meshFeature.meshSpec} geo.gridFeature.gridSpec + * @extends geo.meshFeature.meshSpec + * @property {number} [min] Minimum grid value. If unspecified, taken from + * the computed minimum of the `value` style. + * @property {number} [max] Maximum grid value. If unspecified, taken from + * the computed maximum of the `value` style. + * @property {geo.geoColor} [minColor='black'] Color used for any value below + * the minimum. + * @property {number} [minOpacity=0] Opacity used for any value below the + * minimum. + * @property {geo.geoColor} [maxColor='black'] Color used for any value above + * the maximum. + * @property {number} [maxOpacity=0] Opacity used for any value above the + * maximum. + * @property {boolean} [stepped] If falsy but not `undefined`, smooth + * transitions between colors. + * @property {geo.geoColor[]} [colorRange=] An array of colors + * used to show the range of values. The default is a 9-step color table. + * @property {number[]} [opacityRange] An array of opacities used to show the + * range of values. If unspecified, the opacity is 1. If this is a shorter + * list than the `colorRange`, an opacity of 1 is used for the entries near + * the end of the color range. + * @property {number[]} [rangeValues] An array used to map values to the + * `colorRange`. By default, values are spaced linearly. If specified, the + * entries must be increasing weakly monotonic, and there must be one more + * entry then the length of `colorRange`. + */ + +/** + * Computed grid information. + * + * @typedef {geo.meshFeature.meshColoredInfo} geo.gridFeature.gridInfo + * @extends geo.meshFeature.meshColoredInfo + */ + +/** + * Create a new instance of class gridFeature. + * + * @class + * @alias geo.gridFeature + * @extends geo.meshFeature + * + * @borrows geo.gridFeature#mesh as geo.gridFeature#grid + * + * @param {geo.gridFeature.spec} arg + * @returns {geo.gridFeature} + */ +var gridFeature = function (arg) { + 'use strict'; + if (!(this instanceof gridFeature)) { + return new gridFeature(arg); + } + + var $ = require('jquery'); + var util = require('./util'); + var meshUtil = require('./util/mesh'); + + arg = arg || {}; + meshFeature.call(this, arg); + + /** + * @private + */ + var m_this = this, + s_init = this._init; + + /** + * Create a set of vertices and values and opacities inside triangles. + * Create a set of triangles of indices into the vertex array. Create a + * color and opacity map corresponding to the values. + * + * @returns {geo.gridFeature.gridInfo} An object with the grid + * information. + */ + this._createGrids = function () { + return meshUtil.createColoredMesh(m_this, true); + }; + + this.grid = m_this.mesh; + + /** + * Initialize. + * + * @param {geo.gridFeature.spec} arg The grid feature specification. + */ + this._init = function (arg) { + s_init.call(m_this, arg); + + var defaultStyle = $.extend( + {}, + { + opacity: 1.0, + value: function (d, i) { + return util.isNonNullFinite(d) ? d : m_this.position()(d, i).z; + }, + position: (d) => d || {x: 0, y: 0}, + origin: (p) => (p.length >= 3 ? [p[0], p[1], 0] : [0, 0, 0]) + }, + arg.style === undefined ? {} : arg.style + ); + + m_this.style(defaultStyle); + + m_this.grid($.extend({}, { + minColor: 'black', + minOpacity: 0, + maxColor: 'black', + maxOpacity: 0, + /* 9-step based on paraview bwr colortable */ + colorRange: [ + {r: 0.07514311, g: 0.468049805, b: 1}, + {r: 0.468487184, g: 0.588057293, b: 1}, + {r: 0.656658579, g: 0.707001303, b: 1}, + {r: 0.821573924, g: 0.837809045, b: 1}, + {r: 0.943467973, g: 0.943498599, b: 0.943398095}, + {r: 1, g: 0.788626485, b: 0.750707739}, + {r: 1, g: 0.6289553, b: 0.568237474}, + {r: 1, g: 0.472800903, b: 0.404551679}, + {r: 0.916482116, g: 0.236630659, b: 0.209939162} + ] + }, arg.mesh || {}, arg.grid || {})); + + if (arg.mesh || arg.grid) { + m_this.dataTime().modified(); + } + }; + + this._init(arg); + return this; +}; + +inherit(gridFeature, meshFeature); +module.exports = gridFeature; diff --git a/src/index.js b/src/index.js index a9a224e6aa..6bb35d59b5 100644 --- a/src/index.js +++ b/src/index.js @@ -63,6 +63,7 @@ module.exports = $.extend({ imageTile: require('./imageTile'), isolineFeature: require('./isolineFeature'), geojsonReader: require('./geojsonReader'), + gridFeature: require('./gridFeature'), layer: require('./layer'), lineFeature: require('./lineFeature'), map: require('./map'), diff --git a/src/meshFeature.js b/src/meshFeature.js index 93c19dc2f6..74bd8645c1 100644 --- a/src/meshFeature.js +++ b/src/meshFeature.js @@ -5,6 +5,7 @@ var feature = require('./feature'); * Mesh feature specification. * * @typedef {geo.feature.spec} geo.meshFeature.spec + * @extends geo.feature.spec * @property {object[]} [data=[]] An array of arbitrary objects used to * construct the feature. * @property {geo.feature.styleSpec} [style] An object that contains style @@ -80,6 +81,10 @@ var feature = require('./feature'); * For `triangle`, each element stands alone. * @property {number[]} elements A packed array of indices into the `pos` array * defining the elements. Each sequential three values forms a triangle. + * @property {number[]} elementIndex An array that has one value for each + * triplet of values in the `elements` array. The value is the 0-based + * index of the element that can be used to correspond it to element-based + * parameters. * @property {number[]} index An array that references which data index is * associated with each vertex. * @property {number[]} pos A packed array of coordinates in the interface gcs @@ -214,7 +219,7 @@ var meshFeature = function (arg) { /** * Create a set of vertices and elements from a mesh specification. * - * This currently takes a set of `vertexValueFuncs` to detemrine which + * This currently takes a set of `vertexValueFuncs` to determine which * vertices are used (shown) and values at those vertices. It could be * extended with `elementValueFuncs`. * @@ -225,10 +230,18 @@ var meshFeature = function (arg) { * function is passed `(data[idx], idx)` and if it returns a falsy value * for a data point, the vertex associated with that data point is removed * from the resultant mesh. + * @param {object} [elementValueFuncs] A dictionary where the keys are the + * names of properties to include in the results and the values are + * functions that are evaluated at each element with the arguments + * `(data[idx], idx)`. If a key is named `used`, then its function is + * passed `(data[idx], idx)` and if it returns a falsy value for a data + * point, the triangle or square associated with that data point is + * removed from the resultant mesh. * @returns {geo.meshFeature.meshInfo} An object with the mesh information. */ - this._createMesh = function (vertexValueFuncs) { + this._createMesh = function (vertexValueFuncs, elementValueFuncs) { vertexValueFuncs = vertexValueFuncs || {}; + elementValueFuncs = elementValueFuncs || {}; var i, i3, j, k, idx, numPts, usedPts, usePos, item, key, data = m_this.data(), posFunc = m_this.position(), posVal, @@ -241,16 +254,23 @@ var meshFeature = function (arg) { y0 = m_this.mesh.get('y0')(data), dx = m_this.mesh.get('dx')(data), dy = m_this.mesh.get('dy')(data), - calcX, skipColumn, x, origI, /* used for wrapping */ + calcX, calcCol, skipColumn, x, origI, /* used for wrapping */ gridWorig = gridW, /* can be different when wrapping */ result = { shape: 'square', - elements: [] + elements: [], + elementIndex: [] }; /* If we are using a grid, calculate the elements and positions. */ if (!elements) { - if (gridW * gridH > data.length) { - gridH = Math.floor(data.length / gridW); + if (Object.keys(vertexValueFuncs).length) { + if (gridW * gridH > data.length) { + gridH = Math.floor(data.length / gridW); + } + } else if (Object.keys(elementValueFuncs).length) { + if ((gridW - 1) * (gridH - 1) > data.length) { + gridH = Math.floor(data.length / (gridW - 1)) + 1; + } } /* If we are not using the position values (we are using x0, y0, dx, dy), * and wrapLongitude is turned on, and the position spans 180 degrees, @@ -262,6 +282,7 @@ var meshFeature = function (arg) { x0 + dx * (gridW - 1) < -180 || x0 + dx * (gridW - 1) > 180) && dx > -180 && dx < 180 && dx * (gridW - 1) < 360 + 1e-4) { calcX = []; + calcCol = []; for (i = 0; i < gridW; i += 1) { x = x0 + i * dx; while (x < -180) { x += 360; } @@ -270,13 +291,18 @@ var meshFeature = function (arg) { if (x > calcX[calcX.length - 1]) { calcX.push(x - 360); calcX.push(calcX[calcX.length - 2] + 360); + calcCol.push(i); + calcCol.push(i + 1); } else { calcX.push(x + 360); calcX.push(calcX[calcX.length - 2] - 360); + calcCol.push(i); + calcCol.push(i + 1); } skipColumn = i; } calcX.push(x); + calcCol.push(i); } gridW += 2; if (Math.abs(Math.abs(gridWorig * dx) - 360) < 0.01) { @@ -285,6 +311,7 @@ var meshFeature = function (arg) { while (x < -180) { x += 360; } while (x > 180) { x -= 360; } calcX.push(x); + calcCol.push(0); } } /* Calculate the value for point */ @@ -312,6 +339,8 @@ var meshFeature = function (arg) { result.elements.push(idx + gridW + 1); result.elements.push(idx + gridW); result.elements.push(idx + 1); + result.elementIndex.push(j * (gridW - 1) + (calcCol ? calcCol[i] : i)); + result.elementIndex.push(j * (gridW - 1) + (calcCol ? calcCol[i] : i)); } } } @@ -330,15 +359,19 @@ var meshFeature = function (arg) { result.elements.push(elements[i][2]); result.elements.push(elements[i][3]); result.elements.push(elements[i][1]); + result.elementIndex.push(i); + result.elementIndex.push(i); } } else { - for (i = 0; i < elements.length - 3; i += 4) { + for (i = j = 0; i < elements.length - 3; i += 4, j += 1) { result.elements.push(elements[i]); result.elements.push(elements[i + 1]); result.elements.push(elements[i + 3]); result.elements.push(elements[i + 2]); result.elements.push(elements[i + 3]); result.elements.push(elements[i + 1]); + result.elementIndex.push(j); + result.elementIndex.push(j); } } } else { @@ -348,19 +381,50 @@ var meshFeature = function (arg) { result.elements.push(elements[i][0]); result.elements.push(elements[i][1]); result.elements.push(elements[i][2]); + result.elementIndex.push(i); } } else { result.elements = elements.slice(0, elements.length - (elements.length % 3)); + for (i = j = 0; i < elements.length - 2; i += 3, j += 1) { + result.elementIndex.push(j); + } } } numPts = data.length; usePos = true; } + /* If we have an `elementValueFuncs.used` function, remove any unused + * elements. Unused vertices are removed later. */ result.verticesPerElement = result.shape === 'triangle' ? 3 : 6; + var vpe = result.verticesPerElement; + if (elementValueFuncs.used) { + var used = new Array(result.elementIndex[result.elementIndex.length - 1] + 1); + for (i = 0; i < used.length; i += 1) { + used[i] = elementValueFuncs.used(data[i], i); + } + for (i = 0; i < result.elementIndex.length; i += 1) { + if (!used[result.elementIndex[i]]) { + break; + } + } + if (i < result.elementIndex.length) { + for (j = i; i < result.elementIndex.length; i += 1) { + if (used[result.elementIndex[i]]) { + result.elementIndex[j] = result.elementIndex[i]; + for (k = 0; k < 3; k += 1) { + result.elements[j * 3 + k] = result.elements[i * 3 + k]; + } + j += 1; + } + } + result.elements.splice(j * 3); + result.elementIndex.splice(j); + } + } /* If we have a `vertexValueFuncs.used` function, remove any unused * vertices. Then, remove any elements that have a vertex that can't be * used. This could leave vertices that are unused by any element, but - * removing those is expensive so it is not done. */ + * they are removed later. */ if (vertexValueFuncs.used) { for (i = 0; i < numPts; i += 1) { idx = result.index ? result.index[i] : i; @@ -370,8 +434,7 @@ var meshFeature = function (arg) { } if (i !== numPts) { usedPts = i; - var remap = new Array(numPts), - vpe = result.verticesPerElement; + var remap = new Array(numPts); for (j = 0; j < usedPts; j += 1) { remap[j] = j; } @@ -401,13 +464,37 @@ var meshFeature = function (arg) { result.elements[k + j] = remap[result.elements[i + j]]; } if (j === vpe) { + result.elementIndex[Math.floor(k / 3)] = result.elementIndex[Math.floor(i / 3)]; + if (vpe === 6) { + result.elementIndex[Math.floor(k / 3) + 1] = result.elementIndex[Math.floor(i / 3) + 1]; + } k += vpe; } } result.elements.splice(k); + result.elementIndex.splice(Math.floor(k / 3)); numPts = usedPts; } } + /* Remove unused vertices -- this could be disabled to save time. It + * cannot be applied if skipColumn is defined, as in that case some + * vertices are used multiple times but with different coordinates. We + * could also do this when vertexValueFuncs.used is used, but that usually + * has a much smaller reduction in values and isn't worth the time. */ + if (elementValueFuncs.used && skipColumn === undefined) { + var vertexMap = new Array(numPts); + var oldindex = result.index; + result.index = []; + for (i = 0; i < result.elements.length; i += 1) { + k = result.elements[i]; + if (vertexMap[k] === undefined) { + vertexMap[k] = result.index.length; + result.index.push(oldindex ? oldindex[k] : k); + } + result.elements[i] = vertexMap[k]; + } + numPts = result.index.length; + } /* Get point locations and store them in a packed array */ result.pos = new Array(numPts * 3); for (key in vertexValueFuncs) { @@ -439,6 +526,17 @@ var meshFeature = function (arg) { } } } + for (key in elementValueFuncs) { + if (key !== 'used' && elementValueFuncs.hasOwnProperty(key)) { + var func = elementValueFuncs[key]; + result[key] = new Array(result.elementIndex.length); + for (i = 0; i < result.elementIndex.length; i += 1) { + idx = result.elementIndex[i]; + item = data[idx]; + result[key][i] = func(item, idx); + } + } + } result.numVertices = numPts; result.numElements = result.elements.length / result.verticesPerElement; return result; diff --git a/src/tile.js b/src/tile.js index 531d003082..ce212a9ffc 100644 --- a/src/tile.js +++ b/src/tile.js @@ -142,7 +142,7 @@ var tile = function (spec) { }; /** - * Return a unique string representation of the given tile useble as a hash + * Return a unique string representation of the given tile usable as a hash * key. Possibly extend later to include url information to make caches * aware of the tile source. * diff --git a/src/util/index.js b/src/util/index.js index 31f7ffcabe..3860fb9aa7 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -7,6 +7,7 @@ module.exports = Object.assign( /* These modules are added under separate names */ { DistanceGrid: require('./distanceGrid'), - ClusterGroup: require('./clustering') + ClusterGroup: require('./clustering'), + mesh: require('./mesh') } ); diff --git a/src/util/mesh.js b/src/util/mesh.js new file mode 100644 index 0000000000..94f2d1b1a3 --- /dev/null +++ b/src/util/mesh.js @@ -0,0 +1,125 @@ +/** + * Computed colored mesh information. + * + * @typedef {geo.meshFeature.meshInfo} geo.meshFeature.meshColoredInfo + * @extends geo.meshFeature.meshInfo + * @property {number[]} value An array of values that have been normalized to a + * range of [0, steps]. There is one value per vertex or element. + * @property {number[]} opacity An array of opacities per vertex or element. + * @property {number} minValue the minimum value used for the contour. If + * `rangeValues` was specified, this is the first entry of that array. + * @property {number} maxValue the maximum value used for the contour. If + * `rangeValues` was specified, this is the last entry of that array. + * @property {number} factor If linear value scaling is used, this is the + * number of color values divided by the difference between the maximum and + * minimum values. It is ignored if non-linear value scaling is used. + * @property {geo.geoColorObject} minColor The color used for values below + * minValue. Includes opacity. + * @property {geo.geoColorObject} maxColor The color used for values above + * maxValue. Includes opacity. + * @property {geo.geoColorObject[]} colorMap The specified `colorRange` and + * `opacityRange` converted into objects that include opacity. + * @property {boolean} elementValues Truthy if the `value` and `opacity` are + * for elements, falsy for vertices. + */ + +/** + * Create a set of vertices, values at the vertices or elements, and opacities + * at the vertices or elements. Create a set of triangles of indices into the + * vertex array. Create a color and opacity map corresponding to the values. + * + * @param {geo.meshFeature} feature A mesh feature. + * @param {boolean} elementValues Truthy to compute values and opacities at + * elements, falsy for vertices. + * @returns {geo.meshFeature.meshColoredInfo} An object with the colored mesh + * information. + */ +function createColoredMesh(feature, elementValues) { + var $ = require('jquery'); + var util = require('../util'); + + var mesh = feature.mesh, + valueFunc = feature.style.get('value'), + usedFunc = feature.style('used') !== undefined ? + feature.style.get('used') : + function (d, i) { return util.isNonNullFinite(valueFunc(d, i)); }, + minmax, val, range, i, k; + var meshParams = { + used: usedFunc, + opacity: feature.style.get('opacity'), + value: valueFunc + }; + var result = feature._createMesh( + !elementValues ? meshParams : {}, + elementValues ? meshParams : {}); + result.elementValues = !!elementValues; + if (!result.numVertices || !result.numElements) { + return result; + } + var stepped = mesh.get('stepped')(result), + opacityRange = mesh.get('opacityRange')(result), + rangeValues = mesh.get('rangeValues')(result); + result.stepped = stepped === undefined || stepped ? true : false; + /* Create the min/max colors and the color array */ + result.colorMap = []; + result.minColor = $.extend( + {a: mesh.get('minOpacity')(result) || 0}, + util.convertColor(mesh.get('minColor')(result))); + result.maxColor = $.extend( + {a: mesh.get('maxOpacity')(result) || 0}, + util.convertColor(mesh.get('maxColor')(result))); + mesh.get('colorRange')(result).forEach(function (clr, idx) { + result.colorMap.push($.extend({ + a: opacityRange && opacityRange[idx] !== undefined ? opacityRange[idx] : 1 + }, util.convertColor(clr))); + }); + /* Get min and max values */ + minmax = util.getMinMaxValues(result.value, mesh.get('min')(result), mesh.get('max')(result)); + result.minValue = minmax.min; + result.maxValue = minmax.max; + if (!rangeValues || !result.colorMap || + (rangeValues.length !== result.colorMap.length + 1 && ( + stepped || rangeValues.length !== result.colorMap.length))) { + rangeValues = null; + } + if (rangeValues) { /* ensure increasing monotonicity */ + for (k = 1; k < rangeValues.length; k += 1) { + if (rangeValues[k - 1] > rangeValues[k]) { + rangeValues = null; + break; + } + } + } + if (rangeValues) { + result.minValue = rangeValues[0]; + result.maxValue = rangeValues[rangeValues.length - 1]; + } + range = result.maxValue - result.minValue; + if (!range) { + result.colorMap = result.colorMap.slice(0, 1); + range = 1; + rangeValues = null; + } + result.rangeValues = rangeValues; + result.factor = (result.colorMap.length - (stepped ? 0 : 1)) / range; + /* Scale values */ + for (i = 0; i < result.value.length; i += 1) { + val = result.value[i]; + if (rangeValues && val >= result.minValue && val <= result.maxValue) { + for (k = 1; k < rangeValues.length; k += 1) { + if (val <= rangeValues[k]) { + result.value[i] = k - 1 + (val - rangeValues[k - 1]) / + (rangeValues[k] - rangeValues[k - 1]); + break; + } + } + } else { + result.value[i] = (val - result.minValue) * result.factor; + } + } + return result; +} + +module.exports = { + createColoredMesh: createColoredMesh +}; diff --git a/src/webgl/contourFeature.js b/src/webgl/contourFeature.js index 237035b729..392adc3524 100644 --- a/src/webgl/contourFeature.js +++ b/src/webgl/contourFeature.js @@ -8,6 +8,7 @@ var contourFeature = require('../contourFeature'); * @class * @alias geo.webgl.contourFeature * @extends geo.contourFeature + * @extends geo.webgl.meshColored * @param {geo.contourFeature.spec} arg * @returns {geo.webgl.contourFeature} */ @@ -20,237 +21,25 @@ var webgl_contourFeature = function (arg) { arg = arg || {}; contourFeature.call(this, arg); - var vgl = require('vgl'); - var transform = require('../transform'); - var util = require('../util'); - var object = require('./object'); - var fragmentShader = require('./contourFeature.frag'); - var vertexShader = require('./contourFeature.vert'); + var meshColored = require('./meshColored'); + meshColored.call(this, arg); - object.call(this); - - /** - * @private - */ - var m_this = this, - s_exit = this._exit, - m_textureUnit = 7, - m_actor = null, - m_mapper = null, - m_material = null, - m_texture = null, - m_minColorUniform = null, - m_maxColorUniform = null, - m_stepsUniform = null, - m_steppedUniform = null, - m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, - m_origin, - m_modelViewUniform, - s_init = this._init, - s_update = this._update; - - function createVertexShader() { - var shader = new vgl.shader(vgl.GL.VERTEX_SHADER); - shader.setShaderSource(vertexShader); - return shader; - } - - function createFragmentShader() { - var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); - shader.setShaderSource(fragmentShader); - return shader; - } - - /* Create the contours. This calls the base class to generate the geometry, - * color map, and other parameters. The generated geometry is then loaded - * into the various gl uniforms and buffers. - */ - function createGLContours() { - var contour = m_this._createContours(), - numPts = contour.elements.length, - colorTable = [], - i, i3, j, j3, - posBuf, opacityBuf, valueBuf, indicesBuf, - geom = m_mapper.geometryData(); - - m_minColorUniform.set([ - contour.minColor.r, - contour.minColor.g, - contour.minColor.b, - contour.minColor.a]); - m_maxColorUniform.set([ - contour.maxColor.r, - contour.maxColor.g, - contour.maxColor.b, - contour.maxColor.a]); - m_stepsUniform.set(contour.colorMap.length); - m_steppedUniform.set(contour.stepped); - // pad the colortable by repeating the end colors an extra time to ensure - // interpolation never goes off of the colormap. - for (i = -1; i < contour.colorMap.length + 1; i += 1) { - j = Math.max(0, Math.min(contour.colorMap.length - 1, i)); - colorTable.push(contour.colorMap[j].r * 255); - colorTable.push(contour.colorMap[j].g * 255); - colorTable.push(contour.colorMap[j].b * 255); - colorTable.push(contour.colorMap[j].a * 255); - } - m_texture.setColorTable(colorTable); - contour.pos = transform.transformCoordinates( - m_this.gcs(), m_this.layer().map().gcs(), contour.pos, 3); - m_origin = new Float32Array(m_this.style.get('origin')(contour.pos)); - if (m_origin[0] || m_origin[1] || m_origin[2]) { - for (i = 0; i < contour.pos.length; i += 3) { - contour.pos[i] -= m_origin[0]; - contour.pos[i + 1] -= m_origin[1]; - contour.pos[i + 2] -= m_origin[2]; - } - } - m_modelViewUniform.setOrigin(m_origin); - - posBuf = util.getGeomBuffer(geom, 'pos', numPts * 3); - opacityBuf = util.getGeomBuffer(geom, 'opacity', numPts); - valueBuf = util.getGeomBuffer(geom, 'value', numPts); - for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { - j = contour.elements[i]; - j3 = j * 3; - posBuf[i3] = contour.pos[j3]; - posBuf[i3 + 1] = contour.pos[j3 + 1]; - posBuf[i3 + 2] = contour.pos[j3 + 2]; - opacityBuf[i] = contour.opacity[j]; - valueBuf[i] = contour.value[j]; - } - indicesBuf = geom.primitive(0).indices(); - if (!(indicesBuf instanceof Uint16Array) || indicesBuf.length !== numPts) { - indicesBuf = new Uint16Array(numPts); - geom.primitive(0).setIndices(indicesBuf); - } - geom.boundsDirty(true); - m_mapper.modified(); - m_mapper.boundsDirtyTimestamp().modified(); - } - - /** - * Initialize. - * - * @param {geo.contourFeature.spec} arg The contour feature specification. - */ - this._init = function (arg) { - var blend = vgl.blend(), - prog = vgl.shaderProgram(), - mat = vgl.material(), - tex = vgl.lookupTable(), - geom = vgl.geometryData(), - projectionUniform = new vgl.projectionUniform('projectionMatrix'), - samplerUniform = new vgl.uniform(vgl.GL.INT, 'sampler2d'), - vertexShader = createVertexShader(), - fragmentShader = createFragmentShader(), - posAttr = vgl.vertexAttribute('pos'), - valueAttr = vgl.vertexAttribute('value'), - opacityAttr = vgl.vertexAttribute('opacity'), - sourcePositions = vgl.sourceDataP3fv({'name': 'pos'}), - sourceValues = vgl.sourceDataAnyfv( - 1, vgl.vertexAttributeKeysIndexed.One, {'name': 'value'}), - sourceOpacity = vgl.sourceDataAnyfv( - 1, vgl.vertexAttributeKeysIndexed.Two, {'name': 'opacity'}), - primitive = new vgl.triangles(); - m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix'); - - s_init.call(m_this, arg); - m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); - - prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); - prog.addVertexAttribute(valueAttr, vgl.vertexAttributeKeysIndexed.One); - prog.addVertexAttribute(opacityAttr, vgl.vertexAttributeKeysIndexed.Two); - - prog.addUniform(m_modelViewUniform); - prog.addUniform(projectionUniform); - m_minColorUniform = new vgl.uniform(vgl.GL.FLOAT_VEC4, 'minColor'); - prog.addUniform(m_minColorUniform); - m_maxColorUniform = new vgl.uniform(vgl.GL.FLOAT_VEC4, 'maxColor'); - prog.addUniform(m_maxColorUniform); - /* steps is always an integer, but it is more efficient if we use a float - */ - m_stepsUniform = new vgl.uniform(vgl.GL.FLOAT, 'steps'); - prog.addUniform(m_stepsUniform); - m_steppedUniform = new vgl.uniform(vgl.GL.BOOL, 'stepped'); - prog.addUniform(m_steppedUniform); - - prog.addShader(fragmentShader); - prog.addShader(vertexShader); - - prog.addUniform(samplerUniform); - tex.setTextureUnit(m_textureUnit); - samplerUniform.set(m_textureUnit); - - m_material = mat; - m_material.addAttribute(prog); - m_material.addAttribute(blend); - m_texture = tex; - m_material.addAttribute(m_texture); - - m_actor = vgl.actor(); - m_actor.setMaterial(m_material); - m_actor.setMapper(m_mapper); - - geom.addSource(sourcePositions); - geom.addSource(sourceValues); - geom.addSource(sourceOpacity); - geom.addPrimitive(primitive); - /* We don't need vgl to compute bounds, so make the geo.computeBounds just - * set them to 0. */ - geom.computeBounds = function () { - geom.setBounds(0, 0, 0, 0, 0, 0); - }; - m_mapper.setGeometryData(geom); - }; - - /** - * List vgl actors. - * - * @returns {vgl.actor[]} The list of actors. - */ - this.actors = function () { - return m_actor ? [m_actor] : []; - }; + var m_this = this; /** * Build. */ this._build = function () { - if (m_actor) { - m_this.renderer().contextRenderer().removeActor(m_actor); + if (m_this.actors()[0]) { + m_this.renderer().contextRenderer().removeActor(m_this.actors()[0]); } - createGLContours(); + m_this.createGLMeshColored(m_this._createContours()); - m_this.renderer().contextRenderer().addActor(m_actor); + m_this.renderer().contextRenderer().addActor(m_this.actors()[0]); m_this.buildTime().modified(); }; - /** - * Update. - */ - this._update = function () { - s_update.call(m_this); - - if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp() || - m_this.updateTime().timestamp() <= m_this.timestamp()) { - m_this._build(); - } - - m_actor.setVisible(m_this.visible()); - m_actor.material().setBinNumber(m_this.bin()); - m_this.updateTime().modified(); - }; - - /** - * Destroy. - */ - this._exit = function () { - m_this.renderer().contextRenderer().removeActor(m_actor); - s_exit(); - }; - this._init(arg); return this; }; diff --git a/src/webgl/gridFeature.js b/src/webgl/gridFeature.js new file mode 100644 index 0000000000..e4f2dfd6c8 --- /dev/null +++ b/src/webgl/gridFeature.js @@ -0,0 +1,52 @@ +var inherit = require('../inherit'); +var registerFeature = require('../registry').registerFeature; +var gridFeature = require('../gridFeature'); + +/** + * Create a new instance of gridFeature. + * + * @class + * @alias geo.webgl.gridFeature + * @extends geo.gridFeature + * @extends geo.webgl.meshColored + * @param {geo.gridFeature.spec} arg + * @returns {geo.webgl.gridFeature} + */ +var webgl_gridFeature = function (arg) { + 'use strict'; + + if (!(this instanceof webgl_gridFeature)) { + return new webgl_gridFeature(arg); + } + arg = arg || {}; + gridFeature.call(this, arg); + + var meshColored = require('./meshColored'); + meshColored.call(this, arg); + + var m_this = this; + + /** + * Build. + */ + this._build = function () { + if (m_this.actors()[0]) { + m_this.renderer().contextRenderer().removeActor(m_this.actors()[0]); + } + + m_this.createGLMeshColored(m_this._createGrids()); + + m_this.renderer().contextRenderer().addActor(m_this.actors()[0]); + m_this.buildTime().modified(); + }; + + this._init(arg); + return this; +}; + +inherit(webgl_gridFeature, gridFeature); + +// Now register it +registerFeature('webgl', 'grid', webgl_gridFeature); + +module.exports = webgl_gridFeature; diff --git a/src/webgl/index.js b/src/webgl/index.js index 0d38e5b8b8..65d6a99412 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -4,10 +4,12 @@ module.exports = { choroplethFeature: require('./choroplethFeature'), contourFeature: require('./contourFeature'), + gridFeature: require('./gridFeature'), isolineFeature: require('./isolineFeature'), layer: require('./layer'), lineFeature: require('./lineFeature'), markerFeature: require('./markerFeature'), + meshColored: require('./meshColored'), pointFeature: require('./pointFeature'), polygonFeature: require('./polygonFeature'), quadFeature: require('./quadFeature'), diff --git a/src/webgl/contourFeature.frag b/src/webgl/meshColored.frag similarity index 100% rename from src/webgl/contourFeature.frag rename to src/webgl/meshColored.frag diff --git a/src/webgl/meshColored.js b/src/webgl/meshColored.js new file mode 100644 index 0000000000..d3480e8f49 --- /dev/null +++ b/src/webgl/meshColored.js @@ -0,0 +1,236 @@ +/** + * Create a new instance of meshColored. + * + * @class + * @alias geo.webgl.meshColored + * @param {geo.meshColored.spec} arg + * @returns {geo.webgl.meshColored} + */ +var webgl_meshColored = function (arg) { + 'use strict'; + + arg = arg || {}; + + var vgl = require('vgl'); + var transform = require('../transform'); + var util = require('../util'); + var fragmentShader = require('./meshColored.frag'); + var vertexShader = require('./meshColored.vert'); + + var object = require('./object'); + object.call(this); + + /** + * @private + */ + var m_this = this, + s_exit = this._exit, + m_textureUnit = 7, + m_actor = null, + m_mapper = null, + m_material = null, + m_texture = null, + m_minColorUniform = null, + m_maxColorUniform = null, + m_stepsUniform = null, + m_steppedUniform = null, + m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, + m_origin, + m_modelViewUniform, + s_init = this._init, + s_update = this._update; + + function createVertexShader() { + var shader = new vgl.shader(vgl.GL.VERTEX_SHADER); + shader.setShaderSource(vertexShader); + return shader; + } + + function createFragmentShader() { + var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); + shader.setShaderSource(fragmentShader); + return shader; + } + + /** + * Create the colored mesh. The generated geometry is loaded into the + * various gl uniforms and buffers. + * + * @param {geo.meshFeature.meshColoredInfo} mesh The mesh to draw. + */ + this.createGLMeshColored = function (mesh) { + var numPts = mesh.elements.length, + colorTable = [], + i, i3, j, j3, e, + posBuf, opacityBuf, valueBuf, indicesBuf, + geom = m_mapper.geometryData(); + + m_minColorUniform.set([ + mesh.minColor.r, + mesh.minColor.g, + mesh.minColor.b, + mesh.minColor.a]); + m_maxColorUniform.set([ + mesh.maxColor.r, + mesh.maxColor.g, + mesh.maxColor.b, + mesh.maxColor.a]); + m_stepsUniform.set(mesh.colorMap.length); + m_steppedUniform.set(mesh.stepped); + // pad the colortable by repeating the end colors an extra time to ensure + // interpolation never goes off of the colormap. + for (i = -1; i < mesh.colorMap.length + 1; i += 1) { + j = Math.max(0, Math.min(mesh.colorMap.length - 1, i)); + colorTable.push(mesh.colorMap[j].r * 255); + colorTable.push(mesh.colorMap[j].g * 255); + colorTable.push(mesh.colorMap[j].b * 255); + colorTable.push(mesh.colorMap[j].a * 255); + } + m_texture.setColorTable(colorTable); + mesh.pos = transform.transformCoordinates( + m_this.gcs(), m_this.layer().map().gcs(), mesh.pos, 3); + m_origin = new Float32Array(m_this.style.get('origin')(mesh.pos)); + if (m_origin[0] || m_origin[1] || m_origin[2]) { + for (i = 0; i < mesh.pos.length; i += 3) { + mesh.pos[i] -= m_origin[0]; + mesh.pos[i + 1] -= m_origin[1]; + mesh.pos[i + 2] -= m_origin[2]; + } + } + m_modelViewUniform.setOrigin(m_origin); + + posBuf = util.getGeomBuffer(geom, 'pos', numPts * 3); + opacityBuf = util.getGeomBuffer(geom, 'opacity', numPts); + valueBuf = util.getGeomBuffer(geom, 'value', numPts); + for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { + j = mesh.elements[i]; + j3 = j * 3; + posBuf[i3] = mesh.pos[j3]; + posBuf[i3 + 1] = mesh.pos[j3 + 1]; + posBuf[i3 + 2] = mesh.pos[j3 + 2]; + e = mesh.elementValues ? Math.floor(i / 3) : j; + opacityBuf[i] = mesh.opacity[e]; + valueBuf[i] = mesh.value[e]; + } + indicesBuf = geom.primitive(0).indices(); + if (!(indicesBuf instanceof Uint16Array) || indicesBuf.length !== numPts) { + indicesBuf = new Uint16Array(numPts); + geom.primitive(0).setIndices(indicesBuf); + } + geom.boundsDirty(true); + m_mapper.modified(); + m_mapper.boundsDirtyTimestamp().modified(); + }; + + /** + * Initialize. + * + * @param {geo.meshColored.spec} arg The contour feature specification. + */ + this._init = function (arg) { + var blend = vgl.blend(), + prog = vgl.shaderProgram(), + mat = vgl.material(), + tex = vgl.lookupTable(), + geom = vgl.geometryData(), + projectionUniform = new vgl.projectionUniform('projectionMatrix'), + samplerUniform = new vgl.uniform(vgl.GL.INT, 'sampler2d'), + vertexShader = createVertexShader(), + fragmentShader = createFragmentShader(), + posAttr = vgl.vertexAttribute('pos'), + valueAttr = vgl.vertexAttribute('value'), + opacityAttr = vgl.vertexAttribute('opacity'), + sourcePositions = vgl.sourceDataP3fv({'name': 'pos'}), + sourceValues = vgl.sourceDataAnyfv( + 1, vgl.vertexAttributeKeysIndexed.One, {'name': 'value'}), + sourceOpacity = vgl.sourceDataAnyfv( + 1, vgl.vertexAttributeKeysIndexed.Two, {'name': 'opacity'}), + primitive = new vgl.triangles(); + m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix'); + + s_init.call(m_this, arg); + m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); + + prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); + prog.addVertexAttribute(valueAttr, vgl.vertexAttributeKeysIndexed.One); + prog.addVertexAttribute(opacityAttr, vgl.vertexAttributeKeysIndexed.Two); + + prog.addUniform(m_modelViewUniform); + prog.addUniform(projectionUniform); + m_minColorUniform = new vgl.uniform(vgl.GL.FLOAT_VEC4, 'minColor'); + prog.addUniform(m_minColorUniform); + m_maxColorUniform = new vgl.uniform(vgl.GL.FLOAT_VEC4, 'maxColor'); + prog.addUniform(m_maxColorUniform); + /* steps is always an integer, but it is more efficient if we use a float + */ + m_stepsUniform = new vgl.uniform(vgl.GL.FLOAT, 'steps'); + prog.addUniform(m_stepsUniform); + m_steppedUniform = new vgl.uniform(vgl.GL.BOOL, 'stepped'); + prog.addUniform(m_steppedUniform); + + prog.addShader(fragmentShader); + prog.addShader(vertexShader); + + prog.addUniform(samplerUniform); + tex.setTextureUnit(m_textureUnit); + samplerUniform.set(m_textureUnit); + + m_material = mat; + m_material.addAttribute(prog); + m_material.addAttribute(blend); + m_texture = tex; + m_material.addAttribute(m_texture); + + m_actor = vgl.actor(); + m_actor.setMaterial(m_material); + m_actor.setMapper(m_mapper); + + geom.addSource(sourcePositions); + geom.addSource(sourceValues); + geom.addSource(sourceOpacity); + geom.addPrimitive(primitive); + /* We don't need vgl to compute bounds, so make the geo.computeBounds just + * set them to 0. */ + geom.computeBounds = function () { + geom.setBounds(0, 0, 0, 0, 0, 0); + }; + m_mapper.setGeometryData(geom); + }; + + /** + * List vgl actors. + * + * @returns {vgl.actor[]} The list of actors. + */ + this.actors = function () { + return m_actor ? [m_actor] : []; + }; + + /** + * Update. + */ + this._update = function () { + s_update.call(m_this); + + if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp() || + m_this.updateTime().timestamp() <= m_this.timestamp()) { + m_this._build(); + } + + m_actor.setVisible(m_this.visible()); + m_actor.material().setBinNumber(m_this.bin()); + m_this.updateTime().modified(); + }; + + /** + * Destroy. + */ + this._exit = function () { + m_this.renderer().contextRenderer().removeActor(m_actor); + s_exit(); + }; + + return this; +}; + +module.exports = webgl_meshColored; diff --git a/src/webgl/contourFeature.vert b/src/webgl/meshColored.vert similarity index 100% rename from src/webgl/contourFeature.vert rename to src/webgl/meshColored.vert diff --git a/tests/cases/contourFeature.js b/tests/cases/contourFeature.js index 351f651217..96083912c4 100644 --- a/tests/cases/contourFeature.js +++ b/tests/cases/contourFeature.js @@ -208,7 +208,8 @@ describe('Contour Feature', function () { dx: 6, dy: 6, rangeValues: [1, -1, 1, -1, 1, -1, 1, -1, 1, -1], - values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] + values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], + stepped: true }; var contour = layer.createFeature('contour', { contour: contour1, style: {value: 0}}).data(contour1.values); @@ -304,15 +305,15 @@ describe('Contour Feature', function () { y0: -30, dx: 6, dy: 6, - colorRange: ['red', 'black', 'blue'], - rangeValues: [0, 6, 15], + colorRange: ['red', 'black', 'green', 'blue'], + rangeValues: [0, 6, 12, 15], values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], stepped: false }; var contour = layer.createFeature('contour', { contour: contour1, style: {value: 0}}).data(contour1.values); var result = contour._createContours(); - expect(result.rangeValues.length).toBe(3); + expect(result.rangeValues.length).toBe(4); expect(result.factor).toBe(0.2); }); }); diff --git a/tests/cases/gridFeature.js b/tests/cases/gridFeature.js new file mode 100644 index 0000000000..78ff924f4a --- /dev/null +++ b/tests/cases/gridFeature.js @@ -0,0 +1,131 @@ +describe('Grid Feature', function () { + 'use strict'; + + var map, layer; + var geo = require('../test-utils').geo; + var createMap = require('../test-utils').createMap; + var destroyMap = require('../test-utils').destroyMap; + var mockWebglRenderer = geo.util.mockWebglRenderer; + var restoreWebglRenderer = geo.util.restoreWebglRenderer; + + beforeEach(function () { + mockWebglRenderer(); + map = createMap({ + 'center': [0, 0], + 'zoom': 3 + }, {width: '500px', height: '300px'}); + layer = map.createLayer('feature', {'renderer': 'webgl'}); + }); + + afterEach(function () { + destroyMap(); + restoreWebglRenderer(); + }); + + describe('create', function () { + it('direct create', function () { + var grid = geo.gridFeature({layer: layer}); + expect(grid instanceof geo.gridFeature).toBe(true); + expect(grid instanceof geo.meshFeature).toBe(true); + var mesh = geo.meshFeature({layer: layer}); + expect(mesh instanceof geo.meshFeature).toBe(true); + }); + }); + + describe('Check public class methods', function () { + it('grid/mesh get and set', function () { + var grid = geo.gridFeature({layer: layer}); + expect(grid.grid().minColor).toEqual('black'); + expect(grid.mesh().minColor).toEqual('black'); + expect(grid.grid('minColor')).toEqual('black'); + expect(grid.grid.get('minColor')()).toEqual('black'); + expect(grid.grid.get().minColor()).toEqual('black'); + expect(grid.grid('minColor', 'white')).toBe(grid); + expect(grid.grid('minColor')).toEqual('white'); + expect(grid.grid({minColor: 'red'})).toBe(grid); + expect(grid.grid('minColor')).toEqual('red'); + }); + it('grid gridWidth and gridHeight', function () { + var grid = geo.gridFeature({layer: layer}); + grid.data(new Array(400)); + expect(grid.grid.get('gridWidth')()).toBe(20); + expect(grid.grid.get('gridHeight')()).toBe(20); + delete grid.grid().gridHeight; + grid.grid({gridWidth: 40}); + expect(grid.grid.get('gridWidth')()).toBe(40); + expect(grid.grid.get('gridHeight')()).toBe(10); + delete grid.grid().gridWidth; + grid.grid({gridHeight: 5}); + expect(grid.grid.get('gridWidth')()).toBe(80); + expect(grid.grid.get('gridHeight')()).toBe(5); + grid.data(new Array(200)); + expect(grid.grid.get('gridWidth')()).toBe(40); + expect(grid.grid.get('gridHeight')()).toBe(5); + }); + it('actors', function () { + var grid = geo.webgl.gridFeature({layer: layer}); + expect(grid.actors().length).toBe(1); + }); + }); + + describe('Create grids', function () { + it('simple', function () { + var grid1 = { + gridWidth: 8, + gridHeight: 3, + x0: -30, + y0: -30, + dx: 6, + dy: 6, + values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] + }; + var grid = layer.createFeature('grid').data( + grid1.values).grid(grid1).style({ + value: function (d) { return d; }}); + var result = grid._createGrids(); + expect(result.minValue).toBe(0); + expect(result.maxValue).toBe(13); + expect(result.elements.length).toBe(84); /* 14 sq. * 2 tri. * 3 pts. */ + expect(result.pos.length).toBe(72); /* 24 distinct points * 3 coor. */ + }); + + it('empty values', function () { + var grid1 = { + gridWidth: 8, + gridHeight: 3, + x0: -30, + y0: -30, + dx: 6, + dy: 6, + values: [0, 1, null, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null] + }; + var grid = layer.createFeature('grid', {grid: grid1}).data(grid1.values); + var result = grid._createGrids(); + expect(result.minValue).toBe(0); + expect(result.maxValue).toBe(12); + expect(result.elements.length).toBe(72); /* 12 sq. * 2 tri. * 3 pts. */ + expect(result.pos.length).toBe(69); /* 23 distinct points * 3 coor. */ + }); + + it('short data', function () { + var grid1 = { + gridWidth: 6, + gridHeight: 3, + x0: 30, + y0: -30, + dx: -60, + dy: 60, + values: [0, 1, 2, 3, 4, 5, 6, 7, 8] + }; + var grid = layer.createFeature('grid').data( + grid1.values).grid(grid1).style({ + value: function (d) { return d; }}); + var result = grid._createGrids(); + /* This will appear to have only two rows */ + expect(result.elements.length).toBe(42); /* 5 + 2 sq. * 2 tri. * 3 pts. */ + expect(result.pos.length).toBe(54); /* 12 + 6 distinct points * 3 coor. */ + expect(result.pos[48]).toBe(90); + expect(result.pos[51]).toBe(30); + }); + }); +}); diff --git a/tests/gl-cases/webglGrid.js b/tests/gl-cases/webglGrid.js new file mode 100644 index 0000000000..03d8de4659 --- /dev/null +++ b/tests/gl-cases/webglGrid.js @@ -0,0 +1,126 @@ +var $ = require('jquery'); + +describe('webglGrid', function () { + var imageTest = require('../image-test'); + var common = require('../test-common'); + + var myMap; + + beforeEach(function () { + imageTest.prepareImageTest(); + }); + + afterEach(function () { + myMap.exit(); + }); + + /** Test grids + * + * @param {string} imageName: name used for the image test. + * @param {object} opts: display options, including: + * url: the url to load. Defaults to oahu.json. + * range: one of false, true, 'nonlinear', or 'iso'. Default false. + * stepped: boolean, default true. + * @param {function} done: function to call when the test is complete. + */ + function testGrid(imageName, opts, done) { + var mapOptions = {center: {x: -157.965, y: 21.482}, zoom: 10}; + myMap = common.createOsmMap(mapOptions, {}, true); + + var layer = myMap.createLayer('feature', {renderer: 'webgl'}); + var url = '/data/' + (opts.url || 'oahu-dense.json'); + $.getJSON(url, {format: 'json'}).done(function (data) { + + var grid = layer.createFeature('grid') + .data(data.position || data.values) + .style({ + opacity: 0.75, + value: function (d) { return d > -9999 ? d : null; } + }) + .grid({ + gridWidth: data.gridWidth + 1, + gridHeight: data.gridHeight + 1, + min: 0, + x0: data.x0, + y0: data.y0, + dx: data.dx, + dy: data.dy + }); + if (opts.range) { + grid + .style({ + opacity: 1 + }) + .grid({ + minColor: 'blue', + minOpacity: 0.5, + maxColor: 'red', + maxOpacity: 0.5 + }); + switch (opts.range) { + case 'nonlinear': + grid + .grid({ + rangeValues: [0, 25, 50, 75, 100, 125, 250, 500, 750, 2000] + }); + break; + case 'iso': + grid + .grid({ + rangeValues: [100, 100, 200, 200, 300, 300, 400, 400, 500, 500], + opacityRange: [1, 0, 1, 0, 1, 0, 1, 0, 1], + minOpacity: 0, + maxOpacity: 0 + }); + break; + default: + grid + .grid({ + min: 100, + max: 500, + colorRange: [ + '#FF00FF', '#CC33CC', '#996699', + '#669966', '#33CC33', '#00FF00' + ], + opacityRange: [0.5, 0.6, 0.7, 0.8, 0.9, 1.0] + }); + break; + } + } + if (opts.stepped === false) { + grid + .grid({ + stepped: false + }); + } + myMap.draw(); + + imageTest.imageTest(imageName, null, 0.0015, done, myMap.onIdle, 5000, 2); + }); + } + + it('grids with options', function (done) { + // geo from x0, specified min-max, set color range, smooth + testGrid('webglGridOptions', { + url: 'oahu-dense.json', + range: true, + stepped: false + }, done); + }, 30000); + + it('grids with nonlinear range', function (done) { + // geo from x0, non-linear range + testGrid('webglGridRange', { + url: 'oahu-dense.json', + range: 'nonlinear' + }, done); + }, 30000); + + it('grids with iso range', function (done) { + // geo from x0, iso-like range + testGrid('webglGridIso', { + url: 'oahu-dense.json', + range: 'iso' + }, done); + }, 30000); +}); diff --git a/tutorials/grid/index.pug b/tutorials/grid/index.pug new file mode 100644 index 0000000000..3d0fb0540d --- /dev/null +++ b/tutorials/grid/index.pug @@ -0,0 +1,62 @@ +extends ../common/index.pug + +block mainTutorial + :markdown-it + # Tutorial - Grid Data + First, let's create our map and add a base map and a feature layer. + + +codeblock('javascript', 1). + var map = geo.map({ + node: '#map', + center: { x: -77, y: 43 }, + zoom: 10 + }); + map.createLayer('osm'); + var layer = map.createLayer('feature', {features: ['grid']}); + + :markdown-it + Create a grid feature with a small amount of data. The grid width and height are the number of vertices in the grid. The grid data is for the elements, so it will have one less row and one less column than the vertex information. + +codeblock('javascript', 2, 1, true). + var grid = layer.createFeature('grid', { + grid: { + gridWidth: 8, + gridHeight: 7, + x0: -77.35, + y0: 42.76, + dx: 0.1, + dy: 0.08, + stepped: false + } + }).data([ + 0, 1, 2, 3, 2, 1, 0, + 1, 2, 3, 4, 3, 2, 1, + 2, 3, 4, 5, 4, 3, 2, + 3, 4, 5, 6, 5, 4, 3, + 2, 3, 4, 5, 6, 5, 4, + 1, 2, 3, 4, 5, 6, 5 + ]).draw(); + + :markdown-it + Data can have missing values, which will result in gaps in the rendered grid. + +codeblock('javascript', 3, 2). + grid + .data([ + 0, 1, 2, 3, 2, 1, 0, + 1, 2, 3, 4, 3, 2, 1, + 2, 3, 4, 5, 4, undefined, 2, + 3, 4, 5, 6, 5, 4, 3, + 2, 3, 4, 5, 6, 5, 4, + null, 2, 3, 4, 5, 6, 5 + ]) + .draw(); + + :markdown-it + Similar to the contour feature, we can change the color range and whether colors are in discrete steps. + + +codeblock('javascript', 4, 3). + grid + .grid({ + colorRange: ['rgb(224,130,20)', 'rgb(254,224,182)', 'rgb(178,171,210)', 'rgb(84,39,136)'], + stepped: true, + }) + .draw(); diff --git a/tutorials/grid/thumb.jpg b/tutorials/grid/thumb.jpg new file mode 100644 index 0000000000..68dfa63410 Binary files /dev/null and b/tutorials/grid/thumb.jpg differ diff --git a/tutorials/grid/tutorial.json b/tutorials/grid/tutorial.json new file mode 100644 index 0000000000..043955721e --- /dev/null +++ b/tutorials/grid/tutorial.json @@ -0,0 +1,10 @@ +{ + "title": "Grid Data", + "hideNavbar": true, + "level": 1, + "tutorialCss": [], + "tutorialJs": [], + "about": { + "text": "Plot grid data where each grid element is a solid color." + } +}