var $ = require('jquery');
var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var pointFeature = require('../pointFeature');
var webglRenderer = require('./webglRenderer');
/**
* Create a new instance of webgl.pointFeature.
*
* @class
* @alias geo.webgl.pointFeature
* @extends geo.pointFeature
* @param {geo.pointFeature.spec} arg
* @returns {geo.webgl.pointFeature}
*/
var webgl_pointFeature = function (arg) {
'use strict';
if (!(this instanceof webgl_pointFeature)) {
return new webgl_pointFeature(arg);
}
arg = arg || {};
pointFeature.call(this, arg);
var vgl = require('../vgl');
var transform = require('../transform');
var util = require('../util');
var object = require('./object');
var pointUtil = require('./pointUtil.js');
var fragmentShaderPoly = require('./pointFeaturePoly.frag');
var fragmentShaderSprite = require('./pointFeatureSprite.frag');
var vertexShaderPoly = require('./pointFeaturePoly.vert');
var vertexShaderSprite = require('./pointFeatureSprite.vert');
object.call(this);
/**
* @private
*/
var m_this = this,
s_exit = this._exit,
m_actor = null,
m_mapper = null,
m_pixelWidthUniform = null,
m_aspectUniform = null,
m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw,
m_modelViewUniform,
m_origin,
s_init = this._init,
s_update = this._update,
s_updateStyleFromArray = this.updateStyleFromArray;
pointUtil(m_this, arg);
/**
* Create the vertex shader for points.
*
* @returns {vgl.shader}
*/
function createVertexShader() {
var shader = new vgl.shader(vgl.GL.VERTEX_SHADER);
shader.setShaderSource(
m_this._primitiveShape === pointFeature.primitiveShapes.sprite ? vertexShaderSprite : vertexShaderPoly);
return shader;
}
/**
* Create the fragment shader for points.
*
* @returns {vgl.shader}
*/
function createFragmentShader() {
var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER);
shader.setShaderSource(
m_this._primitiveShape === pointFeature.primitiveShapes.sprite ? fragmentShaderSprite : fragmentShaderPoly);
return shader;
}
/**
* Create and style the data needed to render the points.
*
* @param {boolean} [onlyStyle] if true, use the existing geometry and just
* recalculate the style.
*/
function createGLPoints(onlyStyle) {
// unit and associated data is not used when drawing sprite
var i, j, numPts = m_this.data().length,
unit = m_this._pointPolygon(0, 0, 1, 1),
position = new Array(numPts * 3), posBuf, posVal, posFunc,
unitBuf, indices,
radius, radiusVal, radFunc,
stroke, strokeVal, strokeFunc,
strokeWidth, strokeWidthVal, strokeWidthFunc,
strokeOpacity, strokeOpacityVal, strokeOpacityFunc,
strokeColor, strokeColorVal, strokeColorFunc,
fill, fillVal, fillFunc,
fillOpacity, fillOpacityVal, fillOpacityFunc,
fillColor, fillColorVal, fillColorFunc,
vpf = m_this.verticesPerFeature(),
data = m_this.data(),
item, ivpf, ivpf3, iunit, i3, maxr = 0,
geom = m_mapper.geometryData();
posFunc = m_this.position();
radFunc = m_this.style.get('radius');
strokeFunc = m_this.style.get('stroke');
strokeWidthFunc = m_this.style.get('strokeWidth');
strokeOpacityFunc = m_this.style.get('strokeOpacity');
strokeColorFunc = m_this.style.get('strokeColor');
fillFunc = m_this.style.get('fill');
fillOpacityFunc = m_this.style.get('fillOpacity');
fillColorFunc = m_this.style.get('fillColor');
if (!onlyStyle) {
/* It is more efficient to do a transform on a single array rather than on
* an array of arrays or an array of objects. */
for (i = i3 = 0; i < numPts; i += 1, i3 += 3) {
posVal = posFunc(data[i], i);
position[i3] = posVal.x;
position[i3 + 1] = posVal.y;
// ignore the z values until we support them
position[i3 + 2] = 0; // posVal.z || 0;
}
position = transform.transformCoordinates(
m_this.gcs(), m_this.layer().map().gcs(), position, 3);
m_origin = new Float32Array(m_this.style.get('origin')(position));
if (m_origin[0] || m_origin[1] || m_origin[2]) {
for (i = i3 = 0; i < numPts; i += 1, i3 += 3) {
position[i3] -= m_origin[0];
position[i3 + 1] -= m_origin[1];
position[i3 + 2] -= m_origin[2];
}
}
m_modelViewUniform.setOrigin(m_origin);
posBuf = util.getGeomBuffer(geom, 'pos', vpf * numPts * 3);
unitBuf = util.getGeomBuffer(geom, 'unit', vpf * numPts * 2);
indices = geom.primitive(0).indices();
if (!(indices instanceof Uint16Array) || indices.length !== vpf * numPts) {
indices = new Uint16Array(vpf * numPts);
geom.primitive(0).setIndices(indices);
}
}
radius = util.getGeomBuffer(geom, 'radius', vpf * numPts);
stroke = util.getGeomBuffer(geom, 'stroke', vpf * numPts);
strokeWidth = util.getGeomBuffer(geom, 'strokeWidth', vpf * numPts);
strokeOpacity = util.getGeomBuffer(geom, 'strokeOpacity', vpf * numPts);
strokeColor = util.getGeomBuffer(geom, 'strokeColor', vpf * numPts * 3);
fill = util.getGeomBuffer(geom, 'fill', vpf * numPts);
fillOpacity = util.getGeomBuffer(geom, 'fillOpacity', vpf * numPts);
fillColor = util.getGeomBuffer(geom, 'fillColor', vpf * numPts * 3);
for (i = ivpf = ivpf3 = iunit = i3 = 0; i < numPts; i += 1, i3 += 3) {
item = data[i];
if (!onlyStyle) {
if (m_this._primitiveShape !== pointFeature.primitiveShapes.sprite) {
for (j = 0; j < unit.length; j += 1, iunit += 1) {
unitBuf[iunit] = unit[j];
}
}
}
/* We can ignore the indices (they will all be zero) */
radiusVal = radFunc(item, i);
strokeVal = strokeFunc(item, i) ? 1.0 : 0.0;
strokeWidthVal = strokeWidthFunc(item, i);
strokeOpacityVal = strokeOpacityFunc(item, i);
strokeColorVal = strokeColorFunc(item, i);
fillVal = fillFunc(item, i) ? 1.0 : 0.0;
fillOpacityVal = fillOpacityFunc(item, i);
fillColorVal = fillColorFunc(item, i);
if (m_this._primitiveShapeAuto &&
((fillVal && fillOpacityVal) || (strokeVal && strokeOpacityVal)) &&
radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0) > maxr) {
maxr = radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0);
}
for (j = 0; j < vpf; j += 1, ivpf += 1, ivpf3 += 3) {
if (!onlyStyle) {
posBuf[ivpf3] = position[i3];
posBuf[ivpf3 + 1] = position[i3 + 1];
posBuf[ivpf3 + 2] = position[i3 + 2];
}
radius[ivpf] = radiusVal;
stroke[ivpf] = strokeVal;
strokeWidth[ivpf] = strokeWidthVal;
strokeOpacity[ivpf] = strokeOpacityVal;
strokeColor[ivpf3] = strokeColorVal.r;
strokeColor[ivpf3 + 1] = strokeColorVal.g;
strokeColor[ivpf3 + 2] = strokeColorVal.b;
fill[ivpf] = fillVal;
fillOpacity[ivpf] = fillOpacityVal;
fillColor[ivpf3] = fillColorVal.r;
fillColor[ivpf3 + 1] = fillColorVal.g;
fillColor[ivpf3 + 2] = fillColorVal.b;
}
}
if (m_this._primitiveShapeAuto &&
((m_this._primitiveShape === pointFeature.primitiveShapes.sprite && maxr > webglRenderer._maxPointSize) ||
(m_this._primitiveShape !== pointFeature.primitiveShapes.sprite && maxr <= webglRenderer._maxPointSize))) {
// Switch primitive
m_this._primitiveShape = maxr > webglRenderer._maxPointSize ? pointFeature.primitiveShapes.triangle : pointFeature.primitiveShapes.sprite;
m_this.renderer().contextRenderer().removeActor(m_actor);
m_actor = null;
m_this._init(true);
createGLPoints();
return;
}
if (!onlyStyle) {
geom.boundsDirty(true);
m_mapper.modified();
m_mapper.boundsDirtyTimestamp().modified();
} else {
m_mapper.updateSourceBuffer('radius');
m_mapper.updateSourceBuffer('stroke');
m_mapper.updateSourceBuffer('strokeWidth');
m_mapper.updateSourceBuffer('strokeColor');
m_mapper.updateSourceBuffer('strokeOpacity');
m_mapper.updateSourceBuffer('fill');
m_mapper.updateSourceBuffer('fillColor');
m_mapper.updateSourceBuffer('fillOpacity');
}
}
/**
* List vgl actors.
*
* @returns {vgl.actor[]} The list of actors.
*/
this.actors = function () {
if (!m_actor) {
return [];
}
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.
*
* @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 = {
fill: 'bool',
fillColor: 3,
fillOpacity: 1,
radius: 1,
stroke: 'bool',
strokeColor: 3,
strokeOpacity: 1,
strokeWidth: 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;
bpv = bufferedKeys[key] === 'bool' ? 1 : bufferedKeys[key];
numPts = m_this.data().length;
mapper = m_actor.mapper();
buffer = mapper.getSourceBuffer(key);
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;
case 'bool':
for (i = 0, v = 0; i < numPts; i += 1) {
value = styleArray[i] ? 1.0 : 0.0;
for (j = 0; j < vpf; j += 1, v += 1) {
buffer[v] = value;
}
}
break;
}
mapper.updateSourceBuffer(key);
/* 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;
}
const 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;
};
/**
* Initialize.
*
* @param {boolean} [reinit] If truthy, skip the parent class's init method.
*/
this._init = function (reinit) {
var prog = vgl.shaderProgram(),
vertexShader = createVertexShader(),
fragmentShader = createFragmentShader(),
posAttr = vgl.vertexAttribute('pos'),
unitAttr = vgl.vertexAttribute('unit'),
radAttr = vgl.vertexAttribute('radius'),
strokeWidthAttr = vgl.vertexAttribute('strokeWidth'),
fillColorAttr = vgl.vertexAttribute('fillColor'),
fillAttr = vgl.vertexAttribute('fill'),
strokeColorAttr = vgl.vertexAttribute('strokeColor'),
strokeAttr = vgl.vertexAttribute('stroke'),
fillOpacityAttr = vgl.vertexAttribute('fillOpacity'),
strokeOpacityAttr = vgl.vertexAttribute('strokeOpacity'),
projectionUniform = new vgl.projectionUniform('projectionMatrix'),
mat = vgl.material(),
blend = vgl.blend(),
geom = vgl.geometryData(),
sourcePositions = vgl.sourceDataP3fv({name: 'pos'}),
sourceUnits = vgl.sourceDataAnyfv(
2, vgl.vertexAttributeKeysIndexed.One, {name: 'unit'}),
sourceRadius = vgl.sourceDataAnyfv(
1, vgl.vertexAttributeKeysIndexed.Two, {name: 'radius'}),
sourceStrokeWidth = vgl.sourceDataAnyfv(
1, vgl.vertexAttributeKeysIndexed.Three, {name: 'strokeWidth'}),
sourceFillColor = vgl.sourceDataAnyfv(
3, vgl.vertexAttributeKeysIndexed.Four, {name: 'fillColor'}),
sourceFill = vgl.sourceDataAnyfv(
1, vgl.vertexAttributeKeysIndexed.Five, {name: 'fill'}),
sourceStrokeColor = vgl.sourceDataAnyfv(
3, vgl.vertexAttributeKeysIndexed.Six, {name: 'strokeColor'}),
sourceStroke = vgl.sourceDataAnyfv(
1, vgl.vertexAttributeKeysIndexed.Seven, {name: 'stroke'}),
sourceAlpha = vgl.sourceDataAnyfv(
1, vgl.vertexAttributeKeysIndexed.Eight, {name: 'fillOpacity'}),
sourceStrokeOpacity = vgl.sourceDataAnyfv(
1, vgl.vertexAttributeKeysIndexed.Nine, {name: 'strokeOpacity'}),
primitive;
m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', m_origin);
if (m_this._primitiveShape === pointFeature.primitiveShapes.sprite) {
primitive = new vgl.points();
} else {
primitive = new vgl.triangles();
}
primitive.setIndices(new Uint16Array());
m_pixelWidthUniform = new vgl.floatUniform(
'pixelWidth', 2.0 / m_this.renderer().width());
m_aspectUniform = new vgl.floatUniform(
'aspect', m_this.renderer().width() / m_this.renderer().height());
if (!reinit) {
s_init.call(m_this, arg);
}
m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw});
prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position);
if (m_this._primitiveShape !== pointFeature.primitiveShapes.sprite) {
prog.addVertexAttribute(unitAttr, vgl.vertexAttributeKeysIndexed.One);
}
prog.addVertexAttribute(radAttr, vgl.vertexAttributeKeysIndexed.Two);
prog.addVertexAttribute(strokeWidthAttr, vgl.vertexAttributeKeysIndexed.Three);
prog.addVertexAttribute(fillColorAttr, vgl.vertexAttributeKeysIndexed.Four);
prog.addVertexAttribute(fillAttr, vgl.vertexAttributeKeysIndexed.Five);
prog.addVertexAttribute(strokeColorAttr, vgl.vertexAttributeKeysIndexed.Six);
prog.addVertexAttribute(strokeAttr, vgl.vertexAttributeKeysIndexed.Seven);
prog.addVertexAttribute(fillOpacityAttr, vgl.vertexAttributeKeysIndexed.Eight);
prog.addVertexAttribute(strokeOpacityAttr, vgl.vertexAttributeKeysIndexed.Nine);
prog.addUniform(m_pixelWidthUniform);
prog.addUniform(m_aspectUniform);
prog.addUniform(m_modelViewUniform);
prog.addUniform(projectionUniform);
prog.addShader(fragmentShader);
prog.addShader(vertexShader);
mat.addAttribute(prog);
mat.addAttribute(blend);
m_actor = vgl.actor();
m_actor.setMaterial(mat);
m_actor.setMapper(m_mapper);
geom.addSource(sourcePositions);
geom.addSource(sourceUnits);
geom.addSource(sourceRadius);
geom.addSource(sourceStrokeWidth);
geom.addSource(sourceFillColor);
geom.addSource(sourceFill);
geom.addSource(sourceStrokeColor);
geom.addSource(sourceStroke);
geom.addSource(sourceAlpha);
geom.addSource(sourceStrokeOpacity);
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);
};
/**
* Build. Create the necessary elements to render points.
*
* @returns {this}
*/
this._build = function () {
createGLPoints(m_this.dataTime().timestamp() < m_this.buildTime().timestamp());
if (!m_this.renderer().contextRenderer().hasActor(m_actor)) {
m_this.renderer().contextRenderer().addActor(m_actor);
}
m_this.buildTime().modified();
return m_this;
};
/**
* Update. Rebuild if necessary.
*
* @returns {this}
*/
this._update = function () {
s_update.call(m_this);
// For now build if the data or style changes. In the future we may
// we able to partially update the data using dynamic gl buffers.
if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp() ||
m_this.updateTime().timestamp() < m_this.timestamp()) {
m_this._build();
}
// Update uniforms
m_pixelWidthUniform.set(2.0 / m_this.renderer().width());
m_aspectUniform.set(m_this.renderer().width() /
m_this.renderer().height());
m_actor.setVisible(m_this.visible());
m_actor.material().setBinNumber(m_this.bin());
m_this.updateTime().modified();
return m_this;
};
/**
* Destroy. Free used resources.
*/
this._exit = function () {
m_this.renderer().contextRenderer().removeActor(m_actor);
m_actor = null;
s_exit();
};
m_this._init();
return this;
};
inherit(webgl_pointFeature, pointFeature);
var capabilities = {};
capabilities[pointFeature.capabilities.stroke] = true;
// Now register it
registerFeature('webgl', 'point', webgl_pointFeature, capabilities);
module.exports = webgl_pointFeature;