diff --git a/CHANGELOG.md b/CHANGELOG.md index 99eca53417..78c1f59e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,8 @@ ## Unreleased ### Features -- The osmLayer now has predefined `tileSources` that can be used with the `source` method or property to switch multiple parameters at once. (#1020) +- The osmLayer now has predefined `tileSources` that can be used with the `source` method or property to switch multiple parameters at once (#1020) +- Added a marker feature ### Improvements - Points with small radii or thin strokes are rendered better (#1021) @@ -11,10 +12,10 @@ - WebGL point features automatically use the most memory-efficient primitive shape for the point sizes used based on the system's graphics capabilities (#1031) ### Changes -- Switched the default tile server to Stamen Design's toner-lite. (#1020) +- Switched the default tile server to Stamen Design's toner-lite (#1020) ### Bug Fixes -- Mouse wheel events didn't recompute gcs coordinates, so a wheel event without a previous move event coult list the wrong location (#1027) +- Mouse wheel events didn't recompute gcs coordinates, so a wheel event without a previous move event could list the wrong location (#1027) - Original event data was not included with actionwheel events (#1030) ## Version 0.19.6 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..ffdb221f01 --- /dev/null +++ b/src/markerFeature.js @@ -0,0 +1,454 @@ +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. + * @property {number|function} [symbolValue=0] A value the affects the + * appearance of the symbol. + * @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: 5.0, + 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 + circle: 0, + ellipse: 0, + // for triangle, the symbolValue is the ratio of the base to the other sides + triangle: 1, + // for square (alias rectangle), the symbolValue is the ratio of the minor to + // major axes + square: 2, + rectangle: 2, + // for ovals, the symbolValue is the ratio of the minor to major axes + oval: 3, + // for drops, the symbol value is the ratio of the arc to the main radius + drop: 4, + // for crosses, the symbolValue is the width of the arm compared to the + // length of the cross + crossBase: 5, + crossMax: 16, + flowerBase: 20, + flowerMax: 16, + diamond: 35, + starBase: 35, + starMax: 16, + jackBase: 50, + jackMax: 16, + dropBase: 65, + dropMax: 16, + length: 80 + // possible other symbols: + // half inner stellations (bowtie/hourglass), hash (#), inner curved shapes +}; +['cross', 'flower', 'star', 'jack', 'drop'].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..e8d5124c2f --- /dev/null +++ b/src/webgl/markerFeature.js @@ -0,0 +1,368 @@ +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; + } + + /** + * 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 (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; + } + 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..d289b0b0b2 --- /dev/null +++ b/src/webgl/markerFeatureFS.glsl @@ -0,0 +1,273 @@ +/* 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 symbolTriangle = 1; +const int symbolRectangle = 2; +const int symbolOval = 3; +const int symbolDrop = 4; +const int symbolCrossBase = 5; +const int symbolCrossMax = 16; +const int symbolFlowerBase = 20; +const int symbolFlowerMax = 16; +const int symbolStarBase = 35; +const int symbolStarMax = 16; +const int symbolJackBase = 50; +const int symbolJackMax = 16; +const int symbolDropBase = 65; +const int symbolDropMax = 16; + +/* 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 = min(acos(0.0), max(0.0, t)); + 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 = max(0.0, min(2.0, abs(ratio))); + 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) { + // 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; + + // rad is a value in pixels from the edge of the symbol where negative is + // inside the shape + float rad = length(pos.xy) - radiusVar; + 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 >= 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 { // 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; + // 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; + // Distance to antialias in pixels + float antialiasDist = 1.5; + 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 8672c1123d..b202721d2f 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 && ((fillVal && fillOpacityVal) || (strokeVal && strokeOpacityVal)) && radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0) > maxr) { + if (m_this._primitiveShapeAuto && ((fillVal && fillOpacityVal) || (strokeVal && strokeOpacityVal)) && radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0) > maxr) { maxr = radiusVal + (strokeVal && strokeOpacityVal ? strokeWidthVal : 0); } for (j = 0; j < vpf; j += 1, ivpf += 1, ivpf3 += 3) { @@ -244,11 +193,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); @@ -284,16 +233,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 @@ -392,41 +331,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. * @@ -472,7 +376,7 @@ 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(); @@ -489,7 +393,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/pointFeatureFS.glsl b/src/webgl/pointFeatureFS.glsl index 9085398c0a..2e37b59497 100644 --- a/src/webgl/pointFeatureFS.glsl +++ b/src/webgl/pointFeatureFS.glsl @@ -15,7 +15,7 @@ void pointFeatureFragment(float rad) { if (rad > 1.0) discard; // If there is no stroke, the fill region should transition to nothing - if (strokeColorVar.a == 0.0) { + if (strokeColorVar.a == 0.0 || strokeWidthVar <= 0.0) { strokeColor = vec4(fillColorVar.rgb, 0.0); endStep = 1.0; } else { @@ -29,7 +29,7 @@ void pointFeatureFragment(float rad) { fillColor = fillColorVar; // Distance to antialias over. First number is in pixels float antialiasDist = 1.5 / (radiusVar + strokeWidthVar); - if (rad < endStep) { + if (rad <= endStep) { float step = smoothstep(max(0.0, endStep - antialiasDist), endStep, rad); vec4 color = mix(fillColor, strokeColor, step); float step2 = smoothstep(max(0.0, 1.0 - antialiasDist), 1.0, rad); 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..0f03a141a8 --- /dev/null +++ b/tests/cases/markerFeature.js @@ -0,0 +1,201 @@ +// 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; } + }); + 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..3d9c3af2c3 --- /dev/null +++ b/tutorials/marker/index.pug @@ -0,0 +1,124 @@ +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) { + 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, + symbolValue: Math.random(), + symbol: Math.floor(Math.random() * geo.markerFeature.symbols.length), + 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 0000000000..c243f540a7 Binary files /dev/null and b/tutorials/marker/thumb.jpg differ 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." + } +}