diff --git a/src/markerFeature.js b/src/markerFeature.js index 65cdd175e2..a075061af6 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,15 @@ 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); + let r = radius(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 switch (scaleWithZoom(d, i)) { 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 +126,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 +143,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 +169,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 +187,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 +215,21 @@ 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; 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 +279,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 +336,19 @@ 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 + 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 +384,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);