From ab63e748aed6c7d815c61871e42521001a6494ba Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 20 Sep 2019 13:05:54 -0400 Subject: [PATCH 1/2] Add a marker feature. This adds an efficient webgl marker feature. It uses slightly more memory than the point feature (one more float value per marker is passed to the GPU than for points). A variety of shapes are defined: ellipses, rectangles, isosceles triangles, and ovals, plus stellations of each of these. The shapes can specify orientation, and can automatically scale and/or rotate with the map. There is one distinct difference between markers and points. The radius of the marker is the total radius, including any stroke. The size of the point is the combination of the radius and the stroke. These use nearly the same vertex shaders as the point feature, but a vastly more complex fragment shader. As such, rendering complex shapes will saturate the GPU sooner than for basic points. At some future point, it is intended to add image markers, where images or svgs could be rendered via a texture map. In this case, the intent is to use a single texture buffer and pack multiple images into it with a small space between each. Resolves #820 (though a new issue should be created for image/svg markers). --- CHANGELOG.md | 3 + src/index.js | 1 + src/markerFeature.js | 470 +++++++++++++++++++++++++++++ src/pointFeature.js | 1 + src/webgl/index.js | 1 + src/webgl/markerFeature.js | 393 ++++++++++++++++++++++++ src/webgl/markerFeatureFS.glsl | 338 +++++++++++++++++++++ src/webgl/markerFeaturePoly.frag | 11 + src/webgl/markerFeaturePoly.vert | 25 ++ src/webgl/markerFeatureSprite.frag | 14 + src/webgl/markerFeatureSprite.vert | 14 + src/webgl/markerFeatureVS.glsl | 57 ++++ src/webgl/pointFeature.js | 123 +------- src/webgl/pointFeaturePoly.vert | 2 +- src/webgl/pointUtil.js | 112 +++++++ tests/cases/markerFeature.js | 203 +++++++++++++ tutorials/marker/index.pug | 143 +++++++++ tutorials/marker/thumb.jpg | Bin 0 -> 117815 bytes tutorials/marker/tutorial.json | 10 + 19 files changed, 1811 insertions(+), 110 deletions(-) create mode 100644 src/markerFeature.js create mode 100644 src/webgl/markerFeature.js create mode 100644 src/webgl/markerFeatureFS.glsl create mode 100644 src/webgl/markerFeaturePoly.frag create mode 100644 src/webgl/markerFeaturePoly.vert create mode 100644 src/webgl/markerFeatureSprite.frag create mode 100644 src/webgl/markerFeatureSprite.vert create mode 100644 src/webgl/markerFeatureVS.glsl create mode 100644 src/webgl/pointUtil.js create mode 100644 tests/cases/markerFeature.js create mode 100644 tutorials/marker/index.pug create mode 100644 tutorials/marker/thumb.jpg create mode 100644 tutorials/marker/tutorial.json diff --git a/CHANGELOG.md b/CHANGELOG.md index e6171f89a8..7b17275395 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ ### Changes - Line segments with zero width or zero opacity won't be found by pointSearch or polygonSearch (#1041) +### Features +- Added a marker feature (#1035) + ### Bug Fixes - Removed extra calls to sceneObject constructors (#1039) - Fixed an issue with rendering on hidden tabs in Chrome (#1042) diff --git a/src/index.js b/src/index.js index 016328eb87..61a235b00e 100644 --- a/src/index.js +++ b/src/index.js @@ -59,6 +59,7 @@ module.exports = $.extend({ lineFeature: require('./lineFeature'), map: require('./map'), mapInteractor: require('./mapInteractor'), + markerFeature: require('./markerFeature'), meshFeature: require('./meshFeature'), object: require('./object'), osmLayer: require('./osmLayer'), diff --git a/src/markerFeature.js b/src/markerFeature.js new file mode 100644 index 0000000000..65cdd175e2 --- /dev/null +++ b/src/markerFeature.js @@ -0,0 +1,470 @@ +var inherit = require('./inherit'); +var feature = require('./feature'); +var pointFeature = require('./pointFeature'); + +/** + * Object specification for a marker feature. + * + * @typedef {geo.feature.spec} geo.markerFeature.spec + * @property {geo.geoPosition|function} [position] Position of the data. + * Default is (data). + * @property {geo.markerFeature.styleSpec} [style] Style object with default + * style options. + */ + +/** + * Style specification for a marker feature. + * + * @typedef {geo.feature.styleSpec} geo.markerFeature.styleSpec + * @extends geo.feature.styleSpec + * @property {number|function} [radius=5] Radius of each marker in pixels. + * This includes the stroke width and the fill. + * @property {geo.geoColor|function} [strokeColor] Color to stroke each marker. + * @property {number|function} [strokeOpacity=1] Opacity for each marker's + * stroke. Opacity is on a [0-1] scale. Set this or `strokeWidth` to zero + * to not have a stroke. + * @property {number|function} [strokeWidth=1.25] The weight of the marker's + * stroke in pixels. Set this or `strokeOpacity` to zero to not have a + * stroke. + * @property {geo.geoColor|function} [fillColor] Color to fill each marker. + * @property {number|function} [fillOpacity=1] Opacity for each marker. + * Opacity is on a [0-1] scale. Set to zero to have no fill. + * @property {number|function} [symbol=0] One of the predefined symbol numbers. + * This is one of `geo.markerFeature.symbols`. + * @property {number|number[]|function} [symbolValue=0] A value the affects the + * appearance of the symbol. Some symbols can take an array of numbers. + * @property {number|function} [rotation=0] The rotation of the symbol in + * clockwise radians. + * @property {geo.markerFeature.scaleMode|function} [scaleWithZoom='none'] This + * determines if the fill, stroke, or both scale with zoom. If set, the + * values for radius and strokeWidth are the values at zoom-level zero. + * @property {boolean|function} [rotateWithMap=false] If truthy, rotate symbols + * with the map. If falsy, symbol orientation is absolute. + * @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 maker positions as a single continuous array + * in map gcs coordinates. It defaults to the first marker's position. + */ + +/** + * Create a new instance of class markerFeature. + * + * @class + * @alias geo.markerFeature + * @extends geo.feature + * @param {geo.markerFeature.spec} arg + * @returns {geo.markerFeature} + */ +var markerFeature = function (arg) { + 'use strict'; + if (!(this instanceof markerFeature)) { + return new markerFeature(arg); + } + arg = arg || {}; + pointFeature.call(this, arg); + + var $ = require('jquery'); + var timestamp = require('./timestamp'); + var util = require('./util'); + var KDBush = require('kdbush'); + + /** + * @private + */ + var m_this = this, + s_init = this._init, + m_rangeTree = null, + m_rangeTreeTime = timestamp(), + m_maxFixedRadius = 0, + m_maxZoomRadius = 0, + m_maxZoomStroke = 0; + + this.featureType = 'marker'; + + /** + * Update the current range tree object. Should be called whenever the + * data changes. + */ + this._updateRangeTree = function () { + if (m_rangeTreeTime.timestamp() >= m_this.dataTime().timestamp() && m_rangeTreeTime.timestamp() >= m_this.timestamp()) { + return; + } + var pts, position, + radius = m_this.style.get('radius'), + strokeWidth = m_this.style.get('strokeWidth'), + scaleWithZoom = m_this.style.get('scaleWithZoom'); + + position = m_this.position(); + + m_maxFixedRadius = 0; + m_maxZoomRadius = 0; + m_maxZoomStroke = 0; + + // create an array of positions in geo coordinates + pts = m_this.data().map(function (d, i) { + var pt = position(d, i); + + let r = radius(d, i), s = strokeWidth(d, i); + switch (scaleWithZoom(d, i)) { + case markerFeature.scaleMode.stroke: + if (r - s > m_maxFixedRadius) { + m_maxFixedRadius = r - s; + } + if (s > m_maxZoomStroke) { + m_maxZoomStroke = s; + } + break; + case markerFeature.scaleMode.fill: + case markerFeature.scaleMode.all: + if (r > m_maxZoomRadius) { + m_maxZoomRadius = r; + } + break; + default: + if (r > m_maxFixedRadius) { + m_maxFixedRadius = r; + } + break; + } + return [pt.x, pt.y]; + }); + + m_rangeTree = new KDBush(pts); + m_rangeTreeTime.modified(); + }; + + /** + * Returns an array of datum indices that contain the given marker. + * + * @param {geo.geoPosition} p marker to search for in map interface gcs. + * @returns {object} An object with `index`: a list of marker indices, and + * `found`: a list of markers that contain the specified coordinate. + */ + this.pointSearch = function (p) { + var min, max, data, idx = [], found = [], ifound = [], + fgcs = m_this.gcs(), // this feature's gcs + corners, + radius = m_this.style.get('radius'), + strokeWidth = m_this.style.get('strokeWidth'), + scaleWithZoom = m_this.style.get('scaleWithZoom'); + + data = m_this.data(); + if (!data || !data.length) { + return { + found: [], + index: [] + }; + } + + // We need to do this before we find corners, since the max radius is + // determined then + m_this._updateRangeTree(); + + var map = m_this.layer().map(), + pt = map.gcsToDisplay(p), + zoom = map.zoom(), + zfactor = Math.pow(2, zoom), + maxr = Math.max(m_maxFixedRadius + m_maxZoomStroke * zfactor, m_maxZoomRadius * zfactor); + + // check all corners to make sure we handle rotations + corners = [ + map.displayToGcs({x: pt.x - maxr, y: pt.y - maxr}, fgcs), + map.displayToGcs({x: pt.x + maxr, y: pt.y - maxr}, fgcs), + map.displayToGcs({x: pt.x - maxr, y: pt.y + maxr}, fgcs), + map.displayToGcs({x: pt.x + maxr, y: pt.y + maxr}, fgcs) + ]; + min = { + x: Math.min(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.min(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + }; + max = { + x: Math.max(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.max(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + }; + + // Find markers inside the bounding box + idx = m_rangeTree.range(min.x, min.y, max.x, max.y); + + idx.sort((a, b) => a - b); + // Filter by circular region + idx.forEach(function (i) { + var d = data[i], + rad = radius(data[i], i), + swz = scaleWithZoom(data[i], i), + s = swz ? strokeWidth(data[i], i) : 0; + var p = m_this.position()(d, i), + dx, dy, rad2; + switch (swz) { + case markerFeature.scaleMode.fill: + rad = (rad - s) * zfactor + s; + break; + case markerFeature.scaleMode.stroke: + rad = (rad - s) + s * zfactor; + break; + case markerFeature.scaleMode.all: + rad *= zfactor; + break; + } + + if (rad) { + rad2 = rad * rad; + p = map.gcsToDisplay(p, fgcs); + dx = p.x - pt.x; + dy = p.y - pt.y; + if (dx * dx + dy * dy <= rad2) { + found.push(d); + ifound.push(i); + } + } + }); + + return { + found: found, + index: ifound + }; + }; + + /** + * Returns an array of datum indices that are contained in the given polygon. + * This does not take clustering into account. + * + * @param {geo.polygonObject} poly A polygon as an array of coordinates or an + * object with `outer` and optionally `inner` parameters. All coordinates + * are in map interface gcs. + * @param {object} [opts] Additional search options. + * @param {boolean} [opts.partial=false] If truthy, include markers that are + * partially in the polygon, otherwise only include markers that are fully + * within the region. If 'center', only markers whose centers are inside + * the polygon are returned. + * @returns {object} An object with `index`: a list of marker indices, + * `found`: a list of markers within the polygon, and `extra`: an object + * with index keys containing an object with a `partial` key and a boolean + * value to indicate if the marker is on the polygon's border and a + * `distance` key to indicate how far within the polygon the marker is + * located. + */ + this.polygonSearch = function (poly, opts) { + var fgcs = m_this.gcs(), // this feature's gcs + found = [], + ifound = [], + extra = {}, + map = m_this.layer().map(), + data = m_this.data(), + radius = m_this.style.get('radius'), + strokeWidth = m_this.style.get('strokeWidth'), + scaleWithZoom = m_this.style.get('scaleWithZoom'), + idx, min, max, corners, + zoom = map.zoom(), + zfactor = Math.pow(2, zoom), + maxr = Math.max(m_maxFixedRadius + m_maxZoomStroke * zfactor, m_maxZoomRadius * zfactor); + + if (!poly.outer) { + poly = {outer: poly, inner: []}; + } + if (poly.outer.length < 3 || !data || !data.length) { + return { + found: [], + index: [], + extra: {} + }; + } + opts = opts || {}; + opts.partial = opts.partial || false; + poly = {outer: map.gcsToDisplay(poly.outer), inner: (poly.inner || []).map(inner => map.gcsToDisplay(inner))}; + poly.outer.forEach(p => { + if (!min) { + min = {x: p.x, y: p.y}; + max = {x: p.x, y: p.y}; + } + if (p.x < min.x) { min.x = p.x; } + if (p.x > max.x) { max.x = p.x; } + if (p.y < min.y) { min.y = p.y; } + if (p.y > max.y) { max.y = p.y; } + }); + // We need to do this before we find corners, since the max radius is + // determined then + m_this._updateRangeTree(); + corners = [ + map.displayToGcs({x: min.x - maxr, y: min.y - maxr}, fgcs), + map.displayToGcs({x: max.x + maxr, y: min.y - maxr}, fgcs), + map.displayToGcs({x: max.x + maxr, y: max.y + maxr}, fgcs), + map.displayToGcs({x: min.x - maxr, y: max.y + maxr}, fgcs) + ]; + min = { + x: Math.min(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.min(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + }; + max = { + x: Math.max(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.max(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + }; + // Find markers inside the bounding box. Only these could be in the polygon + idx = m_rangeTree.range(min.x, min.y, max.x, max.y); + // sort by index + idx.sort((a, b) => a - b); + // filter markers within the polygon + idx.forEach(function (i) { + var d = data[i]; + let p = m_this.position()(d, i); + let rad = radius(data[i], i), + swz = scaleWithZoom(data[i], i), + s = swz ? strokeWidth(data[i], i) : 0; + switch (swz) { + case markerFeature.scaleMode.fill: + rad = (rad - s) * zfactor + s; + break; + case markerFeature.scaleMode.stroke: + rad = (rad - s) + s * zfactor; + break; + case markerFeature.scaleMode.all: + rad *= zfactor; + break; + } + if (rad) { + p = map.gcsToDisplay(p, fgcs); + let dist = util.distanceToPolygon2d(p, poly); + if (dist >= rad || (dist >= 0 && opts.partial === 'center') || (dist >= -rad && opts.partial && opts.partial !== 'center')) { + found.push(d); + ifound.push(i); + extra[i] = {partial: dist < rad, distance: dist}; + } + } + }); + return { + found: found, + index: ifound, + extra: extra + }; + }; + + /** + * Initialize. + * + * @param {geo.markerFeature.spec} arg The feature specification. + * @returns {this} + */ + this._init = function (arg) { + arg = $.extend( + true, + {}, + { + style: $.extend( + {}, + { + radius: 6.25, + strokeColor: { r: 0.851, g: 0.604, b: 0.0 }, + strokeOpacity: 1.0, + strokeWidth: 1.25, + fillColor: { r: 1.0, g: 0.839, b: 0.439 }, + fillOpacity: 0.8, + symbol: 0, + symbolValue: 0, + rotation: 0, + scaleWithZoom: markerFeature.scaleMode.none, + rotateWithMap: false + // position and origin are the same as the pointFeature + }, + arg && arg.style === undefined ? {} : arg.style + ) + }, + arg + ); + s_init.call(m_this, arg); + return m_this; + }; + + return m_this; +}; + +/** + * Create a markerFeature from an object. + * @see {@link geo.feature.create} + * @param {geo.layer} layer The layer to add the feature to + * @param {geo.markerFeature.spec} spec The object specification + * @returns {geo.markerFeature|null} + */ +markerFeature.create = function (layer, spec) { + 'use strict'; + + spec = spec || {}; + spec.type = 'marker'; + return feature.create(layer, spec); +}; + +markerFeature.capabilities = { + /* core feature name -- support in any manner */ + feature: 'marker' +}; + +markerFeature.primitiveShapes = pointFeature.primitiveShapes; + +/** + * Marker symbols + * @enum + */ +markerFeature.symbols = { + // for circle (alias ellipse), the symbolValue is the ratio of the minor to + // major axes. Range (0, infinity) + circle: 0, + ellipse: 0, + flowerBase: 1, + flowerMax: 16, + // for triangle, the symbolValue is the ratio of the base to the other sides. + // Ranges (0, 2) + triangle: 16, + diamond: 17, + starBase: 17, + starMax: 16, + // for square (alias rectangle), the symbolValue is the ratio of the minor to + // major axes. Range (0, infinity) + square: 32, + rectangle: 32, + // for crosses, the symbolValue is the width of the arm compared to the + // length of the cross + crossBase: 33, + crossMax: 16, + // for ovals, the symbolValue is the ratio of the minor to major axes. Range + // (0, 1] + oval: 48, + jackBase: 49, + jackMax: 16, + // for drops, the symbol value is the ratio of the arc to the main radius. + // Range (0, 1] + drop: 64, + dropBase: 65, + dropMax: 16, + // for arrow, the symbol value is an array of up to four values: + // headWidth : the ratio of the head width to the radius. Range (0, 1]. + // Default 2/3. + // headLength : the ratio of head length to the diameter. Range (0, 1]. + // Default 1/2. + // stemWidth : the ratio of the stem width to the head width. Range + // [0, 1]. Default 1/3. + // sweep : a boolean; if true the back of head is swept; if false the back + // of the head is square. Default false. + arrow: 80, + arrowBase: 81, + arrowMax: 16, + length: 96 + // possible other symbols: + // half inner stellations (bowtie/hourglass), hash (#), inner curved shapes +}; +['flower', 'star', 'cross', 'jack', 'drop', 'arrow'].forEach(key => { + for (let i = 2; i <= markerFeature.symbols[key + 'Max']; i += 1) { + markerFeature.symbols[key + i] = markerFeature.symbols[key + 'Base'] - 2 + i; + } +}); + +/** + * Marker scale modes + * @enum + */ +markerFeature.scaleMode = { + none: 0, + fill: 1, + stroke: 2, + all: 3 +}; + +inherit(markerFeature, pointFeature); +module.exports = markerFeature; diff --git a/src/pointFeature.js b/src/pointFeature.js index 8287521421..fa59268e45 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -489,6 +489,7 @@ var pointFeature = function (arg) { s_init.call(m_this, arg); var defaultStyle = $.extend( + true, {}, { radius: 5.0, diff --git a/src/webgl/index.js b/src/webgl/index.js index 1a8942c818..2c149e0f37 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -7,6 +7,7 @@ module.exports = { isolineFeature: require('./isolineFeature'), layer: require('./layer'), lineFeature: require('./lineFeature'), + markerFeature: require('./markerFeature'), pointFeature: require('./pointFeature'), polygonFeature: require('./polygonFeature'), quadFeature: require('./quadFeature'), diff --git a/src/webgl/markerFeature.js b/src/webgl/markerFeature.js new file mode 100644 index 0000000000..8f8d27273b --- /dev/null +++ b/src/webgl/markerFeature.js @@ -0,0 +1,393 @@ +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 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; + + 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 geoemtry 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, + zoomFactor = Math.pow(2, m_this.renderer().map().zoom()), + styleBuf = {}, styleVal = {}, styleFunc = {}, styleUni = {}, + styleKeys = { + radius: 1, + fillColor: 3, + fillOpacity: 1, + strokeColor: 3, + strokeOpacity: 1, + strokeWidth: 1, + symbol: 1, + symbolValue: 1, + rotation: 1, + scaleWithZoom: 0, + rotateWithMap: 0 + }, + vpf = m_this.verticesPerFeature(), + data = m_this.data(), + item, ivpf, ivpf3, iunit, i3, maxr = 0, + geom = m_mapper.geometryData(); + + posFunc = m_this.position(); + + for (let 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 (let 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 + styleVal.symbol * 16); + if (styleVal.symbolValue && styleVal.symbol >= markerFeature.symbols.arrow && styleVal.symbol < markerFeature.symbols.arrow + markerFeature.symbols.arrowMax) { + styleVal.symbolValue = packFloats(styleVal.symbolValue); + } + if (m_this._primitiveShapeAuto && + (styleVal.scaleWithZoom ? styleVal.radius * zoomFactor : styleVal.radius) > maxr) { + maxr = styleVal.scaleWithZoom ? styleVal.radius * zoomFactor : styleVal.radius; + } + 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 && + ((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]; + }; + + /** + * 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})); + }); + 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); + }; + + /** + * 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; + 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; diff --git a/src/webgl/markerFeatureFS.glsl b/src/webgl/markerFeatureFS.glsl new file mode 100644 index 0000000000..8aa5516c44 --- /dev/null +++ b/src/webgl/markerFeatureFS.glsl @@ -0,0 +1,338 @@ +/* markerFeature common fragment shader */ + +#ifdef GL_ES + precision highp float; +#endif +varying float radiusVar; +varying vec4 fillColorVar; +varying vec4 strokeColorVar; +varying float strokeWidthVar; +varying float symbolVar; /* contains some bit fields */ +varying float symbolValueVar; +varying float rotationVar; +// the square/triangle shader defines unitVar +const int symbolEllipse = 0; +const int symbolFlowerBase = 1; +const int symbolFlowerMax = 16; +const int symbolTriangle = 16; +const int symbolStarBase = 17; +const int symbolStarMax = 16; +const int symbolRectangle = 32; +const int symbolCrossBase = 33; +const int symbolCrossMax = 16; +const int symbolOval = 48; +const int symbolJackBase = 49; +const int symbolJackMax = 16; +const int symbolDrop = 64; +const int symbolDropBase = 65; +const int symbolDropMax = 16; +const int symbolArrow = 80; +const int symbolArrowBase = 81; +const int symbolArrowMax = 16; +// Distance to antialias in pixels +const float antialiasDist = 1.5; + +/* Compute the distance from a point to an arrow defined by the radiusVar and a + * packed value containing + * headWidth: ratio to radius (0, 1] + * headLength: ratio to diameter (0, 1] + * stemWidth: ratio to headWidth [0, 1] + * sweep: boolean + * + * Enter: vec2 pos: the point in pixel coordinates. + * float packed: the packed value. + * Exit: float dist: the distance to the drop in pixels. Negative is inside. + */ +float distanceToArrow(vec2 pos, float value) { + bool sweep = value < 0.0; + value = abs(value); + float pack0 = (mod(value, 252.0) - 1.0) / 250.0; + value = floor(value / 252.0); + float pack1 = (mod(value, 252.0) - 1.0) / 250.0; + value = floor(value / 252.0); + float pack2 = (mod(value, 252.0) - 1.0) / 250.0; + + float headWidth = (pack0 > 0.0 ? pack0 : 2.0 / 3.0); + float headEnd = 1.0 - 2.0 * (pack1 > 0.0 ? pack1 : 1.0 / 2.0); + float stemWidth = headWidth * (pack2 >= 0.0 ? pack2 : 1.0 / 3.0); + if (length(vec2(headWidth, headEnd)) > 1.0) { + vec2 scaledHead = normalize(vec2(headWidth, headEnd)); + headWidth = scaledHead.x; + headEnd = scaledHead.y; + } + float stemEnd = -cos(asin(stemWidth)); + + headWidth *= radiusVar; + headEnd *= radiusVar; + stemWidth *= radiusVar; + stemEnd *= radiusVar; + pos.y = abs(pos.y); + float stemDist = pos.y - stemWidth; + float stemEndDist = stemEnd - pos.x; + float B = (radiusVar - headEnd) / headWidth; + float headDist = (pos.x + B * pos.y - radiusVar) / sqrt(1.0 + B * B); + float headEndDist; + if (sweep) { + B = (radiusVar - headEnd) / 3.0 / headWidth; + headEndDist = -(pos.x + B * pos.y - (radiusVar + headEnd * 2.0) / 3.0) / sqrt(1.0 + B * B); + } else { + headEndDist = headEnd - pos.x; + } + headEndDist = min(headEndDist, stemDist); + return max(max(headDist, headEndDist), stemEndDist); +} + +/* Compute the distance from a point to a drop defined by the radiusVar (the + * semimajor axis) and ratio (between the minor and major radii). + * + * Enter: vec2 pos: the point in pixel coordinates. + * float ratio: ratio of minor / major axes. + * Exit: float dist: the distance to the drop in pixels. Negative is inside. + */ +float distanceToDrop(vec2 pos, float ratio) { + ratio = abs(ratio); + if (ratio == 0.0 || ratio >= 1.0) { + ratio = 1.0; + } + float r = radiusVar * ratio; + float cx = radiusVar - r; + float rad = distance(pos.xy, vec2(cx, 0.0)) - r; + if (ratio < 0.5) { + float x = radiusVar - r - r * r / cx; + float y = sqrt(r * r - (cx - x) * (cx - x)); + float Bt = (cx - x) / y; + if ((pos.x + Bt * abs(pos.y) - cx) / sqrt(1.0 + Bt * Bt) < 0.0) { + float B = -x / y; + return -(pos.x + B * abs(pos.y)) / sqrt(1.0 + B * B); + } + } + return rad; +} + +/* Compute the distance from a point to the ellipse defined by the radiusVar + * (the semimajor axis) and ratio (between the minor and major axes). + * + * Enter: vec2 pos: the point in pixel coordinates. + * float ratio: ratio of minor / major axes. + * Exit: float dist: the distance to the ellipse in pixels. Negative is + * inside. + */ +float distanceToEllipse(vec2 pos, float ratio) { + // a and b are the semi-major and semi-minor axes + // ratio is the between the minor and major axes. If > 1, swap these and + // rotate 90 degrees. + if (ratio == 0.0) { + ratio = 1.0; + } + float a = radiusVar, b = abs(radiusVar * ratio); + if (b > a) { + pos = vec2(pos.y, -pos.x); + b = abs(radiusVar / ratio); + } + float a2 = a * a, b2 = b * b; + // compute the distance to the ellipse. See this discussion: + // https://stackoverflow.com/questions/22959698 + float f = sqrt(a2 - b2), + // this value will be positive if outside and negative if inside. If + // we used it directly, the stroke would be too thick along the long + // edges of the ellipse + d = (distance(pos, vec2(f, 0.0)) + distance(pos, vec2(-f, 0.0))) * 0.5 - a; + // if we are outside of the ellipse, accuracy is not important, so return + // early. + if (d >= 0.0) { + return d; + } + // work in one quadrant + pos = abs(pos); + // t (the angle from center) could start as `atan(pos.y, pos.x)`, but this + // results in a slower solution near narrow ends + float t = 0.7; + float cost = cos(t), sint = sin(t), x = a * cost, y = b * sint; + vec2 lastxy; + for (int iter = 0; iter < 10; iter += 1) { + lastxy = vec2(x, y); + float ex = (a2 - b2) * pow(cost, 3.0) / a, + ey = (b2 - a2) * pow(sint, 3.0) / b; + vec2 r = vec2(x - ex, y - ey), + q = vec2(pos.x - ex, pos.y - ey); + float lenr = length(r); + t += lenr * asin((r.x * q.y - r.y * q.x) / lenr / length(q)) / sqrt(a2 + b2 - x * x - y * y); + t = clamp(t, 0.0, acos(0.0)); + cost = cos(t); + sint = sin(t); + x = a * cost; + y = b * sint; + if (distance(lastxy, vec2(x, y)) < 0.05) { + break; + } + } + return sign(d) * distance(pos, vec2(x, y)); +} + +/* Compute the distance from a point to an isosceles triangle. + * + * Enter: vec2 pos: the point in pixel coordinates. + * float ratio: length of the base compared to the other sides. + * Exit: float dist: the distance to the rectangle in pixels. Negative is + * inside. + */ +float distanceToIsoscelesTriangle(vec2 pos, float ratio) { + ratio = clamp(abs(ratio), 0.0, 2.0); + if (ratio == 0.0 || ratio == 2.0) { + ratio = 1.0; + } + float s1, s2, x0, x1, y1; + if (ratio < sqrt(2.0)) { + s1 = radiusVar * sqrt(4.0 - ratio * ratio); // length of equal sides + s2 = s1 * ratio; + y1 = s2 / 2.0; + x0 = radiusVar; + x1 = x0 - sqrt(s1 * s1 - y1 * y1); + } else { + s2 = radiusVar * 2.0; + s1 = s2 / ratio; + y1 = s2 / 2.0; + x0 = sqrt(s1 * s1 - y1 * y1); + x1 = 0.0; + } + float B = (x0 - x1) / y1; + return max(x1 - pos.x, (pos.x + B * abs(pos.y) - x0) / sqrt(1.0 + B * B)); +} + +/* Compute the distance from a point to an oval defined by the radiusVar (the + * semimajor axis) and ratio (between the minor and major axes). Here an oval + * is defined as two semicircles connected by straight line segments. + * + * Enter: vec2 pos: the point in pixel coordinates. + * float ratio: ratio of minor / major axes. + * Exit: float dist: the distance to the oval in pixels. Negative is inside. + */ +float distanceToOval(vec2 pos, float ratio) { + ratio = abs(ratio); + if (ratio == 0.0 || ratio >= 1.0) { + ratio = 1.0; + } + float minor = radiusVar * ratio; + float center = radiusVar - minor; + pos = abs(pos); + if (pos.x <= center) { + return pos.y - minor; + } + return distance(pos, vec2(center, 0.0)) - minor; +} + +/* Compute the distance from a point to a rectangle defined by the radiusVar + * (the semidiagonal) and ratio (between the minor and major axes). + * + * Enter: vec2 pos: the point in pixel coordinates. + * float ratio: ratio of minor / major axes. + * Exit: float dist: the distance to the rectangle in pixels. Negative is + * inside. + */ +float distanceToRectangle(vec2 pos, float ratio) { + ratio = abs(ratio); + if (ratio == 0.0) { + ratio = 1.0; + } + vec2 wh = normalize(vec2(1.0, abs(ratio))) * radiusVar; + vec2 dist = abs(pos) - wh; + return max(dist.x, dist.y); +} + +/* Based on a repetition value, return a position for rotational symmetry. + * + * Enter: vec2 pos: the point in pixel coordinates. + * int repetitions: number of repetitions on the cross. + * Exit: float dist: a point in the primary position. + */ +vec2 rotationalSymmetry(vec2 pos, int repetitions) { + float pi = acos(-1.0); + float limit = pi / float(repetitions); + float ang = atan(pos.y, pos.x); + ang = mod(ang + pi * 2.0, limit * 2.0); + if (ang > limit) { + ang -= limit * 2.0; + } + return vec2(cos(ang), sin(ang)) * length(pos); +} + +void markerFeatureFragment(vec2 pos) { + // rad is a value in pixels from the edge of the symbol where negative is + // inside the shape + float rad = length(pos.xy) - radiusVar; + // never allow points outside of the main radius + if (rad > 0.0) { + discard; + return; + } + // apply clockwise rotation + if (rotationVar != 0.0) { + float cosr = cos(rotationVar), sinr = sin(rotationVar); + pos = vec2(pos.x * cosr + pos.y * sinr, -pos.x * sinr + pos.y * cosr); + } + + int symbol = int(floor(symbolVar / 16.0)); + bool isimage = bool(mod(floor(symbolVar / 8.0), 2.0)); + vec4 fillColor, strokeColor; + float endStep; + + float ratio = symbolValueVar; + // When ratio is 0, it usually gets changed to 1; some shapes could have + // better defaults + /* Symbol shapes */ + if (symbol == symbolTriangle) { + rad = distanceToIsoscelesTriangle(pos, ratio); + } else if (symbol == symbolRectangle) { + rad = distanceToRectangle(pos, ratio); + } else if (symbol == symbolOval) { + rad = distanceToOval(pos, ratio); + } else if (symbol == symbolDrop) { + rad = distanceToDrop(pos, ratio); + } else if (symbol == symbolArrow) { + rad = distanceToArrow(pos, ratio); + } else if (symbol >= symbolCrossBase && symbol <= symbolCrossBase + symbolCrossMax - 2) { + rad = distanceToRectangle(rotationalSymmetry(pos, symbol - symbolCrossBase + 2), ratio); + } else if (symbol >= symbolFlowerBase && symbol <= symbolFlowerBase + symbolFlowerMax - 2) { + rad = distanceToEllipse(rotationalSymmetry(pos, symbol - symbolFlowerBase + 2), ratio); + } else if (symbol >= symbolStarBase && symbol <= symbolStarBase + symbolStarMax - 2) { + rad = distanceToIsoscelesTriangle(rotationalSymmetry(pos, symbol - symbolStarBase + 2), ratio); + } else if (symbol >= symbolJackBase && symbol <= symbolJackBase + symbolJackMax - 2) { + rad = distanceToOval(rotationalSymmetry(pos, symbol - symbolJackBase + 2), ratio); + } else if (symbol >= symbolDropBase && symbol <= symbolDropBase + symbolDropMax - 2) { + rad = distanceToDrop(rotationalSymmetry(pos, symbol - symbolDropBase + 2), ratio); + } else if (symbol >= symbolArrowBase && symbol <= symbolArrowBase + symbolArrowMax - 2) { + rad = distanceToArrow(rotationalSymmetry(pos, symbol - symbolArrowBase + 2), ratio); + } else { // default - circle or ellipse; a value of 0 or 1 is a circle + if (ratio != 0.0 && ratio != 1.0) { + rad = distanceToEllipse(pos, ratio); + } + } + + if (rad >= 0.0) { + discard; + return; + } + // If there is no stroke, the fill region should transition to nothing + if (strokeColorVar.a == 0.0 || strokeWidthVar <= 0.0) { + strokeColor = vec4(fillColorVar.rgb, 0.0); + endStep = 0.0; + } else { + strokeColor = strokeColorVar; + endStep = -strokeWidthVar; + } + // Likewise, if there is no fill, the stroke should transition to nothing + if (fillColorVar.a == 0.0) { + fillColor = vec4(strokeColorVar.rgb, 0.0); + } else { + fillColor = fillColorVar; + } + if (rad <= endStep) { + float step = smoothstep(endStep - antialiasDist, endStep, rad); + vec4 color = mix(fillColor, strokeColor, step); + float step2 = smoothstep(-antialiasDist, 0.0, rad); + gl_FragColor = mix(color, vec4(color.rgb, 0.0), step2); + } else { + float step = smoothstep(-antialiasDist, 0.0, rad); + gl_FragColor = mix(strokeColor, vec4(strokeColor.rgb, 0.0), step); + } +} diff --git a/src/webgl/markerFeaturePoly.frag b/src/webgl/markerFeaturePoly.frag new file mode 100644 index 0000000000..8192976e24 --- /dev/null +++ b/src/webgl/markerFeaturePoly.frag @@ -0,0 +1,11 @@ +/* markerFeature square/triangle fragment shader */ + +$markerFeatureFS + +varying vec2 unitVar; // distinct for square/triangle + +void main() { + if (fillColorVar.a == 0.0 && strokeColorVar.a == 0.0) + discard; + markerFeatureFragment(unitVar); +} diff --git a/src/webgl/markerFeaturePoly.vert b/src/webgl/markerFeaturePoly.vert new file mode 100644 index 0000000000..ae69f55fac --- /dev/null +++ b/src/webgl/markerFeaturePoly.vert @@ -0,0 +1,25 @@ +/* markerFeature square/triangle vertex shader */ + +$markerFeatureVS + +uniform float pixelWidth; // for non-sprite +uniform float aspect; // for non-sprite +attribute vec2 unit; // for non-sprite +varying vec2 unitVar; // for non-sprite + +void main(void) +{ + radiusVar = markerFeaturePrep(); + if (radiusVar == 0.0) { + return; + } + // for non-sprite + unitVar = unit * radiusVar; + unitVar.y *= -1.0; + vec4 p = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw; + if (p.w != 0.0) { + p = p / p.w; + } + p += radiusVar * vec4(unit.x * pixelWidth, unit.y * pixelWidth * aspect, 0.0, 1.0); + gl_Position = vec4(p.xyz, 1.0); +} diff --git a/src/webgl/markerFeatureSprite.frag b/src/webgl/markerFeatureSprite.frag new file mode 100644 index 0000000000..a88c31d6ff --- /dev/null +++ b/src/webgl/markerFeatureSprite.frag @@ -0,0 +1,14 @@ +/* markerFeature sprite fragment shader */ + +$markerFeatureFS + +// the square/triangle shader defines unitVar + +void main(void) { + // No stroke or fill implies nothing to draw + if (fillColorVar.a == 0.0 && strokeColorVar.a == 0.0) + discard; + // for sprites, convert the position to [-radius,radius],[-radius,radius] + vec2 pos = (gl_PointCoord.xy - 0.5) * 2.0 * radiusVar; + markerFeatureFragment(pos); +} diff --git a/src/webgl/markerFeatureSprite.vert b/src/webgl/markerFeatureSprite.vert new file mode 100644 index 0000000000..cd71d09a6a --- /dev/null +++ b/src/webgl/markerFeatureSprite.vert @@ -0,0 +1,14 @@ +/* markerFeature sprite vertex shader */ + +$markerFeatureVS + +void main(void) +{ + radiusVar = markerFeaturePrep(); + if (radiusVar == 0.0) { + return; + } + // for sprite + gl_Position = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw; + gl_PointSize = 2.0 * radiusVar; +} diff --git a/src/webgl/markerFeatureVS.glsl b/src/webgl/markerFeatureVS.glsl new file mode 100644 index 0000000000..4fdc8bfd5f --- /dev/null +++ b/src/webgl/markerFeatureVS.glsl @@ -0,0 +1,57 @@ +/* markerFeature common vertex shader */ + +#ifdef GL_ES + precision highp float; +#endif +attribute vec3 pos; +attribute float radius; +attribute vec3 fillColor; +attribute float fillOpacity; +attribute vec3 strokeColor; +attribute float strokeOpacity; +attribute float strokeWidth; +attribute float symbol; /* contains some bit fields */ +attribute float symbolValue; +attribute float rotation; +uniform float zoom; +uniform float rotationUniform; +uniform mat4 modelViewMatrix; +uniform mat4 projectionMatrix; +// non-sprite has other definitions. +varying float radiusVar; +varying vec4 fillColorVar; +varying vec4 strokeColorVar; +varying float strokeWidthVar; +varying float symbolVar; /* contains some bit fields */ +varying float symbolValueVar; +varying float rotationVar; + +float markerFeaturePrep(void) +{ + // No stroke or fill implies nothing to draw + if (radius <= 0.0 || (strokeOpacity <= 0.0 && fillOpacity <= 0.0)) { + gl_Position = vec4(2.0, 2.0, 0.0, 1.0); + return 0.0; + } + radiusVar = radius; + strokeWidthVar = strokeWidth; + int scaleMode = int(mod(symbol, 4.0)); + if (scaleMode == 1) { // fill + radiusVar = (radiusVar - strokeWidthVar) * exp2(zoom) + strokeWidthVar; + } else if (scaleMode == 2) { // stroke + radiusVar += strokeWidthVar * (exp2(zoom) - 1.0); + strokeWidthVar *= exp2(zoom); + } else if (scaleMode == 3) { // all + radiusVar *= exp2(zoom); + strokeWidthVar *= exp2(zoom); + } + fillColorVar = vec4(fillColor, fillOpacity); + strokeColorVar = vec4(strokeColor, strokeOpacity); + symbolVar = symbol; + symbolValueVar = symbolValue; + rotationVar = rotation; + if (bool(mod(floor(symbolVar / 4.0), 2.0))) { + rotationVar += rotationUniform; + } + return radiusVar; +} diff --git a/src/webgl/pointFeature.js b/src/webgl/pointFeature.js index 2bc4f58fa2..5fb0f3d37c 100644 --- a/src/webgl/pointFeature.js +++ b/src/webgl/pointFeature.js @@ -25,6 +25,7 @@ var webgl_pointFeature = function (arg) { 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'); @@ -42,22 +43,13 @@ var webgl_pointFeature = function (arg) { m_pixelWidthUniform = null, m_aspectUniform = null, m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, - m_primitiveShapeAuto = true, - m_primitiveShape = pointFeature.primitiveShapes.auto, // arg can change this, below m_modelViewUniform, m_origin, s_init = this._init, s_update = this._update, s_updateStyleFromArray = this.updateStyleFromArray; - if (pointFeature.primitiveShapes[arg.primitiveShape] !== undefined) { - m_primitiveShape = arg.primitiveShape; - } - m_primitiveShapeAuto = m_primitiveShape === pointFeature.primitiveShapes.auto; - if (m_primitiveShapeAuto) { - m_primitiveShape = pointFeature.primitiveShapes.sprite; - m_primitiveShapeAuto = true; - } + pointUtil(m_this, arg); /** * Create the vertex shader for points. @@ -67,7 +59,7 @@ var webgl_pointFeature = function (arg) { function createVertexShader() { var shader = new vgl.shader(vgl.GL.VERTEX_SHADER); shader.setShaderSource( - m_primitiveShape === pointFeature.primitiveShapes.sprite ? vertexShaderSprite : vertexShaderPoly); + m_this._primitiveShape === pointFeature.primitiveShapes.sprite ? vertexShaderSprite : vertexShaderPoly); return shader; } @@ -79,53 +71,10 @@ var webgl_pointFeature = function (arg) { function createFragmentShader() { var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); shader.setShaderSource( - m_primitiveShape === pointFeature.primitiveShapes.sprite ? fragmentShaderSprite : fragmentShaderPoly); + m_this._primitiveShape === pointFeature.primitiveShapes.sprite ? fragmentShaderSprite : fragmentShaderPoly); return shader; } - /** - * Given the current primitive shape and a basic size, return a set of - * vertices that can be used for a generic point. - * - * @param {number} x The base x coordinate. Usually 0. - * @param {number} y The base y coordinate. Usually 0. - * @param {number} w The base width. Usually 1. - * @param {number} h The base height. Usually 1. - * @returns {number[]} A flat array of vertices in the form of - * `[x0, y0, x1, y1, ...]`. - */ - function pointPolygon(x, y, w, h) { - var verts; - switch (m_primitiveShape) { - case pointFeature.primitiveShapes.triangle: - /* Use an equilateral triangle. While this has 30% more area than a - * square, the reduction in vertices should help more than the - * processing the additional fragments. */ - verts = [ - x, y - h * 2, - x - w * Math.sqrt(3.0), y + h, - x + w * Math.sqrt(3.0), y + h - ]; - break; - case pointFeature.primitiveShapes.square: - /* Use a surrounding square split diagonally into two triangles. */ - verts = [ - x - w, y + h, - x - w, y - h, - x + w, y + h, - x - w, y - h, - x + w, y - h, - x + w, y + h - ]; - break; - default: // sprite - /* Point sprite uses only one vertex per point. */ - verts = [x, y]; - break; - } - return verts; - } - /** * Create and style the data needed to render the points. * @@ -135,7 +84,7 @@ var webgl_pointFeature = function (arg) { function createGLPoints(onlyStyle) { // unit and associated data is not used when drawing sprite var i, j, numPts = m_this.data().length, - unit = pointPolygon(0, 0, 1, 1), + unit = m_this._pointPolygon(0, 0, 1, 1), position = new Array(numPts * 3), posBuf, posVal, posFunc, unitBuf, indices, radius, radiusVal, radFunc, @@ -205,7 +154,7 @@ var webgl_pointFeature = function (arg) { for (i = ivpf = ivpf3 = iunit = i3 = 0; i < numPts; i += 1, i3 += 3) { item = data[i]; if (!onlyStyle) { - if (m_primitiveShape !== pointFeature.primitiveShapes.sprite) { + if (m_this._primitiveShape !== pointFeature.primitiveShapes.sprite) { for (j = 0; j < unit.length; j += 1, iunit += 1) { unitBuf[iunit] = unit[j]; } @@ -220,7 +169,7 @@ var webgl_pointFeature = function (arg) { fillVal = fillFunc(item, i) ? 1.0 : 0.0; fillOpacityVal = fillOpacityFunc(item, i); fillColorVal = fillColorFunc(item, i); - if (m_primitiveShapeAuto && + if (m_this._primitiveShapeAuto && ((fillVal && fillOpacityVal) || (strokeVal && strokeOpacityVal)) && radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0) > maxr) { maxr = radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0); @@ -246,11 +195,11 @@ var webgl_pointFeature = function (arg) { } } - if (m_primitiveShapeAuto && - ((m_primitiveShape === pointFeature.primitiveShapes.sprite && maxr > webglRenderer._maxPointSize) || - (m_primitiveShape !== pointFeature.primitiveShapes.sprite && maxr <= webglRenderer._maxPointSize))) { + 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_primitiveShape = maxr > webglRenderer._maxPointSize ? pointFeature.primitiveShapes.triangle : pointFeature.primitiveShapes.sprite; + 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); @@ -286,16 +235,6 @@ var webgl_pointFeature = function (arg) { return [m_actor]; }; - /** - * Return the number of vertices used for each point. - * - * @returns {number} - */ - this.verticesPerFeature = function () { - var unit = pointPolygon(0, 0, 1, 1); - return unit.length / 2; - }; - /** * 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 @@ -394,41 +333,6 @@ var webgl_pointFeature = function (arg) { return m_this; }; - /** - * Get or set the primitiveShape. - * - * @param {geo.pointFeature.primitiveShapes} [primitiveShape] If specified, - * the new primitive shape. - * @param {boolean} [currentShape] If truthy and getting the shape, return - * the shape currently in use if the shape is set to `auto`. If falsy, - * return the specifiec primitiveShape, which may be `auto`. - * @returns {geo.pointFeature.primitiveShapes|this} The primitiveShape or - * this instance of the feature. - */ - this.primitiveShape = function (primitiveShape, currentShape) { - if (primitiveShape === undefined) { - return currentShape || !m_primitiveShapeAuto ? m_primitiveShape : pointFeature.primitiveShapes.auto; - } - if (pointFeature.primitiveShapes[primitiveShape] !== undefined) { - var update = false; - if (primitiveShape === pointFeature.primitiveShapes.auto) { - update = !m_primitiveShapeAuto; - m_primitiveShapeAuto = true; - } else { - update = m_primitiveShapeAuto || m_primitiveShape !== primitiveShape; - m_primitiveShapeAuto = false; - m_primitiveShape = primitiveShape; - } - if (update) { - m_this.renderer().contextRenderer().removeActor(m_actor); - m_actor = null; - m_this._init(true); - m_this.modified(); - } - } - return m_this; - }; - /** * Initialize. * @@ -474,11 +378,12 @@ var webgl_pointFeature = function (arg) { primitive; m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', m_origin); - if (m_primitiveShape === pointFeature.primitiveShapes.sprite) { + 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()); @@ -491,7 +396,7 @@ var webgl_pointFeature = function (arg) { m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); - if (m_primitiveShape !== pointFeature.primitiveShapes.sprite) { + if (m_this._primitiveShape !== pointFeature.primitiveShapes.sprite) { prog.addVertexAttribute(unitAttr, vgl.vertexAttributeKeysIndexed.One); } diff --git a/src/webgl/pointFeaturePoly.vert b/src/webgl/pointFeaturePoly.vert index d27b6c2d3f..a09b780108 100644 --- a/src/webgl/pointFeaturePoly.vert +++ b/src/webgl/pointFeaturePoly.vert @@ -20,6 +20,6 @@ void main(void) p = p / p.w; } p += (radius + strokeWidthVar) * - vec4 (unit.x * pixelWidth, unit.y * pixelWidth * aspect, 0.0, 1.0); + vec4(unit.x * pixelWidth, unit.y * pixelWidth * aspect, 0.0, 1.0); gl_Position = vec4(p.xyz, 1.0); } diff --git a/src/webgl/pointUtil.js b/src/webgl/pointUtil.js new file mode 100644 index 0000000000..1127c7ac98 --- /dev/null +++ b/src/webgl/pointUtil.js @@ -0,0 +1,112 @@ +var pointFeature = require('../pointFeature'); + +/** + * Extend a point-like feature with additional functions. + * + * @param {this} m_this The point-like feature. + * @param {object} [arg] Feature defintion object that might specify the + * primitive shape. + */ +function pointUtil(m_this, arg) { + arg = arg || {}; + + m_this._primitiveShapeAuto = true; + m_this._primitiveShape = pointFeature.primitiveShapes.auto; + if (pointFeature.primitiveShapes[arg.primitiveShape] !== undefined) { + m_this._primitiveShape = arg.primitiveShape; + } + m_this._primitiveShapeAuto = m_this._primitiveShape === pointFeature.primitiveShapes.auto; + if (m_this._primitiveShapeAuto) { + m_this._primitiveShape = pointFeature.primitiveShapes.sprite; + m_this._primitiveShapeAuto = true; + } + + /** + * Given the current primitive shape and a basic size, return a set of + * vertices that can be used for a generic point. + * + * @param {number} x The base x coordinate. Usually 0. + * @param {number} y The base y coordinate. Usually 0. + * @param {number} w The base width. Usually 1. + * @param {number} h The base height. Usually 1. + * @returns {number[]} A flat array of vertices in the form of + * `[x0, y0, x1, y1, ...]`. + */ + m_this._pointPolygon = function (x, y, w, h) { + var verts; + switch (m_this._primitiveShape) { + case pointFeature.primitiveShapes.triangle: + /* Use an equilateral triangle. While this has 30% more area than a + * square, the reduction in vertices should help more than the + * processing the additional fragments. */ + verts = [ + x, y - h * 2, + x - w * Math.sqrt(3.0), y + h, + x + w * Math.sqrt(3.0), y + h + ]; + break; + case pointFeature.primitiveShapes.square: + /* Use a surrounding square split diagonally into two triangles. */ + verts = [ + x - w, y + h, + x - w, y - h, + x + w, y + h, + x - w, y - h, + x + w, y - h, + x + w, y + h + ]; + break; + default: // sprite + /* Point sprite uses only one vertex per point. */ + verts = [x, y]; + break; + } + return verts; + }; + + /** + * Return the number of vertices used for each point. + * + * @returns {number} + */ + m_this.verticesPerFeature = function () { + var unit = m_this._pointPolygon(0, 0, 1, 1); + return unit.length / 2; + }; + + /** + * Get or set the primitiveShape. + * + * @param {geo.pointFeature.primitiveShapes} [primitiveShape] If specified, + * the new primitive shape. + * @param {boolean} [currentShape] If truthy and getting the shape, return + * the shape currently in use if the shape is set to `auto`. If falsy, + * return the specifiec primitiveShape, which may be `auto`. + * @returns {geo.pointFeature.primitiveShapes|this} The primitiveShape or + * this instance of the feature. + */ + m_this.primitiveShape = function (primitiveShape, currentShape) { + if (primitiveShape === undefined) { + return currentShape || !m_this._primitiveShapeAuto ? m_this._primitiveShape : pointFeature.primitiveShapes.auto; + } + if (pointFeature.primitiveShapes[primitiveShape] !== undefined) { + var update = false; + if (primitiveShape === pointFeature.primitiveShapes.auto) { + update = !m_this._primitiveShapeAuto; + m_this._primitiveShapeAuto = true; + } else { + update = m_this._primitiveShapeAuto || m_this._primitiveShape !== primitiveShape; + m_this._primitiveShapeAuto = false; + m_this._primitiveShape = primitiveShape; + } + if (update) { + m_this.renderer().contextRenderer().removeActor(m_this.actors()[0]); + m_this._init(true); + m_this.modified(); + } + } + return m_this; + }; +} + +module.exports = pointUtil; diff --git a/tests/cases/markerFeature.js b/tests/cases/markerFeature.js new file mode 100644 index 0000000000..5db3aee54d --- /dev/null +++ b/tests/cases/markerFeature.js @@ -0,0 +1,203 @@ +// Test geo.markerFeature, geo.svg.markerFeature, and geo.webgl.markerFeature + +var $ = require('jquery'); +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; +var vgl = require('vgl'); +var waitForIt = require('../test-utils').waitForIt; + +describe('geo.markerFeature', function () { + 'use strict'; + + var testMarkers = [ + {x: 20, y: 10}, + {x: 25, y: 10, radius: 1, strokeWidth: 0.1, scaleWithZoom: geo.markerFeature.scaleMode.all}, + {x: 30, y: 10, radius: 2.5, scaleWithZoom: geo.markerFeature.scaleMode.fill}, + {x: 35, y: 10, strokeWidth: 0.1, scaleWithZoom: geo.markerFeature.scaleMode.stroke}, + {x: 40, y: 10, radius: 10, strokeWidth: 0.1, scaleWithZoom: geo.markerFeature.scaleMode.stroke} + ]; + + describe('create', function () { + it('create function', function () { + mockWebglRenderer(); + var map, layer, marker; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'webgl'}); + marker = geo.markerFeature.create(layer); + expect(marker instanceof geo.markerFeature).toBe(true); + destroyMap(); + restoreWebglRenderer(); + }); + it('direct create', function () { + mockWebglRenderer(); + var map, layer, marker; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'webgl'}); + marker = geo.markerFeature({layer: layer}); + expect(marker instanceof geo.markerFeature).toBe(true); + destroyMap(); + restoreWebglRenderer(); + }); + }); + + describe('Public utility methods', function () { + it('pointSearch', function () { + mockWebglRenderer(); + var map, layer, marker, pt, p, data = testMarkers; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'webgl'}); + marker = layer.createFeature('marker', {selectionAPI: true}); + marker.data(data).style({ + radius: function (d) { return d.radius || 5; }, + strokeWidth: function (d) { return d.strokeWidth || 2; }, + scaleWithZoom: function (d) { return d.scaleWithZoom; } + }); + pt = marker.pointSearch({x: 20, y: 10}); + expect(pt.index).toEqual([0]); + expect(pt.found.length).toBe(1); + expect(pt.found[0]).toEqual(data[0]); + /* We should land on the marker if we are near the specified radius */ + var psTest = [ + // radius = 5, strokeWidth = 2 + {x: 20, y: 10, zoom3: 5, zoom4: 5}, + // radius = zoom * 1, strokeWidth = zoom * 0.1 + {x: 25, y: 10, zoom3: 8, zoom4: 16}, + // radius = zoom * (2.5 - 2), strokeWidth = 2 + {x: 30, y: 10, zoom3: 6, zoom4: 10}, + // radius = 5 - 0.1 + zoom * 0.1, strokeWidth = zoom * 0.1 + {x: 35, y: 10, zoom3: 5.7, zoom4: 6.5} + ]; + [3, 4].forEach(function (zoomVal) { + map.zoom(zoomVal); + var zoomKey = 'zoom' + zoomVal; + psTest.forEach(function (entry) { + p = marker.featureGcsToDisplay(entry); + pt = marker.pointSearch(map.displayToGcs({x: p.x, y: p.y})); + expect(pt.found.length).toBe(1); + pt = marker.pointSearch(map.displayToGcs({x: p.x, y: p.y + entry[zoomKey] - 0.5})); + expect(pt.found.length).toBe(1); + pt = marker.pointSearch(map.displayToGcs({x: p.x, y: p.y + entry[zoomKey] + 0.5})); + expect(pt.found.length).toBe(0); + }); + }); + /* If we have zero-length data, we get no matches */ + marker.data([]); + pt = marker.pointSearch({x: 22, y: 10}); + expect(pt.found.length).toBe(0); + /* Exceptions will be returned properly */ + marker.data(data).style('strokeWidth', function (d, idx) { + throw new Error('no width'); + }); + expect(function () { + marker.pointSearch({x: 20, y: 10}); + }).toThrow(new Error('no width')); + /* Stop throwing the exception */ + marker.style('strokeWidth', 2); + /* Test with an alternate gcs; pointSearch always uses the map's ingcs */ + marker.gcs(map.gcs()); + marker.data([{x: 2226390, y: 1118890}]); + pt = marker.pointSearch({x: 20, y: 10}); + expect(pt.index).toEqual([0]); + expect(pt.found.length).toBe(1); + destroyMap(); + restoreWebglRenderer(); + }); + it('polygonSearch', function () { + mockWebglRenderer(); + var map, layer, marker, result, data = testMarkers; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'webgl'}); + marker = layer.createFeature('marker', {selectionAPI: true}); + marker.data(data).style({ + radius: function (d) { return d.radius || 5; }, + strokeWidth: function (d) { return d.strokeWidth || 2; }, + scaleWithZoom: function (d) { return d.scaleWithZoom; } + }); + result = marker.polygonSearch([{x: 19, y: 8}, {x: 27, y: 8}, {x: 27, y: 12}, {x: 19, y: 12}]); + expect(result.index).toEqual([0, 1]); + result = marker.polygonSearch([{x: 19, y: 10}, {x: 27, y: 10}]); + expect(result.index).toEqual([]); + result = marker.polygonSearch([{x: 20, y: 8}, {x: 27, y: 8}, {x: 27, y: 12}, {x: 20, y: 12}]); + expect(result.index).toEqual([1]); + result = marker.polygonSearch([{x: 35, y: 8}, {x: 35.01, y: 8}, {x: 35, y: 12}]); + expect(result.index).toEqual([]); + result = marker.polygonSearch([{x: 35, y: 8}, {x: 35.01, y: 8}, {x: 35, y: 12}], {partial: true}); + expect(result.index).toEqual([3]); + destroyMap(); + restoreWebglRenderer(); + }); + }); + + /* This is a basic integration test of geo.webgl.markerFeature. */ + describe('geo.webgl.markerFeature', function () { + var map, layer, marker, marker2, glCounts; + it('basic usage', function () { + mockWebglRenderer(); + map = createMap(); + layer = map.createLayer('feature', {renderer: 'webgl'}); + marker = layer.createFeature('marker'); + marker.data(testMarkers).style({ + radius: function (d) { return d.radius || 5; }, + strokeWidth: function (d) { return d.strokeWidth || 2; }, + scaleWithZoom: function (d) { return d.scaleWithZoom; }, + symbol: geo.markerFeature.symbols.arrow, + symbolValue: function (d, i) { return i ? [1, 1, 1 / 5, true] : 0.5; } + }); + glCounts = $.extend({}, vgl.mockCounts()); + marker.draw(); + expect(marker.verticesPerFeature()).toBe(3); + }); + waitForIt('next render gl A', function () { + return vgl.mockCounts().createProgram >= (glCounts.createProgram || 0) + 1; + }); + it('other primitive shapes', function () { + expect(marker.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.auto); + expect(marker.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.triangle); + marker2 = layer.createFeature('marker', { + primitiveShape: geo.markerFeature.primitiveShapes.triangle + }).data(testMarkers); + expect(marker2.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.triangle); + expect(marker2.verticesPerFeature()).toBe(3); + layer.deleteFeature(marker2); + marker2 = layer.createFeature('marker', { + primitiveShape: geo.markerFeature.primitiveShapes.square + }).data(testMarkers); + expect(marker2.verticesPerFeature()).toBe(6); + glCounts = $.extend({}, vgl.mockCounts()); + marker2.draw(); + }); + waitForIt('next render gl B', function () { + return vgl.mockCounts().drawArrays >= (glCounts.drawArrays || 0) + 1; + }); + it('change primitive shapes', function () { + expect(marker2.primitiveShape(geo.markerFeature.primitiveShapes.auto)).toBe(marker2); + marker2.draw(); + expect(marker2.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.auto); + expect(marker2.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.sprite); + marker2.style('radius', 20000); + marker2.draw(); + expect(marker2.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.auto); + expect(marker2.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.triangle); + marker2.style('radius', 20); + marker2.draw(); + expect(marker2.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.auto); + expect(marker2.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.sprite); + expect(marker2.primitiveShape(geo.markerFeature.primitiveShapes.triangle)).toBe(marker2); + marker2.draw(); + expect(marker2.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.triangle); + expect(marker2.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.triangle); + }); + it('_exit', function () { + expect(marker.actors().length).toBe(1); + layer.deleteFeature(marker); + expect(marker.actors().length).toBe(0); + marker.data(testMarkers); + map.draw(); + destroyMap(); + restoreWebglRenderer(); + }); + }); +}); diff --git a/tutorials/marker/index.pug b/tutorials/marker/index.pug new file mode 100644 index 0000000000..0856cb8a7d --- /dev/null +++ b/tutorials/marker/index.pug @@ -0,0 +1,143 @@ +extends ../common/index.pug + +block mainTutorial + :markdown-it + # Tutorial - Markers + Create a map with a tile layer, define some data, and create a feature layer. + + +codeblock('javascript', 1). + var map = geo.map({ + node: "#map", + center: { x: -97.67, y: 31.80 }, + zoom: 4 + }); + map.createLayer('osm'); + + var cities = [ + {lon: -74.0059413, lat: 40.7127837, name: "New York", population: 8405837}, + {lon: -118.2436849, lat: 34.0522342, name: "Los Angeles", population: 3884307}, + {lon: -87.6297982, lat: 41.8781136, name: "Chicago", population: 2718782}, + {lon: -95.3698028, lat: 29.7604267, name: "Houston", population: 2195914}, + {lon: -75.1652215, lat: 39.9525839, name: "Philadelphia", population: 1553165}, + {lon: -112.0740373, lat: 33.4483771, name: "Phoenix", population: 1513367} + ]; + + var layer = map.createLayer('feature', {features: ['marker']}); + + + :markdown-it + Create a marker feature and set the data. The default marker feature looks just line a point feature -- circles with borders. + + +codeblock('javascript', 2)(webgl=true). + var feature = layer.createFeature('marker') + .data(cities) + .position(function (city) { + return { + x: city.lon, + y: city.lat + }; + }) + .draw(); + + +codeblock_test('map has a feature layer with six markers', [ + 'map.layers().length === 2', + 'map.layers()[1] instanceof geo.featureLayer', + 'map.layers()[1].features()[0] instanceof geo.markerFeature', + 'map.layers()[1].features()[0].data().length === 6' + ]) + + :markdown-it + Markers have many different symbols and style options: + + - `symbol`: the symbol shape, enumerated in `geo.markerFeature.symbols`. This includes `ellipse`, `triangle` (isosceles triangles), `rectangle`, `oval`, and `drop`. There are symmetric repeating patterns such as `cross4`, `flower6`, `star5`, `jack6`, and `drop8`, where the number is the rotational symmetry from 2 to 16. + - `symbolValue`: a number that modifies how the symbol appears, usually in the range of [0, 1]. This is often the ratio of the minor axis of the symbol to its major axis. For `triangle`, it is the length of the base side compared to the identical sides. + - `rotation`: the rotation in radians. + - `rotateWithMap`: a boolean; if true, the symbol rotates when the map is rotated. If false, it remains in the same orientation on the screen. + - `radius`: in pixels. This includes the stroke width, if any. + - `strokeWidth`: the width of the stroke around the symbol in pixels. + - `scaleWithZoom`: one of the values defined in `geo.markerFeature.scaleModes`. If `none`, the symbols remains the same size when the map is zoomed. This can also be `fill`, `stroke`, or `all`, in which case the symbol will change size when the map is zoomed. The `radius` and `strokeWidth` are in pixels at zoom level 0. + - `fillColor` + - `fillOpacity` + - `strokeColor` + - `strokeOpacity`: + + +codeblock('javascript', 3, undefined, true). + feature.style({ + symbol: geo.markerFeature.symbols.drop, + symbolValue: 1 / 3, + rotation: -Math.PI / 2, + radius: 30, + strokeWidth: 5, + strokeColor: 'blue', + fillColor: 'yellow', + rotateWithMap: false + }) + .draw(); + + :markdown-it + When scaling a marker with the map, the radius and stroke width are the values at zoom level 0. + + +codeblock('javascript', 4). + feature.style({ + symbol: geo.markerFeature.symbols.star5, + symbolValue: Math.sin(Math.PI / 5), + radius: 1, + strokeWidth: 0.1, + scaleWithZoom: geo.markerFeature.scaleMode.all, + rotateWithMap: true + }) + .draw(); + + :markdown-it + Markers are drawn efficiently, even if they all have different styles. This creates a large sample of symbols, each with a different style. + + +codeblock('javascript', 5). + let count = 1000, markers = []; + + for (let i = 0; i < count; i += 1) { + let symbol = Math.floor(Math.random() * geo.markerFeature.symbols.length); + let symbolValue = Math.random(); + if (symbol === geo.markerFeature.symbols.triangle) { + // triangles use a larger range than most symbols + symbolValue *= 2; + } else if (symbol >= geo.markerFeature.symbols.arrow && symbol < geo.markerFeature.symbols.arrow + geo.markerFeature.symbols.arrowMax) { + // arrow symbols can take an array to specify their shape + symbolValue = [ + Math.random(), + Math.random(), + Math.random() > 0.1 ? Math.random() : 0, + Math.random() >= 0.5 + ]; + if (symbol >= geo.markerFeature.symbols.arrowBase) { + symbolValue[0] /= 2; + symbolValue[1] /= 2; + symbolValue[2] /= 2; + } + } + markers.push({ + x: Math.random() * 50 - 120, + y: Math.random() * 40 + 10, + radius: Math.random() * 30 + 5, + strokeWidth: Math.random() > 0.1 ? Math.random() * 5 : 0, + symbol: symbol, + symbolValue: symbolValue, + fillColor: {r: Math.random(), g: Math.random(), b: Math.random()}, + fillOpacity: Math.random(), + strokeColor: {r: Math.random(), g: Math.random(), b: Math.random()}, + strokeOpacity: 0.5 + Math.random() * 0.5, + rotation: Math.random() * Math.PI * 2 + }); + } + feature.data(markers).position(d => d).style({ + radius: d => d.radius, + strokeWidth: d => d.strokeWidth, + symbol: d => d.symbol, + symbolValue: d => d.symbolValue, + fillColor: d => d.fillColor, + fillOpacity: d => d.fillOpacity, + strokeColor: d => d.strokeColor, + strokeOpacity: d => d.strokeOpacity, + rotation: d => d.rotation, + scaleWithZoom: geo.markerFeature.scaleMode.none, + rotateWithMap: true + }).draw(); diff --git a/tutorials/marker/thumb.jpg b/tutorials/marker/thumb.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c243f540a74cf86bf1ccd2eb862dc26307586efa GIT binary patch literal 117815 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 I-;58r6FXqzW=UWKy{#x0j;&-F0RT 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 literal 0 HcmV?d00001 diff --git a/tutorials/marker/tutorial.json b/tutorials/marker/tutorial.json new file mode 100644 index 0000000000..3c299a464d --- /dev/null +++ b/tutorials/marker/tutorial.json @@ -0,0 +1,10 @@ +{ + "title": "Markers", + "hideNavbar": true, + "level": 0, + "tutorialCss": [], + "tutorialJs": [], + "about": { + "text": "Use markers with different styles." + } +} From 76f712c63ae9906d54c4d79e88b12f7da36e481e Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 10 Oct 2019 10:46:17 -0400 Subject: [PATCH 2/2] Add a strokeOffset option to markers. This is useful, for instance, when using the ellipse marker and having the ellipse scale with the map. Setting the strokeOffset to 0 gives behavior similar to a polygon. Setting the strokeOffset to 1 allows setting the radius and strokeWidth in the same manner as the pointFeature. The default strokeOffset of -1 allows the radius to be the actual maximal size of the marker. --- CHANGELOG.md | 8 ++-- src/markerFeature.js | 74 +++++++++++++++++++++++++--------- src/webgl/markerFeature.js | 62 +++++++++++++++++++--------- src/webgl/markerFeatureFS.glsl | 2 +- src/webgl/markerFeatureVS.glsl | 2 + src/webgl/pointUtil.js | 2 +- tests/cases/markerFeature.js | 33 ++++++++++++++- 7 files changed, 139 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b17275395..3a529ad0ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,15 @@ # Change Log +## Unreleased + +### Features +- Added a marker feature (#1035) + ## Version 0.19.8 ### Changes - Line segments with zero width or zero opacity won't be found by pointSearch or polygonSearch (#1041) -### Features -- Added a marker feature (#1035) - ### Bug Fixes - Removed extra calls to sceneObject constructors (#1039) - Fixed an issue with rendering on hidden tabs in Chrome (#1042) diff --git a/src/markerFeature.js b/src/markerFeature.js index 65cdd175e2..5f1d36506b 100644 --- a/src/markerFeature.js +++ b/src/markerFeature.js @@ -18,7 +18,9 @@ var pointFeature = require('./pointFeature'); * @typedef {geo.feature.styleSpec} geo.markerFeature.styleSpec * @extends geo.feature.styleSpec * @property {number|function} [radius=5] Radius of each marker in pixels. - * This includes the stroke width and the fill. + * This includes the stroke width if `strokeOffset` is -1, excludes it if + * `strokeOffset` is 1, and includes half the stroke width if `strokeOffset` + * is 0. * @property {geo.geoColor|function} [strokeColor] Color to stroke each marker. * @property {number|function} [strokeOpacity=1] Opacity for each marker's * stroke. Opacity is on a [0-1] scale. Set this or `strokeWidth` to zero @@ -26,6 +28,9 @@ var pointFeature = require('./pointFeature'); * @property {number|function} [strokeWidth=1.25] The weight of the marker's * stroke in pixels. Set this or `strokeOpacity` to zero to not have a * stroke. + * @property {number|function} [strokeOffset=-1] The position of the stroke + * compared to the radius. This can only be -1, 0, or 1 (the sign of the + * value is used). * @property {geo.geoColor|function} [fillColor] Color to fill each marker. * @property {number|function} [fillOpacity=1] Opacity for each marker. * Opacity is on a [0-1] scale. Set to zero to have no fill. @@ -92,6 +97,7 @@ var markerFeature = function (arg) { var pts, position, radius = m_this.style.get('radius'), strokeWidth = m_this.style.get('strokeWidth'), + strokeOffset = m_this.style.get('strokeOffset'), scaleWithZoom = m_this.style.get('scaleWithZoom'); position = m_this.position(); @@ -104,11 +110,17 @@ var markerFeature = function (arg) { pts = m_this.data().map(function (d, i) { var pt = position(d, i); - let r = radius(d, i), s = strokeWidth(d, i); - switch (scaleWithZoom(d, i)) { + let r = radius(d, i), + swz = scaleWithZoom(d, i), + s = strokeWidth(d, i), + so = Math.sign(strokeOffset(d, i)); + let rwiths = r + s * (so + 1) / 2, // radius with stroke + rwos = r + s * (so - 1) / 2; // radius without stroke + swz = markerFeature.scaleMode[swz] || (swz >= 1 && swz <= 3 ? swz : 0); + switch (swz) { case markerFeature.scaleMode.stroke: - if (r - s > m_maxFixedRadius) { - m_maxFixedRadius = r - s; + if (rwos > m_maxFixedRadius) { + m_maxFixedRadius = rwos; } if (s > m_maxZoomStroke) { m_maxZoomStroke = s; @@ -116,13 +128,13 @@ var markerFeature = function (arg) { break; case markerFeature.scaleMode.fill: case markerFeature.scaleMode.all: - if (r > m_maxZoomRadius) { - m_maxZoomRadius = r; + if (rwiths > m_maxZoomRadius) { + m_maxZoomRadius = rwiths; } break; default: - if (r > m_maxFixedRadius) { - m_maxFixedRadius = r; + if (rwiths > m_maxFixedRadius) { + m_maxFixedRadius = rwiths; } break; } @@ -133,6 +145,19 @@ var markerFeature = function (arg) { m_rangeTreeTime.modified(); }; + /** + * Determine an approximate maximum radius based on the zoom factor. + * + * @param {number} zoom The zoom level. + * @returns {number} The maximum radius. May be somewhat larger than the + * actual maximum. + */ + this._approximateMaxRadius = function (zoom) { + m_this._updateRangeTree(); + let zoomFactor = Math.pow(2, zoom); + return Math.max(m_maxFixedRadius + m_maxZoomStroke * zoomFactor, m_maxZoomRadius * zoomFactor); + }; + /** * Returns an array of datum indices that contain the given marker. * @@ -146,6 +171,7 @@ var markerFeature = function (arg) { corners, radius = m_this.style.get('radius'), strokeWidth = m_this.style.get('strokeWidth'), + strokeOffset = m_this.style.get('strokeOffset'), scaleWithZoom = m_this.style.get('scaleWithZoom'); data = m_this.data(); @@ -163,8 +189,8 @@ var markerFeature = function (arg) { var map = m_this.layer().map(), pt = map.gcsToDisplay(p), zoom = map.zoom(), - zfactor = Math.pow(2, zoom), - maxr = Math.max(m_maxFixedRadius + m_maxZoomStroke * zfactor, m_maxZoomRadius * zfactor); + zoomFactor = Math.pow(2, zoom), + maxr = this._approximateMaxRadius(zoom); // check all corners to make sure we handle rotations corners = [ @@ -191,18 +217,22 @@ var markerFeature = function (arg) { var d = data[i], rad = radius(data[i], i), swz = scaleWithZoom(data[i], i), + so = strokeOffset(data[i], i), s = swz ? strokeWidth(data[i], i) : 0; + var rwos = rad + s * (so - 1) / 2; // radius without stroke + rad = rwos + s; var p = m_this.position()(d, i), dx, dy, rad2; + swz = markerFeature.scaleMode[swz] || (swz >= 1 && swz <= 3 ? swz : 0); switch (swz) { case markerFeature.scaleMode.fill: - rad = (rad - s) * zfactor + s; + rad = rwos * zoomFactor + s; break; case markerFeature.scaleMode.stroke: - rad = (rad - s) + s * zfactor; + rad = rwos + s * zoomFactor; break; case markerFeature.scaleMode.all: - rad *= zfactor; + rad *= zoomFactor; break; } @@ -252,11 +282,12 @@ var markerFeature = function (arg) { data = m_this.data(), radius = m_this.style.get('radius'), strokeWidth = m_this.style.get('strokeWidth'), + strokeOffset = m_this.style.get('strokeOffset'), scaleWithZoom = m_this.style.get('scaleWithZoom'), idx, min, max, corners, zoom = map.zoom(), - zfactor = Math.pow(2, zoom), - maxr = Math.max(m_maxFixedRadius + m_maxZoomStroke * zfactor, m_maxZoomRadius * zfactor); + zoomFactor = Math.pow(2, zoom), + maxr = this._approximateMaxRadius(zoom); if (!poly.outer) { poly = {outer: poly, inner: []}; @@ -308,16 +339,20 @@ var markerFeature = function (arg) { let p = m_this.position()(d, i); let rad = radius(data[i], i), swz = scaleWithZoom(data[i], i), + so = strokeOffset(data[i], i), s = swz ? strokeWidth(data[i], i) : 0; + let rwos = rad + s * (so - 1) / 2; // radius without stroke + swz = markerFeature.scaleMode[swz] || (swz >= 1 && swz <= 3 ? swz : 0); + rad = rwos + s; switch (swz) { case markerFeature.scaleMode.fill: - rad = (rad - s) * zfactor + s; + rad = rwos * zoomFactor + s; break; case markerFeature.scaleMode.stroke: - rad = (rad - s) + s * zfactor; + rad = rwos + s * zoomFactor; break; case markerFeature.scaleMode.all: - rad *= zfactor; + rad *= zoomFactor; break; } if (rad) { @@ -353,6 +388,7 @@ var markerFeature = function (arg) { { radius: 6.25, strokeColor: { r: 0.851, g: 0.604, b: 0.0 }, + strokeOffset: -1.0, strokeOpacity: 1.0, strokeWidth: 1.25, fillColor: { r: 1.0, g: 0.839, b: 0.439 }, diff --git a/src/webgl/markerFeature.js b/src/webgl/markerFeature.js index 8f8d27273b..e8458dea36 100644 --- a/src/webgl/markerFeature.js +++ b/src/webgl/markerFeature.js @@ -25,6 +25,7 @@ var webgl_markerFeature = function (arg) { 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'); @@ -104,7 +105,6 @@ var webgl_markerFeature = function (arg) { unit = m_this._pointPolygon(0, 0, 1, 1), position = new Array(numPts * 3), posBuf, posVal, posFunc, indices, unitBuf, - zoomFactor = Math.pow(2, m_this.renderer().map().zoom()), styleBuf = {}, styleVal = {}, styleFunc = {}, styleUni = {}, styleKeys = { radius: 1, @@ -112,6 +112,7 @@ var webgl_markerFeature = function (arg) { fillOpacity: 1, strokeColor: 3, strokeOpacity: 1, + strokeOffset: 0, strokeWidth: 1, symbol: 1, symbolValue: 1, @@ -121,7 +122,7 @@ var webgl_markerFeature = function (arg) { }, vpf = m_this.verticesPerFeature(), data = m_this.data(), - item, ivpf, ivpf3, iunit, i3, maxr = 0, + item, ivpf, ivpf3, iunit, i3, geom = m_mapper.geometryData(); posFunc = m_this.position(); @@ -188,14 +189,11 @@ var webgl_markerFeature = function (arg) { styleVal.scaleWithZoom + (styleVal.rotateWithMap ? 4 : 0) + // bit 3 reserved - styleVal.symbol * 16); + ((Math.sign(styleVal.strokeOffset) + 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); } - if (m_this._primitiveShapeAuto && - (styleVal.scaleWithZoom ? styleVal.radius * zoomFactor : styleVal.radius) > maxr) { - maxr = styleVal.scaleWithZoom ? styleVal.radius * zoomFactor : styleVal.radius; - } for (j = 0; j < vpf; j += 1, ivpf += 1, ivpf3 += 3) { if (!onlyStyle) { posBuf[ivpf3] = position[i3]; @@ -218,16 +216,18 @@ var webgl_markerFeature = function (arg) { } } - if (m_this._primitiveShapeAuto && - ((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 (m_this._primitiveShapeAuto) { + let 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) { @@ -251,6 +251,24 @@ var webgl_markerFeature = function (arg) { return [m_actor]; }; + /** + * Handle zoom events for automatic primive 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. * @@ -263,7 +281,7 @@ var webgl_markerFeature = function (arg) { mat = vgl.material(), blend = vgl.blend(), geom = vgl.geometryData(), - sourcePositions = vgl.sourceDataP3fv({'name': 'pos'}), + sourcePositions = vgl.sourceDataP3fv({name: 'pos'}), attr = { radius: 1, fillColor: 3, @@ -301,10 +319,12 @@ var webgl_markerFeature = function (arg) { 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); @@ -325,6 +345,11 @@ var webgl_markerFeature = function (arg) { 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); + }); + } }; /** @@ -376,6 +401,7 @@ var webgl_markerFeature = function (arg) { this._exit = function () { m_this.renderer().contextRenderer().removeActor(m_actor); m_actor = null; + m_uniforms = {}; s_exit(); }; diff --git a/src/webgl/markerFeatureFS.glsl b/src/webgl/markerFeatureFS.glsl index 8aa5516c44..96bd20a57f 100644 --- a/src/webgl/markerFeatureFS.glsl +++ b/src/webgl/markerFeatureFS.glsl @@ -271,7 +271,7 @@ void markerFeatureFragment(vec2 pos) { pos = vec2(pos.x * cosr + pos.y * sinr, -pos.x * sinr + pos.y * cosr); } - int symbol = int(floor(symbolVar / 16.0)); + int symbol = int(floor(symbolVar / 64.0)); bool isimage = bool(mod(floor(symbolVar / 8.0), 2.0)); vec4 fillColor, strokeColor; float endStep; diff --git a/src/webgl/markerFeatureVS.glsl b/src/webgl/markerFeatureVS.glsl index 4fdc8bfd5f..9f34aaa9b4 100644 --- a/src/webgl/markerFeatureVS.glsl +++ b/src/webgl/markerFeatureVS.glsl @@ -36,6 +36,8 @@ float markerFeaturePrep(void) radiusVar = radius; strokeWidthVar = strokeWidth; int scaleMode = int(mod(symbol, 4.0)); + float strokeOffset = mod(floor(symbol / 16.0), 4.0) - 1.0; + radiusVar += (strokeOffset + 1.0) / 2.0 * strokeWidthVar; if (scaleMode == 1) { // fill radiusVar = (radiusVar - strokeWidthVar) * exp2(zoom) + strokeWidthVar; } else if (scaleMode == 2) { // stroke diff --git a/src/webgl/pointUtil.js b/src/webgl/pointUtil.js index 1127c7ac98..92dcd687db 100644 --- a/src/webgl/pointUtil.js +++ b/src/webgl/pointUtil.js @@ -102,7 +102,7 @@ function pointUtil(m_this, arg) { if (update) { m_this.renderer().contextRenderer().removeActor(m_this.actors()[0]); m_this._init(true); - m_this.modified(); + m_this.dataTime().modified(); } } return m_this; diff --git a/tests/cases/markerFeature.js b/tests/cases/markerFeature.js index 5db3aee54d..29b205d0f2 100644 --- a/tests/cases/markerFeature.js +++ b/tests/cases/markerFeature.js @@ -131,6 +131,35 @@ describe('geo.markerFeature', function () { }); }); + describe('Private utility methods', function () { + it('_approximateMaxRadius', function () { + mockWebglRenderer(); + var map, layer, marker; + map = createMap(); + map.zoom(3); + layer = map.createLayer('feature', {renderer: 'webgl'}); + marker = layer.createFeature('marker', {selectionAPI: true}); + marker.data(testMarkers).style({ + radius: function (d) { return d.radius || 5; }, + strokeWidth: function (d) { return d.strokeWidth || 2; }, + strokeOffset: function (d, i) { return i % 3 - 1; }, + scaleWithZoom: function (d) { return d.scaleWithZoom; } + }); + marker.draw(); + expect(marker._approximateMaxRadius(map.zoom())).toBe(36); + map.zoom(5); + marker.draw(); + expect(marker._approximateMaxRadius(map.zoom())).toBe(144); + map.zoom(6); + marker.draw(); + expect(marker._approximateMaxRadius(map.zoom())).toBe(288); + marker.style('strokeOffset', -1).draw(); + expect(marker._approximateMaxRadius(map.zoom())).toBe(160); + destroyMap(); + restoreWebglRenderer(); + }); + }); + /* This is a basic integration test of geo.webgl.markerFeature. */ describe('geo.webgl.markerFeature', function () { var map, layer, marker, marker2, glCounts; @@ -148,14 +177,14 @@ describe('geo.markerFeature', function () { }); glCounts = $.extend({}, vgl.mockCounts()); marker.draw(); - expect(marker.verticesPerFeature()).toBe(3); + expect(marker.verticesPerFeature()).toBe(1); }); waitForIt('next render gl A', function () { return vgl.mockCounts().createProgram >= (glCounts.createProgram || 0) + 1; }); it('other primitive shapes', function () { expect(marker.primitiveShape()).toBe(geo.markerFeature.primitiveShapes.auto); - expect(marker.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.triangle); + expect(marker.primitiveShape(undefined, true)).toBe(geo.markerFeature.primitiveShapes.sprite); marker2 = layer.createFeature('marker', { primitiveShape: geo.markerFeature.primitiveShapes.triangle }).data(testMarkers);