diff --git a/examples/polygons/example.json b/examples/polygons/example.json new file mode 100644 index 0000000000..4498ee619c --- /dev/null +++ b/examples/polygons/example.json @@ -0,0 +1,9 @@ +{ + "path": "polygons", + "title": "Polygon features", + "exampleCss": ["main.css"], + "exampleJs": ["main.js"], + "about": { + "text": "This example shows how to add polygons to a map." + } +} diff --git a/examples/polygons/index.jade b/examples/polygons/index.jade new file mode 100644 index 0000000000..a2432ab791 --- /dev/null +++ b/examples/polygons/index.jade @@ -0,0 +1 @@ +extends ../common/templates/index.jade diff --git a/examples/polygons/main.css b/examples/polygons/main.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/polygons/main.js b/examples/polygons/main.js new file mode 100644 index 0000000000..5c8084a026 --- /dev/null +++ b/examples/polygons/main.js @@ -0,0 +1,68 @@ +/* globals $, geo, utils */ + +var polygonDebug = {}; + +// Run after the DOM loads +$(function () { + 'use strict'; + + var query = utils.getQuery(); + var map = geo.map({ + node: '#map', + center: { + x: -88.0, + y: 29 + }, + zoom: 4 + }); + if (query.map !== 'false') { + map.createLayer('osm'); + } + var layer = map.createLayer('feature', { + renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined, + features: query.renderer ? undefined : ['polygon'] + }); + var polygons = layer.createFeature('polygon', {selectionAPI: true}); + var hoverColor = query.hover || 'blue'; + var polyColor = query.color ? geo.util.convertColor(query.color) : undefined; + $.getJSON(query.url || '../../data/land_polygons.json').done(function (data) { + polygons + /* This is the default accessor, so we don't have to define it ourselves. + .polygon(function (d) { + return d; + }) + */ + .position(function (d) { + return {x: d[0], y: d[1]}; + }) + .data(data) + .style('uniformPolygon', true) + .style('fillOpacity', query.opacity ? parseFloat(query.opacity) : 0.5) + .style('fillColor', function (d, idx, poly, polyidx) { + return poly.hover ? hoverColor : (polyColor ? polyColor : { + r: (polyidx % 256) / 255, + g: polyidx / (data.length - 1), + b: 0.25 + }); + }) + .geoOn(geo.event.feature.mouseover, function (evt) { + if (!evt.data.hover) { + evt.data.hover = true; + this.modified(); + this.draw(); + } + }) + .geoOn(geo.event.feature.mouseout, function (evt) { + if (evt.data.hover) { + evt.data.hover = false; + this.modified(); + this.draw(); + } + }) + .draw(); + + polygonDebug.map = map; + polygonDebug.layer = layer; + polygonDebug.polygons = polygons; + }); +}); diff --git a/examples/polygons/thumb.jpg b/examples/polygons/thumb.jpg new file mode 100755 index 0000000000..7ed24adbc5 Binary files /dev/null and b/examples/polygons/thumb.jpg differ diff --git a/src/feature.js b/src/feature.js index aca6efcd15..39cbe275f4 100644 --- a/src/feature.js +++ b/src/feature.js @@ -305,7 +305,7 @@ var feature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this.style.get = function (key) { - var tmp, out; + var out; if (key === undefined) { var all = {}, k; for (k in m_style) { @@ -317,10 +317,9 @@ var feature = function (arg) { } if (key.toLowerCase().match(/color$/)) { if (util.isFunction(m_style[key])) { - tmp = util.ensureFunction(m_style[key]); out = function () { return util.convertColor( - tmp.apply(this, arguments) + m_style[key].apply(this, arguments) ); }; } else { diff --git a/src/gl/object.js b/src/gl/object.js index b529d4ec26..3ce4c028bc 100644 --- a/src/gl/object.js +++ b/src/gl/object.js @@ -30,7 +30,7 @@ var gl_object = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this.draw = function () { - m_this._update(); + m_this._update({mayDelay: true}); m_this.renderer()._render(); s_draw(); return m_this; diff --git a/src/gl/polygonFeature.js b/src/gl/polygonFeature.js index 3dd9d45a54..f4a6398d05 100644 --- a/src/gl/polygonFeature.js +++ b/src/gl/polygonFeature.js @@ -22,6 +22,7 @@ var gl_polygonFeature = function (arg) { var vgl = require('vgl'); var earcut = require('earcut'); var transform = require('../transform'); + var util = require('../util'); var object = require('./object'); object.call(this); @@ -36,8 +37,10 @@ var gl_polygonFeature = function (arg) { m_actor = vgl.actor(), m_mapper = vgl.mapper(), m_material = vgl.material(), + m_geometry, s_init = this._init, - s_update = this._update; + s_update = this._update, + m_updateAnimFrameRef; function createVertexShader() { var vertexShaderSource = [ @@ -82,122 +85,195 @@ var gl_polygonFeature = function (arg) { return shader; } - function createGLPolygons() { - var posFunc = null, - fillColorFunc = null, - fillOpacityFunc = null, - buffers = vgl.DataBuffers(1024), - sourcePositions = vgl.sourceDataP3fv(), - sourceFillColor = - vgl.sourceDataAnyfv(3, vgl.vertexAttributeKeysIndexed.Two), - sourceFillOpacity = - vgl.sourceDataAnyfv(1, vgl.vertexAttributeKeysIndexed.Three), - trianglePrimitive = vgl.triangles(), - geom = vgl.geometryData(), - triangles = [], + /** + * Create and style the triangles need to render the polygons. + * + * There are several optimizations to do less work when possible. If only + * styles have changed, the triangulation is not recomputed, nor is the + * geometry re-transformed. If styles use static values (rather than + * functions), they are only calculated once. If a polygon reports that it + * has a uniform style, then styles are only calculated once for that polygon + * (the uniform property may be different per polygon or per update). + * Array.map is slower in Chrome that using a loop, so loops are used in + * places that would be conceptually served by maps. + * + * @memberof geo.gl.polygonFeature + * @param {boolean} onlyStyle if true, use the existing geoemtry and just + * recalculate the style. + */ + function createGLPolygons(onlyStyle) { + var posBuf, posFunc, + fillColor, fillColorFunc, fillColorVal, + fillOpacity, fillOpacityFunc, fillOpacityVal, + uniformPolyFunc, uniform, + indices, + items = [], target_gcs = m_this.gcs(), map_gcs = m_this.layer().map().gcs(), - color; + numPts = 0, + geom = m_mapper.geometryData(), + color, opacity, d, d3, vertices, i, j, k, n, + record, item, itemIndex, original; - posFunc = m_this.position(); fillColorFunc = m_this.style.get('fillColor'); + fillColorVal = util.isFunction(m_this.style('fillColor')) ? undefined : fillColorFunc(); fillOpacityFunc = m_this.style.get('fillOpacity'); + fillOpacityVal = util.isFunction(m_this.style('fillOpacity')) ? undefined : fillOpacityFunc(); + uniformPolyFunc = m_this.style.get('uniformPolygon'); + + if (!onlyStyle) { + posFunc = m_this.position(); + m_this.data().forEach(function (item, itemIndex) { + var polygon, outer, geometry, c; + + polygon = m_this.polygon()(item, itemIndex); + outer = polygon.outer || (polygon instanceof Array ? polygon : []); + + /* expand to an earcut polygon geometry. We had been using a map call, + * but using loops is much faster in Chrome (4 versus 33 ms for one + * test). */ + geometry = new Array(outer.length * 3); + for (i = d3 = 0; i < outer.length; i += 1, d3 += 3) { + c = posFunc(outer[i], i, item, itemIndex); + geometry[d3] = c.x; + geometry[d3 + 1] = c.y; + geometry[d3 + 2] = c.z || 0; + } + geometry = {vertices: geometry, dimensions: 3, holes: []}; + original = outer; + + if (polygon.inner) { + polygon.inner.forEach(function (hole) { + original = original.concat(hole); + geometry.holes.push(d3 / 3); + for (i = 0; i < hole.length; i += 1, d3 += 3) { + c = posFunc(hole[i], i, item, itemIndex); + geometry.vertices[d3] = c.x; + geometry.vertices[d3 + 1] = c.y; + geometry.vertices[d3 + 2] = c.z || 0; + } + }); + } + + // tranform to map gcs + geometry.vertices = transform.transformCoordinates( + target_gcs, + map_gcs, + geometry.vertices, + geometry.dimensions + ); - buffers.create('pos', 3); - buffers.create('indices', 1); - buffers.create('fillColor', 3); - buffers.create('fillOpacity', 1); - - m_this.data().forEach(function (item, itemIndex) { - var polygon, geometry, numPts, start, i, vertex, j; - - function position(d, i) { - var c = posFunc(d, i, item, itemIndex); - return [c.x, c.y, c.z || 0]; - } - - polygon = m_this.polygon()(item, itemIndex); - polygon.outer = polygon.outer || []; - polygon.inner = polygon.inner || []; - - // expand to a geojson polygon geometry - geometry = [(polygon.outer || []).map(position)]; - (polygon.inner || []).forEach(function (hole) { - geometry.push(hole.map(position)); + record = { + // triangulate + triangles: earcut(geometry.vertices, geometry.holes, geometry.dimensions), + vertices: geometry.vertices, + original: original, + item: item, + itemIndex: itemIndex + }; + items.push(record); + numPts += record.triangles.length; }); - - // convert to an earcut geometry - geometry = earcut.flatten(geometry); - - // tranform to map gcs - geometry.vertices = transform.transformCoordinates( - target_gcs, - map_gcs, - geometry.vertices, - geometry.dimensions - ); - - // triangulate - triangles = earcut(geometry.vertices, geometry.holes, geometry.dimensions); - - // append to buffers - numPts = triangles.length; - start = buffers.alloc(triangles.length); - - for (i = 0; i < numPts; i += 1) { - j = triangles[i] * 3; - vertex = geometry.vertices.slice(triangles[i] * 3, j + 3); - buffers.write('pos', vertex, start + i, 1); - buffers.write('indices', [i], start + i, 1); - color = fillColorFunc(vertex, i, item, itemIndex); - - buffers.write( - 'fillColor', - [color.r, color.g, color.b], - start + i, - 1 - ); - buffers.write( - 'fillOpacity', - [fillOpacityFunc(vertex, i, item, itemIndex)], - start + i, - 1 - ); + posBuf = util.getGeomBuffer(geom, 'pos', numPts * 3); + indices = geom.primitive(0).indices(); + if (!(indices instanceof Uint16Array) || indices.length !== numPts) { + indices = new Uint16Array(numPts); + geom.primitive(0).setIndices(indices); } - }); - - sourcePositions.pushBack(buffers.get('pos')); - geom.addSource(sourcePositions); - - sourceFillColor.pushBack(buffers.get('fillColor')); - geom.addSource(sourceFillColor); - - sourceFillOpacity.pushBack(buffers.get('fillOpacity')); - geom.addSource(sourceFillOpacity); - - trianglePrimitive.setIndices(buffers.get('indices')); - geom.addPrimitive(trianglePrimitive); - - m_mapper.setGeometryData(geom); + m_geometry = {items: items, numPts: numPts}; + } else { + items = m_geometry.items; + numPts = m_geometry.numPts; + } + fillColor = util.getGeomBuffer(geom, 'fillColor', numPts * 3); + fillOpacity = util.getGeomBuffer(geom, 'fillOpacity', numPts); + d = d3 = 0; + color = fillColorVal; + opacity = fillOpacityVal; + for (k = 0; k < items.length; k += 1) { + n = items[k].triangles.length; + vertices = items[k].vertices; + item = items[k].item; + itemIndex = items[k].itemIndex; + original = items[k].original; + uniform = uniformPolyFunc(item, itemIndex); + if (uniform) { + if (fillColorVal === undefined) { + color = fillColorFunc(vertices[0], 0, item, itemIndex); + } + if (fillOpacityVal === undefined) { + opacity = fillOpacityFunc(vertices[0], 0, item, itemIndex); + } + } + if (uniform && onlyStyle && items[k].uniform && items[k].color && + color.r === items[k].color.r && color.g === items[k].color.g && + color.b === items[k].color.b && opacity === items[k].opacity) { + d += n; + d3 += n * 3; + continue; + } + for (i = 0; i < n; i += 1, d += 1, d3 += 3) { + if (onlyStyle && uniform) { + fillColor[d3] = color.r; + fillColor[d3 + 1] = color.g; + fillColor[d3 + 2] = color.b; + fillOpacity[d] = opacity; + } else { + j = items[k].triangles[i] * 3; + if (!onlyStyle) { + posBuf[d3] = vertices[j]; + posBuf[d3 + 1] = vertices[j + 1]; + posBuf[d3 + 2] = vertices[j + 2]; + indices[d] = i; + } + if (!uniform && fillColorVal === undefined) { + color = fillColorFunc(original[j], j, item, itemIndex); + } + fillColor[d3] = color.r; + fillColor[d3 + 1] = color.g; + fillColor[d3 + 2] = color.b; + if (!uniform && fillOpacityVal === undefined) { + opacity = fillOpacityFunc(original[j], j, item, itemIndex); + } + fillOpacity[d] = opacity; + } + } + if (uniform || items[k].uniform) { + items[k].uniform = uniform; + items[k].color = color; + items[k].opacity = opacity; + } + } + m_mapper.modified(); + if (!onlyStyle) { + geom.boundsDirty(true); + m_mapper.boundsDirtyTimestamp().modified(); + } } //////////////////////////////////////////////////////////////////////////// /** * Initialize + * @memberof geo.gl.polygonFeature */ //////////////////////////////////////////////////////////////////////////// this._init = function (arg) { - var blend = vgl.blend(), - prog = vgl.shaderProgram(), + var prog = vgl.shaderProgram(), posAttr = vgl.vertexAttribute('pos'), fillColorAttr = vgl.vertexAttribute('fillColor'), fillOpacityAttr = vgl.vertexAttribute('fillOpacity'), modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), projectionUniform = new vgl.projectionUniform('projectionMatrix'), vertexShader = createVertexShader(), - fragmentShader = createFragmentShader(); - - s_init.call(m_this, arg); + fragmentShader = createFragmentShader(), + blend = vgl.blend(), + geom = vgl.geometryData(), + sourcePositions = vgl.sourceDataP3fv({'name': 'pos'}), + sourceFillColor = vgl.sourceDataAnyfv( + 3, vgl.vertexAttributeKeysIndexed.Two, {'name': 'fillColor'}), + sourceFillOpacity = vgl.sourceDataAnyfv( + 1, vgl.vertexAttributeKeysIndexed.Three, {'name': 'fillOpacity'}), + trianglePrimitive = vgl.triangles(); prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); prog.addVertexAttribute(fillColorAttr, vgl.vertexAttributeKeysIndexed.Two); @@ -212,25 +288,33 @@ var gl_polygonFeature = function (arg) { m_material.addAttribute(prog); m_material.addAttribute(blend); - m_actor.setMapper(m_mapper); m_actor.setMaterial(m_material); + m_actor.setMapper(m_mapper); + + geom.addSource(sourcePositions); + geom.addSource(sourceFillColor); + geom.addSource(sourceFillOpacity); + geom.addPrimitive(trianglePrimitive); + m_mapper.setGeometryData(geom); + + s_init.call(m_this, arg); }; //////////////////////////////////////////////////////////////////////////// /** * Build * + * @memberof geo.gl.polygonFeature * @override */ //////////////////////////////////////////////////////////////////////////// this._build = function () { - if (m_actor) { - m_this.renderer().contextRenderer().removeActor(m_actor); - } - createGLPolygons(); + createGLPolygons(m_this.dataTime().getMTime() < m_this.buildTime().getMTime() && m_geometry); - m_this.renderer().contextRenderer().addActor(m_actor); + if (!m_this.renderer().contextRenderer().hasActor(m_actor)) { + m_this.renderer().contextRenderer().addActor(m_actor); + } m_this.buildTime().modified(); }; @@ -238,10 +322,19 @@ var gl_polygonFeature = function (arg) { /** * Update * + * @memberof geo.gl.polygonFeature * @override */ //////////////////////////////////////////////////////////////////////////// - this._update = function () { + this._update = function (opts) { + if (opts && opts.mayDelay) { + m_updateAnimFrameRef = window.requestAnimationFrame(this._update); + return; + } + if (m_updateAnimFrameRef) { + window.cancelAnimationFrame(m_updateAnimFrameRef); + m_updateAnimFrameRef = null; + } s_update.call(m_this); if (m_this.dataTime().getMTime() >= m_this.buildTime().getMTime() || @@ -257,6 +350,7 @@ var gl_polygonFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** * Destroy + * @memberof geo.gl.polygonFeature */ //////////////////////////////////////////////////////////////////////////// this._exit = function () { diff --git a/src/gl/vglRenderer.js b/src/gl/vglRenderer.js index 4c81db49b8..2c5f8bf651 100644 --- a/src/gl/vglRenderer.js +++ b/src/gl/vglRenderer.js @@ -132,9 +132,10 @@ var vglRenderer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._render = function () { - if (m_renderAnimFrameRef === null) { - m_renderAnimFrameRef = window.requestAnimationFrame(this._renderFrame); + if (m_renderAnimFrameRef) { + window.cancelAnimationFrame(m_renderAnimFrameRef); } + m_renderAnimFrameRef = window.requestAnimationFrame(this._renderFrame); return m_this; }; diff --git a/src/heatmapFeature.js b/src/heatmapFeature.js index 4c8d71b176..7ed2fdfd7f 100644 --- a/src/heatmapFeature.js +++ b/src/heatmapFeature.js @@ -11,8 +11,7 @@ var transform = require('./transform'); * @param {Object} arg Options object * @extends geo.feature * @param {Object|Function} [position] Position of the data. Default is - * (data). The position is an Object which specifies the location of the - * data in geo-spatial context. + * (data). * @param {Object|Function} [intensity] Scalar value of each data point. Scalar * value must be a positive real number and will be used to compute * the weight for each data point. diff --git a/src/polygonFeature.js b/src/polygonFeature.js index 8d704d9873..a0ce5f66cf 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -8,6 +8,26 @@ var feature = require('./feature'); * * @class geo.polygonFeature * @extends geo.feature + * @param {Object} arg Options object + * @param {Object|Function} [arg.position] Position of the data. Default is + * (data). + * @param {Object|Function} [arg.polygon] Polygons from the data. Default is + * (data). Typically, the data is an array of polygons, each of which is + * of the form {outer: [(coordinates)], inner: [[(coordinates of first + * hole)], [(coordinates of second hole)], ...]}. The inner record is + * optional. Alternately, if there are no holes, a polygon can just be an + * array of coordinates. Coordinates are in the form {x: (x), y: (y), + * z: (z)}, with z being optional. The first and last point of each polygon + * must be the same. + * @param {Object} [arg.style] Style object with default style options. + * @param {Object|Function} [arg.style.fillColor] Color to fill each polygon. + * The color can vary by vertex. Colors can be css names or hex values, or + * an object with r, g, b on a [0-1] scale. + * @param {number|Function} [arg.style.fillOpacity] Opacity for each polygon. + * The opacity can vary by vertex. Opacity is on a [0-1] scale. + * @param {boolean|Function} [arg.style.uniformPolygon] Boolean indicating if + * each polygon has a uniform style (uniform fill color and opacity). + * Defaults to false. Can vary by polygon. * @returns {geo.polygonFeature} */ ////////////////////////////////////////////////////////////////////////////// @@ -31,7 +51,7 @@ var polygonFeature = function (arg) { m_polygon, s_init = this._init, s_data = this.data, - m_coordinates = {outer: [], inner: []}; + m_coordinates = []; if (arg.polygon === undefined) { m_polygon = function (d) { @@ -51,8 +71,12 @@ var polygonFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** - * Override the parent data method to keep track of changes to the - * internal coordinates. + * Get/set data. + * + * @memberof geo.polygonFeature + * @param {Object} [data] if specified, use this for the data and return the + * feature. If not specified, return the current data. + * @returns {geo.polygonFeature|Object} */ //////////////////////////////////////////////////////////////////////////// this.data = function (arg) { @@ -67,7 +91,9 @@ var polygonFeature = function (arg) { /** * Get the internal coordinates whenever the data changes. For now, we do * the computation in world coordinates, but we will need to work in GCS - * for other projections. + * for other projections. Also compute the extents of the outside of each + * polygon for faster checking if points are in the polygon. + * @memberof geo.polygonFeature * @private */ //////////////////////////////////////////////////////////////////////////// @@ -76,29 +102,47 @@ var polygonFeature = function (arg) { polyFunc = m_this.polygon(); m_coordinates = m_this.data().map(function (d, i) { var poly = polyFunc(d); - var outer, inner; - - outer = (poly.outer || []).map(function (d0, j) { - return posFunc.call(m_this, d0, j, d, i); - }); + var outer, inner, range, coord, j, x, y; + coord = poly.outer || (poly instanceof Array ? poly : []); + outer = new Array(coord.length); + for (j = 0; j < coord.length; j += 1) { + outer[j] = posFunc.call(m_this, coord[j], j, d, i); + x = outer[j].x || outer[j][0] || 0; + y = outer[j].y || outer[j][1] || 0; + if (!j) { + range = {min: {x: x, y: y}, max: {x: x, y: y}}; + } else { + if (x < range.min.x) { range.min.x = x; } + if (y < range.min.y) { range.min.y = y; } + if (x > range.max.x) { range.max.x = x; } + if (y > range.max.y) { range.max.y = y; } + } + } inner = (poly.inner || []).map(function (hole) { - return (hole || []).map(function (d0, k) { - return posFunc.call(m_this, d0, k, d, i); - }); + coord = hole || []; + var trans = new Array(coord.length); + for (j = 0; j < coord.length; j += 1) { + trans[j] = posFunc.call(m_this, coord[j], j, d, i); + } + return trans; }); return { outer: outer, - inner: inner + inner: inner, + range: range }; }); } //////////////////////////////////////////////////////////////////////////// /** - * Get/Set polygon accessor + * Get/set polygon accessor. * - * @returns {geo.pointFeature} + * @memberof geo.polygonFeature + * @param {Object} [polygon] if specified, use this for the polygon accessor + * and return the feature. If not specified, return the current polygon. + * @returns {geo.polygonFeature|Object} */ //////////////////////////////////////////////////////////////////////////// this.polygon = function (val) { @@ -115,9 +159,13 @@ var polygonFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** - * Get/Set position accessor + * Get/Set position accessor. * - * @returns {geo.pointFeature} + * @memberof geo.polygonFeature + * @param {Object} [position] if specified, use this for the position + * accessor and return the feature. If not specified, return the current + * position. + * @returns {geo.polygonFeature|Object} */ //////////////////////////////////////////////////////////////////////////// this.position = function (val) { @@ -134,8 +182,10 @@ var polygonFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** - * Point searce method for selection api. Returns markers containing the + * Point search method for selection api. Returns markers containing the * given point. + * + * @memberof geo.polygonFeature * @argument {object} coordinate * @returns {object} */ @@ -146,7 +196,8 @@ var polygonFeature = function (arg) { var inside = util.pointInPolygon( coordinate, coord.outer, - coord.inner + coord.inner, + coord.range ); if (inside) { indices.push(i); @@ -162,9 +213,11 @@ var polygonFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** * Initialize + * @memberof geo.polygonFeature */ //////////////////////////////////////////////////////////////////////////// this._init = function (arg) { + arg = arg || {}; s_init.call(m_this, arg); var defaultStyle = $.extend( @@ -187,5 +240,21 @@ var polygonFeature = function (arg) { return this; }; +/** + * Create a polygonFeature from an object. + * + * @see {@link geo.feature.create} + * @param {geo.layer} layer The layer to add the feature to + * @param {geo.polygonFeature.spec} spec The object specification + * @returns {geo.polygonFeature|null} + */ +polygonFeature.create = function (layer, spec) { + 'use strict'; + + spec = spec || {}; + spec.type = 'polygon'; + return feature.create(layer, spec); +}; + inherit(polygonFeature, feature); module.exports = polygonFeature; diff --git a/src/util/init.js b/src/util/init.js index 2296649c94..db4a1a285b 100644 --- a/src/util/init.js +++ b/src/util/init.js @@ -38,32 +38,40 @@ * http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html * @param {geo.screenPosition} point The test point * @param {geo.screenPosition[]} outer The outer boundary of the polygon - * @param {geo.screenPosition[][]?} inner Inner boundaries (holes) + * @param {geo.screenPosition[][]} [inner] Inner boundaries (holes) + * @param {Object} [range] If specified, range.min.x, range.min.y, + * range.max.x, and range.max.y specified the extents of the outer + * polygon and are used for early detection. + * @returns {boolean} true if the point is inside the polygon. */ - pointInPolygon: function (point, outer, inner) { - var inside = false, n = outer.length; + pointInPolygon: function (point, outer, inner, range) { + var inside = false, n = outer.length, i, j; + + if (range && range.min && range.max) { + if (point.x < range.min.x || point.y < range.min.y || + point.x > range.max.x || point.y > range.max.y) { + return; + } + } if (n < 3) { // we need 3 coordinates for this to make sense return false; } - outer.forEach(function (vert, i) { - var j = (n + i - 1) % n; - var intersect = ( - ((outer[i].y > point.y) !== (outer[j].y > point.y)) && - (point.x < (outer[j].x - outer[i].x) * - (point.y - outer[i].y) / - (outer[j].y - outer[i].y) + outer[i].x) - ); - if (intersect) { + for (i = 0, j = n - 1; i < n; j = i, i += 1) { + if (((outer[i].y > point.y) !== (outer[j].y > point.y)) && + (point.x < (outer[j].x - outer[i].x) * + (point.y - outer[i].y) / (outer[j].y - outer[i].y) + outer[i].x)) { inside = !inside; } - }); + } - (inner || []).forEach(function (hole) { - inside = inside && !geo.util.pointInPolygon(point, hole); - }); + if (inner && inside) { + (inner || []).forEach(function (hole) { + inside = inside && !geo.util.pointInPolygon(point, hole); + }); + } return inside; }, diff --git a/testing/test-data/land_polygons.json.md5 b/testing/test-data/land_polygons.json.md5 new file mode 100644 index 0000000000..2a98efdc0a --- /dev/null +++ b/testing/test-data/land_polygons.json.md5 @@ -0,0 +1 @@ +f88d833b7614754e914ef56de76d889c \ No newline at end of file diff --git a/testing/test-data/land_polygons.json.url b/testing/test-data/land_polygons.json.url new file mode 100644 index 0000000000..450484bd8e --- /dev/null +++ b/testing/test-data/land_polygons.json.url @@ -0,0 +1 @@ +https://data.kitware.com/api/v1/file/5762bd868d777f68be8f4017/download diff --git a/tests/cases/polygonFeature.js b/tests/cases/polygonFeature.js new file mode 100644 index 0000000000..c329e3e53d --- /dev/null +++ b/tests/cases/polygonFeature.js @@ -0,0 +1,228 @@ +// Test geo.polygonFeature and geo.gl.polygonFeature + +var geo = require('../test-utils').geo; +var $ = require('jquery'); +var vgl = require('vgl'); +var mockVGLRenderer = require('../test-utils').mockVGLRenderer; +var restoreVGLRenderer = require('../test-utils').restoreVGLRenderer; +var waitForIt = require('../test-utils').waitForIt; +// var closeToArray = require('../test-utils').closeToArray; + +describe('geo.polygonFeature', function () { + 'use strict'; + + var testPolygons = [ + [{x: 0, y: 0}, {x: 10, y: 5}, {x: 5, y: 10}], + { + outer: + [{x: 20, y: 10}, {x: 30, y: 0}, {x: 40, y: 10}, {x: 30, y: 20}], + inner: [ + [{x: 25, y: 10}, {x: 30, y: 5}, {x: 35, y: 10}, {x: 30, y: 15}] + ], + fillOpacity: 0.5 + }, { + outer: + [{x: 50, y: 10}, {x: 60, y: 0}, {x: 70, y: 10}, {x: 60, y: 20}], + inner: [ + [{x: 55, y: 10}, {x: 60, y: 15}, {x: 65, y: 10}, {x: 60, y: 5}] + ], + fillColor: '#FF8000' + }, { + outer: + [{x: 50, y: 8}, {x: 70, y: 8}, {x: 70, y: 12}, {x: 50, y: 12}], + inner: [ + [{x: 58, y: 10}, {x: 60, y: 15}, {x: 62, y: 10}, {x: 60, y: 5}] + ], + uniformPolygon: true + } + ]; + var testStyle = { + fillOpacity: function (d, idx, poly, polyidx) { + return poly.fillOpacity !== undefined ? poly.fillOpacity : 1; + }, + fillColor: function (d, idx, poly, polyidx) { + return poly.fillColor !== undefined ? poly.fillColor : 'blue'; + }, + uniformPolygon: function (d) { + return d.uniformPolygon !== undefined ? d.uniformPolygon : false; + } + }; + + function create_map(opts) { + var node = $('
').css({width: '640px', height: '360px'}); + $('#map').remove(); + $('body').append(node); + opts = $.extend({}, opts); + opts.node = node; + return geo.map(opts); + } + + describe('create', function () { + it('create function', function () { + mockVGLRenderer(); + var map, layer, polygon; + map = create_map(); + layer = map.createLayer('feature', {renderer: 'vgl'}); + polygon = geo.polygonFeature.create(layer); + expect(polygon instanceof geo.polygonFeature).toBe(true); + restoreVGLRenderer(); + }); + }); + + describe('Check class accessors', function () { + var map, layer, polygon; + var pos = [[[0, 0], [10, 5], [5, 10]]]; + it('position', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + polygon = geo.polygonFeature({layer: layer}); + polygon._init(); + expect(polygon.position()('a')).toBe('a'); + polygon.position(pos); + expect(polygon.position()).toEqual(pos); + polygon.position(function () { return 'b'; }); + expect(polygon.position()('a')).toEqual('b'); + + polygon = geo.polygonFeature({layer: layer, position: pos}); + polygon._init(); + expect(polygon.position()).toEqual(pos); + }); + + it('polygon', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + polygon = geo.polygonFeature({layer: layer}); + polygon._init(); + expect(polygon.polygon()('a')).toBe('a'); + polygon.polygon(pos); + expect(polygon.polygon()).toEqual(pos); + polygon.polygon(function () { return 'b'; }); + expect(polygon.polygon()('a')).toEqual('b'); + + polygon = geo.polygonFeature({layer: layer, polygon: pos}); + polygon._init(); + expect(polygon.polygon()).toEqual(pos); + }); + + it('data', function () { + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + polygon = geo.polygonFeature({layer: layer}); + polygon._init(); + expect(polygon.data()).toEqual([]); + polygon.data(pos); + expect(polygon.data()).toEqual(pos); + + polygon = geo.polygonFeature({layer: layer}); + polygon._init({style: {data: pos}}); + expect(polygon.data()).toEqual(pos); + }); + }); + + describe('Public utility methods', function () { + describe('pointSearch', function () { + it('basic usage', function () { + var map, layer, polygon, data, pt; + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + polygon = geo.polygonFeature({layer: layer}); + polygon._init(); + data = testPolygons; + polygon.data(data); + pt = polygon.pointSearch({x: 5, y: 5}); + expect(pt.index).toEqual([0]); + expect(pt.found.length).toBe(1); + expect(pt.found[0][0]).toEqual(data[0][0]); + pt = polygon.pointSearch({x: 21, y: 10}); + expect(pt.index).toEqual([1]); + expect(pt.found.length).toBe(1); + pt = polygon.pointSearch({x: 30, y: 10}); + expect(pt.index).toEqual([]); + expect(pt.found.length).toBe(0); + pt = polygon.pointSearch({x: 51, y: 10}); + expect(pt.index).toEqual([2, 3]); + expect(pt.found.length).toBe(2); + pt = polygon.pointSearch({x: 57, y: 10}); + expect(pt.index).toEqual([3]); + expect(pt.found.length).toBe(1); + /* If the inner hole extends past the outside, it doesn't make that + * point in the polygon */ + pt = polygon.pointSearch({x: 60, y: 13}); + expect(pt.index).toEqual([]); + expect(pt.found.length).toBe(0); + }); + }); + }); + + describe('Private utility methods', function () { + describe('_init', function () { + var map, layer; + it('arg gets added to style', function () { + var polygon; + + map = create_map(); + layer = map.createLayer('feature', {renderer: null}); + polygon = geo.polygonFeature({layer: layer}); + /* init is not automatically called on the geo.polygonFeature (it is on + * geo.gl.polygonFeature). */ + polygon._init({ + style: {fillColor: '#FFFFFF'} + }); + expect(polygon.style('fillColor')).toBe('#FFFFFF'); + }); + }); + }); + + /* This is a basic integration test of geo.gl.polygonFeature. */ + describe('geo.gl.polygonFeature', function () { + var map, layer, polygons, glCounts, buildTime; + it('basic usage', function () { + + mockVGLRenderer(); + map = create_map(); + layer = map.createLayer('feature'); + polygons = layer.createFeature('polygon', {style: testStyle, data: testPolygons}); + buildTime = polygons.buildTime().getMTime(); + /* Trigger rerendering */ + polygons.data(testPolygons); + map.draw(); + expect(buildTime).not.toEqual(polygons.buildTime().getMTime()); + glCounts = $.extend({}, vgl.mockCounts()); + }); + waitForIt('next render gl A', function () { + return vgl.mockCounts().createProgram === (glCounts.createProgram || 0) + 2; + }); + it('update the style', function () { + polygons.style('fillColor', function (d) { + return 'red'; + }); + glCounts = $.extend({}, vgl.mockCounts()); + buildTime = polygons.buildTime().getMTime(); + polygons.draw(); + }); + waitForIt('next render gl B', function () { + return vgl.mockCounts().bufferData >= (glCounts.bufferData || 0) + 1 && + buildTime !== polygons.buildTime().getMTime(); + }); + it('update the style', function () { + polygons.style('fillColor', function (d) { + return '#ff0000'; + }); + glCounts = $.extend({}, vgl.mockCounts()); + buildTime = polygons.buildTime().getMTime(); + polygons.draw(); + }); + waitForIt('next render gl C', function () { + return vgl.mockCounts().bufferData >= (glCounts.bufferData || 0) + 1 && + buildTime !== polygons.buildTime().getMTime(); + }); + it('_exit', function () { + var buildTime = polygons.buildTime().getMTime(); + layer.deleteFeature(polygons); + polygons.data(testPolygons); + map.draw(); + expect(buildTime).toEqual(polygons.buildTime().getMTime()); + restoreVGLRenderer(); + }); + }); +});