var $ = require('jquery');
var inherit = require('../inherit');
var registerFeature = require('../registry').registerFeature;
var markerFeature = require('../markerFeature');
var webglRenderer = require('./webglRenderer');
/**
* Create a new instance of webgl.markerFeature.
*
* @class
* @alias geo.webgl.markerFeature
* @extends geo.markerFeature
* @param {geo.markerFeature.spec} arg
* @returns {geo.webgl.markerFeature}
*/
var webgl_markerFeature = function (arg) {
'use strict';
if (!(this instanceof webgl_markerFeature)) {
return new webgl_markerFeature(arg);
}
arg = arg || {};
markerFeature.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 geo_event = require('../event');
var fragmentShaderPoly = require('./markerFeaturePoly.frag');
var fragmentShaderSprite = require('./markerFeatureSprite.frag');
var vertexShaderPoly = require('./markerFeaturePoly.vert');
var vertexShaderSprite = require('./markerFeatureSprite.vert');
object.call(this);
/**
* @private
*/
var m_this = this,
s_exit = this._exit,
m_actor = null,
m_mapper = null,
m_uniforms = {},
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 markers.
*
* @returns {vgl.shader}
*/
function createVertexShader() {
var shader = new vgl.shader(vgl.GL.VERTEX_SHADER);
shader.setShaderSource(
m_this._primitiveShape === markerFeature.primitiveShapes.sprite ? vertexShaderSprite : vertexShaderPoly);
return shader;
}
/**
* Create the fragment shader for markers.
*
* @returns {vgl.shader}
*/
function createFragmentShader() {
var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER);
shader.setShaderSource(
m_this._primitiveShape === markerFeature.primitiveShapes.sprite ? fragmentShaderSprite : fragmentShaderPoly);
return shader;
}
/**
* Pack an array of three numbers and one boolean into a single float. Each
* numerical value is either undefined or on the scale of [0, 1] and is
* mapped to an integer range of [0, 250].
*
* @param {number|number[]} value A single value or an array of up to four
* values where the first three values are numbers and the last is a
* boolean.
* @returns {number} A packed number.
*/
function packFloats(value) {
if (!value.length) {
return value === undefined ? 0 : Math.floor(Math.abs(value) * 250) + 1;
}
return (
(value[0] === undefined ? 0 : Math.floor(Math.abs(value[0]) * 250) + 1) +
(value[1] === undefined ? 0 : Math.floor(Math.abs(value[1]) * 250) + 1) * 252 +
(value[2] === undefined ? 0 : Math.floor(Math.abs(value[2]) * 250) + 1) * 252 * 252
) * (value[3] ? -1 : 1);
}
/**
* Create and style the data needed to render the markers.
*
@param {boolean} [onlyStyle] if true, use the existing geometry and just
* recalculate the style.
*/
function createGLMarkers(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,
indices, unitBuf,
styleBuf = {}, styleVal = {}, styleFunc = {}, styleUni = {},
styleKeys = {
radius: 1,
fillColor: 3,
fillOpacity: 1,
strokeColor: 3,
strokeOpacity: 1,
strokeOffset: 0,
strokeWidth: 1,
symbol: 1,
symbolValue: 1,
rotation: 1,
scaleWithZoom: 0,
rotateWithMap: 0,
radiusIncludesStroke: 0
},
vpf = m_this.verticesPerFeature(),
data = m_this.data(),
item, ivpf, ivpf3, iunit, i3,
geom = m_mapper.geometryData();
posFunc = m_this.position();
for (const key in styleKeys) {
styleFunc[key] = m_this.style.get(key);
if (!util.isFunction(m_this.style(key))) {
styleUni[key] = styleFunc[key]();
}
if (styleKeys[key]) {
styleBuf[key] = util.getGeomBuffer(geom, key, vpf * numPts * styleKeys[key]);
}
}
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);
if (m_this._primitiveShape !== markerFeature.primitiveShapes.sprite) {
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);
}
}
for (i = ivpf = ivpf3 = iunit = i3 = 0; i < numPts; i += 1, i3 += 3) {
item = data[i];
if (!onlyStyle) {
if (m_this._primitiveShape !== markerFeature.primitiveShapes.sprite) {
for (j = 0; j < unit.length; j += 1, iunit += 1) {
unitBuf[iunit] = unit[j];
}
}
}
// unrolling this would speed it up
for (const key in styleKeys) {
styleVal[key] = styleUni[key] === undefined ? styleFunc[key](item, i) : styleUni[key];
}
styleVal.scaleWithZoom = markerFeature.scaleMode[styleVal.scaleWithZoom] || (styleVal.scaleWithZoom >= 1 && styleVal.scaleWithZoom <= 3 ? styleVal.scaleWithZoom : 0);
styleVal.symbolComputed = (
styleVal.scaleWithZoom +
(styleVal.rotateWithMap ? 4 : 0) +
// bit 3 reserved
((Math.sign(styleVal.radiusIncludesStroke !== undefined && styleVal.radiusIncludesStroke ? styleVal.strokeOffset : 1) + 1) * 16) +
styleVal.symbol * 64);
if (styleVal.symbolValue && styleVal.symbol >= markerFeature.symbols.arrow && styleVal.symbol < markerFeature.symbols.arrow + markerFeature.symbols.arrowMax) {
styleVal.symbolValue = packFloats(styleVal.symbolValue);
}
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];
}
styleBuf.radius[ivpf] = styleVal.radius;
styleBuf.fillColor[ivpf3] = styleVal.fillColor.r;
styleBuf.fillColor[ivpf3 + 1] = styleVal.fillColor.g;
styleBuf.fillColor[ivpf3 + 2] = styleVal.fillColor.b;
styleBuf.fillOpacity[ivpf] = styleVal.fillOpacity;
styleBuf.strokeColor[ivpf3] = styleVal.strokeColor.r;
styleBuf.strokeColor[ivpf3 + 1] = styleVal.strokeColor.g;
styleBuf.strokeColor[ivpf3 + 2] = styleVal.strokeColor.b;
styleBuf.strokeOpacity[ivpf] = styleVal.strokeOpacity;
styleBuf.strokeWidth[ivpf] = styleVal.strokeWidth;
styleBuf.symbol[ivpf] = styleVal.symbolComputed;
styleBuf.symbolValue[ivpf] = styleVal.symbolValue;
styleBuf.rotation[ivpf] = styleVal.rotation;
}
}
if (m_this._primitiveShapeAuto) {
const maxr = m_this._approximateMaxRadius(m_this.renderer().map().zoom());
if ((m_this._primitiveShape === markerFeature.primitiveShapes.sprite && maxr > webglRenderer._maxPointSize) ||
(m_this._primitiveShape !== markerFeature.primitiveShapes.sprite && maxr <= webglRenderer._maxPointSize)) {
// Switch primitive
m_this._primitiveShape = maxr > webglRenderer._maxPointSize ? markerFeature.primitiveShapes.triangle : markerFeature.primitiveShapes.sprite;
m_this.renderer().contextRenderer().removeActor(m_actor);
m_actor = null;
m_this._init(true);
createGLMarkers();
return;
}
}
if (!onlyStyle) {
geom.boundsDirty(true);
m_mapper.modified();
m_mapper.boundsDirtyTimestamp().modified();
} else {
Object.keys(styleBuf).forEach(key => m_mapper.updateSourceBuffer(key));
}
}
/**
* 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.
*
* 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) {
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;
};
/**
* Handle zoom events for automatic primitive shape adjustment.
*
* @param {number} zoom The new zoom level.
*/
this._handleZoom = function (zoom) {
if (!m_this._primitiveShapeAuto || m_this._primitiveShape !== markerFeature.primitiveShapes.sprite) {
return;
}
if (m_this._approximateMaxRadius(zoom) > webglRenderer._maxPointSize) {
m_this._primitiveShape = markerFeature.primitiveShapes.triangle;
m_this.renderer().contextRenderer().removeActor(m_this.actors()[0]);
m_this._init(true);
m_this.dataTime().modified();
m_this.draw();
}
};
/**
* 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(),
mat = vgl.material(),
blend = vgl.blend(),
geom = vgl.geometryData(),
sourcePositions = vgl.sourceDataP3fv({name: 'pos'}),
attr = {
radius: 1,
fillColor: 3,
fillOpacity: 1,
strokeColor: 3,
strokeOpacity: 1,
strokeWidth: 1,
symbol: 1,
symbolValue: 1,
rotation: 1
},
uniforms = {
pixelWidth: vgl.GL.FLOAT,
aspect: vgl.GL.FLOAT,
zoom: vgl.GL.FLOAT,
rotationUniform: vgl.GL.FLOAT
},
projectionUniform = new vgl.projectionUniform('projectionMatrix'),
primitive;
m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', m_origin);
if (m_this._primitiveShape === markerFeature.primitiveShapes.sprite) {
primitive = new vgl.points();
} else {
primitive = new vgl.triangles();
attr.unit = 2;
}
primitive.setIndices(new Uint16Array());
if (!reinit) {
s_init.call(m_this, arg);
}
m_mapper = vgl.mapper();
prog.addVertexAttribute(vgl.vertexAttribute('pos'), vgl.vertexAttributeKeys.Position);
geom.addSource(sourcePositions);
Object.keys(attr).forEach((key, idx) => {
prog.addVertexAttribute(vgl.vertexAttribute(key), idx + 1);
geom.addSource(vgl.sourceDataAnyfv(attr[key], idx + 1, {name: key}));
});
m_uniforms = {};
Object.keys(uniforms).forEach((key) => {
m_uniforms[key] = new vgl.uniform(uniforms[key], key);
prog.addUniform(m_uniforms[key]);
});
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.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);
if (!reinit) {
m_this.geoOn(geo_event.zoom, function (evt) {
m_this._handleZoom(evt.zoomLevel);
});
}
};
/**
* Build. Create the necessary elements to render markers.
*
* @returns {this}
*/
this._build = function () {
createGLMarkers(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_uniforms.pixelWidth.set(2.0 / m_this.renderer().width());
m_uniforms.aspect.set(m_this.renderer().width() / m_this.renderer().height());
m_uniforms.zoom.set(m_this.renderer().map().zoom());
m_uniforms.rotationUniform.set(m_this.renderer().map().rotation());
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;
m_uniforms = {};
s_exit();
};
m_this._init();
return this;
};
inherit(webgl_markerFeature, markerFeature);
var capabilities = {};
// Now register it
registerFeature('webgl', 'marker', webgl_markerFeature, capabilities);
module.exports = webgl_markerFeature;