diff --git a/.npmignore b/.npmignore index f57b4f56d5..2b9bb45213 100644 --- a/.npmignore +++ b/.npmignore @@ -7,3 +7,5 @@ scripts/ built/ *build*/ .git/ +bower_components/ +dist/ diff --git a/geo.js b/geo.js index ff9743ca7c..2585484315 100644 --- a/geo.js +++ b/geo.js @@ -5,6 +5,7 @@ window.geo = geo; // jshint ignore: line geo.renderers = {}; geo.features = {}; geo.fileReaders = {}; +geo.rendererLayerAdjustments = {}; ////////////////////////////////////////////////////////////////////////////// /** @@ -12,7 +13,7 @@ geo.fileReaders = {}; */ ////////////////////////////////////////////////////////////////////////////// geo.inherit = function (C, P) { // jshint ignore: line - "use strict"; + 'use strict'; var F = inherit.func(); F.prototype = P.prototype; @@ -20,20 +21,40 @@ geo.inherit = function (C, P) { // jshint ignore: line C.prototype.constructor = C; }; geo.inherit.func = function () { - "use strict"; + 'use strict'; return function () {}; }; // Should get rid of this at some point. window.inherit = geo.inherit; +////////////////////////////////////////////////////////////////////////////// +/** + * This is a helper method for generating new-style subclasses as an + * alternative to the older `inherit` classes. Note: these classes + * intentionally don't support constructors for the moment. We may + * consider alternate semantics such as ES6 classes or stampit + * (https://github.com/stampit-org/stampit) as an alternative to handling + * private variables. + * + * @param {object?} props Instance methods and properties to add/override + * @returns {object} The inherited object + */ +////////////////////////////////////////////////////////////////////////////// +geo.extend = function (props) { + 'use strict'; + var child = Object.create(this.prototype); + $.extend(child.prototype, props || {}); + return child; +}; + ////////////////////////////////////////////////////////////////////////////// /** * Register a new file reader type */ ////////////////////////////////////////////////////////////////////////////// geo.registerFileReader = function (name, func) { - "use strict"; + 'use strict'; if (geo.fileReaders === undefined) { geo.fileReaders = {}; @@ -48,7 +69,7 @@ geo.registerFileReader = function (name, func) { */ ////////////////////////////////////////////////////////////////////////////// geo.createFileReader = function (name, opts) { - "use strict"; + 'use strict'; if (geo.fileReaders.hasOwnProperty(name)) { return geo.fileReaders[name](opts); @@ -62,7 +83,7 @@ geo.createFileReader = function (name, opts) { */ ////////////////////////////////////////////////////////////////////////////// geo.registerRenderer = function (name, func) { - "use strict"; + 'use strict'; if (geo.renderers === undefined) { geo.renderers = {}; @@ -77,7 +98,7 @@ geo.registerRenderer = function (name, func) { */ ////////////////////////////////////////////////////////////////////////////// geo.createRenderer = function (name, layer, canvas, options) { - "use strict"; + 'use strict'; if (geo.renderers.hasOwnProperty(name)) { var ren = geo.renderers[name]( @@ -95,7 +116,7 @@ geo.createRenderer = function (name, layer, canvas, options) { */ ////////////////////////////////////////////////////////////////////////////// geo.registerFeature = function (category, name, func) { - "use strict"; + 'use strict'; if (geo.features === undefined) { geo.features = {}; @@ -115,26 +136,71 @@ geo.registerFeature = function (category, name, func) { */ ////////////////////////////////////////////////////////////////////////////// geo.createFeature = function (name, layer, renderer, arg) { - "use strict"; + 'use strict'; var category = renderer.api(), - options = {"layer": layer, "renderer": renderer}; + options = {'layer': layer, 'renderer': renderer}; if (category in geo.features && name in geo.features[category]) { if (arg !== undefined) { $.extend(true, options, arg); } - return geo.features[category][name](options); + var feature = geo.features[category][name](options); + layer.gcs = function () { + return layer.map().gcs(); + }; + return feature; } return null; }; +////////////////////////////////////////////////////////////////////////////// +/** + * Register a layer adjustment. + */ +////////////////////////////////////////////////////////////////////////////// +geo.registerLayerAdjustment = function (category, name, func) { + 'use strict'; + + if (geo.rendererLayerAdjustments === undefined) { + geo.rendererLayerAdjustments = {}; + } + + if (!(category in geo.rendererLayerAdjustments)) { + geo.rendererLayerAdjustments[category] = {}; + } + + // TODO Add warning if the name already exists + geo.rendererLayerAdjustments[category][name] = func; +}; + +////////////////////////////////////////////////////////////////////////////// +/** + * If a layer needs to be adjusted based on the renderer, call the function + * that adjusts it. + * + * @param {string} name Name of the layer. + * @param {object} layer Instantiated layer object. + */ +////////////////////////////////////////////////////////////////////////////// +geo.adjustLayerForRenderer = function (name, layer) { + 'use strict'; + var rendererName = layer.rendererName(); + if (rendererName) { + if (geo.rendererLayerAdjustments && + geo.rendererLayerAdjustments[rendererName] && + geo.rendererLayerAdjustments[rendererName][name]) { + geo.rendererLayerAdjustments[rendererName][name].apply(layer); + } + } +}; + ////////////////////////////////////////////////////////////////////////////// /** * Register a new layer type */ ////////////////////////////////////////////////////////////////////////////// geo.registerLayer = function (name, func) { - "use strict"; + 'use strict'; if (geo.layers === undefined) { geo.layers = {}; @@ -149,10 +215,10 @@ geo.registerLayer = function (name, func) { */ ////////////////////////////////////////////////////////////////////////////// geo.createLayer = function (name, map, arg) { - "use strict"; + 'use strict'; /// Default renderer is vgl - var options = {"map": map, "renderer": "vgl"}, + var options = {'map': map, 'renderer': 'vgl'}, layer = null; if (name in geo.layers) { @@ -173,7 +239,7 @@ geo.createLayer = function (name, map, arg) { */ ////////////////////////////////////////////////////////////////////////////// geo.registerWidget = function (category, name, func) { - "use strict"; + 'use strict'; if (geo.widgets === undefined) { geo.widgets = {}; @@ -192,24 +258,28 @@ geo.registerWidget = function (category, name, func) { * Create new instance of the widget */ ////////////////////////////////////////////////////////////////////////////// -geo.createWidget = function (name, layer, renderer, arg) { - "use strict"; +geo.createWidget = function (name, layer, arg) { + 'use strict'; - var category = renderer.api(), - options = {"layer": layer, "renderer": renderer}; - if (category in geo.widgets && name in geo.widgets[category]) { + var options = { + layer: layer + }; + + if (name in geo.widgets.dom) { if (arg !== undefined) { $.extend(true, options, arg); } - return geo.widgets[category][name](options); + + return geo.widgets.dom[name](options); } - return null; + + throw new Error('Cannot create unknown widget ' + name); }; // Add a polyfill for window.requestAnimationFrame. if (!window.requestAnimationFrame) { window.requestAnimationFrame = function (func) { - "use strict"; + 'use strict'; window.setTimeout(func, 15); }; @@ -218,15 +288,22 @@ if (!window.requestAnimationFrame) { // Add a polyfill for Math.log2 if (!Math.log2) { Math.log2 = function () { - "use strict"; + 'use strict'; return Math.log.apply(Math, arguments) / Math.LN2; }; } +// Add a polyfill for Math.sinh +Math.sinh = Math.sinh || function (x) { + 'use strict'; + var y = Math.exp(x); + return (y - 1 / y) / 2; +}; + /*global geo*/ -geo.version = "0.5.0"; +geo.version = "0.6.0-rc.1"; ////////////////////////////////////////////////////////////////////////////// /** @@ -6293,6 +6370,19 @@ vgl.camera = function (arg) { return this.computeViewMatrix(); }; + //////////////////////////////////////////////////////////////////////////// + /** + * Set the view-matrix for the camera and mark that it is up to date so that + * it won't be recomputed unless something else changes. + * + * @param {mat4} view: new view matrix. + */ + //////////////////////////////////////////////////////////////////////////// + this.setViewMatrix = function (view) { + mat4.copy(m_viewMatrix, view); + m_computeModelViewMatrixTime.modified(); + }; + //////////////////////////////////////////////////////////////////////////// /** * Return camera projection matrix This method does not compute the @@ -6306,6 +6396,19 @@ vgl.camera = function (arg) { return this.computeProjectionMatrix(); }; + //////////////////////////////////////////////////////////////////////////// + /** + * Set the projection-matrix for the camera and mark that it is up to date so + * that it won't be recomputed unless something else changes. + * + * @param {mat4} proj: new projection matrix. + */ + //////////////////////////////////////////////////////////////////////////// + this.setProjectionMatrix = function (proj) { + mat4.copy(m_projectionMatrix, proj); + m_computeProjectMatrixTime.modified(); + }; + //////////////////////////////////////////////////////////////////////////// /** * Return clear mask used by this camera @@ -6554,7 +6657,7 @@ vgl.camera = function (arg) { parseFloat(m_parallelExtents.zoom.toFixed(0))) { return null; } - var align = {round: unitsPerPixel, dx: 0, dy: 0}; + var align = {roundx: unitsPerPixel, roundy: unitsPerPixel, dx: 0, dy: 0}; /* If the screen is an odd number of pixels, shift the view center to the * center of a pixel so that the pixels fit discretely across the screen. * If an even number of pixels, align the view center between pixels for @@ -9122,8 +9225,8 @@ vgl.modelViewOriginUniform = function (name, origin) { * units-per-pixel, and align.dx and .dy are either 0 or half the size of * a unit-per-pixel. The alignment guarantees that the texels are * aligned with screen pixels. */ - view[12] = Math.round(view[12] / align.round) * align.round + align.dx; - view[13] = Math.round(view[13] / align.round) * align.round + align.dy; + view[12] = Math.round(view[12] / align.roundx) * align.roundx + align.dx; + view[13] = Math.round(view[13] / align.roundy) * align.roundy + align.dy; } this.set(view); }; @@ -12788,6 +12891,62 @@ vgl.DataBuffers = function (initialSize) { 0 ) }; + }, + + /** + * Radius of the earth in meters, from the equatorial radius of SRID 4326. + */ + radiusEarth: 6378137, + + /** + * Linearly combine two "coordinate-like" objects in a uniform way. + * Coordinate like objects have ``x``, ``y``, and optionally a ``z`` + * key. The first object is mutated. + * + * a <= ca * a + cb * b + * + * @param {number} ca + * @param {object} a + * @param {number} [a.x=0] + * @param {number} [a.y=0] + * @param {number} [a.z=0] + * @param {number} cb + * @param {object} b + * @param {number} [b.x=0] + * @param {number} [b.y=0] + * @param {number} [b.z=0] + * @returns {object} ca * a + cb * b + */ + lincomb: function (ca, a, cb, b) { + a.x = ca * (a.x || 0) + cb * (b.x || 0); + a.y = ca * (a.y || 0) + cb * (b.y || 0); + a.z = ca * (a.x || 0) + cb * (b.x || 0); + return a; + }, + + /** + * Element-wise product of two coordinate-like object. Mutates + * the first object. Note the default values for ``b``, which + * are intended to used as a anisotropic scaling factors. + * + * a <= a * b^pow + * + * @param {object} a + * @param {number} [a.x=0] + * @param {number} [a.y=0] + * @param {number} [a.z=0] + * @param {object} b + * @param {number} [b.x=1] + * @param {number} [b.y=1] + * @param {number} [b.z=1] + * @param {number} [pow=1] + * @returns {object} a * b^pow + */ + scale: function (a, b, pow) { + a.x = (a.x || 0) * Math.pow(b.x || 1, pow); + a.y = (a.y || 0) * Math.pow(b.y || 1, pow); + a.z = (a.z || 0) * Math.pow(b.z || 1, pow); + return a; } }; @@ -13817,6 +13976,14 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. geo.util.ClusterGroup = C; })(); +(function () { + 'use strict'; + + geo.util.scale = { + d3: d3.scale + }; +})(); + ////////////////////////////////////////////////////////////////////////////// /** * Create a new instance of class object @@ -13835,18 +14002,19 @@ geo.object = function () { var m_this = this, m_eventHandlers = {}, m_idleHandlers = [], - m_deferredCount = 0; + m_promiseCount = 0; ////////////////////////////////////////////////////////////////////////////// /** - * Bind a handler that will be called once when all deferreds are resolved. + * Bind a handler that will be called once when all internal promises are + * resolved. * * @param {function} handler A function taking no arguments * @returns {geo.object[]|geo.object} this */ ////////////////////////////////////////////////////////////////////////////// this.onIdle = function (handler) { - if (m_deferredCount) { + if (m_promiseCount) { m_idleHandlers.push(handler); } else { handler(); @@ -13856,22 +14024,25 @@ geo.object = function () { ////////////////////////////////////////////////////////////////////////////// /** - * Add a new deferred object preventing idle event handlers from being called. + * Add a new promise object preventing idle event handlers from being called + * until it is resolved. * - * @param {$.defer} defer A jquery defered object + * @param {Promise} promise A promise object */ ////////////////////////////////////////////////////////////////////////////// - this.addDeferred = function (defer) { - m_deferredCount += 1; - defer.done(function () { - m_deferredCount -= 1; - if (!m_deferredCount) { + this.addPromise = function (promise) { + // called on any resolution of the promise + function onDone() { + m_promiseCount -= 1; + if (!m_promiseCount) { m_idleHandlers.splice(0, m_idleHandlers.length) .forEach(function (handler) { handler(); }); } - }); + } + m_promiseCount += 1; + promise.then(onDone, onDone); return m_this; }; @@ -13905,7 +14076,7 @@ geo.object = function () { /** * Trigger an event (or events) on this object and call all handlers * - * @param {String} event An event from {geo.events} + * @param {String} event An event from {geo.event} * @param {Object} args An optional argument to pass to handlers */ ////////////////////////////////////////////////////////////////////////////// @@ -13919,6 +14090,10 @@ geo.object = function () { return m_this; } + // append the event type to the argument object + args = args || {}; + args.event = event; + if (m_eventHandlers.hasOwnProperty(event)) { m_eventHandlers[event].forEach(function (handler) { handler.call(m_this, args); @@ -13942,7 +14117,7 @@ geo.object = function () { if (event === undefined) { m_eventHandlers = {}; m_idleHandlers = []; - m_deferredCount = 0; + m_promiseCount = 0; } if (Array.isArray(event)) { event.forEach(function (e) { @@ -14007,19 +14182,19 @@ geo.sceneObject = function (arg) { m_children = [], s_exit = this._exit, s_trigger = this.geoTrigger, - s_addDeferred = this.addDeferred, + s_addPromise = this.addPromise, s_onIdle = this.onIdle; ////////////////////////////////////////////////////////////////////////////// /** - * Override object.addDeferred to propagate up the scene tree. + * Override object.addPromise to propagate up the scene tree. */ ////////////////////////////////////////////////////////////////////////////// - this.addDeferred = function (defer) { + this.addPromise = function (promise) { if (m_parent) { - m_parent.addDeferred(defer); + m_parent.addPromise(promise); } else { - s_addDeferred(defer); + s_addPromise(promise); } }; @@ -14183,705 +14358,1490 @@ inherit(geo.timestamp, vgl.timestamp); ////////////////////////////////////////////////////////////////////////////// /** - * Create an instance of quadratic surface generator - * in Cartesian coordinates by the equation - * (x / a)^2 + (y / b)^2 + (z / c)^2 = 1. Used - * primarily to create planetary bodies + * This purpose of this class is to provide a generic interface for computing + * coordinate transformationss. The interface is taken from the proj4js, + * which also provides the geospatial projection implementation. The + * interface is intentionally simple to allow for custom, non-geospatial use + * cases. For further details, see http://proj4js.org/ * - * @class - * @param {Number} [x=0] Radius in X direction - * @param {Number} [y=0] Radius in Y direction - * @param {Number} [z=0] Radius in Z direction + * The default transforms lat/long coordinates into web mercator + * for use with standard tile sets. * - * @returns {geo.ellipsoid} + * This class is intended to be extended in the future to support 2.5 and 3 + * dimensional transformations. The forward/inverse methods take optional + * z values that are ignored in current mapping context, but will in the + * future perform more general 3D transformations. + * + * @class + * @extends geo.object + * @param {object} options Constructor options + * @param {string} options.source A proj4 string for the source projection + * @param {string} options.target A proj4 string for the target projection + * @returns {geo.transform} */ - ////////////////////////////////////////////////////////////////////////////// -geo.ellipsoid = function (x, y, z) { - 'use strict'; - if (!(this instanceof geo.ellipsoid)) { - return new geo.ellipsoid(x, y, z); - } - - x = vgl.defaultValue(x, 0.0); - y = vgl.defaultValue(y, 0.0); - z = vgl.defaultValue(z, 0.0); +////////////////////////////////////////////////////////////////////////////// - if (x < 0.0 || y < 0.0 || z < 0.0) { - return console.log('[error] Al radii components must be greater than zero'); +geo.transform = function (options) { + 'use strict'; + if (!(this instanceof geo.transform)) { + return new geo.transform(options); } var m_this = this, - m_radii = new vec3.fromValues(x, y, z), - m_radiiSquared = new vec3.fromValues( - x * x, y * y, z * z), - m_minimumRadius = Math.min(x, y, z), - m_maximumRadius = Math.max(x, y, z); + m_proj, // The raw proj4js object + m_source, // The source projection + m_target; // The target projection - //////////////////////////////////////////////////////////////////////////// /** - * Return radii of ellipsoid + * Generate the internal proj4 object. + * @private */ - //////////////////////////////////////////////////////////////////////////// - this.radii = function () { - return m_radii; - }; + function generate_proj4() { + m_proj = new proj4( + m_this.source(), + m_this.target() + ); + } - //////////////////////////////////////////////////////////////////////////// /** - * Return squared radii of the ellipsoid + * Get/Set the source projection */ - //////////////////////////////////////////////////////////////////////////// - this.radiiSquared = function () { - return m_radiiSquared; + this.source = function (arg) { + if (arg === undefined) { + return m_source || 'EPSG:4326'; + } + m_source = arg; + generate_proj4(); + return m_this; }; - //////////////////////////////////////////////////////////////////////////// /** - * Return maximum radius of the ellipsoid - * - * @return {Number} The maximum radius of the ellipsoid + * Get/Set the target projection */ - //////////////////////////////////////////////////////////////////////////// - this.maximumRadius = function () { - return m_maximumRadius; + this.target = function (arg) { + if (arg === undefined) { + return m_target || 'EPSG:3857'; + } + m_target = arg; + generate_proj4(); + return m_this; }; - //////////////////////////////////////////////////////////////////////////// /** - * Return minimum radius of the ellipsoid + * Perform a forward transformation (source -> target) + * @protected + * + * @param {object} point The point coordinates + * @param {number} point.x The x-coordinate (i.e. longitude) + * @param {number} point.y The y-coordinate (i.e. latitude) + * @param {number} [point.z=0] The z-coordinate (i.e. elevation) * - * @return {Number} The maximum radius of the ellipsoid + * @returns {object} A point object in the target coordinates */ - //////////////////////////////////////////////////////////////////////////// - this.minimumRadius = function () { - return m_minimumRadius; + this._forward = function (point) { + var pt = m_proj.forward(point); + pt.z = point.z || 0; + return pt; }; - //////////////////////////////////////////////////////////////////////////// /** - * Computes the normal of the plane tangent to the surface of - * the ellipsoid at the provided position - * - * @param {Number} lat The cartographic latitude for which - * to to determine the geodetic normal - * @param {Number} lon The cartographic longitude for which - * to to determine the geodetic normal + * Perform an inverse transformation (target -> source) + * @protected * - * @return {vec3} + * @param {object} point The point coordinates + * @param {number} point.x The x-coordinate (i.e. longitude) + * @param {number} point.y The y-coordinate (i.e. latitude) + * @param {number} [point.z=0] The z-coordinate (i.e. elevation) * - * @exception {DeveloperError} cartographic is required. + * @returns {object} A point object in the source coordinates */ - //////////////////////////////////////////////////////////////////////////// - this.computeGeodeticSurfaceNormal = function (lat, lon) { - if (typeof lat === 'undefined' || typeof lon === 'undefined') { - throw '[error] Valid latitude and longitude is required'; - } - - var cosLatitude = Math.cos(lat), - result = vec3.create(); - - result[0] = cosLatitude * Math.cos(lon); - result[1] = cosLatitude * Math.sin(lon); - result[2] = Math.sin(lat); - - vec3.normalize(result, result); - return result; + this._inverse = function (point) { + var pt = m_proj.inverse(point); + pt.z = point.z || 0; + return pt; }; - //////////////////////////////////////////////////////////////////////////// /** - * Converts the provided geographic latitude, longitude, - * and height to WGS84 coordinate system + * Perform a forward transformation (source -> target) in place + * + * @param {object[]} point The point coordinates or array of points + * @param {number} point.x The x-coordinate (i.e. longitude) + * @param {number} point.y The y-coordinate (i.e. latitude) + * @param {number} [point.z=0] The z-coordinate (i.e. elevation) * - * @param {Number} lat Latitude in radians - * @param {Number} lon Longitude in radians - * @param {Number} elev Elevation - * @return {vec3} Position in the WGS84 coordinate system + * @returns {object} A point object or array in the target coordinates */ - //////////////////////////////////////////////////////////////////////////// - this.transformPoint = function (lat, lon, elev) { - lat = lat * (Math.PI / 180.0); - lon = lon * (Math.PI / 180.0); - - var n = m_this.computeGeodeticSurfaceNormal(lat, lon), - k = vec3.create(), - gamma = Math.sqrt(vec3.dot(n, k)), - result = vec3.create(); - - vec3.multiply(k, m_radiiSquared, n); - vec3.scale(k, k, 1 / gamma); - vec3.scale(n, n, elev); - vec3.add(result, n, k); - return result; + this.forward = function (point) { + if (Array.isArray(point)) { + return point.map(m_this._forward); + } + return m_this._forward(point); }; - //////////////////////////////////////////////////////////////////////////// /** - * Converts the provided geographic latitude, longitude, - * and height to WGS84 coordinate system + * Perform an inverse transformation (target -> source) in place + * @protected + * + * @param {object[]} point The point coordinates or array of points + * @param {number} point.x The x-coordinate (i.e. longitude) + * @param {number} point.y The y-coordinate (i.e. latitude) + * @param {number} [point.z=0] The z-coordinate (i.e. elevation) * - * @param {vgl.geometryData} geom + * @returns {object} A point object in the source coordinates */ - //////////////////////////////////////////////////////////////////////////// - this.transformGeometry = function (geom) { - if (!geom) { - throw '[error] Failed to transform to cartesian. Invalid geometry.'; - } - - var sourceData = geom.sourceData(vgl.vertexAttributeKeys.Position), - sourceDataArray = sourceData.data(), - noOfComponents = sourceData.attributeNumberOfComponents( - vgl.vertexAttributeKeys.Position), - stride = sourceData.attributeStride( - vgl.vertexAttributeKeys.Position), - offset = sourceData.attributeOffset( - vgl.vertexAttributeKeys.Position), - sizeOfDataType = sourceData.sizeOfAttributeDataType( - vgl.vertexAttributeKeys.Position), - index = null, - count = sourceDataArray.length * (1.0 / noOfComponents), - gamma = null, - n = null, - j = 0, - k = vec3.create(), - result = vec3.create(); - - stride /= sizeOfDataType; - offset /= sizeOfDataType; - - if (noOfComponents !== 3) { - throw ('[error] Requires positions with three components'); - } - - for (j = 0; j < count; j += 1) { - index = j * stride + offset; - - sourceDataArray[index] = sourceDataArray[index] * (Math.PI / 180.0); - sourceDataArray[index + 1] = sourceDataArray[index + 1] * (Math.PI / 180.0); - - n = m_this.computeGeodeticSurfaceNormal(sourceDataArray[index + 1], - sourceDataArray[index]); - vec3.multiply(k, m_radiiSquared, n); - gamma = Math.sqrt(vec3.dot(n, k)); - vec3.scale(k, k, 1 / gamma); - vec3.scale(n, n, sourceDataArray[index + 2]); - vec3.add(result, n, k); - - sourceDataArray[index] = result[0]; - sourceDataArray[index + 1] = result[1]; - sourceDataArray[index + 2] = result[2]; + this.inverse = function (point) { + if (Array.isArray(point)) { + return point.map(m_this._inverse); } + return m_this._inverse(point); }; - return m_this; -}; - -//////////////////////////////////////////////////////////////////////////// -/** - * An Ellipsoid instance initialized to the WGS84 standard. - * @memberof ellipsoid - * - */ -//////////////////////////////////////////////////////////////////////////// -geo.ellipsoid.WGS84 = vgl.freezeObject( - geo.ellipsoid(6378137.0, 6378137.0, 6356752.3142451793)); - -//////////////////////////////////////////////////////////////////////////// -/** - * An Ellipsoid instance initialized to radii of (1.0, 1.0, 1.0). - * @memberof ellipsoid - */ -//////////////////////////////////////////////////////////////////////////// -geo.ellipsoid.UNIT_SPHERE = vgl.freezeObject( - geo.ellipsoid(1.0, 1.0, 1.0)); + // Set defaults given by the constructor + options = options || {}; + this.source(options.source); + this.target(options.target); -/** @namespace */ -geo.mercator = { - r_major: 6378137.0 //Equatorial Radius, WGS84 + geo.object.call(this); + return this; }; -////////////////////////////////////////////////////////////////////////////// /** - * Returns the polar radius based on the projection. + * Transform an array of coordinates from one projection into another. The + * transformation may occur in place (modifying the input coordinate array), + * depending on the input format. The coordinates can be an object with x, y, + * and (optionally z) or an array of 2 or 3 values, or an array of either of + * those, or a single flat array with 2 or 3 components per coordinate. Arrays + * are always modified in place. Individual point objects are not altered; new + * point objects are returned unless no transform is needed. * - * @param {Boolean} sperical - * @returns {Number} + * @param {string} srcPrj The source projection + * @param {string} tgtPrj The destination projection + * @param {geoPosition[]} coordinates An array of coordinate objects + * @param {number} numberOfComponents for flat arrays, either 2 or 3. + * + * @returns {geoPosition[]} The transformed coordinates */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.r_minor = function (spherical) { +geo.transform.transformCoordinates = function ( + srcPrj, tgtPrj, coordinates, numberOfComponents) { 'use strict'; - var r_minor; - - spherical = spherical !== undefined ? spherical : false; - - if (spherical) { - r_minor = 6378137.0; - } else { - r_minor = 6356752.314245179; + if (srcPrj === tgtPrj) { + return coordinates; } - return r_minor; -}; + var i, count, offset, xAcc, yAcc, zAcc, writer, output, projPoint, + trans = geo.transform({source: srcPrj, target: tgtPrj}); -////////////////////////////////////////////////////////////////////////////// -/** - * 1/f=(a-b)/a , a=r_major, b=r_minor - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.f = function (spherical) { - 'use strict'; + /// Default Z accessor + zAcc = function () { + return 0.0; + }; - return (geo.mercator.r_major - geo.mercator.r_minor(spherical)) / geo.mercator.r_major; -}; + /// Helper methods + function handleArrayCoordinates() { + if (coordinates[0] instanceof Array) { + if (coordinates[0].length === 2) { + xAcc = function (index) { + return coordinates[index][0]; + }; + yAcc = function (index) { + return coordinates[index][1]; + }; + writer = function (index, x, y) { + output[index] = [x, y]; + }; + } else if (coordinates[0].length === 3) { + xAcc = function (index) { + return coordinates[index][0]; + }; + yAcc = function (index) { + return coordinates[index][1]; + }; + zAcc = function (index) { + return coordinates[index][2]; + }; + writer = function (index, x, y, z) { + output[index] = [x, y, z]; + }; + } else { + throw 'Invalid coordinates. Requires two or three components per array'; + } + } else { + if (coordinates.length === 2) { + offset = 2; -////////////////////////////////////////////////////////////////////////////// -/** - * Convert longitude (Degree) to Tile X - * - * @param {float} lon - * @param {integer} z - * @returns {integer} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.long2tilex = function (lon, z) { - 'use strict'; - var rad = (lon + 180.0) / 360.0, - f = Math.floor(rad * Math.pow(2.0, z)); - return f; -}; + xAcc = function (index) { + return coordinates[index * offset]; + }; + yAcc = function (index) { + return coordinates[index * offset + 1]; + }; + writer = function (index, x, y) { + output[index] = x; + output[index + 1] = y; + }; + } else if (coordinates.length === 3) { + offset = 3; -////////////////////////////////////////////////////////////////////////////// -/** - * Convert latitude (Degree) to Tile Y - * - * @param {float} lat - * @param {integer} z - * @returns {integer} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.lat2tiley = function (lat, z) { - 'use strict'; - var rad = lat * Math.PI / 180.0; - return Math.floor((1.0 - rad / Math.PI) / 2.0 * Math.pow(2.0, z)); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Convert Longitute (Degree) to Tile X and fraction. - * - * @param {float} lon - * @param {integer} z - * @returns {number[]} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.long2tilex2 = function (lon, z) { - 'use strict'; - var rad = (lon + 180.0) / 360.0, - f = rad * Math.pow(2.0, z), - ret = Math.floor(f), - frac = f - ret; - return [ret, frac]; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Convert Latitude (Degree) to Tile Y and fraction - * - * @param {float} lat - * @param {integer} z - * @returns {number[]} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.lat2tiley2 = function (lat, z) { - 'use strict'; - var rad = lat * Math.PI / 180.0, - f = (1.0 - Math.log(Math.tan(rad) + 1.0 / Math.cos(rad)) / - Math.PI) / 2.0 * Math.pow(2.0, z), - ret = Math.floor(f), - frac = f - ret; - return [ret, frac]; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Convert Tile X to Longitute (Degree) - * - * @param {integer} x - * @param {integer} z - * @returns {float} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.tilex2long = function (x, z) { - 'use strict'; - return x / Math.pow(2.0, z) * 360.0 - 180.0; -}; + xAcc = function (index) { + return coordinates[index * offset]; + }; + yAcc = function (index) { + return coordinates[index * offset + 1]; + }; + zAcc = function (index) { + return coordinates[index * offset + 2]; + }; + writer = function (index, x, y, z) { + output[index] = x; + output[index + 1] = y; + output[index + 2] = z; + }; + } else if (numberOfComponents) { + if (numberOfComponents === 2 || numberOfComponents === 3) { + offset = numberOfComponents; -////////////////////////////////////////////////////////////////////////////// -/** - * Convert Tile Y to Latitute (Degree) - * - * @param {integer} y - * @param {integer} z - * @returns {float} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.tiley2lat = function (y, z) { - 'use strict'; - var n = Math.PI - 2.0 * Math.PI * y / Math.pow(2.0, z); - return 180.0 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n))); -}; + xAcc = function (index) { + return coordinates[index]; + }; + yAcc = function (index) { + return coordinates[index + 1]; + }; + if (numberOfComponents === 2) { + writer = function (index, x, y) { + output[index] = x; + output[index + 1] = y; + }; + } else { + zAcc = function (index) { + return coordinates[index + 2]; + }; + writer = function (index, x, y, z) { + output[index] = x; + output[index + 1] = y; + output[index + 2] = z; + }; + } + } else { + throw 'Number of components should be two or three'; + } + } else { + throw 'Invalid coordinates'; + } + } + } -////////////////////////////////////////////////////////////////////////////// -/** - * Convert spherical mercator Y to latitude - * - * @param {float} a - * @returns {float} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.y2lat = function (a) { - 'use strict'; - return 180 / Math.PI * (2 * Math.atan(Math.exp(a * Math.PI / 180)) - Math.PI / 2); -}; + /// Helper methods + function handleObjectCoordinates() { + if (coordinates[0] && + 'x' in coordinates[0] && + 'y' in coordinates[0]) { + xAcc = function (index) { + return coordinates[index].x; + }; + yAcc = function (index) { + return coordinates[index].y; + }; -////////////////////////////////////////////////////////////////////////////// -/** - * Convert latitude into Y position in spherical mercator - * - * @param {float} a - * @returns {float} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.lat2y = function (a) { - 'use strict'; - return 180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + a * (Math.PI / 180) / 2)); -}; + if ('z' in coordinates[0]) { + zAcc = function (index) { + return coordinates[index].z; + }; + writer = function (index, x, y, z) { + output[i] = {x: x, y: y, z: z}; + }; + } else { + writer = function (index, x, y) { + output[index] = {x: x, y: y}; + }; + } + } else if (coordinates && 'x' in coordinates && 'y' in coordinates) { + xAcc = function () { + return coordinates.x; + }; + yAcc = function () { + return coordinates.y; + }; -////////////////////////////////////////////////////////////////////////////// -/** - * - * @param {float} d - * @returns {number} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.deg2rad = function (d) { - 'use strict'; - var r = d * (Math.PI / 180.0); - return r; -}; + if ('z' in coordinates) { + zAcc = function () { + return coordinates.z; + }; + writer = function (index, x, y, z) { + output = {x: x, y: y, z: z}; + }; + } else { + writer = function (index, x, y) { + output = {x: x, y: y}; + }; + } + } else { + throw 'Invalid coordinates'; + } + } -////////////////////////////////////////////////////////////////////////////// -/** - * Convert radian to degree - * - * @param {float} r - * @returns {number} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.rad2deg = function (r) { - 'use strict'; - var d = r / (Math.PI / 180.0); - return d; -}; + if (coordinates instanceof Array) { + output = []; + output.length = coordinates.length; + count = coordinates.length; -////////////////////////////////////////////////////////////////////////////// -/** - * Convert latlon to mercator - * - * @param {float} lon - * @param {float} lat - * @returns {object} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.ll2m = function (lon, lat, spherical) { - 'use strict'; + if (coordinates[0] instanceof Array || + coordinates[0] instanceof Object) { + offset = 1; - if (lat > 89.5) { - lat = 89.5; + if (coordinates[0] instanceof Array) { + handleArrayCoordinates(); + } else if (coordinates[0] instanceof Object) { + handleObjectCoordinates(); + } + } else { + handleArrayCoordinates(); + } + } else if (coordinates && coordinates instanceof Object) { + count = 1; + offset = 1; + if (coordinates && 'x' in coordinates && 'y' in coordinates) { + handleObjectCoordinates(); + } else { + throw 'Coordinates are not valid'; + } } - if (lat < -89.5) { - lat = -89.5; + for (i = 0; i < count; i += offset) { + projPoint = trans.forward({x: xAcc(i), y: yAcc(i), z: zAcc(i)}); + writer(i, projPoint.x, projPoint.y, projPoint.z); } - - var x = this.r_major * this.deg2rad(lon), - temp = this.r_minor(spherical) / this.r_major, - es = 1.0 - (temp * temp), - eccent = Math.sqrt(es), - phi = this.deg2rad(lat), - sinphi = Math.sin(phi), - con = eccent * sinphi, - com = 0.5 * eccent, - con2 = Math.pow((1.0 - con) / (1.0 + con), com), - ts = Math.tan(0.5 * (Math.PI * 0.5 - phi)) / con2, - y = -this.r_major * Math.log(ts), - ret = {'x': x, 'y': y}; - - return ret; + return output; }; -////////////////////////////////////////////////////////////////////////////// /** - * Convert mercator to lat lon + * Apply an affine transformation consisting of a translation + * then a scaling to the given coordinate array. Note, the + * transformation occurs in place so the input coordinate + * object are mutated. + * + * (Possibly extend to support rotations as well) * - * @param {float} x - * @param {float} y + * @param {object} def + * @param {object} def.origin The transformed origin + * @param {object} def.scale The transformed scale factor + * @param {object[]} coords An array of coordinate objects + * + * @returns {object[]} The transformed coordinates */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.m2ll = function (x, y, spherical) { +geo.transform.affineForward = function (def, coords) { 'use strict'; - var lon = this.rad2deg((x / this.r_major)), - temp = this.r_minor(spherical) / this.r_major, - e = Math.sqrt(1.0 - (temp * temp)), - lat = this.rad2deg(this.pjPhi2(Math.exp(-(y / this.r_major)), e)), - ret = {'lon': lon, 'lat': lat}; - - return ret; + var i, origin = def.origin, scale = def.scale || {x: 1, y: 1, z: 1}; + for (i = 0; i < coords.length; i += 1) { + coords[i].x = (coords[i].x - origin.x) * scale.x; + coords[i].y = (coords[i].y - origin.y) * scale.y; + coords[i].z = ((coords[i].z || 0) - (origin.z || 0)) * scale.z; + } + return coords; }; -////////////////////////////////////////////////////////////////////////////// /** - * pjPhi2 + * Apply an inverse affine transformation which is the + * inverse to {@link geo.transform.affineForward}. Note, the + * transformation occurs in place so the input coordinate + * object are mutated. * - * @param {float} ts - * @param {float} e - * @returns {number} + * (Possibly extend to support rotations as well) + * + * @param {object} def + * @param {object} def.origin The transformed origin + * @param {object} def.scale The transformed scale factor + * @param {object[]} coords An array of coordinate objects + * + * @returns {object[]} The transformed coordinates */ -////////////////////////////////////////////////////////////////////////////// -geo.mercator.pjPhi2 = function (ts, e) { +geo.transform.affineInverse = function (def, coords) { 'use strict'; - var N_ITER = 15, - HALFPI = Math.PI / 2, - TOL = 0.0000000001, - con, dphi, - i = N_ITER, - eccnth = 0.5 * e, - Phi = HALFPI - 2.0 * Math.atan(ts); - - do { - con = e * Math.sin(Phi); - dphi = HALFPI - 2.0 * Math.atan(ts * Math.pow( - (1.0 - con) / (1.0 + con), eccnth)) - Phi; - Phi += dphi; - i -= 1; - } while (Math.abs(dphi) > TOL && i); - return Phi; + var i, origin = def.origin, scale = def.scale || {x: 1, y: 1, z: 1}; + for (i = 0; i < coords.length; i += 1) { + coords[i].x = coords[i].x / scale.x + origin.x; + coords[i].y = coords[i].y / scale.y + origin.y; + coords[i].z = (coords[i].z || 0) / scale.z + (origin.z || 0); + } + return coords; }; +inherit(geo.transform, geo.object); -////////////////////////////////////////////////////////////////////////////// -/** - * @class - * @extends geo.sceneObject - * @param {Object?} arg An options argument - * @param {string} arg.attribution An attribution string to display - * @returns {geo.layer} - */ -////////////////////////////////////////////////////////////////////////////// -geo.layer = function (arg) { - "use strict"; - - if (!(this instanceof geo.layer)) { - return new geo.layer(arg); - } - arg = arg || {}; - geo.sceneObject.call(this, arg); +(function () { + 'use strict'; ////////////////////////////////////////////////////////////////////////////// /** - * @private - */ - ////////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_exit = this._exit, - m_style = arg.style === undefined ? {"opacity": 0.5, - "color": [0.8, 0.8, 0.8], - "visible": true, - "bin": 100} : arg.style, - m_id = arg.id === undefined ? geo.layer.newLayerId() : arg.id, - m_name = "", - m_gcs = "EPSG:4326", - m_timeRange = null, - m_source = arg.source || null, - m_map = arg.map === undefined ? null : arg.map, - m_isReference = false, - m_x = 0, - m_y = 0, - m_width = 0, - m_height = 0, - m_node = null, - m_canvas = null, - m_renderer = null, - m_initialized = false, - m_rendererName = arg.renderer === undefined ? "vgl" : arg.renderer, - m_dataTime = geo.timestamp(), - m_updateTime = geo.timestamp(), - m_drawTime = geo.timestamp(), - m_sticky = arg.sticky === undefined ? true : arg.sticky, - m_active = arg.active === undefined ? true : arg.active, - m_attribution = arg.attribution || null; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get whether or not the layer is sticky (navigates with the map). + * This class defines the raw interface for a camera. At a low level, the + * camera provides a methods for converting between a map's coordinate system + * to display pixel coordinates. * - * @returns {Boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.sticky = function () { - return m_sticky; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get whether or not the layer is active. An active layer will receive - * native mouse when the layer is on top. Non-active layers will never - * receive native mouse events. + * For the moment, all camera trasforms are assumed to be expressible as + * 4x4 matrices. More general cameras may follow that break this assumption. * - * @returns {Boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.active = function () { - return m_active; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set root node of the layer + * The interface for the camera is relatively stable for "map-like" views, + * i.e. when the camera is pointing in the direction [0, 0, -1], and placed + * above the z=0 plane. More general view changes and events have not yet + * been defined. * - * @returns {div} + * The camera emits the following events when the view changes: + * + * * {@link geo.event.camera.pan} when the camera is translated in the + * x/y plane + * * {@link geo.event.camera.zoom} when the camera is changed in a way + * that modifies the current zoom level + * * {@link geo.event.camera.view} when the visible bounds change for + * any reason + * * {@link geo.event.camera.projection} when the projection type changes + * * {@link geo.event.camera.viewport} when the viewport changes + * + * By convention, protected methods do not update the internal matrix state, + * public methods do. For now, there are two primary methods that are + * inteded to be used by external classes to mutate the internal state: + * + * * bounds: Set the visible bounds (for initialization and zooming) + * * pan: Translate the camera in x/y by an offset (for panning) + * + * @class + * @extends geo.object + * @param {object?} spec Options argument + * @param {string} spec.projection One of the supported geo.camera.projection + * @param {object} spec.viewport The initial camera viewport + * @param {object} spec.viewport.width + * @param {object} spec.viewport.height + * @returns {geo.camera} + */ + ////////////////////////////////////////////////////////////////////////////// + geo.camera = function (spec) { + if (!(this instanceof geo.camera)) { + return new geo.camera(spec); + } + spec = spec || {}; + geo.object.call(this, spec); + + /** + * The view matrix + * @protected + */ + this._view = mat4.create(); + + /** + * The projection matrix + * @protected + */ + this._proj = mat4.create(); + + /** + * The projection type (one of `this.constructor.projection`) + * @protected + */ + this._projection = null; + + /** + * The transform matrix (view * proj) + * @protected + */ + this._transform = mat4.create(); + + /** + * The inverse transform matrix (view * proj)^-1 + * @protected + */ + this._inverse = mat4.create(); + + /** + * Cached bounds object recomputed on demand. + * @protected + */ + this._bounds = null; + + /** + * Cached "display" matrix recomputed on demand. + * @see {@link geo.camera.display} + * @protected + */ + this._display = null; + + /** + * Cached "world" matrix recomputed on demand. + * @see {@link geo.camera.world} + * @protected + */ + this._world = null; + + /** + * The viewport parameters size and offset. + * @property {number} height Viewport height in pixels + * @property {number} width Viewport width in pixels + * @protected + */ + this._viewport = {width: 1, height: 1}; + + /** + * Set up the projection matrix for the current projection type. + * @protected + */ + this._createProj = function () { + var s = this.constructor.bounds.near / this.constructor.bounds.far; + + // call mat4.frustum or mat4.ortho here + if (this._projection === 'perspective') { + mat4.frustum( + this._proj, + this.constructor.bounds.left * s, + this.constructor.bounds.right * s, + this.constructor.bounds.bottom * s, + this.constructor.bounds.top * s, + -this.constructor.bounds.near, + -this.constructor.bounds.far + ); + } else if (this._projection === 'parallel') { + mat4.ortho( + this._proj, + this.constructor.bounds.left, + this.constructor.bounds.right, + this.constructor.bounds.bottom, + this.constructor.bounds.top, + this.constructor.bounds.near, + this.constructor.bounds.far + ); + } + }; + + /** + * Update the internal state of the camera on change to camera + * parameters. + * @protected + */ + this._update = function () { + this._bounds = null; + this._display = null; + this._world = null; + this._transform = geo.camera.combine(this._proj, this._view); + mat4.invert(this._inverse, this._transform); + this.geoTrigger(geo.event.camera.view, { + camera: this + }); + }; + + /** + * Getter/setter for the view matrix. + * @note copies the matrix value on set. + */ + Object.defineProperty(this, 'view', { + get: function () { + return this._view; + }, + set: function (view) { + mat4.copy(this._view, view); + this._update(); + } + }); + + /** + * Getter/setter for the view bounds. + * + * If not provided, near and far bounds will be set to [-1, 1] by + * default. We will probably want to change this to a unit specific + * value initialized by the map when drawing true 3D objects or + * tilting the camera. + * + * Returned near/far bounds are also -1, 1 for the moment. + */ + Object.defineProperty(this, 'bounds', { + get: function () { + if (this._bounds === null) { + this._bounds = this._getBounds(); + } + return this._bounds; + }, + set: function (bounds) { + this._setBounds(bounds); + this._update(); + } + }); + + /** + * Getter for the "display" matrix. This matrix converts from + * world coordinates into display coordinates. This matrix exists to + * generate matrix3d css transforms that can be used in layers that + * render on the DOM. + */ + Object.defineProperty(this, 'display', { + get: function () { + var mat; + if (this._display === null) { + mat = geo.camera.affine( + {x: 1, y: 1}, // translate to: [0, 2] x [0, 2] + { + x: this.viewport.width / 2, + y: this.viewport.height / -2 + } // scale to: [0, width] x [-height, 0] + ); + + // applies mat to the transform (world -> normalized) + this._display = geo.camera.combine( + mat, + this._transform + ); + } + return this._display; + } + }); + + /** + * Getter for the "world" matrix. This matrix converts from + * display coordinates into world coordinates. This is constructed + * by inverting the "display" matrix. + */ + Object.defineProperty(this, 'world', { + get: function () { + if (this._world === null) { + this._world = mat4.invert( + mat4.create(), + this.display + ); + } + return this._world; + } + }); + + /** + * Getter/setter for the projection type. + */ + Object.defineProperty(this, 'projection', { + get: function () { + return this._projection; + }, + set: function (type) { + if (!this.constructor.projection[type]) { + throw new Error('Unsupported projection type: ' + type); + } + if (type !== this._projection) { + this._projection = type; + this._createProj(); + this._update(); + this.geoTrigger(geo.event.camera.projection, { + camera: this, + projection: type + }); + } + } + }); + + /** + * Getter for the projection matrix (when applicable). + * This generally shouldn't be modified directly because + * the rest of the code assumes that the clipping bounds + * are [-1, -1, -1] to [1, 1, 1] in camera coordinates. + */ + Object.defineProperty(this, 'projectionMatrix', { + get: function () { + return this._proj; + } + }); + + /** + * Getter for the transform matrix. + */ + Object.defineProperty(this, 'transform', { + get: function () { + return this._transform; + } + }); + + /** + * Getter for the inverse transform matrix. + */ + Object.defineProperty(this, 'inverse', { + get: function () { + return this._inverse; + } + }); + + /** + * Getter/setter for the viewport. + */ + Object.defineProperty(this, 'viewport', { + get: function () { + return {width: this._viewport.width, height: this._viewport.height}; + }, + set: function (viewport) { + if (!(viewport.width > 0 && + viewport.height > 0)) { + throw new Error('Invalid viewport dimensions'); + } + if (viewport.width === this._viewport.width && + viewport.height === this._viewport.height) { + return; + } + + // apply scaling to the view matrix to account for the new aspect ratio + // without changing the apparent zoom level + if (this._viewport.width && this._viewport.height) { + this._scale( + vec3.fromValues( + this._viewport.width / viewport.width, + this._viewport.height / viewport.height, + 1 + ) + ); + + // translate by half the difference to keep the center the same + this._translate( + vec3.fromValues( + (viewport.width - this._viewport.width) / 2, + (viewport.height - this._viewport.height) / 2, + 0 + ) + ); + } + + this._viewport = {width: viewport.width, height: viewport.height}; + this._update(); + this.geoTrigger(geo.event.camera.viewport, { + camera: this, + viewport: this.viewport + }); + } + }); + + /** + * Reset the view matrix to its initial (identity) state. + * @protected + * @returns {this} Chainable + */ + this._resetView = function () { + mat4.identity(this._view); + return this; + }; + + /** + * Uses `mat4.translate` to translate the camera by the given vector amount. + * @protected + * @param {vec3} offset The camera translation vector + * @returns {this} Chainable + */ + this._translate = function (offset) { + mat4.translate(this._view, this._view, offset); + }; + + /** + * Uses `mat4.scale` to scale the camera by the given vector amount. + * @protected + * @param {vec3} scale The scaling vector + * @returns {this} Chainable + */ + this._scale = function (scale) { + mat4.scale(this._view, this._view, scale); + }; + + /** + * Project a vec4 from world space into clipped space [-1, 1] in place + * @protected + * @param {vec4} point The point in world coordinates (mutated) + * @returns {vec4} The point in clip space coordinates + */ + this._worldToClip4 = function (point) { + return geo.camera.applyTransform(this._transform, point); + }; + + /** + * Project a vec4 from clipped space into world space in place + * @protected + * @param {vec4} point The point in clipped coordinates (mutated) + * @returns {vec4} The point in world space coordinates + */ + this._clipToWorld4 = function (point) { + return geo.camera.applyTransform(this._inverse, point); + }; + + /** + * Apply the camera's projection transform to the given point. + * @param {vec4} pt a point in clipped coordinates + * @returns {vec4} the point in normalized coordinates + */ + this.applyProjection = function (pt) { + var w; + if (this._projection === 'perspective') { + w = 1 / (pt[3] || 1); + pt[0] = w * pt[0]; + pt[1] = w * pt[1]; + pt[2] = w * pt[2]; + pt[3] = w; + } else { + pt[3] = 1; + } + return pt; + }; + + /** + * Unapply the camera's projection transform from the given point. + * @param {vec4} pt a point in normalized coordinates + * @returns {vec4} the point in clipped coordinates + */ + this.unapplyProjection = function (pt) { + var w; + if (this._projection === 'perspective') { + w = pt[3] || 1; + pt[0] = w * pt[0]; + pt[1] = w * pt[1]; + pt[2] = w * pt[2]; + pt[3] = w; + } else { + pt[3] = 1; + } + return pt; + }; + + + /** + * Project a vec4 from world space into viewport space. + * @param {vec4} point The point in world coordinates (mutated) + * @returns {vec4} The point in display coordinates + * + * @note For the moment, this computation assumes the following: + * * point[3] > 0 + * * depth range [0, 1] + * + * The clip space z and w coordinates are returned with the window + * x/y coordinates. + */ + this.worldToDisplay4 = function (point) { + // This is because z = 0 is the far plane exposed to the user, but + // internally the far plane is at -2. + point[2] -= 2; + + // convert to clip space + this._worldToClip4(point); + + // apply projection specific transformation + point = this.applyProjection(point); + + // convert to display space + point[0] = this._viewport.width * (1 + point[0]) / 2.0; + point[1] = this._viewport.height * (1 - point[1]) / 2.0; + point[2] = (1 + point[2]) / 2.0; + return point; + }; + + /** + * Project a vec4 from display space into world space in place. + * @param {vec4} point The point in display coordinates (mutated) + * @returns {vec4} The point in world space coordinates + * + * @note For the moment, this computation assumes the following: + * * point[3] > 0 + * * depth range [0, 1] + */ + this.displayToWorld4 = function (point) { + // convert to clip space + point[0] = 2 * point[0] / this._viewport.width - 1; + point[1] = -2 * point[1] / this._viewport.height + 1; + point[2] = 2 * point[2] - 1; + + // invert projection transform + point = this.unapplyProjection(point); + + // convert to world coordinates + this._clipToWorld4(point); + + // move far surface to z = 0 + point[2] += 2; + return point; + }; + + /** + * Project a point object from world space into viewport space. + * @param {object} point The point in world coordinates + * @param {number} point.x + * @param {number} point.y + * @returns {object} The point in display coordinates + */ + this.worldToDisplay = function (point) { + // define some magic numbers: + var z = 0, // z coordinate of the surface in world coordinates + w = 1; // enables perspective divide (i.e. for point conversion) + point = this.worldToDisplay4( + [point.x, point.y, z, w] + ); + return {x: point[0], y: point[1], z: point[2]}; + }; + + /** + * Project a point object from viewport space into world space. + * @param {object} point The point in display coordinates + * @param {number} point.x + * @param {number} point.y + * @returns {object} The point in world coordinates + */ + this.displayToWorld = function (point) { + // define some magic numbers: + var z = 1, // the z coordinate of the surface + w = 2; // perspective divide at z = 1 + point = this.displayToWorld4( + [point.x, point.y, z, w] + ); + return {x: point[0], y: point[1]}; + }; + + /** + * Calculate the current bounds in world coordinates from the + * current view matrix. This computes a matrix vector multiplication + * so the result is cached for public facing methods. + * + * @protected + * @returns {object} bounds object + */ + this._getBounds = function () { + var pt, bds = {}; + + + // get lower bounds + pt = this.displayToWorld({ + x: 0, y: this._viewport.height + }); + bds.left = pt.x; + bds.bottom = pt.y; + + // get upper bounds + pt = this.displayToWorld({ + x: this._viewport.width, y: 0 + }); + bds.right = pt.x; + bds.top = pt.y; + + return bds; + }; + + /** + * Sets the view matrix so that the given world bounds + * are in view. To account for the viewport aspect ratio, + * the resulting bounds may be larger in width or height than + * the requested bound, but should be centered in the frame. + * + * @protected + * @param {object} bounds + * @param {number} bounds.left + * @param {number} bounds.right + * @param {number} bounds.bottom + * @param {number} bounds.top + * @param {number?} bounds.near Currently ignored + * @param {number?} bounds.far Currently ignored + * @return {this} Chainable + */ + this._setBounds = function (bounds) { + + var translate = vec3.create(), + scale = vec3.create(), + c_ar, v_ar, w, h; + + bounds.near = bounds.near || 0; + bounds.far = bounds.far || 1; + + // reset view to the identity + this._resetView(); + + w = Math.abs(bounds.right - bounds.left); + h = Math.abs(bounds.top - bounds.bottom); + c_ar = w / h; + v_ar = this._viewport.width / this._viewport.height; + + if (c_ar >= v_ar) { + // grow camera bounds vertically + h = w / v_ar; + scale[0] = 2 / w; + scale[1] = 2 / h; + } else { + // grow bounds horizontally + w = h * v_ar; + scale[0] = 2 / w; + scale[1] = 2 / h; + } + + scale[2] = 1; + this._scale(scale); + + // translate to the new center. + translate[0] = -(bounds.left + bounds.right) / 2; + translate[1] = -(bounds.bottom + bounds.top) / 2; + translate[2] = 0; + + this._translate(translate); + return this; + }; + + /** + * Pans the view matrix by the given amount. + * + * @param {object} offset The delta in world space coordinates. + * @param {number} offset.x + * @param {number} offset.y + * @param {number} [offset.z=0] + */ + this.pan = function (offset) { + this._translate(vec3.fromValues( + offset.x, + offset.y, + offset.z || 0 + )); + this._update(); + }; + + /** + * Zooms the view matrix by the given amount. + * + * @param {number} zoom The zoom scale to apply + */ + this.zoom = function (zoom) { + mat4.scale(this._view, this._view, + vec3.fromValues( + zoom, + zoom, + zoom + ) + ); + this._update(); + }; + + /** + * Returns a CSS transform that converts (by default) from world coordinates + * into display coordinates. This allows users of this module to + * position elements using world coordinates directly inside DOM + * elements. + * + * @note This transform will not take into account projection specific + * transforms. For perspective projections, one can use the properties + * `perspective` and `perspective-origin` to apply the projection + * in css directly. + * + * @param {string} transform The transform to return + * * display + * * world + * @returns {string} The css transform string + */ + this.css = function (transform) { + var m; + switch ((transform || '').toLowerCase()) { + case 'display': + case '': + m = this.display; + break; + case 'world': + m = this.world; + break; + default: + throw new Error('Unknown transform ' + transform); + } + return geo.camera.css(m); + }; + + /** + * Represent a glmatrix as a pretty-printed string. + * @param {mat4} mat A 4 x 4 matrix + * @param {number} prec The number of decimal places + * @returns {string} + */ + this.ppMatrix = function (mat, prec) { + var t = mat; + prec = prec || 2; + function f(i) { + var d = t[i], s = d.toExponential(prec); + if (d >= 0) { + s = ' ' + s; + } + return s; + } + return [ + [f(0), f(4), f(8), f(12)].join(' '), + [f(1), f(5), f(9), f(13)].join(' '), + [f(2), f(6), f(10), f(14)].join(' '), + [f(3), f(7), f(11), f(15)].join(' ') + ].join('\n'); + }; + + /** + * Pretty print the transform matrix. + */ + this.toString = function () { + return this.ppMatrix(this._transform); + }; + + /** + * Return a debugging string of the current camera state. + */ + this.debug = function () { + return [ + 'bounds', + JSON.stringify(this.bounds), + 'view:', + this.ppMatrix(this._view), + 'projection:', + this.ppMatrix(this._proj), + 'transform:', + this.ppMatrix(this._transform) + ].join('\n'); + }; + + /** + * Represent the value of the camera as its transform matrix. + */ + this.valueOf = function () { + return this._transform; + }; + + // initialize the view matrix + this._resetView(); + + // set up the projection matrix + this.projection = spec.projection || 'parallel'; + + // initialize the viewport + if (spec.viewport) { + this.viewport = spec.viewport; + } + + // trigger an initial update to set up the camera state + this._update(); + + return this; + }; + + /** + * Supported projection types. + */ + geo.camera.projection = { + perspective: true, + parallel: true + }; + + /** + * Camera clipping bounds, probably shouldn't be modified. + */ + geo.camera.bounds = { + left: -1, + right: 1, + top: 1, + bottom: -1, + far: -2, + near: -1 + }; + + /** + * Output a mat4 as a css transform. + * @param {mat4} t A matrix transform + * @returns {string} A css transform string + */ + geo.camera.css = function (t) { + return ( + 'matrix3d(' + + [ + t[0].toFixed(20), + t[1].toFixed(20), + t[2].toFixed(20), + t[3].toFixed(20), + t[4].toFixed(20), + t[5].toFixed(20), + t[6].toFixed(20), + t[7].toFixed(20), + t[8].toFixed(20), + t[9].toFixed(20), + t[10].toFixed(20), + t[11].toFixed(20), + t[12].toFixed(20), + t[13].toFixed(20), + t[14].toFixed(20), + t[15].toFixed(20) + ].join(',') + + ')' + ); + }; + + /** + * Generate a mat4 representing an affine coordinate transformation. + * + * For the following affine transform: + * + * x |-> m * (x + a) + b + * + * applies the css transform: + * + * translate(b) scale(m) translate(a) + * + * @param {object?} pre Coordinate offset **before** scaling + * @param {object?} scale Coordinate scaling + * @param {object?} post Coordinate offset **after** scaling + * @returns {mat4} The new transform matrix + */ + geo.camera.affine = function (pre, scale, post) { + var mat = mat4.create(); + + // Note: mat4 operations are applied to the right side of the current + // transform, so the first applied here is the last applied to the + // coordinate. + if (post) { + mat4.translate(mat, mat, [post.x || 0, post.y || 0, post.z || 0]); + } + if (scale) { + mat4.scale(mat, mat, [scale.x || 1, scale.y || 1, scale.z || 1]); + } + if (pre) { + mat4.translate(mat, mat, [pre.x || 0, pre.y || 0, pre.z || 0]); + } + return mat; + }; + + /** + * Apply the given transform matrix to a point in place. + * @param {mat4} t + * @param {vec4} pt + * @returns {vec4} + */ + geo.camera.applyTransform = function (t, pt) { + return vec4.transformMat4(pt, pt, t); + }; + + /** + * Combine two transforms by multiplying their matrix representations. + * @note The second transform provided will be the first applied in the + * coordinate transform. + * @param {mat4} A + * @param {mat4} B + * @returns {mat4} A * B + */ + geo.camera.combine = function (A, B) { + return mat4.mul(mat4.create(), A, B); + }; + + inherit(geo.camera, geo.object); +})(); + +////////////////////////////////////////////////////////////////////////////// +/** + * @class + * @extends geo.sceneObject + * @param {Object?} arg An options argument + * @param {string} arg.attribution An attribution string to display + * @param {number} arg.zIndex The z-index to assign to the layer (defaults + * to the index of the layer inside the map) + * @returns {geo.layer} + */ +////////////////////////////////////////////////////////////////////////////// +geo.layer = function (arg) { + 'use strict'; + + if (!(this instanceof geo.layer)) { + return new geo.layer(arg); + } + arg = arg || {}; + geo.sceneObject.call(this, arg); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_exit = this._exit, + m_id = arg.id === undefined ? geo.layer.newLayerId() : arg.id, + m_name = '', + m_map = arg.map === undefined ? null : arg.map, + m_node = null, + m_canvas = null, + m_renderer = null, + m_initialized = false, + m_rendererName = arg.renderer === undefined ? 'vgl' : arg.renderer, + m_dataTime = geo.timestamp(), + m_updateTime = geo.timestamp(), + m_sticky = arg.sticky === undefined ? true : arg.sticky, + m_active = arg.active === undefined ? true : arg.active, + m_opacity = arg.opacity === undefined ? 1 : arg.opacity, + m_attribution = arg.attribution || null, + m_zIndex; + + if (!m_map) { + throw new Error('Layers must be initialized on a map.'); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the name of the renderer. + * + * @returns {string} */ //////////////////////////////////////////////////////////////////////////// - this.node = function () { - return m_node; + this.rendererName = function () { + return m_rendererName; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set id of the layer + * Get or set the z-index of the layer. The z-index controls the display + * order of the layers in much the same way as the CSS z-index property. * - * @returns {String} + * @param {number} [zIndex] The new z-index + * @returns {number|this} */ //////////////////////////////////////////////////////////////////////////// - this.id = function (val) { - if (val === undefined) { - return m_id; + this.zIndex = function (zIndex) { + if (zIndex === undefined) { + return m_zIndex; } - m_id = geo.newLayerId(); - m_this.modified(); + m_zIndex = zIndex; + m_node.css('z-index', m_zIndex); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set name of the layer + * Bring the layer above the given number of layers. This will rotate the + * current z-indices for this and the next `n` layers. * - * @returns {String} + * @param {number} [n=1] The number of positions to move + * @returns {this} */ //////////////////////////////////////////////////////////////////////////// - this.name = function (val) { - if (val === undefined) { - return m_name; + this.moveUp = function (n) { + var order, i, me = null, tmp, sign; + + // set the default + if (n === undefined) { + n = 1; + } + + // set the sort direction that controls if we are moving up + // or down the z-index + sign = 1; + if (n < 0) { + sign = -1; + n = -n; + } + + // get a sorted list of layers + order = m_this.map().layers().sort( + function (a, b) { return sign * (a.zIndex() - b.zIndex()); } + ); + + for (i = 0; i < order.length; i += 1) { + if (me === null) { + // loop until we get to the current layer + if (order[i] === m_this) { + me = i; + } + } else if (i - me <= n) { + // swap the next n layers + tmp = m_this.zIndex(); + m_this.zIndex(order[i].zIndex()); + order[i].zIndex(tmp); + } else { + // all the swaps are done now + break; + } } - m_name = val; - m_this.modified(); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set opacity of the layer + * Bring the layer below the given number of layers. This will rotate the + * current z-indices for this and the previous `n` layers. * - * @returns {Number} + * @param {number} [n=1] The number of positions to move + * @returns {this} */ //////////////////////////////////////////////////////////////////////////// - this.opacity = function (val) { - if (val === undefined) { - return m_style.opacity; + this.moveDown = function (n) { + if (n === undefined) { + n = 1; } - m_style.opacity = val; - m_this.modified(); - return m_this; + return m_this.moveUp(-n); }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set visibility of the layer + * Bring the layer to the top of the map layers. + * + * @returns {this} */ //////////////////////////////////////////////////////////////////////////// - this.visible = function (val) { - if (val === undefined) { - return m_style.visible; - } - m_style.visible = val; - m_this.modified(); - return m_this; + this.moveToTop = function () { + return m_this.moveUp(m_this.map().children().length - 1); }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set bin of the layer + * Bring the layer to the bottom of the map layers. * - * @returns {Number} + * @returns {this} */ //////////////////////////////////////////////////////////////////////////// - this.bin = function (val) { - if (val === undefined) { - return m_style.bin; - } - m_style.bin = val; - m_this.modified(); - return m_this; + this.moveToBottom = function () { + return m_this.moveDown(m_this.map().children().length - 1); }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set projection of the layer + * Get whether or not the layer is sticky (navigates with the map). + * + * @returns {Boolean} */ //////////////////////////////////////////////////////////////////////////// - this.gcs = function (val) { - if (val === undefined) { - return m_gcs; - } - m_gcs = val; - m_this.modified(); - return m_this; + this.sticky = function () { + return m_sticky; }; //////////////////////////////////////////////////////////////////////////// /** - * Transform layer to the reference layer gcs + * Get whether or not the layer is active. An active layer will receive + * native mouse when the layer is on top. Non-active layers will never + * receive native mouse events. + * + * @returns {Boolean} */ //////////////////////////////////////////////////////////////////////////// - this.transform = function (val) { - geo.transform.transformLayer(val, m_this, m_map.baseLayer()); - return m_this; + this.active = function () { + return m_active; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set root node of the layer + * + * @returns {div} + */ + //////////////////////////////////////////////////////////////////////////// + this.node = function () { + return m_node; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set time range of the layer + * Get/Set id of the layer + * + * @returns {String} */ //////////////////////////////////////////////////////////////////////////// - this.timeRange = function (val) { + this.id = function (val) { if (val === undefined) { - return m_timeRange; + return m_id; } - m_timeRange = val; + m_id = geo.newLayerId(); m_this.modified(); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set source of the layer + * Get/Set name of the layer + * + * @returns {String} */ //////////////////////////////////////////////////////////////////////////// - this.source = function (val) { + this.name = function (val) { if (val === undefined) { - return m_source; + return m_name; } - m_source = val; + m_name = val; m_this.modified(); return m_this; }; @@ -14891,14 +15851,8 @@ geo.layer = function (arg) { * Get/Set map of the layer */ //////////////////////////////////////////////////////////////////////////// - this.map = function (val) { - if (val === undefined) { - return m_map; - } - m_map = val; - m_map.node().append(m_node); - m_this.modified(); - return m_this; + this.map = function () { + return m_map; }; //////////////////////////////////////////////////////////////////////////// @@ -14920,15 +15874,6 @@ geo.layer = function (arg) { return m_canvas; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Get viewport of the layer - */ - //////////////////////////////////////////////////////////////////////////// - this.viewport = function () { - return [m_x, m_y, m_width, m_height]; - }; - //////////////////////////////////////////////////////////////////////////// /** * Return last time data got changed @@ -14947,37 +15892,6 @@ geo.layer = function (arg) { return m_updateTime; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Return the modified time for the last draw call that did something - */ - //////////////////////////////////////////////////////////////////////////// - this.drawTime = function () { - return m_drawTime; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Run query and return results for it - */ - //////////////////////////////////////////////////////////////////////////// - this.query = function () { - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set layer as the reference layer - */ - //////////////////////////////////////////////////////////////////////////// - this.referenceLayer = function (val) { - if (val !== undefined) { - m_isReference = val; - m_this.modified(); - return m_this; - } - return m_isReference; - }; - //////////////////////////////////////////////////////////////////////////// /** * Get/Set if the layer has been initialized @@ -14993,21 +15907,29 @@ geo.layer = function (arg) { //////////////////////////////////////////////////////////////////////////// /** - * Transform a point or array of points in GCS space to - * local space of the layer + * Transform coordinates from world coordinates into a local coordinate + * system specific to the underlying renderer. This method is exposed + * to allow direct access the rendering context, but otherwise should + * not be called directly. The default implementation is the identity + * operator. */ //////////////////////////////////////////////////////////////////////////// this.toLocal = function (input) { + if (m_this._toLocalMatrix) { + geo.camera.applyTransform(m_this._toLocalMatrix, input); + } return input; }; //////////////////////////////////////////////////////////////////////////// /** - * Transform a point or array of points in local space to - * latitude-longitude space + * Transform coordinates from a local coordinate system to world coordinates. */ //////////////////////////////////////////////////////////////////////////// this.fromLocal = function (input) { + if (m_this._fromLocalMatrix) { + geo.camera.applyTransform(m_this._fromLocalMatrix, input); + } return input; }; @@ -15041,21 +15963,18 @@ geo.layer = function (arg) { return m_this; } - // Create top level div for the layer - m_node = $(document.createElement("div")); - m_node.attr("id", m_name); - m_node.css("position", "absolute"); - - if (m_map) { - m_map.node().append(m_node); - - } + m_map.node().append(m_node); /* Pass along the arguments, but not the map reference */ var options = $.extend({}, arg); delete options.map; - // Share context if have valid one - if (m_canvas) { + + if (m_rendererName === null) { + // if given a "null" renderer, then pass the map element as the + // canvas + m_renderer = null; + m_canvas = m_node; + } else if (m_canvas) { // Share context if have valid one m_renderer = geo.createRenderer(m_rendererName, m_this, m_canvas, options); } else { @@ -15065,11 +15984,24 @@ geo.layer = function (arg) { } if (!m_this.active()) { - m_node.css("pointerEvents", "none"); + m_node.css('pointerEvents', 'none'); } m_initialized = true; + /// Bind events to handlers + m_this.geoOn(geo.event.resize, function (event) { + m_this._update({event: event}); + }); + + m_this.geoOn(geo.event.pan, function (event) { + m_this._update({event: event}); + }); + + m_this.geoOn(geo.event.zoom, function (event) { + m_this._update({event: event}); + }); + return m_this; }; @@ -15079,10 +16011,12 @@ geo.layer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._exit = function () { - m_renderer._exit(); + m_this.geoOff(); + if (m_renderer) { + m_renderer._exit(); + } m_node.off(); m_node.remove(); - m_node = null; arg = {}; m_canvas = null; m_renderer = null; @@ -15099,43 +16033,55 @@ geo.layer = function (arg) { //////////////////////////////////////////////////////////////////////////// /** - * Respond to resize event + * Return the width of the layer in pixels. + * **DEPRECIATED: use map.size instead. */ //////////////////////////////////////////////////////////////////////////// - this._resize = function (x, y, w, h) { - m_x = x; - m_y = y; - m_width = w; - m_height = h; - m_node.width(w); - m_node.height(h); - - m_this.modified(); - m_this.geoTrigger(geo.event.resize, - {x: x, y: y, width: m_width, height: m_height}); - - return m_this; + this.width = function () { + return m_this.map().size().width; }; //////////////////////////////////////////////////////////////////////////// /** - * Return the width of the layer in pixels + * Return the height of the layer in pixels + * **DEPRECIATED: use map.size instead. */ //////////////////////////////////////////////////////////////////////////// - this.width = function () { - return m_width; + this.height = function () { + return m_this.map().size().height; }; //////////////////////////////////////////////////////////////////////////// /** - * Return the height of the layer in pixels + * Get or set the current layer opacity. */ //////////////////////////////////////////////////////////////////////////// - this.height = function () { - return m_height; + this.opacity = function (opac) { + if (opac !== undefined) { + m_opacity = opac; + m_node.css('opacity', m_opacity); + return m_this; + } + return m_opacity; }; - return this; + if (arg.zIndex === undefined) { + arg.zIndex = m_map.children().length; + } + m_zIndex = arg.zIndex; + + // Create top level div for the layer + m_node = $(document.createElement('div')); + m_node.attr('id', m_name); + m_node.css('position', 'absolute'); + m_node.css('width', '100%'); + m_node.css('height', '100%'); + m_this.opacity(m_opacity); + + // set the z-index + m_this.zIndex(m_zIndex); + + return m_this; }; /** @@ -15145,7 +16091,7 @@ geo.layer = function (arg) { * @returns {number} */ geo.layer.newLayerId = (function () { - "use strict"; + 'use strict'; var currentId = 1; return function () { var id = currentId; @@ -15159,11 +16105,11 @@ geo.layer.newLayerId = (function () { * General object specification for feature types. * @typedef geo.layer.spec * @type {object} - * @property {string} [type="feature"] For feature compatibility + * @property {string} [type='feature'] For feature compatibility * with more than one kind of creatable layer * @property {object[]} [data=[]] The default data array to * apply to each feature if none exists - * @property {string} [renderer="vgl"] The renderer to use + * @property {string} [renderer='vgl'] The renderer to use * @property {geo.feature.spec[]} [features=[]] Features * to add to the layer */ @@ -15176,26 +16122,26 @@ geo.layer.newLayerId = (function () { * @returns {geo.layer|null} */ geo.layer.create = function (map, spec) { - "use strict"; + 'use strict'; spec = spec || {}; // add osmLayer later - spec.type = "feature"; - if (spec.type !== "feature") { - console.warn("Unsupported layer type"); + spec.type = 'feature'; + if (spec.type !== 'feature') { + console.warn('Unsupported layer type'); return null; } - spec.renderer = spec.renderer || "vgl"; - if (spec.renderer !== "d3" && spec.renderer !== "vgl") { - console.warn("Invalid renderer"); + spec.renderer = spec.renderer || 'vgl'; + if (spec.renderer !== 'd3' && spec.renderer !== 'vgl') { + console.warn('Invalid renderer'); return null; } var layer = map.createLayer(spec.type, spec); if (!layer) { - console.warn("Unable to create a layer"); + console.warn('Unable to create a layer'); return null; } @@ -15301,8 +16247,6 @@ geo.featureLayer = function (arg) { //////////////////////////////////////////////////////////////////////////// /** * Initialize - * - * Do not call parent _init method as its already been executed */ //////////////////////////////////////////////////////////////////////////// this._init = function () { @@ -15322,12 +16266,16 @@ geo.featureLayer = function (arg) { m_this.geoOn(geo.event.pan, function (event) { m_this._update({event: event}); - m_this.renderer()._render(); + if (m_this.renderer()) { + m_this.renderer()._render(); + } }); m_this.geoOn(geo.event.zoom, function (event) { m_this._update({event: event}); - m_this.renderer()._render(); + if (m_this.renderer()) { + m_this.renderer()._render(); + } }); return m_this; @@ -15348,7 +16296,7 @@ geo.featureLayer = function (arg) { /// Call base class update s_update.call(m_this, request); - if (!m_this.source() && m_features && m_features.length === 0) { + if (m_features && m_features.length === 0) { console.log("[info] No valid data source found."); return; } @@ -15389,7 +16337,9 @@ geo.featureLayer = function (arg) { // Now call render on the renderer. In certain cases it may not do // anything if the if the child objects are drawn on the screen already. - m_this.renderer()._render(); + if (m_this.renderer()) { + m_this.renderer()._render(); + } return m_this; }; @@ -15436,8 +16386,8 @@ geo.registerLayer("feature", geo.featureLayer); * The following properties are common to all event objects: * * @namespace - * @property type {string} The event type that was triggered - * @property geo {object} A universal event object for controlling propagation + * @property {string} type The event type that was triggered + * @property {object} geo A universal event object for controlling propagation * * @example * map.geoOn(geo.event.layerAdd, function (event) { @@ -15457,33 +16407,32 @@ geo.event = {}; // The following were not triggered nor used anywhere. Removing until their // purpose is defined more clearly. // -// geo.event.update = "geo_update"; -// geo.event.opacityUpdate = "geo_opacityUpdate"; -// geo.event.layerToggle = "geo_layerToggle"; -// geo.event.layerSelect = "geo_layerSelect"; -// geo.event.layerUnselect = "geo_layerUnselect"; -// geo.event.rotate = "geo_rotate"; -// geo.event.query = "geo_query"; +// geo.event.update = 'geo_update'; +// geo.event.opacityUpdate = 'geo_opacityUpdate'; +// geo.event.layerToggle = 'geo_layerToggle'; +// geo.event.layerSelect = 'geo_layerSelect'; +// geo.event.layerUnselect = 'geo_layerUnselect'; +// geo.event.query = 'geo_query'; ////////////////////////////////////////////////////////////////////////////// /** * Triggered when a layer is added to the map. * - * @property target {geo.map} The current map - * @property layer {geo.layer} The new layer + * @property {geo.map} target The current map + * @property {geo.layer} layer The new layer */ ////////////////////////////////////////////////////////////////////////////// -geo.event.layerAdd = "geo_layerAdd"; +geo.event.layerAdd = 'geo_layerAdd'; ////////////////////////////////////////////////////////////////////////////// /** * Triggered when a layer is removed from the map. * - * @property target {geo.map} The current map - * @property layer {geo.layer} The old layer + * @property {geo.map} target The current map + * @property {geo.layer} layer The old layer */ ////////////////////////////////////////////////////////////////////////////// -geo.event.layerRemove = "geo_layerRemove"; +geo.event.layerRemove = 'geo_layerRemove'; ////////////////////////////////////////////////////////////////////////////// /** @@ -15491,77 +16440,122 @@ geo.event.layerRemove = "geo_layerRemove"; * triggered on the map itself. Instead it is triggered individually on * layers, starting with the base layer. * - * @property zoomLevel {Number} New zoom level - * @property screenPosition {object} The screen position of mouse pointer + * @property {number} zoomLevel New zoom level + * @property {object} screenPosition The screen position of mouse pointer + */ +////////////////////////////////////////////////////////////////////////////// +geo.event.zoom = 'geo_zoom'; + +////////////////////////////////////////////////////////////////////////////// +/** + * Triggered when the map is rotated around the map center (pointing downward + * so that positive angles are clockwise rotations). + * + * @property {number} angle The angle of the rotation in radians */ ////////////////////////////////////////////////////////////////////////////// -geo.event.zoom = "geo_zoom"; +geo.event.rotate = 'geo_rotate'; ////////////////////////////////////////////////////////////////////////////// /** * Triggered when the map is panned either by user interaction or map * transition. * - * @property screenDelta {object} The number of pixels to pan the map by - * @property center {object} The new map center + * @property {object} screenDelta The number of pixels to pan the map by + * @property {object} center The new map center */ ////////////////////////////////////////////////////////////////////////////// -geo.event.pan = "geo_pan"; +geo.event.pan = 'geo_pan'; ////////////////////////////////////////////////////////////////////////////// /** * Triggered when the map's canvas is resized. * - * @property width {Number} The new width in pixels - * @property height {Number} The new height in pixels + * @property {number} width The new width in pixels + * @property {number} height The new height in pixels + */ +////////////////////////////////////////////////////////////////////////////// +geo.event.resize = 'geo_resize'; + +////////////////////////////////////////////////////////////////////////////// +/** + * Triggered when the world coordinate system changes. Data in GCS + * coordinates can be transformed by the following formulas: + * + * x <- (x - origin.x) * scale.x + * y <- (y - origin.y) * scale.y + * z <- (z - origin.z) * scale.z + * + * Data in world coordinates can be updated using the following formulas: + * + * x <- (x * scaleChange.x - origin.x * (scale.x + scaleChange.x) + * - scale.x * originChange.x) * scale.x / scaleChange.x + * y <- (y * scaleChange.y - origin.y * (scale.y + scaleChange.y) + * - scale.y * originChange.y) * scale.y / scaleChange.y + * z <- (z * scaleChange.z - origin.z * (scale.z + scaleChange.z) + * - scale.z * originChange.z) * scale.z / scaleChange.z + * + * @property {geo.map} map The map whose coordinates changed + * @property {object} origin The new origin in GCS coordinates + * @property {number} origin.x + * @property {number} origin.y + * @property {number} origin.z + * @property {object} scale The new scale factor + * @property {number} scale.x + * @property {number} scale.y + * @property {number} scale.z + * @property {object} originChange Relative change from the old origin defined + * as `origin - oldorigin`. + * @property {object} scaleChange Relative change from the old scale defined + * as `scale / oldscale`. */ ////////////////////////////////////////////////////////////////////////////// -geo.event.resize = "geo_resize"; +geo.event.worldChanged = 'geo_worldChanged'; ////////////////////////////////////////////////////////////////////////////// /** * Triggered on every call to {@link geo.map#draw} before the map is rendered. * - * @property target {geo.map} The current map + * @property {geo.map} target The current map */ ////////////////////////////////////////////////////////////////////////////// -geo.event.draw = "geo_draw"; +geo.event.draw = 'geo_draw'; ////////////////////////////////////////////////////////////////////////////// /** * Triggered on every call to {@link geo.map#draw} after the map is rendered. * - * @property target {geo.map} The current map + * @property {geo.map} target The current map */ ////////////////////////////////////////////////////////////////////////////// -geo.event.drawEnd = "geo_drawEnd"; +geo.event.drawEnd = 'geo_drawEnd'; ////////////////////////////////////////////////////////////////////////////// /** - * Triggered on every "mousemove" over the map's DOM element. The event + * Triggered on every 'mousemove' over the map's DOM element. The event * object extends {@link geo.mouseState}. * @mixes geo.mouseState */ ////////////////////////////////////////////////////////////////////////////// -geo.event.mousemove = "geo_mousemove"; +geo.event.mousemove = 'geo_mousemove'; ////////////////////////////////////////////////////////////////////////////// /** - * Triggered on every "mousedown" over the map's DOM element. The event + * Triggered on every 'mousedown' over the map's DOM element. The event * object extends {@link geo.mouseState}. * @mixes geo.mouseState */ ////////////////////////////////////////////////////////////////////////////// -geo.event.mouseclick = "geo_mouseclick"; +geo.event.mouseclick = 'geo_mouseclick'; ////////////////////////////////////////////////////////////////////////////// /** - * Triggered on every "mousemove" during a brushing selection. + * Triggered on every 'mousemove' during a brushing selection. * The event object extends {@link geo.brushSelection}. * @mixes geo.brushSelection */ ////////////////////////////////////////////////////////////////////////////// -geo.event.brush = "geo_brush"; +geo.event.brush = 'geo_brush'; ////////////////////////////////////////////////////////////////////////////// /** @@ -15570,7 +16564,7 @@ geo.event.brush = "geo_brush"; * @mixes geo.brushSelection */ ////////////////////////////////////////////////////////////////////////////// -geo.event.brushend = "geo_brushend"; +geo.event.brushend = 'geo_brushend'; ////////////////////////////////////////////////////////////////////////////// /** @@ -15579,86 +16573,150 @@ geo.event.brushend = "geo_brushend"; * @mixes geo.brushSelection */ ////////////////////////////////////////////////////////////////////////////// -geo.event.brushstart = "geo_brushstart"; +geo.event.brushstart = 'geo_brushstart'; + + +////////////////////////////////////////////////////////////////////////////// +/** + * Triggered before a map navigation animation begins. Set + * event.geo.cancelAnimation to cancel the animation + * of the navigation. This will cause the map to navigate to the + * target location immediately. Set event.geo.cancelNavigation + * to cancel the navigation completely. The transition options can + * be modified in place. + * + * @property {geo.geoPosition} center The target center + * @property {number} zoom The target zoom level + * @property {number} duration The duration of the transition in milliseconds + * @property {function} ease The easing function + */ +////////////////////////////////////////////////////////////////////////////// +geo.event.transitionstart = 'geo_transitionstart'; + +////////////////////////////////////////////////////////////////////////////// +/** + * Triggered after a map navigation animation ends. + * + * @property {geo.geoPosition} center The target center + * @property {number} zoom The target zoom level + * @property {number} duration The duration of the transition in milliseconds + * @property {function} ease The easing function + */ +////////////////////////////////////////////////////////////////////////////// +geo.event.transitionend = 'geo_transitionend'; + +////////////////////////////////////////////////////////////////////////////// +/** + * Triggered when the parallel projection mode is changes. + * + * @property paralellProjection {boolean} True if parallel projection is turned + * on. + */ +////////////////////////////////////////////////////////////////////////////// +geo.event.parallelprojection = 'geo_parallelprojection'; + +//////////////////////////////////////////////////////////////////////////// +/** + * @namespace + */ +//////////////////////////////////////////////////////////////////////////// +geo.event.clock = { + play: 'geo_clock_play', + stop: 'geo_clock_stop', + pause: 'geo_clock_pause', + change: 'geo_clock_change' +}; + +//////////////////////////////////////////////////////////////////////////// +/** + * This event object provides mouse/keyboard events that can be handled + * by the features. This provides a similar interface as core events, + * but with different names so the events don't interfere. Subclasses + * can override this to provide custom events. + * + * These events will only be triggered on features which were instantiated + * with the option 'selectionAPI'. + * @namespace + */ +//////////////////////////////////////////////////////////////////////////// +geo.event.feature = { + mousemove: 'geo_feature_mousemove', + mouseover: 'geo_feature_mouseover', + mouseout: 'geo_feature_mouseout', + mouseon: 'geo_feature_mouseon', + mouseoff: 'geo_feature_mouseoff', + mouseclick: 'geo_feature_mouseclick', + brushend: 'geo_feature_brushend', + brush: 'geo_feature_brush' +}; +//////////////////////////////////////////////////////////////////////////// +/** + * These events are triggered by the camera when it's internal state is + * mutated. + * @namespace + */ +//////////////////////////////////////////////////////////////////////////// +geo.event.camera = {}; ////////////////////////////////////////////////////////////////////////////// /** - * Triggered before a map navigation animation begins. Set - * event.geo.cancelAnimation to cancel the animation - * of the navigation. This will cause the map to navigate to the - * target location immediately. Set event.geo.cancelNavigation - * to cancel the navigation completely. The transition options can - * be modified in place. + * Triggered after a general view matrix change (any change in the visible + * bounds). This is equivalent to the union of pan and zoom. * - * @property {geo.geoPosition} center The target center - * @property {Number} zoom The target zoom level - * @property {Number} duration The duration of the transition in milliseconds - * @property {function} ease The easing function + * @property {geo.camera} camera The camera instance */ ////////////////////////////////////////////////////////////////////////////// -geo.event.transitionstart = "geo_transitionstart"; +geo.event.camera.view = 'geo_camera_view'; ////////////////////////////////////////////////////////////////////////////// /** - * Triggered after a map navigation animation ends. + * Triggered after a pan in the x/y plane (no zoom level change). * - * @property {geo.geoPosition} center The target center - * @property {Number} zoom The target zoom level - * @property {Number} duration The duration of the transition in milliseconds - * @property {function} ease The easing function + * @property {geo.camera} camera The camera instance + * @property {object} delta The translation delta in world coordinates. */ ////////////////////////////////////////////////////////////////////////////// -geo.event.transitionend = "geo_transitionend"; +geo.event.camera.pan = 'geo_camera_pan'; ////////////////////////////////////////////////////////////////////////////// /** - * Triggered when the parallel projection mode is changes. + * Triggered after a view matrix change that is not a simple pan. This + * includes, but is not limited to, pure zooms. * - * @property paralellProjection {boolean} True if parallel projection is turned - * on. + * @property {geo.camera} camera The camera instance */ ////////////////////////////////////////////////////////////////////////////// -geo.event.parallelprojection = "geo_parallelprojection"; +geo.event.camera.zoom = 'geo_camera_zoom'; -//////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// /** - * @namespace + * Triggered after a projection change. + * + * @property {geo.camera} camera The camera instance + * @property {string} type The projection type ('perspective'|'parallel') */ -//////////////////////////////////////////////////////////////////////////// -geo.event.clock = { - play: "geo_clock_play", - stop: "geo_clock_stop", - pause: "geo_clock_pause", - change: "geo_clock_change" -}; +////////////////////////////////////////////////////////////////////////////// +geo.event.camera.projection = 'geo_camera_projection'; -//////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// /** - * This event object provides mouse/keyboard events that can be handled - * by the features. This provides a similar interface as core events, - * but with different names so the events don't interfere. Subclasses - * can override this to provide custom events. + * Triggered after a viewport change. * - * These events will only be triggered on features which were instantiated - * with the option "selectionAPI". - * @namespace + * @property {geo.camera} camera The camera instance + * @property {object} viewport The new viewport + * @property {number} viewport.width The new width + * @property {number} viewport.height The new height */ -//////////////////////////////////////////////////////////////////////////// -geo.event.feature = { - mousemove: "geo_feature_mousemove", - mouseover: "geo_feature_mouseover", - mouseout: "geo_feature_mouseout", - mouseon: "geo_feature_mouseon", - mouseoff: "geo_feature_mouseoff", - mouseclick: "geo_feature_mouseclick", - brushend: "geo_feature_brushend", - brush: "geo_feature_brush" -}; +////////////////////////////////////////////////////////////////////////////// +geo.event.camera.viewport = 'geo_camera_viewport'; ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of mapInteractor + * The mapInteractor class is responsible for handling raw events from the + * browser and interpreting them as map navigation interactions. This class + * will call the navigation methods on the connected map, which will make + * modifications to the camera directly. * * @class * @extends geo.object @@ -15683,7 +16741,9 @@ geo.mapInteractor = function (args) { m_wait = false, m_disableThrottle = true, m_selectionLayer = null, - m_selectionPlane = null; + m_selectionPlane = null, + m_paused = false, + m_clickMaybe = false; // Helper method to decide if the current button/modifiers match a set of // conditions. @@ -15754,6 +16814,12 @@ geo.mapInteractor = function (args) { spring: { enabled: false, springConstant: 0.00005 + }, + click: { + enabled: true, + buttons: {left: true, right: true, middle: true}, + duration: 0, + cancelOnMove: true } }, m_options @@ -15811,6 +16877,19 @@ geo.mapInteractor = function (args) { // enabled: true | false, // springConstant: number, // } + // + // // enable the "click" event + // // A click will be registered when a mouse down is followed + // // by a mouse up in less than the given number of milliseconds + // // and the standard handler will *not* be called + // // If the duration is <= 0, then clicks will only be canceled by + // // a mousemove. + // click: { + // enabled: true | false, + // buttons: {'left': true, 'right': true, 'middle': true} + // duration: 0, + // cancelOnMove: true // cancels click if the mouse is moved before release + // } // } // A bunch of type definitions for api documentation: @@ -15987,6 +17066,8 @@ geo.mapInteractor = function (args) { $node.on('mousedown.geojs', m_this._handleMouseDown); $node.on('mouseup.geojs', m_this._handleMouseUp); $node.on('wheel.geojs', m_this._handleMouseWheel); + // Disable dragging images and such + $node.on('dragstart', function () { return false; }); if (m_options.panMoveButton === 'right' || m_options.zoomMoveButton === 'right') { $node.on('contextmenu.geojs', function () { return false; }); @@ -16197,6 +17278,10 @@ geo.mapInteractor = function (args) { this._handleMouseDown = function (evt) { var action = null; + if (m_paused) { + return; + } + if (m_state.action === 'momentum') { // cancel momentum on click m_state = {}; @@ -16206,6 +17291,17 @@ geo.mapInteractor = function (args) { m_this._getMouseButton(evt); m_this._getMouseModifiers(evt); + if (m_options.click.enabled && + (!m_mouse.buttons.left || m_options.click.buttons.left) && + (!m_mouse.buttons.right || m_options.click.buttons.right) && + (!m_mouse.buttons.middle || m_options.click.buttons.middle)) { + m_clickMaybe = true; + if (m_options.click.duration > 0) { + window.setTimeout(function () { + m_clickMaybe = false; + }, m_options.click.duration); + } + } if (eventMatch(m_options.panMoveButton, m_options.panMoveModifiers)) { action = 'pan'; } else if (eventMatch(m_options.zoomMoveButton, m_options.zoomMoveModifiers)) { @@ -16257,11 +17353,24 @@ geo.mapInteractor = function (args) { */ //////////////////////////////////////////////////////////////////////////// this._handleMouseMove = function (evt) { + + if (m_paused) { + return; + } + if (m_state.action) { // If currently performing a navigation action, the mouse // coordinates will be captured by the document handler. return; } + + if (m_options.click.cancelOnMove) { + m_clickMaybe = false; + } + if (m_clickMaybe) { + return; + } + m_this._getMousePosition(evt); m_this._getMouseButton(evt); m_this._getMouseModifiers(evt); @@ -16275,10 +17384,22 @@ geo.mapInteractor = function (args) { //////////////////////////////////////////////////////////////////////////// this._handleMouseMoveDocument = function (evt) { var dx, dy, selectionObj; + + if (m_paused) { + return; + } + m_this._getMousePosition(evt); m_this._getMouseButton(evt); m_this._getMouseModifiers(evt); + if (m_options.click.cancelOnMove) { + m_clickMaybe = false; + } + if (m_clickMaybe) { + return; + } + if (!m_state.action) { // This shouldn't happen console.log('WARNING: Invalid state in mapInteractor.'); @@ -16301,7 +17422,8 @@ geo.mapInteractor = function (args) { m_this.map().pan({x: dx, y: dy}); } else if (m_state.action === 'zoom') { m_this.map().zoom( - m_this.map().zoom() - dy * m_options.zoomScale / 120 + m_this.map().zoom() - dy * m_options.zoomScale / 120, + m_state ); } else if (m_state.action === 'select') { // Get the bounds of the current selection @@ -16400,6 +17522,11 @@ geo.mapInteractor = function (args) { this._handleMouseUpDocument = function (evt) { var selectionObj, oldAction; + if (m_paused) { + return; + } + + m_clickMaybe = false; m_this._getMouseButton(evt); m_this._getMouseModifiers(evt); @@ -16437,10 +17564,39 @@ geo.mapInteractor = function (args) { */ //////////////////////////////////////////////////////////////////////////// this._handleMouseUp = function (evt) { + + if (m_paused) { + return; + } + + if (m_clickMaybe) { + m_this._handleMouseClick(evt); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle event when a mouse click is detected. A mouse click is a simulated + * event that occurs when the time between a mouse down and mouse up + * is less than the configured duration and (optionally) if no mousemove + * events were triggered in the interim. + */ + //////////////////////////////////////////////////////////////////////////// + this._handleMouseClick = function (evt) { + m_this._getMouseButton(evt); m_this._getMouseModifiers(evt); - // fire a click event here + // cancel any ongoing pan action + m_this.cancel('pan'); + + // unbind temporary handlers on document + $(document).off('.geojs'); + + // reset click detector variable + m_clickMaybe = false; + + // fire a click event m_this.map().geoTrigger(geo.event.mouseclick, m_this.mouse()); }; @@ -16450,7 +17606,11 @@ geo.mapInteractor = function (args) { */ //////////////////////////////////////////////////////////////////////////// this._handleMouseWheel = function (evt) { - var zoomFactor, direction; + var zoomFactor; + + if (m_paused) { + return; + } // try to normalize deltas using the wheel event standard: // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent @@ -16499,11 +17659,10 @@ geo.mapInteractor = function (args) { eventMatch('wheel', m_options.zoomWheelModifiers)) { zoomFactor = -evt.deltaY; - direction = m_mouse.map; m_this.map().zoom( m_this.map().zoom() + zoomFactor, - direction + m_mouse ); } }; @@ -16629,6 +17788,24 @@ geo.mapInteractor = function (args) { return $.extend(true, {}, m_state); }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the pause state of the interactor, which + * ignores all native mouse and keyboard events. + * + * @param {bool} [value] The pause state to set or undefined to return the + * current state. + * @returns {bool|this} + */ + //////////////////////////////////////////////////////////////////////////// + this.pause = function (value) { + if (value === undefined) { + return m_paused; + } + m_paused = !!value; + return m_this; + }; + //////////////////////////////////////////////////////////////////////////// /** * Simulate a DOM mouse event on connected map. @@ -16915,3331 +18092,4512 @@ geo.clock = function (opts) { ////////////////////////////////////////////////////////////////////////////// /** - * Step to the next frame in the animation. Will set the state to stop - * if the animation has reached the end and there are no more loops. - * @private + * Step to the next frame in the animation. Will set the state to stop + * if the animation has reached the end and there are no more loops. + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + this._setNextFrame = function (step) { + var next = new Date(m_this.now().valueOf() + step * m_this.step()); + + if (next >= m_this.end() || next <= m_this.start()) { + if (m_this.loop() <= m_currentLoop) { + m_this.state('stop'); + return; + } + m_currentLoop += 1; + if (step >= 0) { + m_this.now(m_this.start()); + } else { + m_this.now(m_this.end()); + } + return; + } + m_this.now(next); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Start an animation. + * @param {integer} step The animation frame step (+1 for forward -1 for + * reverse, etc). + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + this._animate = function (step) { + var myAnimation = {}; + m_currentAnimation = myAnimation; + + function frame() { + if (myAnimation !== m_currentAnimation) { + // A new animation has started, so kill this one. + return; + } + m_this._setNextFrame(step); + if (m_this.state() === 'play') { + + // Queue the next frame + if (!m_this.framerate()) { + window.requestAnimationFrame(frame); + } else { + window.setTimeout(frame, 1000 / m_this.framerate()); + } + } else if (m_this._attached()) { + m_this.object().geoTrigger(geo.event.clock[m_this.state()], { + current: m_this.now(), + clock: m_this + }); + } + } + + // trigger the play event + if (m_this._attached()) { + m_this.object().geoTrigger(geo.event.clock.play, { + current: m_this.now(), + clock: m_this + }); + } + + // Queue the first frame + if (!m_this.framerate()) { + window.requestAnimationFrame(frame); + } else { + window.setTimeout(frame, 1000 / m_this.framerate()); + } + }; +}; +inherit(geo.clock, geo.object); + +(function () { + 'use strict'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * This class defines the raw interface for a "tile" on a map. A tile is + * defined as a rectangular section of a map. The base implementation + * is independent of the actual content of the tile, but assumes that + * the content is loaded asynchronously via a url. The tile object + * has a promise-like interface. For example, + * + * tile.then(function (data) {...}).catch(function (data) {...}); + * + * @class + * @param {Object} spec The tile specification object + * + * @param {Object} spec.index The global position of the tile + * @param {Number} spec.index.x The x-coordinate (usually the column number) + * @param {Number} spec.index.y The y-coordinate (usually the row number) + * + * @param {Object} spec.size The size of each tile + * @param {Number} spec.size.x Width (usually in pixels) + * @param {Number} spec.size.y Height (usually in pixels) + * + * @param {Object|String} spec.url A url or jQuery ajax config object + * + * @param {Object?} spec.overlap The size of overlap with neighboring tiles + * @param {Number} [spec.overlap.x=0] + * @param {Number} [spec.overlap.y=0] + */ + ////////////////////////////////////////////////////////////////////////////// + geo.tile = function (spec) { + if (!(this instanceof geo.tile)) { + return new geo.tile(spec); + } + + this._index = spec.index; + this._size = spec.size; + this._overlap = spec.overlap || {x: 0, y: 0}; + this._wrap = spec.wrap || {x: 1, y: 1}; + this._url = spec.url; + this._fetched = false; + + /** + * Return the index coordinates. + */ + Object.defineProperty(this, 'index', { + get: + function () { return this._index; } + }); + + /** + * Return the tile sizes. + */ + Object.defineProperty(this, 'size', { + get: function () { return this._size; } + }); + + /** + * Return the tile overlap sizes. + */ + Object.defineProperty(this, 'overlap', { + get: function () { return this._overlap; } + }); + + /** + * Initiate the ajax request and add a promise interface + * to the tile object. This method exists to allow + * derived classes the ability to override how the tile + * is obtained. For example, imageTile uses an Image + * element rather than $.get. + */ + this.fetch = function () { + if (!this._fetched) { + $.get(this._url).promise(this); + } + return this; + }; + + /** + * Add a method to be called with the data when the ajax request is + * successfully resolved. + * + * @param {function?} onSuccess The success handler + * @param {function?} onFailure The failure handler + * @returns {this} Supports chained calling + * + */ + this.then = function (onSuccess, onFailure) { + this.fetch(); // This will replace the current then method + + // Call then on the new promise + this.then(onSuccess, onFailure); + return this; + }; + + /** + * Add a method to be called with the data when the ajax fails. + * + * @param {function} method The rejection handler + * @returns {this} Supports chained calling + * + */ + this.catch = function (method) { + this.then(undefined, method); + return this; + }; + + /** + * Return a unique string representation of the given tile useable + * as a hash key. Possibly extend later to include url information + * to make caches aware of the tile source. + * @returns {string} + */ + this.toString = function () { + return [this._index.level || 0, this._index.y, this._index.x].join('_'); + }; + + /** + * Return the bounds of the tile given an index offset and + * a translation. + * + * @param {object} index The tile index containing (0, 0) + * @param {object} shift The coordinates of (0, 0) inside the tile + */ + this.bounds = function (index, shift) { + var left, right, bottom, top; + left = this.size.x * (this.index.x - index.x) - this.overlap.x - shift.x; + right = left + this.size.x + this.overlap.x * 2; + top = this.size.y * (this.index.y - index.y) - this.overlap.y - shift.y; + bottom = top + this.size.y + this.overlap.y * 2; + return { + left: left, + right: right, + bottom: bottom, + top: top + }; + }; + + /** + * Computes the global coordinates of the bottom edge. + * @returns {number} + */ + Object.defineProperty(this, 'bottom', { + get: function () { + return this.size.y * (this.index.y + 1) + this.overlap.y; + } + }); + + /** + * Computes the global coordinates of the top edge. + * @returns {number} + */ + Object.defineProperty(this, 'top', { + get: function () { + return this.size.y * this.index.y - this.overlap.y; + } + }); + + /** + * Computes the global coordinates of the left edge. + * @returns {number} + */ + Object.defineProperty(this, 'left', { + get: function () { + return this.size.x * this.index.x - this.overlap.x; + } + }); + + /** + * Computes the global coordinates of the right edge. + * @returns {number} + */ + Object.defineProperty(this, 'right', { + get: function () { + return this.size.x * (this.index.x + 1) + this.overlap.x; + } + }); + + /** + * Returns the global image size at this level. + * @returns {number} + */ + Object.defineProperty(this, 'levelSize', { + value: { + width: Math.pow(2, this.index.level || 0) * this.size.x, + height: Math.pow(2, this.index.level || 0) * this.size.y + } + }); + + /** + * Set the opacity of the tile to 0 and gradually fade in + * over the given number of milliseconds. This will also + * resolve the embedded promise interface. + * @param {number} duration the duration of the animation in ms + * @returns {this} chainable + */ + this.fadeIn = function (duration) { + $.noop(duration); + return this; + }; + }; +})(); + +(function () { + 'use strict'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * This class defines a tile that is part of a standard "image pyramid", such + * as an open street map tile set. Every tile is uniquely indexed by a row, + * column, and zoom level. The number of rows/columns at zoom level z is + * `2^z`, the number of pixels per tile is configurable. + * + * By default, this class assumes that images are fetch from the url, but + * subclasses may define additional rendering steps to produce the images + * before passing them off to the handlers. + * + * @class + * @param {Object} spec The tile specification object + * + * @param {Object} spec.index The global position of the tile + * @param {Number} spec.index.x The x-coordinate (usually the column number) + * @param {Number} spec.index.y The y-coordinate (usually the row number) + * @param {Number} spec.index.level The zoom level + * + * @param {Object?} spec.size The size of each tile + * @param {Number} [spec.size.x=256] Width in pixels + * @param {Number} [spec.size.y=256] Height in pixels + * + * @param {String} spec.url A url to the image + * @param {String} [spec.crossDomain='anonymous'] Image CORS attribute + * + * @param {Object} spec.overlap The size of overlap with neighboring tiles + * @param {Number} [spec.overlap.x=0] + * @param {Number} [spec.overlap.y=0] */ ////////////////////////////////////////////////////////////////////////////// - this._setNextFrame = function (step) { - var next = new Date(m_this.now().valueOf() + step * m_this.step()); + geo.imageTile = function (spec) { + if (!(this instanceof geo.imageTile)) { + return new geo.imageTile(spec); + } - if (next >= m_this.end() || next <= m_this.start()) { - if (m_this.loop() <= m_currentLoop) { - m_this.state('stop'); - return; - } - m_currentLoop += 1; - if (step >= 0) { - m_this.now(m_this.start()); - } else { - m_this.now(m_this.end()); + spec.size = spec.size || {x: 256, y: 256}; + this._image = null; + + // Cache the coordinate scaling + this._cors = spec.crossDomain || 'anonymous'; + + // Call superclass constructor + geo.tile.call(this, spec); + + /** + * Read only accessor to the Image object used by the + * tile. Note, this method does not gaurantee that the + * image data is available. Use the promise interface + * to add asyncronous handlers. + * @returns {Image} + */ + Object.defineProperty(this, 'image', { + get: function () { return this._image; } + }); + + /** + * Initiate the image request. + */ + this.fetch = function () { + var defer; + if (!this._image) { + this._image = new Image(this.size.x, this.size.y); + // Only set the crossOrigin parameter if this is going across origins. + if (this._url.indexOf(':') >= 0 && this._url.indexOf('/') >= 0 && + this._url.indexOf(':') < this._url.indexOf('/')) { + this._image.crossOrigin = this._cors; + } + defer = new $.Deferred(); + this._image.onload = function () { defer.resolve(); }; + this._image.onerror = function () { defer.reject(); }; + this._image.src = this._url; + + // attach a promise interface to `this` + defer.promise(this); } - return; - } - m_this.now(next); + return this; + }; + + /** + * Set the opacity of the tile to 0 and gradually fade in + * over the given number of milliseconds. This will also + * resolve the embedded promise interface. + * @param {number} duration the duration of the animation in ms + * @returns {this} chainable + */ + this.fadeIn = function (duration) { + var promise = this.fetch(), defer = new $.Deferred(); + $(this._image).css('display', 'none'); + promise.then(function () { + $(this._image).fadeIn(duration, function () { + defer.resolve(); + }); + }.bind(this)); + return defer.promise(this); + }; + + return this; }; + inherit(geo.imageTile, geo.tile); +})(); + +(function () { + 'use strict'; + ////////////////////////////////////////////////////////////////////////////// /** - * Start an animation. - * @param {integer} step The animation frame step (+1 for forward -1 for - * reverse, etc). - * @private + * This class implements a simple cache for tile objects. Each tile is + * stored in cache object keyed by a configurable hashing function. Another + * array keeps track of last access times for each tile to purge old tiles + * once the maximum cache size is reached. + * + * @class + * + * @param {Object?} [options] A configuratoin object for the cache + * @param {Number} [options.size=64] The maximum number of tiles to store */ ////////////////////////////////////////////////////////////////////////////// - this._animate = function (step) { - var myAnimation = {}; - m_currentAnimation = myAnimation; + geo.tileCache = function (options) { + if (!(this instanceof geo.tileCache)) { + return new geo.tileCache(options); + } + options = options || {}; + this._size = options.size || 64; - function frame() { - if (myAnimation !== m_currentAnimation) { - // A new animation has started, so kill this one. - return; + /** + * Get/set the maximum cache size. + */ + Object.defineProperty(this, 'size', { + get: function () { return this._size; }, + set: function (n) { + while (this._atime.length > n) { + this.remove(this._atime[this._atime.length - 1]); + } + this._size = n; } - m_this._setNextFrame(step); - if (m_this.state() === 'play') { + }); - // Queue the next frame - if (!m_this.framerate()) { - window.requestAnimationFrame(frame); - } else { - window.setTimeout(frame, 1000 / m_this.framerate()); - } - } else if (m_this._attached()) { - m_this.object().geoTrigger(geo.event.clock[m_this.state()], { - current: m_this.now(), - clock: m_this - }); + /** + * Get the current cache size. + */ + Object.defineProperty(this, 'length', { + get: function () { return this._atime.length; } + }); + + /** + * Get the position of the tile in the access queue. + * @param {string} hash The tile's hash value + * @returns {number} The position in the queue or -1 + */ + this._access = function (hash) { + return this._atime.indexOf(hash); + }; + + /** + * Remove a tile from the cache. + * @param {string|geo.tile} tile The tile or its hash + * @returns {bool} true if a tile was removed + */ + this.remove = function (tile) { + var hash = typeof tile === 'string' ? tile : tile.toString(); + + // if the tile is not in the cache + if (!(hash in this._cache)) { + return false; } - } - // trigger the play event - if (m_this._attached()) { - m_this.object().geoTrigger(geo.event.clock.play, { - current: m_this.now(), - clock: m_this - }); - } + // Remove the tile from the access queue + this._atime.splice(this._access(hash), 1); - // Queue the first frame - if (!m_this.framerate()) { - window.requestAnimationFrame(frame); - } else { - window.setTimeout(frame, 1000 / m_this.framerate()); - } - }; -}; -inherit(geo.clock, geo.object); + // Remove the tile from the cache + delete this._cache[hash]; + return true; + }; -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class fileReader - * - * @class - * @extends geo.object - * @returns {geo.fileReader} - */ -////////////////////////////////////////////////////////////////////////////// -geo.fileReader = function (arg) { - "use strict"; - if (!(this instanceof geo.fileReader)) { - return new geo.fileReader(arg); - } - geo.object.call(this); + /** + * Remove all tiles from the cache. + */ + this.clear = function () { + this._cache = {}; // The hash -> tile mapping + this._atime = []; // The access queue (the hashes are stored) + return this; + }; - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - arg = arg || {}; + /** + * Get a tile from the cache if it exists, otherwise + * return null. This method also moves the tile to the + * front of the access queue. + * + * @param {string|geo.tile} hash The tile or the tile hash value + * @returns {geo.tile|null} + */ + this.get = function (hash) { + hash = typeof hash === 'string' ? hash : hash.toString(); + if (!(hash in this._cache)) { + return null; + } - if (!(arg.layer instanceof geo.featureLayer)) { - throw "fileReader must be given a feature layer"; - } + this._atime.splice(this._access(hash), 1); + this._atime.unshift(hash); + return this._cache[hash]; + }; - var m_layer = arg.layer; + /** + * Add a tile to the cache. + * @param {geo.tile} tile + */ + this.add = function (tile) { + // remove any existing tiles with the same hash + this.remove(tile); + var hash = tile.toString(); + + // add the tile + this._cache[hash] = tile; + this._atime.unshift(hash); + + // purge a tile from the cache if necessary + while (this._atime.length > this.size) { + hash = this._atime.pop(); + delete this._cache[hash]; + } + }; - //////////////////////////////////////////////////////////////////////////// - /** - * Get the feature layer attached to the reader - */ - //////////////////////////////////////////////////////////////////////////// - this.layer = function () { - return m_layer; + this.clear(); + return this; }; +})(); + +(function () { + 'use strict'; - //////////////////////////////////////////////////////////////////////////// /** - * Tells the caller if it can handle the given file by returning a boolean. + * Standard modulo operator where the output is in [0, b) for all inputs. + * @private */ - //////////////////////////////////////////////////////////////////////////// - this.canRead = function () { - return false; - }; + function modulo(a, b) { + return ((a % b) + b) % b; + } - //////////////////////////////////////////////////////////////////////////// /** - * Reads the file object and calls the done function when finished. As an - * argument to done, provides a boolean that reports if the read was a - * success. Possibly, it can call done with an object containing details - * of the read operation. + * Pick a subdomain from a list of subdomains based on a the tile location. + * + * @param {number} x: the x tile coordinate. + * @param {number} y: the y tile coordinate. + * @param {list} subdomains: the list of known subdomains. */ - //////////////////////////////////////////////////////////////////////////// - this.read = function (file, done) { - done(false); - }; + function m_getTileSubdomain(x, y, subdomains) { + return subdomains[modulo(x + y, subdomains.length)]; + } - //////////////////////////////////////////////////////////////////////////// /** - * Return a FileReader with handlers attached. - */ - //////////////////////////////////////////////////////////////////////////// - function newFileReader(done, progress) { - var reader = new FileReader(); - if (progress) { - reader.onprogress = progress; - } - reader.onloadend = function () { - if (!reader.result) { - done(reader.error); - } - done(reader.result); + * Returns an OSM tile server formatting function from a standard format + * string. Replaces {s}, {z}, {x}, and {y}. + * + * @param {string} base The tile format string + * @returns: a conversion function. + * @private. + */ + function m_tileUrlFromTemplate(base) { + return function (x, y, z, subdomains) { + return base.replace('{s}', m_getTileSubdomain(x, y, subdomains)) + .replace('{z}', z) + .replace('{x}', x) + .replace('{y}', y); }; - return reader; } - //////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// /** - * Private method for reading a file object as a string. Calls done with - * the string content when finished or an error object if unsuccessful. - * Optionally, the caller can provide a progress method that is called - * after reading each slice. + * This method defines a tileLayer, which is an abstract class defining a + * layer divided into tiles of arbitrary data. Notably, this class provides + * the core functionality of the osmLayer, but hooks exist to render tiles + * dynamically from vector data, or to display arbitrary grids of images + * in a custom coordinate system. When multiple zoom levels are present + * in a given dataset, this class assumes that the space occupied by + * tile (i, j) at level z is covered by a 2x2 grid of tiles at zoom + * level z + 1: + * + * (2i, 2j), (2i, 2j + 1) + * (2i + 1, 2j), (2i + 1, 2j + 1) + * + * The higher level tile set should represent a 2x increase in resolution. + * + * Although not currently supported, this class is intended to extend to + * 3D grids of tiles as well where 1 tile is covered by a 2x2x2 grid of + * tiles at the next level. The tiles are assumed to be rectangular, + * identically sized, and aligned with x/y axis of the underlying + * coordinate system. The local coordinate system is in pixels relative + * to the current zoom level and changes when the zoom level crosses an + * integer threshold. + * + * The constructor takes a number of optional parameters that customize + * the display of the tiles. The default values of these options are + * stored as the `defaults` attribution on the constructor. Supporting + * alternate tiling protocols often only requires adjusting these + * defaults. + * + * @class + * @extends geo.featureLayer + * @param {object?} options + * @param {number} [options.minLevel=0] The minimum zoom level available + * @param {number} [options.maxLevel=18] The maximum zoom level available + * @param {number} [options.tileOverlap=0] + * Number of pixels of overlap between tiles + * @param {number} [options.tileWidth=256] + * The tile width as displayed without overlap + * @param {number} [options.tileHeight=256] + * The tile height as displayed without overlap + * @param {number} [options.cacheSize=400] The maximum number of tiles to + * cache. The default is 200 if keepLower is false. + * @param {bool} [options.keepLower=true] + * Keep lower zoom level tiles when showing high zoom level tiles. This + * uses more memory but results in smoother transitions. + * @param {bool} [options.wrapX=true] Wrap in the x-direction + * @param {bool} [options.wrapY=false] Wrap in the y-direction + * @param {number} [options.minX=0] The minimum world coordinate in X + * @param {number} [options.maxX=255] The maximum world coordinate in X + * @param {number} [options.minY=0] The minimum world coordinate in Y + * @param {number} [options.maxY=255] The maximum world coordinate in Y + * @param {function|string} [options.url=null] + * A function taking the current tile indices and returning a URL or jquery + * ajax config to be passed to the {geo.tile} constructor. + * Example: + * (x, y, z, subdomains) => "http://example.com/z/y/x.png" + * If this is a string, a template url with {x}, {y}, {z}, and {s} as + * template variables. {s} picks one of the subdomains parameter. + * @param {string|list} [options.subdomain="abc"] Subdomains to use in + * template url strings. If a string, this is converted to a list before + * being passed to a url function. + * @param {string} [options.baseUrl=null] If defined, use the old-style base + * url instead of the options.url parameter. This is functionally the same + * as using a url of baseUrl/{z}/{x}/{y}.(options.imageFormat || png). If + * the specified string does not end in a slash, one is added. + * @param {string} [options.imageFormat='png'] + * This is only used if a baseUrl is specified, in which case it determines + * the image name extension used in the url. + * @param {number} [options.animationDuration=0] + * The number of milliseconds for the tile loading animation to occur. **This + * option is currently buggy because old tiles will purge before the animation + * is complete.** + * @param {string} [options.attribution] + * An attribution to display with the layer (accepts HTML) + * @param {function} [options.tileRounding=Math.round] + * This function determines which tiles will be loaded when the map is at + * a non-integer zoom. For example, `Math.floor`, will use tile level 2 + * when the map is at zoom 2.9. + * @param {function} [options.tileOffset] + * This function takes a zoom level argument and returns, in units of + * pixels, the coordinates of the point (0, 0) at the given zoom level + * relative to the bottom left corner of the domain. + * @param {bool} [options.topDown=false] True if the gcs is top-down, + * false if bottom-up (the ingcs does not matter, only the gcs coordinate + * system). When false, this inverts the gcs y-coordinate when calculating + * local coordinates. + * @returns {geo.tileLayer} */ - //////////////////////////////////////////////////////////////////////////// - this._getString = function (file, done, progress) { - var reader = newFileReader(done, progress); - reader.readAsText(file); - }; + ////////////////////////////////////////////////////////////////////////////// + geo.tileLayer = function (options) { + if (!(this instanceof geo.tileLayer)) { + return new geo.tileLayer(options); + } + geo.featureLayer.call(this, options); + + options = $.extend(true, {}, this.constructor.defaults, options || {}); + if (!options.cacheSize) { + // this size should be sufficient for a 4k display + options.cacheSize = options.keepLower ? 400 : 200; + } + if ($.type(options.subdomains) === 'string') { + options.subdomains = options.subdomains.split(''); + } + /* We used to call the url option baseUrl. If a baseUrl is specified, use + * it instead of url, interpretting it as before. */ + if (options.baseUrl) { + var url = options.baseUrl; + if (url && url.charAt(url.length - 1) !== '/') { + url += '/'; + } + options.url = url + '{z}/{x}/{y}.' + (options.imageFormat || 'png'); + } + /* Save the original url so that we can return it if asked */ + options.originalUrl = options.url; + if ($.type(options.url) === 'string') { + options.url = m_tileUrlFromTemplate(options.url); + } - //////////////////////////////////////////////////////////////////////////// - /** - * Like _getString, but returns an ArrayBuffer object. - */ - //////////////////////////////////////////////////////////////////////////// - this._getArrayBuffer = function (file, done, progress) { - var reader = newFileReader(done, progress); - reader.readAsText(file); - }; + var lastZoom = null, lastX = null, lastY = null, s_init = this._init, + _deferredPurge = null; - return this; -}; + // copy the options into a private variable + this._options = $.extend(true, {}, options); -inherit(geo.fileReader, geo.object); + // set the layer attribution text + this.attribution(options.attribution); -/*global File*/ -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class jsonReader - * - * @class - * @extends geo.fileReader - * @returns {geo.jsonReader} - */ -////////////////////////////////////////////////////////////////////////////// -geo.jsonReader = function (arg) { - 'use strict'; - if (!(this instanceof geo.jsonReader)) { - return new geo.jsonReader(arg); - } + // initialize the object that keeps track of actively drawn tiles + this._activeTiles = {}; - var m_this = this, m_style = arg.style || {}; - m_style = $.extend({ - 'strokeWidth': 2, - 'strokeColor': {r: 0, g: 0, b: 0}, - 'strokeOpacity': 1, - 'fillColor': {r: 1, g: 0, b: 0}, - 'fillOpacity': 1 - }, m_style); + // initialize the object that stores active tile regions in a + // tree-like structure providing quick queries to determine + // if tiles are completely obscured or not. + this._tileTree = {}; + + // initialize the in memory tile cache + this._cache = geo.tileCache({size: options.cacheSize}); + + /** + * Readonly accessor to the options object + */ + Object.defineProperty(this, 'options', {get: function () { + return $.extend({}, this._options); + }}); + + /** + * Readonly accessor to the tile cache object. + */ + Object.defineProperty(this, 'cache', {get: function () { + return this._cache; + }}); - geo.fileReader.call(this, arg); + /** + * Readonly accessor to the active tile mapping. This is an object containing + * all currently drawn tiles (hash(tile) => tile). + */ + Object.defineProperty(this, 'activeTiles', {get: function () { + return $.extend({}, this._activeTiles); // copy on output + }}); - this.canRead = function (file) { - if (file instanceof File) { - return (file.type === 'application/json' || file.name.match(/\.json$/)); - } else if (typeof file === 'string') { - try { - JSON.parse(file); - } catch (e) { + /** + * The number of tiles at the given zoom level + * The default implementation just returns `Math.pow(2, z)`. + * @param {number} level A zoom level + * @returns {{x: nx, y: ny}} The number of tiles in each axis + */ + this.tilesAtZoom = function (level) { + var s = Math.pow(2, level); + return {x: s, y: s}; + }; + + /** + * Returns true if the given tile index is valid: + * * min level <= level <= max level + * * 0 <= x <= 2^level - 1 + * * 0 <= y <= 2^level - 1 + * @param {object} index The tile index + * @param {number} index.x + * @param {number} index.y + * @param {number} index.level + * @returns {geo.tile} + */ + this.isValid = function (index) { + if (!(this._options.minLevel <= index.level && + index.level <= this._options.maxLevel)) { + return false; + } + if (!(this._options.wrapX || + 0 <= index.x && + index.x <= this.tilesAtZoom(index.level).x - 1)) { + return false; + } + if (!(this._options.wrapY || + 0 <= index.y && + index.y <= this.tilesAtZoom(index.level).y - 1)) { return false; } return true; - } - try { - if (Array.isArray(m_this._featureArray(file))) { - return true; + }; + + /** + * Returns the current origin tile and offset at the given zoom level. + * This is intended to be cached in the future to optimize coordinate + * transformations. + * @protected + * @param {number} level The target zoom level + * @returns {object} {index: {x, y}, offset: {x, y}} + */ + this._origin = function (level) { + var origin = this.toLevel(this.toLocal(this.map().origin()), level), + o = this._options, + index, offset; + + // get the tile index + index = { + x: Math.floor(origin.x / o.tileWidth), + y: Math.floor(origin.y / o.tileHeight) + }; + + // get the offset inside the tile (in pixels) + // This computation should contain the only numerically unstable + // subtraction in this class. All other methods will assume + // coordinates are given relative to the map origin. + offset = { + x: origin.x - o.tileWidth * index.x, + y: origin.y - o.tileHeight * index.y + }; + return {index: index, offset: offset}; + }; + + /** + * Returns a tile's bounds in its level coordinates. + * @param {geo.tile} tile + * @returns {object} bounds + */ + this._tileBounds = function (tile) { + var origin = this._origin(tile.index.level); + return tile.bounds(origin.index, origin.offset); + }; + + /** + * Returns the tile indices at the given point. + * @param {object} point The coordinates in pixels relative to the map origin. + * @param {number} point.x + * @param {number} point.y + * @param {number} level The target zoom level + * @returns {object} The tile indices + */ + this.tileAtPoint = function (point, level) { + var o = this._origin(level); + var map = this.map(); + point = this.displayToLevel(map.gcsToDisplay(point, null), level); + var to = this._options.tileOffset(level); + if (to) { + point.x += to.x; + point.y += to.y; } - } catch (e) {} - return false; - }; + var tile = { + x: Math.floor( + o.index.x + (o.offset.x + point.x) / this._options.tileWidth + ), + y: Math.floor( + o.index.y + (o.offset.y + point.y) / this._options.tileHeight + ) + }; + return tile; + }; - this._readObject = function (file, done, progress) { - var object; - function onDone(fileString) { - if (typeof fileString !== 'string') { - done(false); + /** + * Returns a tile's bounds in a gcs. + * @param {object|tile} either a tile or an object with {x, y, level} + * specifying a tile. + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @returns {object} The tile bounds in the specified gcs. + */ + this.gcsTileBounds = function (indexOrTile, gcs) { + var tile = (indexOrTile.index ? indexOrTile : geo.tile({ + index: indexOrTile, + size: {x: this._options.tileWidth, y: this._options.tileHeight}, + url: '' + })); + var to = this._options.tileOffset(tile.index.level), + bounds = tile.bounds({x: 0, y: 0}, to), + map = this.map(), + unit = map.unitsPerPixel(tile.index.level); + var coord = [{ + x: bounds.left * unit, y: this._topDown() * bounds.top * unit + }, { + x: bounds.right * unit, y: this._topDown() * bounds.bottom * unit + }]; + gcs = (gcs === null ? map.gcs() : ( + gcs === undefined ? map.ingcs() : gcs)); + if (gcs !== map.gcs()) { + coord = geo.transform.transformCoordinates(gcs, map.gcs(), coord); } + return { + left: coord[0].x, + top: coord[0].y, + right: coord[1].x, + bottom: coord[1].y + }; + }; - // We have two possibilities here: - // 1) fileString is a JSON string or is - // a URL. - try { - object = JSON.parse(fileString); - done(object); - } catch (e) { - if (!object) { - $.ajax({ - type: 'GET', - url: fileString, - dataType: 'text' - }).done(function (data) { - object = JSON.parse(data); - done(object); - }).fail(function () { - done(false); - }); + /** + * Returns an instantiated tile object with the given indices. This + * method always returns a new tile object. Use `_getTileCached` + * to use the caching layer. + * @param {object} index The tile index + * @param {number} index.x + * @param {number} index.y + * @param {number} index.level + * @param {object} source The tile index used for constructing the url + * @param {number} source.x + * @param {number} source.y + * @param {number} source.level + * @returns {geo.tile} + */ + this._getTile = function (index, source) { + var urlParams = source || index; + return geo.tile({ + index: index, + size: {x: this._options.tileWidth, y: this._options.tileHeight}, + url: this._options.url(urlParams.x, urlParams.y, urlParams.level || 0, + this._options.subdomains) + }); + }; + + /** + * Returns an instantiated tile object with the given indices. This + * method is similar to `_getTile`, but checks the cache before + * generating a new tile. + * @param {object} index The tile index + * @param {number} index.x + * @param {number} index.y + * @param {number} index.level + * @param {object} source The tile index used for constructing the url + * @param {number} source.x + * @param {number} source.y + * @param {number} source.level + * @returns {geo.tile} + */ + this._getTileCached = function (index, source) { + var tile = this.cache.get(this._tileHash(index)); + if (tile === null) { + tile = this._getTile(index, source); + this.cache.add(tile); + } + return tile; + }; + + /** + * Returns a string representation of the tile at the given index. + * This method is used as a hashing function for the caching layer. + * + * Note: This method _must_ return the same string as: + * + * tile({index: index}).toString(); + * + * @param {object} index The tile index + * @param {number} index.x + * @param {number} index.y + * @param {number} index.level + * @returns {string} + */ + this._tileHash = function (index) { + return [index.level || 0, index.y, index.x].join('_'); + }; + + /** + * Returns the optimal starting and ending tile indices + * (inclusive) necessary to fill the given viewport. + * @param {number} level The zoom level + * @param {object} bounds The map bounds in world coordinates + */ + this._getTileRange = function (level, bounds) { + return { + start: this.tileAtPoint({ + x: bounds.left, + y: bounds.top + }, level), + end: this.tileAtPoint({ + x: bounds.right, + y: bounds.bottom + }, level) + }; + }; + + /** + * Returns a list of tiles necessary to fill the screen at the given + * zoom level, center point, and viewport size. The list is optionally + * ordered by loading priority (center tiles first). + * + * @protected + * @param {number} maxLevel The zoom level + * @param {object} bounds The map bounds + * @param {boolean} sorted Return a sorted list + * @returns {geo.tile[]} An array of tile objects + */ + this._getTiles = function (maxLevel, bounds, sorted) { + var i, j, tiles = [], index, nTilesLevel, + start, end, indexRange, source, center, + level, minLevel = this._options.keepLower ? 0 : maxLevel; + + for (level = minLevel; level <= maxLevel; level += 1) { + // get the tile range to fetch + indexRange = this._getTileRange(level, bounds); + start = indexRange.start; + end = indexRange.end; + + // total number of tiles existing at this level + nTilesLevel = this.tilesAtZoom(level); + + // loop over the tile range + index = {level: level}; + index.nx = nTilesLevel.x; + index.ny = nTilesLevel.y; + + for (i = start.x; i <= end.x; i += 1) { + index.x = i; + for (j = start.y; j <= end.y; j += 1) { + index.y = j; + + source = $.extend({}, index); + if (this._options.wrapX) { + source.x = modulo(index.x, index.nx); + } + if (this._options.wrapY) { + source.y = modulo(index.y, index.ny); + } + + if (this.isValid(source)) { + tiles.push(this._getTileCached($.extend({}, index), source)); + } + } } } - } - if (file instanceof File) { - m_this._getString(file, onDone, progress); - } else if (typeof file === 'string') { - onDone(file); - } else { - done(file); - } - }; + if (sorted) { + center = { + x: (start.x + end.x) / 2, + y: (start.y + end.y) / 2 + }; + tiles.sort(this._loadMetric(center)); + } + return tiles; + }; - this._featureArray = function (spec) { - if (spec.type === 'FeatureCollection') { - return spec.features || []; - } - if (spec.type === 'GeometryCollection') { - throw 'GeometryCollection not yet implemented.'; - } - if (Array.isArray(spec.coordinates)) { - return spec; - } - throw 'Unsupported collection type: ' + spec.type; - }; + /** + * Prefetches tiles up to a given zoom level around a given bounding box. + * + * @param {number} level The zoom level + * @param {object} bounds The map bounds + * @returns {$.Deferred} resolves when all of the tiles are fetched + */ + this.prefetch = function (level, bounds) { + var tiles; + tiles = this._getTiles(level, bounds, true); + return $.when.apply($, + tiles.map(function (tile) { + return tile.fetch(); + }) + ); + }; - this._featureType = function (spec) { - var geometry = spec.geometry || {}; - if (geometry.type === 'Point' || geometry.type === 'MultiPoint') { - return 'point'; - } - if (geometry.type === 'LineString') { - return 'line'; - } - if (geometry.type === 'Polygon') { - return 'polygon'; - } - if (geometry.type === 'MultiPolygon') { - return 'multipolygon'; - } - return null; - }; + /** + * This method returns a metric that determines tile loading order. The + * default implementation prioritizes tiles that are closer to the center, + * or at a lower zoom level. + * @protected + * @param {index1} center The center tile + * @param {number} center.x + * @param {number} center.y + * @returns {function} A function accepted by Array.prototype.sort + */ + this._loadMetric = function (center) { + return function (a, b) { + var a0, b0, dx, dy, cx, cy, scale; - this._getCoordinates = function (spec) { - var geometry = spec.geometry || {}, - coordinates = geometry.coordinates || [], elv; + // shortcut if zoom level differs + if (a.level !== b.level) { + return b.level - a.level; + } - if ((coordinates.length === 2 || coordinates.length === 3) && - (isFinite(coordinates[0]) && isFinite(coordinates[1]))) { + // compute the center coordinates relative to a.level + scale = Math.pow(2, a.level - center.level); + cx = center.x * scale; + cy = center.y * scale; - // Do we have a elevation component - if (isFinite(coordinates[2])) { - elv = coordinates[2]; - } + // calculate distances to the center squared + dx = a.x - cx; + dy = a.y - cy; + a0 = dx * dx + dy * dy; - // special handling for single point coordinates - return [{x: coordinates[0], y: coordinates[1], z: elv}]; - } + dx = b.x - cx; + dy = b.y - cy; + b0 = dx * dx + dy * dy; - // need better handling here, but we can plot simple polygons - // by taking just the outer linearring - if (Array.isArray(coordinates[0][0])) { - coordinates = coordinates[0]; - } + // return negative if a < b, or positive if a > b + return a0 - b0; + }; + }; - // return an array of points for LineString, MultiPoint, etc... - return coordinates.map(function (c) { + /** + * Convert a coordinate from pixel coordinates at the given zoom + * level to world coordinates. + * @param {object} coord + * @param {number} coord.x The offset in pixels (level 0) from the left edge + * @param {number} coord.y The offset in pixels (level 0) from the bottom edge + * @param {number} level The zoom level of the source coordinates + */ + this.fromLevel = function (coord, level) { + var s = Math.pow(2, -level); return { - x: c[0], - y: c[1], - z: c[2] + x: coord.x * s, + y: coord.y * s }; - }); - }; + }; - this._getStyle = function (spec) { - return spec.properties; - }; + /** + * Convert a coordinate from layer coordinates to pixel coordinates at the + * given zoom level. + * @param {object} coord + * @param {number} coord.x The offset in pixels (level 0) from the left edge + * @param {number} coord.y The offset in pixels (level 0) from the bottom edge + * @param {number} level The zoom level of the new coordinates + */ + this.toLevel = function (coord, level) { + var s = Math.pow(2, level); + return { + x: coord.x * s, + y: coord.y * s + }; + }; - this.read = function (file, done, progress) { + /** + * Draw the given tile on the active canvas. + * @param {geo.tile} tile The tile to draw + */ + this.drawTile = function (tile) { + var hash = tile.toString(); - function _done(object) { - var features, allFeatures = []; + if (this._activeTiles.hasOwnProperty(hash)) { + // the tile is already drawn, move it to the top + this._moveToTop(tile); + } else { + // pass to the rendering implementation + this._drawTile(tile); + } - features = m_this._featureArray(object); + // add the tile to the active cache + this._activeTiles[hash] = tile; + }; - features.forEach(function (feature) { - var type = m_this._featureType(feature), - coordinates = m_this._getCoordinates(feature), - style = m_this._getStyle(feature); - if (type) { - if (type === 'line') { - style.fill = style.fill || false; - allFeatures.push(m_this._addFeature( - type, - [coordinates], - style, - feature.properties - )); - } else if (type === 'point') { - style.stroke = style.stroke || false; - allFeatures.push(m_this._addFeature( - type, - coordinates, - style, - feature.properties - )); - } else if (type === 'polygon') { - style.fill = style.fill === undefined ? true : style.fill; - style.fillOpacity = ( - style.fillOpacity === undefined ? 0.25 : style.fillOpacity - ); - // polygons not yet supported - allFeatures.push(m_this._addFeature( - 'line', - [coordinates], - style, - feature.properties - )); - } else if (type === 'multipolygon') { - style.fill = style.fill === undefined ? true : style.fill; - style.fillOpacity = ( - style.fillOpacity === undefined ? 0.25 : style.fillOpacity - ); + /** + * Render the tile on the canvas. This implementation draws the tiles directly + * on the DOM using tags. Derived classes should override this method + * to draw the tile on a renderer specific context. + * @protected + * @param {geo.tile} tile The tile to draw + */ + this._drawTile = function (tile) { + // Make sure this method is not called when there is + // a renderer attached. + // + if (this.renderer() !== null) { + throw new Error('This draw method is not valid on renderer managed layers.'); + } - coordinates = feature.geometry.coordinates.map(function (c) { - return c[0].map(function (el) { - return { - x: el[0], - y: el[1], - z: el[2] - }; - }); - }); + // get the layer node + var div = $(this._getSubLayer(tile.index.level)), + bounds = this._tileBounds(tile), + duration = this._options.animationDuration, + container = $('
').attr( + 'tile-reference', tile.toString()); + + // apply a transform to place the image correctly + container.append(tile.image); + container.css({ + 'position': 'absolute', + 'left': bounds.left + 'px', + 'top': bounds.top + 'px' + }); - allFeatures.push(m_this._addFeature( - 'line', - coordinates, - style, - feature.properties - )); - } + // apply fade in animation + if (duration > 0) { + tile.fadeIn(duration); + } + + // append the image element + div.append(container); + + // add an error handler + tile.catch(function () { + // May want to do something special here later + console.warn('Could not load tile at ' + tile.index); + this._remove(tile); + }.bind(this)); + }; + + /** + * Remove the given tile from the canvas and the active cache. + * @param {geo.tile|string} tile The tile (or hash) to remove + * @returns {geo.tile} the tile removed from the active layer + */ + this.remove = function (tile) { + var hash = tile.toString(); + var value = this._activeTiles[hash]; + + if (value instanceof geo.tile) { + this._remove(value); + } + + delete this._activeTiles[hash]; + return value; + }; + + /** + * Remove the given tile from the canvas. This implementation just + * finds and removes the element created for the tile. + * @param {geo.tile|string} tile The tile object to remove + */ + this._remove = function (tile) { + if (tile.image) { + if (tile.image.parentElement) { + $(tile.image.parentElement).remove(); } else { - console.log('unsupported feature type: ' + feature.geometry.type); + /* This shouldn't happen, but sometimes does. Originally it happened + * when a tile was removed from the cache before it was finished + * being used; there is still some much rarer condition that can + * cause it. Log that it happened until we can figure out how to fix + * the issue. */ + console.log('No parent element to remove ' + tile.toString(), tile); } - }); - - if (done) { - done(allFeatures); + $(tile.image).remove(); } - } - - m_this._readObject(file, _done, progress); - }; + }; + /** + * Move the given tile to the top on the canvas. + * @param {geo.tile} tile The tile object to move + */ + this._moveToTop = function (tile) { + $.noop(tile); + }; - //////////////////////////////////////////////////////////////////////////// - /** - * Build the data array for a feature given the coordinates and properties - * from the geojson. - * - * @private - * @param {Object[]} coordinates Coordinate data array - * @param {Object} properties Geojson properties object - * @param {Object} style Global style defaults - * @returns {Object[]} - */ - ////////////////////////////////////////////////////////////////////////////// - this._buildData = function (coordinates, properties, style) { - return coordinates.map(function (coord) { + /** + * Query the attached map for the current bounds and return them + * as pixels at the current zoom level. + * @returns {object} + * Bounds object with left, right, top, bottom keys + * @protected + */ + this._getViewBounds = function () { + var map = this.map(), + mapZoom = map.zoom(), + zoom = this._options.tileRounding(mapZoom), + scale = Math.pow(2, mapZoom - zoom), + size = map.size(); + var ul = this.displayToLevel({x: 0, y: 0}); + var lr = this.displayToLevel({x: size.width, y: size.height}); return { - coordinates: coord, - properties: properties, - style: style + level: zoom, + scale: scale, + left: ul.x, + right: lr.x, + bottom: lr.y, + top: ul.y }; - }); - }; - - this._addFeature = function (type, coordinates, style, properties) { - var _style = $.extend({}, m_style, style); - var feature = m_this.layer().createFeature(type) - .data(m_this._buildData(coordinates, properties, style)) - .style(_style); + }; - if (type === 'line') { - feature.line(function (d) { return d.coordinates; }); - } else { - feature.position(function (d) { return d.coordinates; }); - } - return feature; - }; + /** + * Remove all inactive tiles from the display. An inactive tile + * is one that is no longer visible either because it was panned + * out of the active view or the zoom has changed. + * @protected + * @param {number} zoom Tiles (in bounds) at this zoom level will be kept + * @param {boolean} doneLoading If true, allow purging additional tiles. + */ + this._purge = function (zoom, doneLoading) { + var tile, hash, bounds = {}; -}; + // Don't purge tiles in an active update + if (this._updating) { + return; + } -inherit(geo.jsonReader, geo.fileReader); + // get the view bounds + bounds = this._getViewBounds(); -geo.registerFileReader('jsonReader', geo.jsonReader); + for (hash in this._activeTiles) {// jshint ignore: line -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class map. - * - * Creation tags a dictionary of arguments, which can include: - * center: {x: (center x value), y: (center y value)} - * gcs: - * uigcs: - * node: - * layers: - * zoom: (number) - initial zoom level - * min: (number) - minimum zoom level - * max: (number) - maximum zoom level - * width: - * height: - * parallelProjection: (bool) - true to use parallel projection, false to use - * perspective. - * discreteZoom: (bool) - true to only allow integer zoom levels, false to - * allow any zoom level. - * autoResize: - * clampBounds: - * interactor: - * clock: - * - * Creates a new map inside of the given HTML layer (Typically DIV) - * @class - * @extends geo.sceneObject - * @returns {geo.map} - */ -////////////////////////////////////////////////////////////////////////////// -geo.map = function (arg) { - 'use strict'; - if (!(this instanceof geo.map)) { - return new geo.map(arg); - } - arg = arg || {}; - geo.sceneObject.call(this, arg); - arg.layers = arg.layers === undefined ? [] : arg.layers; + tile = this._activeTiles[hash]; + if (this._canPurge(tile, bounds, zoom, doneLoading)) { + this.remove(tile); + } + } + return this; + }; - //////////////////////////////////////////////////////////////////////////// - /** - * Private member variables - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_exit = this._exit, - m_x = 0, - m_y = 0, - m_node = $(arg.node), - m_width = arg.width || m_node.width(), - m_height = arg.height || m_node.height(), - m_gcs = arg.gcs === undefined ? 'EPSG:4326' : arg.gcs, - m_uigcs = arg.uigcs === undefined ? 'EPSG:4326' : arg.uigcs, - m_center = { x: 0, y: 0 }, - m_zoom = arg.zoom === undefined ? 4 : arg.zoom, - m_baseLayer = null, - m_fileReader = null, - m_interactor = null, - m_validZoomRange = { min: 0, max: 16 }, - m_transition = null, - m_queuedTransition = null, - m_clock = null, - m_parallelProjection = arg.parallelProjection ? true : false, - m_discreteZoom = arg.discreteZoom ? true : false, - m_bounds = {}; + /** + * Remove all active tiles from the canvas. + * @returns {geo.tile[]} The array of tiles removed + */ + this.clear = function () { + var tiles = [], tile; - arg.center = geo.util.normalizeCoordinates(arg.center); - arg.autoResize = arg.autoResize === undefined ? true : arg.autoResize; - arg.clampBounds = arg.clampBounds === undefined ? true : arg.clampBounds; + // ignoring the warning here because this is a privately + // controlled object with simple keys + for (tile in this._activeTiles) { // jshint ignore: line + tiles.push(this.remove(tile)); + } - //////////////////////////////////////////////////////////////////////////// - /** - * Get map gcs - * - * @returns {string} - */ - //////////////////////////////////////////////////////////////////////////// - this.gcs = function (arg) { - if (arg === undefined) { - return m_gcs; - } - m_gcs = arg; - return m_this; - }; + // clear out the tile coverage tree + this._tileTree = {}; - //////////////////////////////////////////////////////////////////////////// - /** - * Get map user interface GCS - * - * @returns {string} - */ - //////////////////////////////////////////////////////////////////////////// - this.uigcs = function () { - return m_uigcs; - }; + return tiles; + }; - //////////////////////////////////////////////////////////////////////////// - /** - * Get root node of the map - * - * @returns {object} - */ - //////////////////////////////////////////////////////////////////////////// - this.node = function () { - return m_node; - }; + /** + * Reset the layer to the initial state, clearing the canvas + * and resetting the tile cache. + * @returns {this} Chainable + */ + this.reset = function () { + this.clear(); + this._cache.clear(); + }; - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set zoom level of the map - * - * @returns {Number|geo.map} - */ - //////////////////////////////////////////////////////////////////////////// - this.zoom = function (val, direction, ignoreDiscreteZoom) { - var base, evt, recenter = false; - if (val === undefined) { - return m_zoom; - } + /** + * Compute local coordinates from the given world coordinates. The + * tile layer uses units of pixels relative to the world space + * coordinate origin. + * @param {object} pt A point in world space coordinates + * @param {number|undefined} zoom If unspecified, use the map zoom. + * @returns {object} Local coordinates + */ + this.toLocal = function (pt, zoom) { + var map = this.map(), + unit = map.unitsPerPixel(zoom === undefined ? map.zoom() : zoom); + return { + x: pt.x / unit, + y: this._topDown() * pt.y / unit + }; + }; - /* The ignoreDiscreteZoom flag is intended to allow non-integer zoom values - * during animation. */ - if (m_discreteZoom && val !== Math.round(val) && !ignoreDiscreteZoom) { - /* If we are using discrete zoom levels and the value we were given is - * not an integer, then try to detect if we are enlarging or shrinking - * and perform the expected behavior. Otherwise, make sure we are at an - * integer level. We may need to revisit for touch zoom events. */ - if (m_zoom !== Math.round(m_zoom) || Math.abs(val - m_zoom) < 0.01) { - val = Math.round(m_zoom); - } else if (val < m_zoom) { - val = Math.min(Math.round(val), m_zoom - 1); - } else if (val > m_zoom) { - val = Math.max(Math.round(val), m_zoom + 1); - } - } - val = Math.min(m_validZoomRange.max, Math.max(val, m_validZoomRange.min)); - if (val === m_zoom) { - return m_this; - } + /** + * Compute world coordinates from the given local coordinates. The + * tile layer uses units of pixels relative to the world space + * coordinate origin. + * @param {object} pt A point in world space coordinates + * @param {number|undefined} zoom If unspecified, use the map zoom. + * @returns {object} Local coordinates + */ + this.fromLocal = function (pt, zoom) { + var map = this.map(), + unit = map.unitsPerPixel(zoom === undefined ? map.zoom() : zoom); + return { + x: pt.x * unit, + y: this._topDown() * pt.y * unit + }; + }; - base = m_this.baseLayer(); + /** + * Return a factor for invertin the y units as appropriate. + * @return {number} + */ + this._topDown = function () { + return this._options.topDown ? 1 : -1; + }; - evt = { - geo: {}, - zoomLevel: val, - screenPosition: direction, - eventType: geo.event.zoom + /** + * Return the DOM element containing a level specific layer. This will + * create the element if it doesn't already exist. + * @param {number} level The zoom level of the layer to fetch + * @return {DOM} + */ + this._getSubLayer = function (level) { + var node = this.canvas() + .find('div[data-tile-layer=' + level.toFixed() + ']').get(0); + if (!node) { + node = $( + '
' + ).css('transform-origin', '0px').get(0); + this.canvas().append(node); + } + return node; }; - if (base) { - base.geoTrigger(geo.event.zoom, evt, true); - } + /** + * Set sublayer transforms to align them with the given zoom level. + * @param {number} level The target zoom level + */ + this._updateSubLayers = function (level) { + this.canvas().find('.geo-tile-layer').each(function (idx, el) { + var $el = $(el), + layer = parseInt($el.data('tileLayer')); + $el.css( + 'transform', + 'scale(' + Math.pow(2, level - layer) + ')' + ); + }.bind(this)); + }; - recenter = evt.center; - if (!evt.geo.preventDefault) { + /** + * Update the view according to the map/camera. + * @returns {this} Chainable + */ + this._update = function () { + var map = this.map(), + mapZoom = map.zoom(), + zoom = this._options.tileRounding(mapZoom), + center = this.displayToLevel(undefined, zoom), + bounds = map.bounds(undefined, null), + tiles, view = this._getViewBounds(), myPurge = {}; + + _deferredPurge = myPurge; + tiles = this._getTiles( + zoom, bounds, true + ); - m_zoom = val; - m_this._updateBounds(); + // Update the transform for the local layer coordinates + this._updateSubLayers(zoom); - m_this.children().forEach(function (child) { - child.geoTrigger(geo.event.zoom, evt, true); + var to = this._options.tileOffset(zoom); + if (this.renderer() === null) { + this.canvas().css( + 'transform-origin', + 'center center' + ); + this.canvas().css( + 'transform', + 'scale(' + (Math.pow(2, mapZoom - zoom)) + ')' + + 'translate(' + + (-to.x) + 'px' + ',' + + (-to.y) + 'px' + ')' + + 'translate(' + + (map.size().width / 2) + 'px' + ',' + + (map.size().height / 2) + 'px' + ')' + + 'translate(' + + (-(view.left + view.right) / 2) + 'px' + ',' + + (-(view.bottom + view.top) / 2) + 'px' + ')' + + '' + ); + } + /* Set some attributes that can be used by non-css based viewers. This + * doesn't include the map center, as that may need to be handled + * differently from the view center. */ + this.canvas().attr({ + scale: Math.pow(2, mapZoom - zoom), + dx: -to.x + -(view.left + view.right) / 2, + dy: -to.y + -(view.bottom + view.top) / 2 }); - m_this.modified(); - } + lastZoom = mapZoom; + lastX = center.x; + lastY = center.y; - if (evt.center) { - m_this.center(recenter); - } else { - m_this.pan({x: 0, y: 0}); - } - return m_this; - }; + // reset the tile coverage tree + this._tileTree = {}; - //////////////////////////////////////////////////////////////////////////// - /** - * Pan the map by (x: dx, y: dy) pixels. - * - * @param {Object} delta - * @param {bool?} force Disable bounds clamping - * @returns {geo.map} - */ - //////////////////////////////////////////////////////////////////////////// - this.pan = function (delta, force) { + tiles.forEach(function (tile) { + tile.then(function () { + if (tile !== this.cache.get(tile.toString())) { + /* If the tile has fallen out of the cache, don't draw it -- it is + * untracked. This may be an indication that a larger cache should + * have been used. */ + return; + } + /* Check if a tile is still desired. Don't draw it if it isn't. */ + var mapZoom = map.zoom(), + zoom = this._options.tileRounding(mapZoom), + bounds = this._getViewBounds(); + if (this._canPurge(tile, bounds, zoom)) { + this.remove(tile); + return; + } - var base = m_this.baseLayer(), - evt, pt, corner1, corner2; + this.drawTile(tile); - if (arg.clampBounds && !force && m_width && m_height) { - pt = m_this.displayToGcs({ - x: delta.x, - y: delta.y - }); + // mark the tile as covered + this._setTileTree(tile); + }.bind(this)); - corner1 = m_this.gcsToDisplay({ - x: -180, - y: 82 - }); - corner2 = m_this.gcsToDisplay({ - x: 180, - y: -82 + this.addPromise(tile); + }.bind(this)); + + // purge all old tiles when the new tiles are loaded (successfully or not) + $.when.apply($, tiles) + .done(// called on success and failure + function () { + if (_deferredPurge === myPurge) { + this._purge(zoom, true); + } + }.bind(this) + ); + }; + + /** + * Set a value in the tile tree object indicating that the given area of + * the canvas is covered by the tile. + * @protected + * @param {geo.tile} tile + */ + this._setTileTree = function (tile) { + var index = tile.index; + this._tileTree[index.level] = this._tileTree[index.level] || {}; + this._tileTree[index.level][index.x] = this._tileTree[index.level][index.x] || {}; + this._tileTree[index.level][index.x][index.y] = tile; + }; + + /** + * Get a value in the tile tree object if it exists or return null. + * @protected + * @param {object} index A tile index object + * @param {object} index.level + * @param {object} index.x + * @param {object} index.y + * @returns {geo.tile|null} + */ + this._getTileTree = function (index) { + return ( + ( + this._tileTree[index.level] || {} + )[index.x] || {} + )[index.y] || null; + }; + + /** + * Returns true if the tile is completely covered by other tiles on the canvas. + * Currently this method only checks layers +/- 1 away from `tile`. If the + * zoom level is allowed to change by 2 or more in a single update step, this + * method will need to be refactored to make a more robust check. Returns + * an array of tiles covering it or null if any part of the tile is exposed. + * @protected + * @param {geo.tile} tile + * @returns {geo.tile[]|null} + */ + this._isCovered = function (tile) { + var level = tile.index.level, + x = tile.index.x, + y = tile.index.y, + tiles = []; + + // Check one level up + tiles = this._getTileTree({ + level: level - 1, + x: Math.floor(x / 2), + y: Math.floor(y / 2) }); + if (tiles) { + return [tiles]; + } - if (corner1.x > 0 && corner2.x < m_width) { - // if the map is too small horizontally - delta.x = (-corner1.x + m_width - corner2.x) / 2; - } else { - delta.x = Math.max(Math.min(delta.x, -corner1.x), m_width - corner2.x); + // Check one level down + tiles = [ + this._getTileTree({ + level: level + 1, + x: 2 * x, + y: 2 * y + }), + this._getTileTree({ + level: level + 1, + x: 2 * x + 1, + y: 2 * y + }), + this._getTileTree({ + level: level + 1, + x: 2 * x, + y: 2 * y + 1 + }), + this._getTileTree({ + level: level + 1, + x: 2 * x + 1, + y: 2 * y + 1 + }) + ]; + if (tiles.every(function (t) { return t !== null; })) { + return tiles; + } + + return null; + }; + + /** + * Returns true if the provided tile is outside of the current view bounds + * and can be removed from the canvas. + * @protected + * @param {geo.tile} tile + * @param {object?} bounds The view bounds + * @param {object?} bounds.left + * @param {object?} bounds.right + * @param {object?} bounds.top + * @param {object?} bounds.bottom + * @returns {boolean} + */ + this._outOfBounds = function (tile, bounds) { + /* We may want to add an (n) tile edge buffer so we appear more + * responsive */ + var to = this._options.tileOffset(tile.index.level); + var scale = 1; + if (tile.index.level !== bounds.level) { + scale = Math.pow(2, (bounds.level || 0) - (tile.index.level || 0)); } - if (corner1.y > 0 && corner2.y < m_height) { - // if the map is too small horizontally - delta.y = (-corner1.y + m_height - corner2.y) / 2; + return (tile.bottom - to.y) * scale < bounds.top || + (tile.left - to.x) * scale > bounds.right || + (tile.top - to.y) * scale > bounds.bottom || + (tile.right - to.x) * scale < bounds.left; + }; + + /** + * Returns true if the provided tile can be purged from the canvas. This method + * will return `true` if the tile is completely covered by one or more other tiles + * or it is outside of the active view bounds. This method returns the logical and + * of `_isCovered` and `_outOfBounds`. + * @protected + * @param {geo.tile} tile + * @param {object?} bounds The view bounds (if empty, assume global bounds) + * @param {number} bounds.left + * @param {number} bounds.right + * @param {number} bounds.top + * @param {number} bounds.bottom + * @param {number} bounds.level The zoom level the bounds are given as + * @param {number} zoom Keep in bound tile at this zoom level + * @param {boolean} doneLoading If true, allow purging additional tiles. + * @returns {boolean} + */ + this._canPurge = function (tile, bounds, zoom, doneLoading) { + if (this._options.keepLower) { + zoom = zoom || 0; + if (zoom < tile.index.level) { + return true; + } } else { - delta.y = Math.max(Math.min(delta.y, -corner1.y), m_height - corner2.y); + /* For tile layers that should only keep one layer, if loading is + * finished, purge all but the current layer. This is important for + * semi-transparanet layers. */ + if ((doneLoading || this._isCovered(tile)) && + zoom !== tile.index.level) { + return true; + } } - } + if (bounds) { + return this._outOfBounds(tile, bounds); + } + return false; + }; - evt = { - geo: {}, - screenDelta: delta, - eventType: geo.event.pan + /** + * Convert display pixel coordinates (where (0,0) is the upper left) to + * layer pixel coordinates (typically (0,0) is the center of the map and + * the upper-left has the most negative values). + * By default, this is done at the current base zoom level. + * + * @param pt: the point to convert. If undefined, use the center of the + * display. + * @param zoom: if specified, the zoom level to use. + * @returns: the point in level coordinates. + */ + this.displayToLevel = function (pt, zoom) { + var map = this.map(), + mapzoom = map.zoom(), + roundzoom = this._options.tileRounding(mapzoom), + unit = map.unitsPerPixel(zoom === undefined ? roundzoom : zoom); + if (pt === undefined) { + var size = map.size(); + pt = {x: size.width / 2, y: size.height / 2}; + } + /* Reverse the y coordinate, since we expect the gcs coordinate system + * to be right-handed and the level coordinate system to be + * left-handed. */ + var gcsPt = map.displayToGcs(pt, null), + lvlPt = {x: gcsPt.x / unit, y: this._topDown() * gcsPt.y / unit}; + return lvlPt; + }; + + /** + * Get or set the tile url string or function. If changed, load the new + * tiles. + * + * @param {string|function} [url] The new tile url. + * @returns {string|function|this} + */ + this.url = function (url) { + if (url === undefined) { + return this._options.originalUrl; + } + if (url === this._options.originalUrl) { + return this; + } + this._options.originalUrl = url; + if ($.type(url) === 'string') { + url = m_tileUrlFromTemplate(url); + } + this._options.url = url; + this.reset(); + this.map().draw(); + return this; + }; + + /** + * Initialize after the layer is added to the map. + */ + this._init = function () { + var sublayer; + + // call super method + s_init.apply(this, arguments); + + if (this.renderer() === null) { + // Initialize sublayers in the correct order + for (sublayer = 0; sublayer <= this._options.maxLevel; sublayer += 1) { + this._getSubLayer(sublayer); + } + } + return this; }; - // first pan the base layer - if (base) { - base.geoTrigger(geo.event.pan, evt, true); - } - // If the base renderer says the pan is invalid, then cancel the action. - if (evt.geo.preventDefault) { - return; - } - m_center = m_this.displayToGcs({ - x: m_width / 2, - y: m_height / 2 - }); - m_this._updateBounds(); + geo.adjustLayerForRenderer('tile', this); - m_this.children().forEach(function (child) { - if (child !== base) { - child.geoTrigger(geo.event.pan, evt, true); - } - }); + return this; + }; - m_this.modified(); - return m_this; + /** + * This object contains the default options used to initialize the tileLayer. + */ + geo.tileLayer.defaults = { + minLevel: 0, + maxLevel: 18, + tileOverlap: 0, + tileWidth: 256, + tileHeight: 256, + wrapX: true, + wrapY: false, + url: null, + subdomains: 'abc', + minX: 0, + maxX: 255, + minY: 0, + maxY: 255, + tileOffset: function (level) { + void(level); + return {x: 0, y: 0}; + }, + topDown: false, + keepLower: true, + // cacheSize: 400, // set depending on keepLower + tileRounding: Math.round, + attribution: '', + animationDuration: 0 }; + inherit(geo.tileLayer, geo.featureLayer); +})(); + +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of class fileReader + * + * @class + * @extends geo.object + * @returns {geo.fileReader} + */ +////////////////////////////////////////////////////////////////////////////// +geo.fileReader = function (arg) { + "use strict"; + if (!(this instanceof geo.fileReader)) { + return new geo.fileReader(arg); + } + geo.object.call(this); + //////////////////////////////////////////////////////////////////////////// /** - * Set center of the map to the given geographic coordinates, or get the - * current center. Uses bare objects {x: 0, y: 0}. - * - * @param {Object} coordinates - * @returns {Object|geo.map} + * @private */ //////////////////////////////////////////////////////////////////////////// - this.center = function (coordinates, force) { - var newCenter, currentCenter; - - if (coordinates === undefined) { - return m_center; - } + arg = arg || {}; - // get the screen coordinates of the new center - coordinates = geo.util.normalizeCoordinates(coordinates); - newCenter = m_this.gcsToDisplay(coordinates); - currentCenter = m_this.gcsToDisplay(m_center); + if (!(arg.layer instanceof geo.featureLayer)) { + throw "fileReader must be given a feature layer"; + } - // call the pan method - m_this.pan({ - x: currentCenter.x - newCenter.x, - y: currentCenter.y - newCenter.y - }, force); + var m_layer = arg.layer; - return m_this; + //////////////////////////////////////////////////////////////////////////// + /** + * Get the feature layer attached to the reader + */ + //////////////////////////////////////////////////////////////////////////// + this.layer = function () { + return m_layer; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set parallel projection setting of the map - * - * @returns {Boolean|geo.map} + * Tells the caller if it can handle the given file by returning a boolean. */ //////////////////////////////////////////////////////////////////////////// - this.parallelProjection = function (val) { - if (val === undefined) { - return m_parallelProjection; - } - val = val ? true : false; - if (m_parallelProjection !== val) { - var base, evt = { - eventType: geo.event.parallelprojection, - parallelProjection: val - }; - - m_parallelProjection = val; - base = m_this.baseLayer(); - base.geoTrigger(geo.event.parallelprojection, evt, true); - m_this.children().forEach(function (child) { - child.geoTrigger(geo.event.parallelprojection, evt, true); - }); - m_this.modified(); - } - return m_this; + this.canRead = function () { + return false; }; //////////////////////////////////////////////////////////////////////////// /** - * Add layer to the map - * - * @param {geo.layer} layer to be added to the map - * @return {geom.map} + * Reads the file object and calls the done function when finished. As an + * argument to done, provides a boolean that reports if the read was a + * success. Possibly, it can call done with an object containing details + * of the read operation. */ //////////////////////////////////////////////////////////////////////////// - this.createLayer = function (layerName, arg) { - arg = arg || {}; - arg.parallelProjection = m_parallelProjection; - var newLayer = geo.createLayer( - layerName, m_this, arg); - - if (newLayer) { - newLayer._resize(m_x, m_y, m_width, m_height); - } else { - return null; - } - - if (newLayer.referenceLayer() || m_this.children().length === 0) { - m_this.baseLayer(newLayer); - } - - newLayer._resize(m_x, m_y, m_width, m_height); // this call initializes the camera - m_this.addChild(newLayer); - m_this.modified(); - - m_this.geoTrigger(geo.event.layerAdd, { - type: geo.event.layerAdd, - target: m_this, - layer: newLayer - }); - - return newLayer; + this.read = function (file, done) { + done(false); }; //////////////////////////////////////////////////////////////////////////// /** - * Remove layer from the map - * - * @param {geo.layer} layer that should be removed from the map - * @return {geo.map} + * Return a FileReader with handlers attached. */ //////////////////////////////////////////////////////////////////////////// - this.deleteLayer = function (layer) { - - if (layer !== null && layer !== undefined) { - layer._exit(); - - m_this.removeChild(layer); - - m_this.modified(); - - m_this.geoTrigger(geo.event.layerRemove, { - type: geo.event.layerRemove, - target: m_this, - layer: layer - }); + function newFileReader(done, progress) { + var reader = new FileReader(); + if (progress) { + reader.onprogress = progress; } - - /// Return deleted layer (similar to createLayer) as in the future - /// we may provide extension of this method to support deletion of - /// layer using id or some sort. - return layer; - }; + reader.onloadend = function () { + if (!reader.result) { + done(reader.error); + } + done(reader.result); + }; + return reader; + } //////////////////////////////////////////////////////////////////////////// /** - * Toggle visibility of a layer - * - * @param {geo.layer} layer - * @returns {Boolean} + * Private method for reading a file object as a string. Calls done with + * the string content when finished or an error object if unsuccessful. + * Optionally, the caller can provide a progress method that is called + * after reading each slice. */ //////////////////////////////////////////////////////////////////////////// - this.toggle = function (layer) { - if (layer !== null && layer !== undefined) { - layer.visible(!layer.visible()); - m_this.modified(); - - m_this.geoTrigger(geo.event.layerToggle, { - type: geo.event.layerToggle, - target: m_this, - layer: layer - }); - } - return m_this; + this._getString = function (file, done, progress) { + var reader = newFileReader(done, progress); + reader.readAsText(file); }; //////////////////////////////////////////////////////////////////////////// /** - * Resize map - * - * @param {Number} x x-offset in display space - * @param {Number} y y-offset in display space - * @param {Number} w width in display space - * @param {Number} h height in display space + * Like _getString, but returns an ArrayBuffer object. */ //////////////////////////////////////////////////////////////////////////// - this.resize = function (x, y, w, h) { - var i, layers = m_this.children(); + this._getArrayBuffer = function (file, done, progress) { + var reader = newFileReader(done, progress); + reader.readAsText(file); + }; - m_x = x; - m_y = y; - m_width = w; - m_height = h; + return this; +}; - for (i = 0; i < layers.length; i += 1) { - layers[i]._resize(x, y, w, h); - } +inherit(geo.fileReader, geo.object); - m_this.geoTrigger(geo.event.resize, { - type: geo.event.resize, - target: m_this, - x: m_x, - y: m_y, - width: w, - height: h - }); +/*global File*/ +////////////////////////////////////////////////////////////////////////////// +/** +* Create a new instance of class jsonReader +* +* @class +* @extends geo.fileReader +* @returns {geo.jsonReader} +*/ +////////////////////////////////////////////////////////////////////////////// +geo.jsonReader = function (arg) { + 'use strict'; + if (!(this instanceof geo.jsonReader)) { + return new geo.jsonReader(arg); + } - m_this._updateBounds(); - m_this.pan({x: 0, y: 0}); - m_this.modified(); + var m_this = this, m_style = arg.style || {}; + m_style = $.extend({ + 'strokeWidth': 2, + 'strokeColor': {r: 0, g: 0, b: 0}, + 'strokeOpacity': 1, + 'fillColor': {r: 1, g: 0, b: 0}, + 'fillOpacity': 1 + }, m_style); - return m_this; + geo.fileReader.call(this, arg); + + this.canRead = function (file) { + if (file instanceof File) { + return (file.type === 'application/json' || file.name.match(/\.json$/)); + } else if (typeof file === 'string') { + try { + JSON.parse(file); + } catch (e) { + return false; + } + return true; + } + try { + if (Array.isArray(m_this._featureArray(file))) { + return true; + } + } catch (e) {} + return false; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from gcs coordinates to display coordinates - * - * @param {*} input {[[{x:_x, y: _y}], [x1,y1, x2, y2]} - * @return {object} - * - * @note Currently only lat-lon inputs are supported - */ - //////////////////////////////////////////////////////////////////////////// - this.gcsToDisplay = function (input) { - var world, output; + this._readObject = function (file, done, progress) { + var object; + function onDone(fileString) { + if (typeof fileString !== 'string') { + done(false); + } + + // We have two possibilities here: + // 1) fileString is a JSON string or is + // a URL. + try { + object = JSON.parse(fileString); + done(object); + } catch (e) { + if (!object) { + $.ajax({ + type: 'GET', + url: fileString, + dataType: 'text' + }).done(function (data) { + object = JSON.parse(data); + done(object); + }).fail(function () { + done(false); + }); + } + } + } - /// Now handle different data types - if ((input instanceof Array && - input.length > 0) || input instanceof Object) { - world = m_baseLayer.toLocal(input); - output = m_baseLayer.renderer().worldToDisplay(world); + if (file instanceof File) { + m_this._getString(file, onDone, progress); + } else if (typeof file === 'string') { + onDone(file); } else { - /// Everything else - throw 'Conversion method latLonToDisplay does not handle ' + input; + done(file); } - - return output; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from display to latitude longitude coordinates - */ - //////////////////////////////////////////////////////////////////////////// - this.displayToGcs = function (input) { - var output; - - /// Now handle different data types - if ((input instanceof Array && input.length > 0) || - input instanceof Object) { - output = m_baseLayer.renderer().displayToWorld(input); - output = m_baseLayer.fromLocal(output); - } else { - throw 'Conversion method displayToGcs does not handle ' + input; + this._featureArray = function (spec) { + if (spec.type === 'FeatureCollection') { + return spec.features || []; + } + if (spec.type === 'GeometryCollection') { + throw 'GeometryCollection not yet implemented.'; } - return output; + if (Array.isArray(spec.coordinates)) { + return spec; + } + throw 'Unsupported collection type: ' + spec.type; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Queries each layer for information at this location. - */ - //////////////////////////////////////////////////////////////////////////// - this.query = function () { - // TODO Implement this + this._featureType = function (spec) { + var geometry = spec.geometry || {}; + if (geometry.type === 'Point' || geometry.type === 'MultiPoint') { + return 'point'; + } + if (geometry.type === 'LineString') { + return 'line'; + } + if (geometry.type === 'Polygon') { + return 'polygon'; + } + if (geometry.type === 'MultiPolygon') { + return 'multipolygon'; + } + return null; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Sets or gets base layer for this map - * - * @param {geo.layer} baseLayer optional - * @returns {geo.map|geo.layer} - */ - //////////////////////////////////////////////////////////////////////////// - this.baseLayer = function (baseLayer) { - var save; - if (baseLayer !== undefined) { - - // The GCS of the layer must match the map - if (m_gcs !== baseLayer.gcs()) { - m_this.gcs(baseLayer.gcs()); - } - - m_baseLayer = baseLayer; + this._getCoordinates = function (spec) { + var geometry = spec.geometry || {}, + coordinates = geometry.coordinates || [], elv; - // Set the layer as the reference layer - m_baseLayer.referenceLayer(true); + if ((coordinates.length === 2 || coordinates.length === 3) && + (isFinite(coordinates[0]) && isFinite(coordinates[1]))) { - if (arg.center) { - // This assumes that the base layer is initially centered at - // (0, 0). May want to add an explicit call to the base layer - // to set a given center. - m_this.center(arg.center, true); + // Do we have a elevation component + if (isFinite(coordinates[2])) { + elv = coordinates[2]; } - save = m_zoom; - m_zoom = null; - m_this.zoom(save); - - m_this._updateBounds(); - // This forces the map into a state with valid bounds - // when clamping is on. The original call to center - // is forced to initialize the camera position in the - // base layer so no adjustment is done there. - m_this.pan({x: 0, y: 0}); - return m_this; + // special handling for single point coordinates + return [{x: coordinates[0], y: coordinates[1], z: elv}]; } - return m_baseLayer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Manually force to render map - */ - //////////////////////////////////////////////////////////////////////////// - this.draw = function () { - var i, layers = m_this.children(); - - m_this.geoTrigger(geo.event.draw, { - type: geo.event.draw, - target: m_this - } - ); - - m_this._update(); - for (i = 0; i < layers.length; i += 1) { - layers[i].draw(); + // need better handling here, but we can plot simple polygons + // by taking just the outer linearring + if (Array.isArray(coordinates[0][0])) { + coordinates = coordinates[0]; } - m_this.geoTrigger(geo.event.drawEnd, { - type: geo.event.drawEnd, - target: m_this - } - ); - - return m_this; + // return an array of points for LineString, MultiPoint, etc... + return coordinates.map(function (c) { + return { + x: c[0], + y: c[1], + z: c[2] + }; + }); }; - //////////////////////////////////////////////////////////////////////////// - /** - * Attach a file reader to a layer in the map to be used as a drop target. - */ - //////////////////////////////////////////////////////////////////////////// - this.fileReader = function (readerType, opts) { - var layer, renderer; - opts = opts || {}; - if (!readerType) { - return m_fileReader; - } - layer = opts.layer; - if (!layer) { - renderer = opts.renderer; - if (!renderer) { - renderer = 'd3'; - } - layer = m_this.createLayer('feature', {renderer: renderer}); - } - opts.layer = layer; - opts.renderer = renderer; - m_fileReader = geo.createFileReader(readerType, opts); - return m_this; + this._getStyle = function (spec) { + return spec.properties; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize the map - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - var i; + this.read = function (file, done, progress) { - if (m_node === undefined || m_node === null) { - throw 'Map require DIV node'; - } + function _done(object) { + var features, allFeatures = []; - m_node.css('position', 'relative'); - if (arg !== undefined && arg.layers !== undefined) { - for (i = 0; i < arg.layers.length; i += 1) { - if (i === 0) { - m_this.baseLayer(arg.layers[i]); + features = m_this._featureArray(object); + + features.forEach(function (feature) { + var type = m_this._featureType(feature), + coordinates = m_this._getCoordinates(feature), + style = m_this._getStyle(feature); + if (type) { + if (type === 'line') { + style.fill = style.fill || false; + allFeatures.push(m_this._addFeature( + type, + [coordinates], + style, + feature.properties + )); + } else if (type === 'point') { + style.stroke = style.stroke || false; + allFeatures.push(m_this._addFeature( + type, + coordinates, + style, + feature.properties + )); + } else if (type === 'polygon') { + style.fill = style.fill === undefined ? true : style.fill; + style.fillOpacity = ( + style.fillOpacity === undefined ? 0.25 : style.fillOpacity + ); + // polygons not yet supported + allFeatures.push(m_this._addFeature( + type, + [[coordinates]], //double wrap for the data method below + style, + feature.properties + )); + } else if (type === 'multipolygon') { + style.fill = style.fill === undefined ? true : style.fill; + style.fillOpacity = ( + style.fillOpacity === undefined ? 0.25 : style.fillOpacity + ); + coordinates = feature.geometry.coordinates.map(function (c) { + return [m_this._getCoordinates({ + geometry: { + type: 'Polygon', + coordinates: c + } + })]; + }); + allFeatures.push(m_this._addFeature( + 'polygon', //there is no multipolygon feature class + coordinates, + style, + feature.properties + )); + } + } else { + console.log('unsupported feature type: ' + feature.geometry.type); } + }); - m_this.addLayer(arg.layers[i]); + if (done) { + done(allFeatures); } } - return m_this; + + m_this._readObject(file, _done, progress); }; + //////////////////////////////////////////////////////////////////////////// /** - * Update map - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function (request) { - var i, layers = m_this.children(); - for (i = 0; i < layers.length; i += 1) { - layers[i]._update(request); - } - return m_this; + * Build the data array for a feature given the coordinates and properties + * from the geojson. + * + * @private + * @param {Object[]} coordinates Coordinate data array + * @param {Object} properties Geojson properties object + * @param {Object} style Global style defaults + * @returns {Object[]} + */ + ////////////////////////////////////////////////////////////////////////////// + this._buildData = function (coordinates, properties, style) { + return coordinates.map(function (coord) { + return { + coordinates: coord, + properties: properties, + style: style + }; + }); }; - //////////////////////////////////////////////////////////////////////////// - /** - * Exit this map - */ - //////////////////////////////////////////////////////////////////////////// - this.exit = function () { - var i, layers = m_this.children(); - for (i = 0; i < layers.length; i += 1) { - layers[i]._exit(); - } - if (m_this.interactor()) { - m_this.interactor().destroy(); - m_this.interactor(null); + this._addFeature = function (type, coordinates, style, properties) { + var _style = $.extend({}, m_style, style); + var feature = m_this.layer().createFeature(type) + .data(m_this._buildData(coordinates, properties, style)) + .style(_style); + + if (type === 'line') { + feature.line(function (d) { return d.coordinates; }); + } else if (type === 'polygon') { + feature.position(function (d) { + return { + x: d.x, + y: d.y, + z: d.z + }; + }).polygon(function (d) { + return { + 'outer': d.coordinates[0], + 'inner': d.coordinates[1] + }; + }); + } else { + feature.position(function (d) { + return d.coordinates; + }); } - m_this.node().off('.geo'); - $(window).off('resize', resizeSelf); - s_exit(); + return feature; }; - this._init(arg); - - // set up drag/drop handling - this.node().on('dragover.geo', function (e) { - var evt = e.originalEvent; +}; - if (m_this.fileReader()) { - evt.stopPropagation(); - evt.preventDefault(); - evt.dataTransfer.dropEffect = 'copy'; - } - }) - .on('drop.geo', function (e) { - var evt = e.originalEvent, reader = m_this.fileReader(), - i, file; +inherit(geo.jsonReader, geo.fileReader); - function done() { - m_this.draw(); - } +geo.registerFileReader('jsonReader', geo.jsonReader); - if (reader) { - evt.stopPropagation(); - evt.preventDefault(); +////////////////////////////////////////////////////////////////////////////// +/** + * Creates a new map object + * + * Map coordinates for default world map, where c = half circumference at + * equator in meters, o = origin: + * (-c, c) + o (c, c) + o + * (center.x, center.y) + o <-- center of viewport + * (-c, -c) + o (c, -c) + o + * + * @class + * @extends geo.sceneObject + * + * *** Always required *** + * @param {string} node DOM selector for the map container + * + * *** Required when using a domain/CS different from OSM *** + * @param {string|geo.transform} [gcs='EPSG:3857'] + * The main coordinate system of the map + * @param {number} [maxZoom=16] The maximum zoom level + * @param {string|geo.transform} [ingcs='EPSG:4326'] + * The default coordinate system of interface calls. + * @param {number} [unitsPerPixel=156543] GCS to pixel unit scaling at zoom 0 + * (i.e. meters per pixel or degrees per pixel). + * @param {object?} maxBounds The maximum visable map bounds + * @param {number} [maxBounds.left=-20037508] The left bound + * @param {number} [maxBounds.right=20037508] The right bound + * @param {number} [maxBounds.bottom=-20037508] The bottom bound + * @param {number} [maxBounds.top=20037508] The top bound + * + * *** Initial view *** + * @param {number} [zoom=4] Initial zoom + * @param {object?} center Map center + * @param {number} [center.x=0] + * @param {number} [center.y=0] + * @param {number?} width The map width (default node width) + * @param {number?} height The map height (default node height) + * + * *** Navigation *** + * @param {number} [min=0] Minimum zoom level (though fitting to the viewport + * may make it so this is smaller than the smallest possible value) + * @param {number} [max=16] Maximum zoom level + * @param {boolean} [discreteZoom=false] True to only allow integer zoom + * levels. False for any zoom level. + * + * *** Advanced parameters *** + * @param {geo.camera?} camera The camera to control the view + * @param {geo.mapInteractor?} interactor The UI event handler + * @param {geo.clock?} clock The clock used to synchronize time events + * @param {boolean} [autoResize=true] Adjust map size on window resize + * @param {boolean} [clampBoundsX=false] Prevent panning outside of the + * maximum bounds in the horizontal direction. + * @param {boolean} [clampBoundsY=true] Prevent panning outside of the + * maximum bounds in the vertical direction. + * @param {boolean} [clampZoom=true] Prevent zooming out so that the map area + * is smaller than the window. + * + * @returns {geo.map} + */ +////////////////////////////////////////////////////////////////////////////// +geo.map = function (arg) { + 'use strict'; + if (!(this instanceof geo.map)) { + return new geo.map(arg); + } + arg = arg || {}; + geo.sceneObject.call(this, arg); - for (i = 0; i < evt.dataTransfer.files.length; i += 1) { - file = evt.dataTransfer.files[i]; - if (reader.canRead(file)) { - reader.read(file, done); // to do: trigger event on done - } - } - } - }); + //////////////////////////////////////////////////////////////////////////// + /** + * Private member variables + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_exit = this._exit, + // See https://en.wikipedia.org/wiki/Web_Mercator + // phiMax = 180 / Math.PI * (2 * Math.atan(Math.exp(Math.PI)) - Math.PI / 2), + m_x = 0, + m_y = 0, + m_node = $(arg.node), + m_width = arg.width || m_node.width() || 512, + m_height = arg.height || m_node.height() || 512, + m_gcs = arg.gcs === undefined ? 'EPSG:3857' : arg.gcs, + m_ingcs = arg.ingcs === undefined ? 'EPSG:4326' : arg.ingcs, + m_center = {x: 0, y: 0}, + m_zoom = arg.zoom === undefined ? 4 : arg.zoom, + m_fileReader = null, + m_interactor = null, + m_validZoomRange = {min: 0, max: 16, origMin: 0}, + m_transition = null, + m_queuedTransition = null, + m_clock = null, + m_discreteZoom = arg.discreteZoom ? true : false, + m_maxBounds = arg.maxBounds || {}, + m_camera = arg.camera || geo.camera(), + m_unitsPerPixel, + m_clampBoundsX, + m_clampBoundsY, + m_clampZoom, + m_origin, + m_scale = {x: 1, y: 1, z: 1}; // constant for the moment + + /* Compute the maximum bounds on our map projection. By default, x ranges + * from [-180, 180] in the interface projection, and y matches the x range in + * the map (not the interface) projection. For images, this might be + * [0, width] and [0, height] instead. */ + m_maxBounds.left = geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: m_maxBounds.left !== undefined ? m_maxBounds.left : -180, y: 0}])[0].x; + m_maxBounds.right = geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: m_maxBounds.right !== undefined ? m_maxBounds.right : 180, y: 0}])[0].x; + m_maxBounds.top = (m_maxBounds.top !== undefined ? + geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: 0, y: m_maxBounds.top}])[0].y : m_maxBounds.right); + m_maxBounds.bottom = (m_maxBounds.bottom !== undefined ? + geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: 0, y: m_maxBounds.bottom}])[0].y : m_maxBounds.left); + m_unitsPerPixel = (arg.unitsPerPixel || ( + m_maxBounds.right - m_maxBounds.left) / 256); + + m_camera.viewport = {width: m_width, height: m_height}; + arg.center = geo.util.normalizeCoordinates(arg.center); + arg.autoResize = arg.autoResize === undefined ? true : arg.autoResize; + m_clampBoundsX = arg.clampBoundsX === undefined ? false : arg.clampBoundsX; + m_clampBoundsY = arg.clampBoundsY === undefined ? true : arg.clampBoundsY; + m_clampZoom = arg.clampZoom === undefined ? true : arg.clampZoom; //////////////////////////////////////////////////////////////////////////// /** - * Get or set the map interactor + * Get/set the number of world space units per display pixel at the given + * zoom level. + * + * @param {Number} [zoom=0] The target zoom level + * @param {Number?} unit If present, set the unitsPerPixel otherwise return + * the current value. + * @returns {Number|this} */ //////////////////////////////////////////////////////////////////////////// - this.interactor = function (arg) { - if (arg === undefined) { - return m_interactor; - } - m_interactor = arg; + this.unitsPerPixel = function (zoom, unit) { + zoom = zoom || 0; + if (unit) { + // get the units at level 0 + m_unitsPerPixel = Math.pow(2, zoom) * unit; - // this makes it possible to set a null interactor - // i.e. map.interactor(null); - if (m_interactor) { - m_interactor.map(m_this); + // redraw all the things + m_this.draw(); + return m_this; } - return m_this; + return Math.pow(2, -zoom) * m_unitsPerPixel; }; //////////////////////////////////////////////////////////////////////////// /** - * Get or set the map clock + * Get the map's world coordinate origin in gcs coordinates + * + * @returns {object} */ //////////////////////////////////////////////////////////////////////////// - this.clock = function (arg) { - if (arg === undefined) { - return m_clock; - } - m_clock = arg; - - if (m_clock) { - m_clock.object(m_this); - } - return m_this; + this.origin = function () { + return $.extend({}, m_origin); }; //////////////////////////////////////////////////////////////////////////// /** - * Get or set the min/max zoom range. + * Get the map's world coordinate scaling relative gcs units * - * @param {Object} arg {min: minimumzoom, max: maximumzom} - * @returns {Object|geo.map} + * @returns {object} */ //////////////////////////////////////////////////////////////////////////// - this.zoomRange = function (arg) { - if (arg === undefined) { - return $.extend({}, m_validZoomRange); - } - m_validZoomRange.min = arg.min; - m_validZoomRange.max = arg.max; - return m_this; + this.scale = function () { + return $.extend({}, m_scale); }; //////////////////////////////////////////////////////////////////////////// /** - * Start an animated zoom/pan. - * - * Options: - *
-   *   opts = {
-   *     center: { x: ... , y: ... } // the new center
-   *     zoom: ... // the new zoom level
-   *     duration: ... // the duration (in ms) of the transition
-   *     ease: ... // an easing function [0, 1] -> [0, 1]
-   *   }
-   * 
+ * Get the camera * - * Call with no arguments to return the current transition information. - * - * @param {object?} opts - * @returns {geo.map} + * @returns {geo.camera} */ //////////////////////////////////////////////////////////////////////////// - this.transition = function (opts) { - - if (opts === undefined) { - return m_transition; - } - - if (m_transition) { - m_queuedTransition = opts; - return m_this; - } - - function interp1(p0, p1, t) { - return p0 + (p1 - p0) * t; - } - function defaultInterp(p0, p1) { - return function (t) { - return [ - interp1(p0[0], p1[0], t), - interp1(p0[1], p1[1], t), - interp1(p0[2], p1[2], t) - ]; - }; - } - - // Transform zoom level into z-coordinate and inverse - function zoom2z(z) { - return vgl.zoomToHeight(z + 1, m_width, m_height); - } - function z2zoom(z) { - return vgl.heightToZoom(z, m_width, m_height) - 1; - } - - var defaultOpts = { - center: m_this.center(), - zoom: m_this.zoom(), - duration: 1000, - ease: function (t) { - return t; - }, - interp: defaultInterp, - done: null, - zCoord: true - }; - - if (opts.center) { - opts.center = geo.util.normalizeCoordinates(opts.center); - } - $.extend(defaultOpts, opts); - - m_transition = { - start: { - center: m_this.center(), - zoom: m_this.zoom() - }, - end: { - center: defaultOpts.center, - zoom: m_discreteZoom ? Math.round(defaultOpts.zoom) : defaultOpts.zoom - }, - ease: defaultOpts.ease, - zCoord: defaultOpts.zCoord, - done: defaultOpts.done, - duration: defaultOpts.duration - }; - - if (defaultOpts.zCoord) { - m_transition.interp = defaultOpts.interp( - [ - m_transition.start.center.x, - m_transition.start.center.y, - zoom2z(m_transition.start.zoom) - ], - [ - m_transition.end.center.x, - m_transition.end.center.y, - zoom2z(m_transition.end.zoom) - ] - ); - } else { - m_transition.interp = defaultOpts.interp( - [ - m_transition.start.center.x, - m_transition.start.center.y, - m_transition.start.zoom - ], - [ - m_transition.end.center.x, - m_transition.end.center.y, - m_transition.end.zoom - ] - ); - } - - function anim(time) { - var done = m_transition.done, next; - next = m_queuedTransition; - - if (!m_transition.start.time) { - m_transition.start.time = time; - m_transition.end.time = time + defaultOpts.duration; - } - m_transition.time = time - m_transition.start.time; - if (time >= m_transition.end.time || next) { - if (!next) { - m_this.center(m_transition.end.center); - m_this.zoom(m_transition.end.zoom); - } - - m_transition = null; - - m_this.geoTrigger(geo.event.transitionend, defaultOpts); - - if (done) { - done(); - } - - if (next) { - m_queuedTransition = null; - m_this.transition(next); - } - - return; - } - - var z = m_transition.ease( - (time - m_transition.start.time) / defaultOpts.duration - ); - - var p = m_transition.interp(z); - if (m_transition.zCoord) { - p[2] = z2zoom(p[2]); - } - m_this.center({ - x: p[0], - y: p[1] - }); - m_this.zoom(p[2], undefined, true); - - window.requestAnimationFrame(anim); - } - - m_this.geoTrigger(geo.event.transitionstart, defaultOpts); + this.camera = function () { + return m_camera; + }; - if (defaultOpts.cancelNavigation) { - m_this.geoTrigger(geo.event.transitionend, defaultOpts); - return m_this; - } else if (defaultOpts.cancelAnimation) { - // run the navigation synchronously - defaultOpts.duration = 0; - anim(0); - } else { - window.requestAnimationFrame(anim); + //////////////////////////////////////////////////////////////////////////// + /** + * Get map gcs + * + * @returns {string} + */ + //////////////////////////////////////////////////////////////////////////// + this.gcs = function (arg) { + if (arg === undefined) { + return m_gcs; } + m_gcs = arg; return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Update the internally cached map bounds. - * @private + * Get map interface gcs + * + * @returns {string} */ //////////////////////////////////////////////////////////////////////////// - this._updateBounds = function () { - m_bounds.lowerLeft = m_this.displayToGcs({ - x: 0, - y: m_height - }); - m_bounds.lowerRight = m_this.displayToGcs({ - x: m_width, - y: m_height - }); - m_bounds.upperLeft = m_this.displayToGcs({ - x: 0, - y: 0 - }); - m_bounds.upperRight = m_this.displayToGcs({ - x: m_width, - y: 0 - }); + this.ingcs = function (arg) { + if (arg === undefined) { + return m_ingcs; + } + m_ingcs = arg; + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/set the locations of the current map corners as latitudes/longitudes. - * When provided the argument should be an object containing the keys - * lowerLeft and upperRight declaring the desired new map bounds. The - * new bounds will contain at least the min/max lat/lngs provided. In any - * case, the actual new bounds will be returned by this function. + * Get root node of the map * - * @param {geo.geoBounds} [bds] The requested map bounds - * @return {geo.geoBounds} The actual new map bounds + * @returns {object} */ //////////////////////////////////////////////////////////////////////////// - this.bounds = function (bds) { - var nav; - - if (bds === undefined) { - return m_bounds; - } - - nav = m_this.zoomAndCenterFromBounds(bds); - m_this.zoom(nav.zoom); - m_this.center(nav.center); - return m_bounds; + this.node = function () { + return m_node; }; //////////////////////////////////////////////////////////////////////////// /** - * Get the center zoom level necessary to display the given lat/lon bounds. + * Get/Set zoom level of the map * - * @param {geo.geoBounds} [bds] The requested map bounds - * @return {object} Object containing keys 'center' and 'zoom' + * @returns {Number|geo.map} */ //////////////////////////////////////////////////////////////////////////// - this.zoomAndCenterFromBounds = function (bds) { - var ll, ur, dx, dy, zx, zy, center, zoom; + this.zoom = function (val, origin, ignoreDiscreteZoom) { + var evt, oldZoom, bounds; + if (val === undefined) { + return m_zoom; + } - // Caveat: - // Much of the following is invalid for alternative map projections. These - // computations should really be defered to the base layer, but there is - // no clear path for doing that with the current base layer api. + oldZoom = m_zoom; + /* The ignoreDiscreteZoom flag is intended to allow non-integer zoom values + * during animation. */ + val = fix_zoom(val, ignoreDiscreteZoom); + if (val === m_zoom) { + return m_this; + } - // extract bounds info and check for validity - ll = geo.util.normalizeCoordinates(bds.lowerLeft || {}); - ur = geo.util.normalizeCoordinates(bds.upperRight || {}); + m_zoom = val; - if (ll.x >= ur.x || ll.y >= ur.y) { - throw new Error('Invalid bounds provided'); - } + bounds = m_this.boundsFromZoomAndCenter(val, m_center, null); + m_this.modified(); - center = { - x: (ll.x + ur.x) / 2, - y: (ll.y + ur.y) / 2 + camera_bounds(bounds); + evt = { + geo: {}, + zoomLevel: m_zoom, + screenPosition: origin ? origin.map : undefined, + eventType: geo.event.zoom }; + m_this.geoTrigger(geo.event.zoom, evt); - // calculate the current extend - dx = m_bounds.upperRight.x - m_bounds.lowerLeft.x; - dy = m_bounds.upperRight.y - m_bounds.lowerLeft.y; - - // calculate the zoom levels necessary to fit x and y bounds - zx = m_zoom - Math.log2((ur.x - ll.x) / dx); - zy = m_zoom - Math.log2((ur.y - ll.y) / dy); - zoom = Math.min(zx, zy); - if (m_discreteZoom) { - zoom = Math.floor(zoom); + if (origin && origin.geo && origin.map) { + var shifted = m_this.gcsToDisplay(origin.geo); + m_this.pan({x: origin.map.x - shifted.x, y: origin.map.y - shifted.y}); + } else { + m_this.pan({x: 0, y: 0}); } + return m_this; + }; - return { - zoom: zoom, - center: center + //////////////////////////////////////////////////////////////////////////// + /** + * Pan the map by (x: dx, y: dy) pixels. + * + * @param {Object} delta + * @returns {geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.pan = function (delta) { + var evt, unit; + evt = { + geo: {}, + screenDelta: delta, + eventType: geo.event.pan }; + + unit = m_this.unitsPerPixel(m_zoom); + + m_camera.pan({ + x: delta.x * unit, + y: -delta.y * unit + }); + /* If m_clampBounds* is true, clamp the pan */ + var bounds = fix_bounds(m_camera.bounds); + if (bounds !== m_camera.bounds) { + var panPos = m_this.gcsToDisplay({ + x: m_camera.bounds.left, y: m_camera.bounds.top}, null); + camera_bounds(bounds); + var clampPos = m_this.gcsToDisplay({ + x: m_camera.bounds.left, y: m_camera.bounds.top}, null); + evt.screenDelta.x += clampPos.x - panPos.x; + evt.screenDelta.y += clampPos.y - panPos.y; + } + + m_center = m_camera.displayToWorld({ + x: m_width / 2, + y: m_height / 2 + }); + + m_this.geoTrigger(geo.event.pan, evt); + + m_this.modified(); + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/set the discrete zoom flag. + * Set center of the map to the given geographic coordinates, or get the + * current center. Uses bare objects {x: 0, y: 0}. * - * @param {bool} If specified, the discrete zoom flag. - * @return {bool} The current discrete zoom flag if no parameter is - * specified, otherwise the map object. + * @param {Object} coordinates + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. If setting the + * center, they are converted from this gcs to the map projection. The + * returned center are converted from the map projection to this gcs. + * @returns {Object|geo.map} */ //////////////////////////////////////////////////////////////////////////// - this.discreteZoom = function (discreteZoom) { - if (discreteZoom === undefined) { - return m_discreteZoom; + this.center = function (coordinates, gcs) { + var center; + if (coordinates === undefined) { + center = $.extend({}, m_this.worldToGcs(m_center, gcs)); + return center; } - discreteZoom = discreteZoom ? true : false; - if (m_discreteZoom !== discreteZoom) { - m_discreteZoom = discreteZoom; - if (m_discreteZoom) { - m_this.zoom(Math.round(m_this.zoom())); + + // get the screen coordinates of the new center + m_center = $.extend({}, m_this.gcsToWorld(coordinates, gcs)); + + camera_bounds(m_this.boundsFromZoomAndCenter(m_zoom, m_center, null)); + // trigger a pan event + m_this.geoTrigger( + geo.event.pan, + { + geo: coordinates, + screenDelta: null, + eventType: geo.event.pan } - } + ); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Update the attribution notice displayed on the bottom right corner of - * the map. The content of this notice is managed by individual layers. - * This method queries all of the visible layers and joins the individual - * attribution notices into a single element. By default, this method - * is called on each of the following events: - * - * * geo.event.layerAdd - * * geo.event.layerRemove + * Add layer to the map * - * In addition, layers should call this method when their own attribution - * notices has changed. Users, in general, should not need to call this. - * @returns {this} Chainable + * @param {geo.layer} layer to be added to the map + * @return {geom.map} */ //////////////////////////////////////////////////////////////////////////// - this.updateAttribution = function () { - // clear any existing attribution content - m_this.node().find('.geo-attribution').remove(); + this.createLayer = function (layerName, arg) { + arg = arg || {}; + var newLayer = geo.createLayer( + layerName, m_this, arg); - // generate a new attribution node - var $a = $('
') - .addClass('geo-attribution') - .css({ - position: 'absolute', - right: '0px', - bottom: '0px', - 'padding-right': '5px', - cursor: 'auto', - font: '11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif', - 'z-index': '1001', - background: 'rgba(255,255,255,0.7)', - clear: 'both', - display: 'block', - 'pointer-events': 'auto' - }).on('mousedown', function (evt) { - evt.stopPropagation(); - }); + if (newLayer) { - // append content from each layer - m_this.children().forEach(function (layer) { - var content = layer.attribution(); - if (content) { - $('') - .addClass('geo-attribution-layer') - .css({ - 'padding-left': '5px' - }) - .html(content) - .appendTo($a); - } - }); + m_this.addChild(newLayer); + newLayer._update(); + m_this.modified(); - $a.appendTo(m_this.node()); - return m_this; + m_this.geoTrigger(geo.event.layerAdd, { + type: geo.event.layerAdd, + target: m_this, + layer: newLayer + }); + } + + return newLayer; }; - this.interactor(arg.interactor || geo.mapInteractor()); - this.clock(arg.clock || geo.clock()); + //////////////////////////////////////////////////////////////////////////// + /** + * Remove layer from the map + * + * @param {geo.layer} layer that should be removed from the map + * @return {geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.deleteLayer = function (layer) { - function resizeSelf() { - m_this.resize(0, 0, m_node.width(), m_node.height()); - } + if (layer !== null && layer !== undefined) { + layer._exit(); - if (arg.autoResize) { - $(window).resize(resizeSelf); - } + m_this.removeChild(layer); - // attach attribution updates to layer events - m_this.geoOn([ - geo.event.layerAdd, - geo.event.layerRemove - ], m_this.updateAttribution); + m_this.modified(); - return this; -}; + m_this.geoTrigger(geo.event.layerRemove, { + type: geo.event.layerRemove, + target: m_this, + layer: layer + }); + } -/** - * General object specification for map types. Any additional - * values in the object are passed to the map constructor. - * @typedef geo.map.spec - * @type {object} - * @property {object[]} [data=[]] The default data array to - * apply to each feature if none exists - * @property {geo.layer.spec[]} [layers=[]] Layers to create - */ + /// Return deleted layer (similar to createLayer) as in the future + /// we may provide extension of this method to support deletion of + /// layer using id or some sort. + return layer; + }; -/** - * Create a map from an object. Any errors in the creation - * of the map will result in returning null. - * @param {geo.map.spec} spec The object specification - * @returns {geo.map|null} - */ -geo.map.create = function (spec) { - 'use strict'; + //////////////////////////////////////////////////////////////////////////// + /** + * Toggle visibility of a layer + * + * @param {geo.layer} layer + * @returns {Boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.toggle = function (layer) { + if (layer !== null && layer !== undefined) { + layer.visible(!layer.visible()); + m_this.modified(); - var map = geo.map(spec); + m_this.geoTrigger(geo.event.layerToggle, { + type: geo.event.layerToggle, + target: m_this, + layer: layer + }); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the size of the map. + * + * @param {Object?} arg + * @param {Number} arg.width width in pixels + * @param {Number} arg.height height in pixels + * @returns {Object} An object containing width and height as keys + */ + //////////////////////////////////////////////////////////////////////////// + this.size = function (arg) { + if (arg === undefined) { + return { + width: m_width, + height: m_height + }; + } + m_this.resize(0, 0, arg.width, arg.height); + return m_this; + }; - if (!map) { - console.warn('Could not create map.'); - return null; - } + //////////////////////////////////////////////////////////////////////////// + /** + * Resize map (deprecated) + * + * @param {Number} x x-offset in display space + * @param {Number} y y-offset in display space + * @param {Number} w width in display space + * @param {Number} h height in display space + */ + //////////////////////////////////////////////////////////////////////////// + this.resize = function (x, y, w, h) { - spec.data = spec.data || []; - spec.layers = spec.layers || []; + // store the original center and restore it after the resize + var oldCenter = m_this.center(); + m_x = x; + m_y = y; + m_width = w; + m_height = h; - spec.layers.forEach(function (l) { - l.data = l.data || spec.data; - l.layer = geo.layer.create(map, l); - }); + m_this.camera().viewport = {width: w, height: h}; - return map; -}; + reset_minimum_zoom(); + var newZoom = fix_zoom(m_zoom); + if (newZoom !== m_zoom) { + m_this.zoom(newZoom); + } -inherit(geo.map, geo.sceneObject); + m_this.geoTrigger(geo.event.resize, { + type: geo.event.resize, + target: m_this, + x: m_x, + y: m_y, + width: w, + height: h + }); -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class feature - * - * @class - * @extends geo.sceneObject - * @returns {geo.feature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.feature = function (arg) { - "use strict"; - if (!(this instanceof geo.feature)) { - return new geo.feature(arg); - } - geo.sceneObject.call(this); + m_this.center(oldCenter); + m_this.modified(); + return m_this; + }; //////////////////////////////////////////////////////////////////////////// /** - * @private + * Convert from gcs coordinates to map world coordinates. + * @param {object} c The input coordinate to convert + * @param {object} c.x + * @param {object} c.y + * @param {object} [c.z=0] + * @param {string?} gcs The gcs of the input (map.gcs() by default) + * @return {object} World space coordinates */ //////////////////////////////////////////////////////////////////////////// - arg = arg || {}; - - var m_this = this, - s_exit = this._exit, - m_selectionAPI = arg.selectionAPI === undefined ? false : arg.selectionAPI, - m_style = {}, - m_layer = arg.layer === undefined ? null : arg.layer, - m_gcs = arg.gcs === undefined ? "EPSG:4326" : arg.gcs, - m_visible = arg.visible === undefined ? true : arg.visible, - m_bin = arg.bin === undefined ? 0 : arg.bin, - m_renderer = arg.renderer === undefined ? null : arg.renderer, - m_dataTime = geo.timestamp(), - m_buildTime = geo.timestamp(), - m_updateTime = geo.timestamp(), - m_selectedFeatures = []; + this.gcsToWorld = function (c, gcs) { + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (gcs !== m_gcs) { + c = geo.transform.transformCoordinates(gcs, m_gcs, [c])[0]; + } + return geo.transform.affineForward( + {origin: m_origin}, + [c] + )[0]; + }; //////////////////////////////////////////////////////////////////////////// /** - * Private method to bind mouse handlers on the map element. + * Convert from map world coordinates to gcs coordinates. + * @param {object} c The input coordinate to convert + * @param {object} c.x + * @param {object} c.y + * @param {object} [c.z=0] + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @return {object} GCS space coordinates */ //////////////////////////////////////////////////////////////////////////// - this._bindMouseHandlers = function () { - - // Don't bind handlers for improved performance on features that don't - // require it. - if (!m_selectionAPI) { - return; + this.worldToGcs = function (c, gcs) { + c = geo.transform.affineInverse( + {origin: m_origin}, + [c] + )[0]; + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (gcs !== m_gcs) { + c = geo.transform.transformCoordinates(m_gcs, gcs, [c])[0]; } + return c; + }; - // First unbind to be sure that the handlers aren't bound twice. - m_this._unbindMouseHandlers(); - - m_this.geoOn(geo.event.mousemove, m_this._handleMousemove); - m_this.geoOn(geo.event.mouseclick, m_this._handleMouseclick); - m_this.geoOn(geo.event.brushend, m_this._handleBrushend); - m_this.geoOn(geo.event.brush, m_this._handleBrush); + //////////////////////////////////////////////////////////////////////////// + /** + * Convert from gcs coordinates to display coordinates. + * + * gcsToWorld | worldToDisplay + * + * @param {object} c The input coordinate to convert + * @param {object} c.x + * @param {object} c.y + * @param {object} [c.z=0] + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @return {object} Display space coordinates + */ + //////////////////////////////////////////////////////////////////////////// + this.gcsToDisplay = function (c, gcs) { + c = m_this.gcsToWorld(c, gcs); + return m_this.worldToDisplay(c); }; //////////////////////////////////////////////////////////////////////////// /** - * Private method to unbind mouse handlers on the map element. + * Convert from world coordinates to display coordinates using the attached + * camera. + * @param {object} c The input coordinate to convert + * @param {object} c.x + * @param {object} c.y + * @param {object} [c.z=0] + * @return {object} Display space coordinates */ //////////////////////////////////////////////////////////////////////////// - this._unbindMouseHandlers = function () { - m_this.geoOff(geo.event.mousemove, m_this._handleMousemove); - m_this.geoOff(geo.event.mouseclick, m_this._handleMouseclick); - m_this.geoOff(geo.event.brushend, m_this._handleBrushend); - m_this.geoOff(geo.event.brush, m_this._handleBrush); + this.worldToDisplay = function (c) { + return m_camera.worldToDisplay(c); }; //////////////////////////////////////////////////////////////////////////// /** - * For binding mouse events, use functions with - * the following call signatures: - * - * function handler(arg) { - * // arg.data - the data object of the feature - * // arg.index - the index inside the data array of the featue - * // arg.mouse - mouse information object (see src/core/mapInteractor.js) - * } + * Convert from display to gcs coordinates * - * i.e. + * displayToWorld | worldToGcs * - * feature.geoOn(geo.event.feature.mousemove, function (arg) { - * // do something with the feature marker. - * }); + * @param {object} c The input display coordinate to convert + * @param {object} c.x + * @param {object} c.y + * @param {object} [c.z=0] + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @return {object} GCS space coordinates */ //////////////////////////////////////////////////////////////////////////// + this.displayToGcs = function (c, gcs) { + c = m_this.displayToWorld(c); // done via camera + return m_this.worldToGcs(c, gcs); + }; //////////////////////////////////////////////////////////////////////////// /** - * Search for features containing the given point. - * - * Returns an object: :: - * - * { - * data: [...] // an array of data objects for matching features - * index: [...] // an array of indices of the matching features - * } - * - * @argument {Object} coordinate - * @returns {Object} + * Convert from display coordinates to world coordinates using the attached + * camera. + * @param {object} c The input coordinate to convert + * @param {object} c.x + * @param {object} c.y + * @param {object} [c.z=0] + * @return {object} World space coordinates */ //////////////////////////////////////////////////////////////////////////// - this.pointSearch = function () { - // base class method does nothing - return { - index: [], - found: [] - }; + this.displayToWorld = function (c) { + return m_camera.displayToWorld(c); }; //////////////////////////////////////////////////////////////////////////// /** - * Private mousemove handler + * Manually force to render map */ //////////////////////////////////////////////////////////////////////////// - this._handleMousemove = function () { - var mouse = m_this.layer().map().interactor().mouse(), - data = m_this.data(), - over = m_this.pointSearch(mouse.geo), - newFeatures = [], oldFeatures = [], lastTop = -1, top = -1; + this.draw = function () { + var i, layers = m_this.children(); - // Get the index of the element that was previously on top - if (m_selectedFeatures.length) { - lastTop = m_selectedFeatures[m_selectedFeatures.length - 1]; + m_this.geoTrigger(geo.event.draw, { + type: geo.event.draw, + target: m_this + } + ); + + m_this._update(); + + for (i = 0; i < layers.length; i += 1) { + layers[i].draw(); } - // There are probably faster ways of doing this: - newFeatures = over.index.filter(function (i) { - return m_selectedFeatures.indexOf(i) < 0; - }); - oldFeatures = m_selectedFeatures.filter(function (i) { - return over.index.indexOf(i) < 0; - }); + m_this.geoTrigger(geo.event.drawEnd, { + type: geo.event.drawEnd, + target: m_this + } + ); - geo.feature.eventID += 1; - // Fire events for mouse in first. - newFeatures.forEach(function (i, idx) { - m_this.geoTrigger(geo.event.feature.mouseover, { - data: data[i], - index: i, - mouse: mouse, - eventID: geo.feature.eventID, - top: idx === newFeatures.length - 1 - }, true); - }); + return m_this; + }; - geo.feature.eventID += 1; - // Fire events for mouse out next - oldFeatures.forEach(function (i, idx) { - m_this.geoTrigger(geo.event.feature.mouseout, { - data: data[i], - index: i, - mouse: mouse, - eventID: geo.feature.eventID, - top: idx === oldFeatures.length - 1 - }, true); - }); + //////////////////////////////////////////////////////////////////////////// + /** + * Attach a file reader to a layer in the map to be used as a drop target. + */ + //////////////////////////////////////////////////////////////////////////// + this.fileReader = function (readerType, opts) { + var layer, renderer; + opts = opts || {}; + if (!readerType) { + return m_fileReader; + } + layer = opts.layer; + if (!layer) { + renderer = opts.renderer; + if (!renderer) { + renderer = 'd3'; + } + layer = m_this.createLayer('feature', {renderer: renderer}); + } + opts.layer = layer; + opts.renderer = renderer; + m_fileReader = geo.createFileReader(readerType, opts); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize the map + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + + if (m_node === undefined || m_node === null) { + throw 'Map require DIV node'; + } + + m_node.css('position', 'relative'); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update map + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function (request) { + var i, layers = m_this.children(); + for (i = 0; i < layers.length; i += 1) { + layers[i]._update(request); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Exit this map + */ + //////////////////////////////////////////////////////////////////////////// + this.exit = function () { + var i, layers = m_this.children(); + for (i = 0; i < layers.length; i += 1) { + layers[i]._exit(); + } + if (m_this.interactor()) { + m_this.interactor().destroy(); + m_this.interactor(null); + } + m_this.node().off('.geo'); + $(window).off('resize', resizeSelf); + s_exit(); + }; - geo.feature.eventID += 1; - // Fire events for mouse move last - over.index.forEach(function (i, idx) { - m_this.geoTrigger(geo.event.feature.mousemove, { - data: data[i], - index: i, - mouse: mouse, - eventID: geo.feature.eventID, - top: idx === over.index.length - 1 - }, true); - }); + this._init(arg); - // Replace the selected features array - m_selectedFeatures = over.index; + // set up drag/drop handling + this.node().on('dragover.geo', function (e) { + var evt = e.originalEvent; - // Get the index of the element that is now on top - if (m_selectedFeatures.length) { - top = m_selectedFeatures[m_selectedFeatures.length - 1]; + if (m_this.fileReader()) { + evt.stopPropagation(); + evt.preventDefault(); + evt.dataTransfer.dropEffect = 'copy'; + } + }) + .on('drop.geo', function (e) { + var evt = e.originalEvent, reader = m_this.fileReader(), + i, file; + + function done() { + m_this.draw(); } - if (lastTop !== top) { - // The element on top changed so we need to fire mouseon/mouseoff - if (lastTop !== -1) { - m_this.geoTrigger(geo.event.feature.mouseoff, { - data: data[lastTop], - index: lastTop, - mouse: mouse - }, true); - } + if (reader) { + evt.stopPropagation(); + evt.preventDefault(); - if (top !== -1) { - m_this.geoTrigger(geo.event.feature.mouseon, { - data: data[top], - index: top, - mouse: mouse - }, true); + for (i = 0; i < evt.dataTransfer.files.length; i += 1) { + file = evt.dataTransfer.files[i]; + if (reader.canRead(file)) { + reader.read(file, done); // to do: trigger event on done + } } } - }; + }); //////////////////////////////////////////////////////////////////////////// /** - * Private mouseclick handler + * Get or set the map interactor */ //////////////////////////////////////////////////////////////////////////// - this._handleMouseclick = function () { - var mouse = m_this.layer().map().interactor().mouse(), - data = m_this.data(), - over = m_this.pointSearch(mouse.geo); + this.interactor = function (arg) { + if (arg === undefined) { + return m_interactor; + } + m_interactor = arg; - geo.feature.eventID += 1; - over.index.forEach(function (i, idx) { - m_this.geoTrigger(geo.event.feature.mouseclick, { - data: data[i], - index: i, - mouse: mouse, - eventID: geo.feature.eventID, - top: idx === over.index.length - 1 - }, true); - }); + // this makes it possible to set a null interactor + // i.e. map.interactor(null); + if (m_interactor) { + m_interactor.map(m_this); + } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Private brush handler. + * Get or set the map clock */ //////////////////////////////////////////////////////////////////////////// - this._handleBrush = function (brush) { - var idx = m_this.boxSearch(brush.gcs.lowerLeft, brush.gcs.upperRight), - data = m_this.data(); + this.clock = function (arg) { + if (arg === undefined) { + return m_clock; + } + m_clock = arg; - geo.feature.eventID += 1; - idx.forEach(function (i, idx) { - m_this.geoTrigger(geo.event.feature.brush, { - data: data[i], - index: i, - mouse: brush.mouse, - brush: brush, - eventID: geo.feature.eventID, - top: idx === idx.length - 1 - }, true); - }); + if (m_clock) { + m_clock.object(m_this); + } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Private brushend handler. + * Get or set the min/max zoom range. + * + * @param {Object} arg {min: minimumzoom, max: maximumzom} + * @returns {Object|geo.map} */ //////////////////////////////////////////////////////////////////////////// - this._handleBrushend = function (brush) { - var idx = m_this.boxSearch(brush.gcs.lowerLeft, brush.gcs.upperRight), - data = m_this.data(); + this.zoomRange = function (arg) { + if (arg === undefined) { + return $.extend({}, m_validZoomRange); + } + if (arg.max !== undefined) { + m_validZoomRange.max = arg.max; + } - geo.feature.eventID += 1; - idx.forEach(function (i, idx) { - m_this.geoTrigger(geo.event.feature.brushend, { - data: data[i], - index: i, - mouse: brush.mouse, - brush: brush, - eventID: geo.feature.eventID, - top: idx === idx.length - 1 - }, true); - }); + // don't allow the minimum zoom to go below what will + // fit in the view port + if (arg.min !== undefined) { + m_validZoomRange.min = m_validZoomRange.origMin = fix_zoom(arg.min); + } + reset_minimum_zoom(); + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set style used by the feature + * Start an animated zoom/pan. + * + * Options: + *
+   *   opts = {
+   *     center: { x: ... , y: ... } // the new center
+   *     zoom: ... // the new zoom level
+   *     duration: ... // the duration (in ms) of the transition
+   *     ease: ... // an easing function [0, 1] -> [0, 1]
+   *   }
+   * 
+ * + * Call with no arguments to return the current transition information. + * + * @param {object?} opts + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. Applies only to the + * center coordinate of the opts and to converting zoom values to height, + * if specified. + * @returns {geo.map} */ //////////////////////////////////////////////////////////////////////////// - this.style = function (arg1, arg2) { - if (arg1 === undefined) { - return m_style; - } else if (typeof arg1 === "string" && arg2 === undefined) { - return m_style[arg1]; - } else if (arg2 === undefined) { - m_style = $.extend({}, m_style, arg1); - m_this.modified(); - return m_this; - } else { - m_style[arg1] = arg2; - m_this.modified(); + this.transition = function (opts, gcs) { + + if (opts === undefined) { + return m_transition; + } + + if (m_transition) { + m_queuedTransition = opts; return m_this; } - }; - //////////////////////////////////////////////////////////////////////////// - /** - * A uniform getter that always returns a function even for constant styles. - * Maybe extend later to support accessor-like objects. If undefined input, - * return all the styles as an object. - * - * @param {string|undefined} key - * @return {function} - */ - //////////////////////////////////////////////////////////////////////////// - this.style.get = function (key) { - var tmp, out; - if (key === undefined) { - var all = {}, k; - for (k in m_style) { - if (m_style.hasOwnProperty(k)) { - all[k] = m_this.style.get(k); - } - } - return all; + function interp1(p0, p1, t) { + return p0 + (p1 - p0) * t; } - if (key.toLowerCase().match(/color$/)) { - if (geo.util.isFunction(m_style[key])) { - tmp = geo.util.ensureFunction(m_style[key]); - out = function () { - return geo.util.convertColor( - tmp.apply(this, arguments) - ); - }; - } else { - // if the color is not a function, only convert it once - out = geo.util.ensureFunction(geo.util.convertColor(m_style[key])); + function defaultInterp(p0, p1) { + return function (t) { + return [ + interp1(p0[0], p1[0], t), + interp1(p0[1], p1[1], t), + interp1(p0[2], p1[2], t) + ]; + }; + } + + var units = m_this.unitsPerPixel(0); + + // Transform zoom level into z-coordinate and inverse + function zoom2z(z) { + return vgl.zoomToHeight(z + 1, m_width, m_height) * units; + } + function z2zoom(z) { + return vgl.heightToZoom(z / units, m_width, m_height) - 1; + } + + var defaultOpts = { + center: m_this.center(undefined, null), + zoom: m_this.zoom(), + duration: 1000, + ease: function (t) { + return t; + }, + interp: defaultInterp, + done: null, + zCoord: true + }; + + if (opts.center) { + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + opts.center = geo.util.normalizeCoordinates(opts.center); + if (gcs !== m_gcs) { + opts.center = geo.transform.transformCoordinates(gcs, m_gcs, [ + opts.center])[0]; } + } + $.extend(defaultOpts, opts); + + m_transition = { + start: { + center: m_this.center(undefined, null), + zoom: m_this.zoom() + }, + end: { + center: defaultOpts.center, + zoom: fix_zoom(defaultOpts.zoom) + }, + ease: defaultOpts.ease, + zCoord: defaultOpts.zCoord, + done: defaultOpts.done, + duration: defaultOpts.duration + }; + + if (defaultOpts.zCoord) { + m_transition.interp = defaultOpts.interp( + [ + m_transition.start.center.x, + m_transition.start.center.y, + zoom2z(m_transition.start.zoom) + ], + [ + m_transition.end.center.x, + m_transition.end.center.y, + zoom2z(m_transition.end.zoom) + ] + ); } else { - out = geo.util.ensureFunction(m_style[key]); + m_transition.interp = defaultOpts.interp( + [ + m_transition.start.center.x, + m_transition.start.center.y, + m_transition.start.zoom + ], + [ + m_transition.end.center.x, + m_transition.end.center.y, + m_transition.end.zoom + ] + ); } - return out; - }; - //////////////////////////////////////////////////////////////////////////// - /** - * Get layer referenced by the feature - */ - //////////////////////////////////////////////////////////////////////////// - this.layer = function () { - return m_layer; - }; + function anim(time) { + var done = m_transition.done, next; + next = m_queuedTransition; - //////////////////////////////////////////////////////////////////////////// - /** - * Get renderer used by the feature - */ - //////////////////////////////////////////////////////////////////////////// - this.renderer = function () { - return m_renderer; - }; + if (!m_transition.start.time) { + m_transition.start.time = time; + m_transition.end.time = time + defaultOpts.duration; + } + m_transition.time = time - m_transition.start.time; + if (time >= m_transition.end.time || next) { + if (!next) { + m_this.center(m_transition.end.center, null); + m_this.zoom(m_transition.end.zoom); + } - //////////////////////////////////////////////////////////////////////////// - /** - * Get list of drawables or nodes that are context/api specific. - */ - //////////////////////////////////////////////////////////////////////////// - this.drawables = function () { - return m_this._drawables(); - }; + m_transition = null; - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set projection of the feature - */ - //////////////////////////////////////////////////////////////////////////// - this.gcs = function (val) { - if (val === undefined) { - return m_gcs; - } else { - m_gcs = val; - m_this.modified(); - return m_this; - } - }; + m_this.geoTrigger(geo.event.transitionend, defaultOpts); - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set visibility of the feature - */ - //////////////////////////////////////////////////////////////////////////// - this.visible = function (val) { - if (val === undefined) { - return m_visible; - } else { - m_visible = val; - m_this.modified(); + if (done) { + done(); + } - // bind or unbind mouse handlers on visibility change - if (m_visible) { - m_this._bindMouseHandlers(); - } else { - m_this._unbindMouseHandlers(); + if (next) { + m_queuedTransition = null; + m_this.transition(next); + } + + return; } - return m_this; - } - }; + var z = m_transition.ease( + (time - m_transition.start.time) / defaultOpts.duration + ); - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set bin of the feature - * - * Bin number is typically used for sorting the order of rendering - */ - //////////////////////////////////////////////////////////////////////////// - this.bin = function (val) { - if (val === undefined) { - return m_bin; - } else { - m_bin = val; - m_this.modified(); - return m_this; - } - }; + var p = m_transition.interp(z); + if (m_transition.zCoord) { + p[2] = z2zoom(p[2]); + } + m_this.center({ + x: p[0], + y: p[1] + }, null); + m_this.zoom(p[2], undefined, true); - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set timestamp of data change - */ - //////////////////////////////////////////////////////////////////////////// - this.dataTime = function (val) { - if (val === undefined) { - return m_dataTime; - } else { - m_dataTime = val; - m_this.modified(); - return m_this; + window.requestAnimationFrame(anim); } - }; - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set timestamp of last time build happened - */ - //////////////////////////////////////////////////////////////////////////// - this.buildTime = function (val) { - if (val === undefined) { - return m_buildTime; - } else { - m_buildTime = val; - m_this.modified(); + m_this.geoTrigger(geo.event.transitionstart, defaultOpts); + + if (defaultOpts.cancelNavigation) { + m_this.geoTrigger(geo.event.transitionend, defaultOpts); return m_this; + } else if (defaultOpts.cancelAnimation) { + // run the navigation synchronously + defaultOpts.duration = 0; + anim(0); + } else { + window.requestAnimationFrame(anim); } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set timestamp of last time update happened + * Get/set the locations of the current map corners as latitudes/longitudes. + * When provided the argument should be an object containing the keys + * lowerLeft and upperRight declaring the desired new map bounds. The + * new bounds will contain at least the min/max lat/lngs provided. In any + * case, the actual new bounds will be returned by this function. + * + * @param {geo.geoBounds} [bds] The requested map bounds + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. If setting the + * bounds, they are converted from this gcs to the map projection. The + * returned bounds are converted from the map projection to this gcs. + * @return {geo.geoBounds} The actual new map bounds */ //////////////////////////////////////////////////////////////////////////// - this.updateTime = function (val) { - if (val === undefined) { - return m_updateTime; - } else { - m_updateTime = val; - m_this.modified(); - return m_this; + this.bounds = function (bds, gcs) { + var nav; + + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (bds !== undefined) { + if (gcs !== m_gcs) { + var trans = geo.transform.transformCoordinates(gcs, m_gcs, [{ + x: bds.left, y: bds.top}, {x: bds.right, y: bds.bottom}]); + bds = { + left: trans[0].x, + top: trans[0].y, + right: trans[1].x, + bottom: trans[1].y + }; + } + bds = fix_bounds(bds); + nav = m_this.zoomAndCenterFromBounds(bds, null); + + // This might have concequences in terms of bounds/zoom clamping. + // What behavior do we expect from this method in that case? + m_this.zoom(nav.zoom); + m_this.center(nav.center, null); } + + return m_this.boundsFromZoomAndCenter(m_zoom, m_center, gcs); }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set data + * Get the center zoom level necessary to display the given lat/lon bounds. * - * @returns {Array} + * @param {geo.geoBounds} [bds] The requested map bounds + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @return {object} Object containing keys 'center' and 'zoom' */ //////////////////////////////////////////////////////////////////////////// - this.data = function (data) { - if (data === undefined) { - return m_this.style("data") || []; - } else { - m_this.style("data", data); - m_this.dataTime().modified(); - m_this.modified(); - return m_this; + this.zoomAndCenterFromBounds = function (bounds, gcs) { + var center, zoom; + + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (gcs !== m_gcs) { + var trans = geo.transform.transformCoordinates(gcs, m_gcs, [{ + x: bounds.left, y: bounds.top}, {x: bounds.right, y: bounds.bottom}]); + bounds = { + left: trans[0].x, + top: trans[0].y, + right: trans[1].x, + bottom: trans[1].y + }; + } + if (bounds.left >= bounds.right || bounds.bottom >= bounds.top) { + throw new Error('Invalid bounds provided'); } - }; - //////////////////////////////////////////////////////////////////////////// - /** - * Query if the selection API is enabled for this feature. - * @returns {bool} - */ - //////////////////////////////////////////////////////////////////////////// - this.selectionAPI = function () { - return m_selectionAPI; + // calculate the zoom to fit the bounds + zoom = fix_zoom(calculate_zoom(bounds)); + + // clamp bounds if necessary + bounds = fix_bounds(bounds); + + /* This relies on having the map projection coordinates be uniform + * regardless of location. If not, the center will not be correct. */ + // calculate new center + center = { + x: (bounds.left + bounds.right) / 2 - m_origin.x, + y: (bounds.top + bounds.bottom) / 2 - m_origin.y + }; + + return { + zoom: zoom, + center: center + }; }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Get the bounds that will be displayed with the given zoom and center. * - * Derived class should implement this + * Note: the bounds may not have the requested zoom and center due to map + * restrictions. + * + * @param {number} zoom The requested zoom level + * @param {geo.geoPosition} center The requested center + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @return {geo.geoBounds} */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - if (!m_layer) { - throw "Feature requires a valid layer"; - } - m_style = $.extend({}, - {"opacity": 1.0}, arg.style === undefined ? {} : - arg.style); - m_this._bindMouseHandlers(); + this.boundsFromZoomAndCenter = function (zoom, center, gcs) { + var width, height, bounds, units; + + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + // preprocess the arguments + zoom = fix_zoom(zoom); + units = m_this.unitsPerPixel(zoom); + center = m_this.gcsToWorld(center, gcs); + + // get half the width and height in world coordinates + width = m_width * units / 2; + height = m_height * units / 2; + + // calculate the bounds. This is only valid if the map projection has + // uniform units in each direction. If not, then worldToGcs should be + // used. + bounds = { + left: center.x - width + m_origin.x, + right: center.x + width + m_origin.x, + bottom: center.y - height + m_origin.y, + top: center.y + height + m_origin.y + }; + + // correct the bounds when clamping is enabled + return fix_bounds(bounds); }; //////////////////////////////////////////////////////////////////////////// /** - * Build + * Get/set the discrete zoom flag. * - * Derived class should implement this + * @param {bool} If specified, the discrete zoom flag. + * @return {bool} The current discrete zoom flag if no parameter is + * specified, otherwise the map object. */ //////////////////////////////////////////////////////////////////////////// - this._build = function () { + this.discreteZoom = function (discreteZoom) { + if (discreteZoom === undefined) { + return m_discreteZoom; + } + discreteZoom = discreteZoom ? true : false; + if (m_discreteZoom !== discreteZoom) { + m_discreteZoom = discreteZoom; + if (m_discreteZoom) { + m_this.zoom(Math.round(m_this.zoom())); + } + } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get context specific drawables - * - * Derived class should implement this + * Get the layers contained in the map. + * Alias of {@linkcode geo.sceneObject.children}. */ //////////////////////////////////////////////////////////////////////////// - this._drawables = function () { - }; + this.layers = this.children; //////////////////////////////////////////////////////////////////////////// /** - * Update + * Update the attribution notice displayed on the bottom right corner of + * the map. The content of this notice is managed by individual layers. + * This method queries all of the visible layers and joins the individual + * attribution notices into a single element. By default, this method + * is called on each of the following events: * - * Derived class should implement this + * * geo.event.layerAdd + * * geo.event.layerRemove + * + * In addition, layers should call this method when their own attribution + * notices has changed. Users, in general, should not need to call this. + * @returns {this} Chainable */ //////////////////////////////////////////////////////////////////////////// - this._update = function () { + this.updateAttribution = function () { + // clear any existing attribution content + m_this.node().find('.geo-attribution').remove(); + + // generate a new attribution node + var $a = $('
') + .addClass('geo-attribution') + .css({ + position: 'absolute', + right: '0px', + bottom: '0px', + 'padding-right': '5px', + cursor: 'auto', + font: '11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif', + 'z-index': '1001', + background: 'rgba(255,255,255,0.7)', + clear: 'both', + display: 'block', + 'pointer-events': 'auto' + }).on('mousedown', function (evt) { + evt.stopPropagation(); + }); + + // append content from each layer + m_this.children().forEach(function (layer) { + var content = layer.attribution(); + if (content) { + $('') + .addClass('geo-attribution-layer') + .css({ + 'padding-left': '5px' + }) + .html(content) + .appendTo($a); + } + }); + + $a.appendTo(m_this.node()); + return m_this; }; + //////////////////////////////////////////////////////////////////////////// + // + // The following are some private methods for interacting with the camera. + // In order to hide the complexity of dealing with map aspect ratios, + // clamping behavior, reseting zoom levels on resize, etc. from the + // layers, the map handles camera movements directly. This requires + // passing all camera movement events through the map initially. The + // map uses these methods to fix up the events according to the constraints + // of the display and passes the event to the layers. + // //////////////////////////////////////////////////////////////////////////// /** - * Destroy - * - * Derived class should implement this + * Calculate the scaling factor to fit the given map bounds + * into the viewport with the correct aspect ratio. + * @param {object} bounds A desired bounds + * @return {object} Multiplicative aspect ratio correction + * @private */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this._unbindMouseHandlers(); - m_selectedFeatures = []; - m_style = {}; - arg = {}; - s_exit(); - }; - - this._init(arg); - return this; -}; - -/** - * The most recent feature event triggered. - * @type {number} - */ -geo.feature.eventID = 0; - -/** - * General object specification for feature types. - * @typedef geo.feature.spec - * @type {object} - * @property {string} type A supported feature type. - * @property {object[]} [data=[]] An array of arbitrary objects used to - * construct the feature. These objects (and their associated - * indices in the array) will be passed back to style and attribute - * accessors provided by the user. In general the number of - * "markers" drawn will be equal to the length of this array. - */ + function camera_scaling(bounds) { + var width = bounds.right - bounds.left, + height = bounds.top - bounds.bottom, + ar_bds = Math.abs(width / height), + ar_vp = m_width / m_height, + sclx, scly; -/** - * Create a feature from an object. The implementation here is - * meant to define the general interface of creating features - * from a javascript object. See documentation from individual - * feature types for specific details. In case of an error in - * the arguments this method will return null; - * @param {geo.layer} layer The layer to add the feature to - * @param {geo.feature.spec} [spec={}] The object specification - * @returns {geo.feature|null} - */ -geo.feature.create = function (layer, spec) { - "use strict"; + if (ar_bds > ar_vp) { + // fit left and right + sclx = 1; - var type = spec.type; + // grow top and bottom + scly = ar_bds / ar_vp; + } else { + // fit top and bottom + scly = 1; - // Check arguments - if (!layer instanceof geo.layer) { - console.warn("Invalid layer"); - return null; - } - if (typeof spec !== "object") { - console.warn("Invalid spec"); - return null; - } - var feature = layer.createFeature(type); - if (!feature) { - console.warn("Could not create feature type '" + type + "'"); - return null; + // grow left and right + sclx = ar_vp / ar_bds; + } + return {x: sclx, y: scly}; } - spec = spec || {}; - spec.data = spec.data || []; - return feature.style(spec); -}; - -inherit(geo.feature, geo.sceneObject); + /** + * Calculate the minimum zoom level to fit the given + * bounds inside the view port using the view port size, + * the given bounds, and the number of units per + * pixel. The method sets the valid zoom bounds as well + * as the current zoom level to be within that range. + * @private + */ + function calculate_zoom(bounds) { + // compare the aspect ratios of the viewport and bounds + var scl = camera_scaling(bounds), z; + + if (scl.y > scl.x) { + // left to right matches exactly + // center map vertically and have blank borders on the + // top and bottom (or repeat tiles) + z = -Math.log2( + Math.abs(bounds.right - bounds.left) * scl.x / + (m_width * m_unitsPerPixel) + ); + } else { + // top to bottom matches exactly, blank border on the + // left and right (or repeat tiles) + z = -Math.log2( + Math.abs(bounds.top - bounds.bottom) * scl.y / + (m_height * m_unitsPerPixel) + ); + } + return z; + } -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class pointFeature - * - * @class - * @param {object} arg Options object - * @param {boolean} arg.clustering Enable point clustering - * @extends geo.feature - * @returns {geo.pointFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.pointFeature = function (arg) { - "use strict"; - if (!(this instanceof geo.pointFeature)) { - return new geo.pointFeature(arg); + /** + * Reset the minimum zoom level given the current window size. + * @private + */ + function reset_minimum_zoom() { + if (m_clampZoom) { + m_validZoomRange.min = Math.max( + m_validZoomRange.origMin, calculate_zoom(m_maxBounds)); + } else { + m_validZoomRange.min = m_validZoomRange.origMin; + } } - arg = arg || {}; - geo.feature.call(this, arg); - //////////////////////////////////////////////////////////////////////////// /** + * Return the nearest valid zoom level to the requested zoom. * @private */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - m_rangeTree = null, - m_rangeTreeTime = geo.timestamp(), - s_data = this.data, - m_maxRadius = 0, - m_clustering = arg.clustering, - m_clusterTree = null, - m_allData = [], - m_lastZoom = null, - m_ignoreData = false; // flag to ignore data() calls made locally + function fix_zoom(zoom, ignoreDiscreteZoom) { + zoom = Math.max( + Math.min( + m_validZoomRange.max, + zoom + ), + m_validZoomRange.min + ); + if (m_discreteZoom && !ignoreDiscreteZoom) { + zoom = Math.round(zoom); + if (zoom < m_validZoomRange.min) { + zoom = Math.ceil(m_validZoomRange.min); + } + } + return zoom; + } - //////////////////////////////////////////////////////////////////////////// /** - * Get/Set clustering option - * - * @returns {geo.pointFeature|boolean} + * Return the nearest valid bounds maintaining the + * width and height. Does nothing if m_clampBounds* is + * false. + * @private */ - //////////////////////////////////////////////////////////////////////////// - this.clustering = function (val) { - if (val === undefined) { - return m_clustering; + function fix_bounds(bounds) { + var dx, dy; + if (m_clampBoundsX) { + if (bounds.right - bounds.left > m_maxBounds.right - m_maxBounds.left) { + dx = m_maxBounds.left - ((bounds.right - bounds.left - ( + m_maxBounds.right - m_maxBounds.left)) / 2) - bounds.left; + } else if (bounds.left < m_maxBounds.left) { + dx = m_maxBounds.left - bounds.left; + } else if (bounds.right > m_maxBounds.right) { + dx = m_maxBounds.right - bounds.right; + } + if (dx) { + bounds = { + left: bounds.left += dx, + right: bounds.right += dx, + top: bounds.top, + bottom: bounds.bottom + }; + } } - if (m_clustering && !val) { - // Throw out the cluster tree and reset the data - m_clusterTree = null; - m_clustering = false; - s_data(m_allData); - m_allData = null; - } else if (!m_clustering && val) { - // Generate the cluster tree - m_clustering = true; - m_this._clusterData(); + if (m_clampBoundsY) { + if (bounds.top - bounds.bottom > m_maxBounds.top - m_maxBounds.bottom) { + dy = m_maxBounds.bottom - ((bounds.top - bounds.bottom - ( + m_maxBounds.top - m_maxBounds.bottom)) / 2) - bounds.bottom; + } else if (bounds.top > m_maxBounds.top) { + dy = m_maxBounds.top - bounds.top; + } else if (bounds.bottom < m_maxBounds.bottom) { + dy = m_maxBounds.bottom - bounds.bottom; + } + if (dy) { + bounds = { + top: bounds.top += dy, + bottom: bounds.bottom += dy, + left: bounds.left, + right: bounds.right + }; + } } - return m_this; - }; + return bounds; + } - //////////////////////////////////////////////////////////////////////////// /** - * Generate the clustering tree from positions. This might be async in the - * future. + * Call the camera bounds method with the given bounds, but + * correct for the viewport aspect ratio. + * @private */ + function camera_bounds(bounds) { + m_camera.bounds = bounds; + } + + //////////////////////////////////////////////////////////////////////////// + // + // All the methods are now defined. From here, we are initializing all + // internal variables and event handlers. + // //////////////////////////////////////////////////////////////////////////// - this._clusterData = function () { - if (!m_clustering) { - // clustering is not enabled, so this is a no-op - return; - } - // set clustering options to default if an options argument wasn't supplied - var opts = m_clustering === true ? {radius: 0.01} : m_clustering; + // Set the world origin + m_origin = {x: 0, y: 0}; - // generate the cluster tree from the raw data - var position = m_this.position(); - m_clusterTree = new geo.util.ClusterGroup( - opts, this.layer().width(), this.layer().height()); + // Fix the zoom level (minimum and initial) + this.zoomRange(arg); + m_zoom = fix_zoom(m_zoom); + // Now update to the correct center and zoom level + this.center($.extend({}, arg.center || m_center), undefined); - m_allData.forEach(function (d, i) { + this.interactor(arg.interactor || geo.mapInteractor()); + this.clock(arg.clock || geo.clock()); - // for each point in the data set normalize the coordinate - // representation and add the point to the cluster treee - var pt = geo.util.normalizeCoordinates(position(d, i)); - pt.index = i; - m_clusterTree.addPoint(pt); - }); + function resizeSelf() { + m_this.resize(0, 0, m_node.width(), m_node.height()); + } - // reset the last zoom state and trigger a redraw at the current zoom level - m_lastZoom = null; - m_this._handleZoom(m_this.layer().map().zoom()); - }; + if (arg.autoResize) { + $(window).resize(resizeSelf); + } + + // attach attribution updates to layer events + m_this.geoOn([ + geo.event.layerAdd, + geo.event.layerRemove + ], m_this.updateAttribution); + + return this; +}; + +/** + * General object specification for map types. Any additional + * values in the object are passed to the map constructor. + * @typedef geo.map.spec + * @type {object} + * @property {object[]} [data=[]] The default data array to + * apply to each feature if none exists + * @property {geo.layer.spec[]} [layers=[]] Layers to create + */ - //////////////////////////////////////////////////////////////////////////// - /** - * Handle zoom events for clustering. This keeps track of the last - * clustering level, and only regenerates the displayed points when the - * zoom level changes. - */ - //////////////////////////////////////////////////////////////////////////// - this._handleZoom = function (zoom) { - // get the current zoom level rounded down - var z = Math.floor(zoom); +/** + * Create a map from an object. Any errors in the creation + * of the map will result in returning null. + * @param {geo.map.spec} spec The object specification + * @returns {geo.map|null} + */ +geo.map.create = function (spec) { + 'use strict'; - if (!m_clustering || z === m_lastZoom) { - // short cut when there is nothing to do - return; - } + var map = geo.map(spec); - // store the current zoom level privately - m_lastZoom = z; + if (!map) { + console.warn('Could not create map.'); + return null; + } - // get the raw data elements for the points at the current level - var data = m_clusterTree.points(z).map(function (d) { - return m_allData[d.index]; - }); + spec.data = spec.data || []; + spec.layers = spec.layers || []; - // append the clusters at the current level - m_clusterTree.clusters(z).forEach(function (d) { - // mark the datum as a cluster for accessor methods - d.__cluster = true; + spec.layers.forEach(function (l) { + l.data = l.data || spec.data; + l.layer = geo.layer.create(map, l); + }); - // store all of the data objects for each point in the cluster as __data - d.__data = []; - d.obj.each(function (e) { - d.__data.push(m_allData[e.index]); - }); - data.push(d); - }); + return map; +}; - // prevent recomputing the clustering and set the new data array - m_ignoreData = true; - m_this.data(data); - m_this.layer().map().draw(); // replace with m_this.draw() when gl is fixed - }; +inherit(geo.map, geo.sceneObject); - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set position - * - * @returns {geo.pointFeature} - */ - //////////////////////////////////////////////////////////////////////////// - this.position = function (val) { - if (val === undefined) { - return m_this.style("position"); - } else { - val = geo.util.ensureFunction(val); - m_this.style("position", function (d, i) { - if (d.__cluster) { - return d; - } else { - return val(d, i); - } - }); - m_this.dataTime().modified(); - m_this.modified(); - } - return m_this; - }; +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of class feature + * + * @class + * @extends geo.sceneObject + * @returns {geo.feature} + */ +////////////////////////////////////////////////////////////////////////////// +geo.feature = function (arg) { + "use strict"; + if (!(this instanceof geo.feature)) { + return new geo.feature(arg); + } + geo.sceneObject.call(this); //////////////////////////////////////////////////////////////////////////// /** - * Update the current range tree object. Should be called whenever the - * data changes. + * @private */ //////////////////////////////////////////////////////////////////////////// - this._updateRangeTree = function () { - if (m_rangeTreeTime.getMTime() >= m_this.dataTime().getMTime()) { - return; - } - var pts, position, - radius = m_this.style.get("radius"), - stroke = m_this.style.get("stroke"), - strokeWidth = m_this.style.get("strokeWidth"); - - position = m_this.position(); - - m_maxRadius = 0; - - // create an array of positions in geo coordinates - pts = m_this.data().map(function (d, i) { - var pt = position(d); - pt.idx = i; - - // store the maximum point radius - m_maxRadius = Math.max( - m_maxRadius, - radius(d, i) + (stroke(d, i) ? strokeWidth(d, i) : 0) - ); - - return pt; - }); + arg = arg || {}; - m_rangeTree = new geo.util.RangeTree(pts); - m_rangeTreeTime.modified(); - }; + var m_this = this, + s_exit = this._exit, + m_selectionAPI = arg.selectionAPI === undefined ? false : arg.selectionAPI, + m_style = {}, + m_layer = arg.layer === undefined ? null : arg.layer, + m_gcs = arg.gcs, + m_visible = arg.visible === undefined ? true : arg.visible, + m_bin = arg.bin === undefined ? 0 : arg.bin, + m_renderer = arg.renderer === undefined ? null : arg.renderer, + m_dataTime = geo.timestamp(), + m_buildTime = geo.timestamp(), + m_updateTime = geo.timestamp(), + m_selectedFeatures = []; //////////////////////////////////////////////////////////////////////////// /** - * Returns an array of datum indices that contain the given point. - * Largely adapted from wigglemaps pointQuerier: - * - * https://github.com/dotskapes/wigglemaps/blob/cf5bed3fbfe2c3e48d31799462a80c564be1fb60/src/query/PointQuerier.js + * Private method to bind mouse handlers on the map element. */ //////////////////////////////////////////////////////////////////////////// - this.pointSearch = function (p) { - var min, max, data, idx = [], box, found = [], ifound = [], map, pt, - stroke = m_this.style.get("stroke"), - strokeWidth = m_this.style.get("strokeWidth"), - radius = m_this.style.get("radius"); - - if (!m_this.selectionAPI()) { - return []; - } + this._bindMouseHandlers = function () { - data = m_this.data(); - if (!data || !data.length) { - return { - found: [], - index: [] - }; + // Don't bind handlers for improved performance on features that don't + // require it. + if (!m_selectionAPI) { + return; } - map = m_this.layer().map(); - pt = map.gcsToDisplay(p); - - // Get the upper right corner in geo coordinates - min = map.displayToGcs({ - x: pt.x - m_maxRadius, - y: pt.y + m_maxRadius // GCS coordinates are bottom to top - }); - - // Get the lower left corner in geo coordinates - max = map.displayToGcs({ - x: pt.x + m_maxRadius, - y: pt.y - m_maxRadius - }); - - // Find points inside the bounding box - box = new geo.util.Box(geo.util.vect(min.x, min.y), geo.util.vect(max.x, max.y)); - m_this._updateRangeTree(); - m_rangeTree.search(box).forEach(function (q) { - idx.push(q.idx); - }); - - // Filter by circular region - idx.forEach(function (i) { - var d = data[i], - p = m_this.position()(d, i), - dx, dy, rad; - - rad = radius(data[i], i); - rad += stroke(data[i], i) ? strokeWidth(data[i], i) : 0; - p = map.gcsToDisplay(p); - dx = p.x - pt.x; - dy = p.y - pt.y; - if (Math.sqrt(dx * dx + dy * dy) <= rad) { - found.push(d); - ifound.push(i); - } - }); + // First unbind to be sure that the handlers aren't bound twice. + m_this._unbindMouseHandlers(); - return { - data: found, - index: ifound - }; + m_this.geoOn(geo.event.mousemove, m_this._handleMousemove); + m_this.geoOn(geo.event.mouseclick, m_this._handleMouseclick); + m_this.geoOn(geo.event.brushend, m_this._handleBrushend); + m_this.geoOn(geo.event.brush, m_this._handleBrush); }; //////////////////////////////////////////////////////////////////////////// /** - * Returns an array of datum indices that are contained in the given box. + * Private method to unbind mouse handlers on the map element. */ //////////////////////////////////////////////////////////////////////////// - this.boxSearch = function (lowerLeft, upperRight) { - var pos = m_this.position(), - idx = []; - // TODO: use the range tree - m_this.data().forEach(function (d, i) { - var p = pos(d); - if (p.x >= lowerLeft.x && - p.x <= upperRight.x && - p.y >= lowerLeft.y && - p.y <= upperRight.y - ) { - idx.push(i); - } - }); - return idx; + this._unbindMouseHandlers = function () { + m_this.geoOff(geo.event.mousemove, m_this._handleMousemove); + m_this.geoOff(geo.event.mouseclick, m_this._handleMouseclick); + m_this.geoOff(geo.event.brushend, m_this._handleBrushend); + m_this.geoOff(geo.event.brush, m_this._handleBrush); }; //////////////////////////////////////////////////////////////////////////// /** - * Overloaded data method that updates the internal range tree on write. + * For binding mouse events, use functions with + * the following call signatures: + * + * function handler(arg) { + * // arg.data - the data object of the feature + * // arg.index - the index inside the data array of the featue + * // arg.mouse - mouse information object (see src/core/mapInteractor.js) + * } + * + * i.e. + * + * feature.geoOn(geo.event.feature.mousemove, function (arg) { + * // do something with the feature marker. + * }); */ //////////////////////////////////////////////////////////////////////////// - this.data = function (data) { - if (data === undefined) { - return s_data(); - } - if (m_clustering && !m_ignoreData) { - m_allData = data; - m_this._clusterData(); - } else { - s_data(data); - } - m_ignoreData = false; - return m_this; - }; //////////////////////////////////////////////////////////////////////////// /** - * Returns the bounding box for a given datum in screen coordinates as an - * object: :: + * Search for features containing the given point. + * + * Returns an object: :: * * { - * min: { - * x: value, - * y: value - * }, - * max: { - * x: value, - * y: value - * } + * data: [...] // an array of data objects for matching features + * index: [...] // an array of indices of the matching features * } * - * @returns {object} + * @argument {Object} coordinate + * @returns {Object} */ //////////////////////////////////////////////////////////////////////////// - this._boundingBox = function (d) { - var pt, radius; - - // get the position in geo coordinates - pt = m_this.position()(d); - - // convert to screen coordinates - pt = m_this.layer().map().gcsToDisplay(pt); - - // get the radius of the points (should we add stroke width?) - radius = m_this.style().radius(d); - + this.pointSearch = function () { + // base class method does nothing return { - min: { - x: pt.x - radius, - y: pt.y - radius - }, - max: { - x: pt.x + radius, - y: pt.y + radius - } + index: [], + found: [] }; }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Private mousemove handler */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - - var defaultStyle = $.extend( - {}, - { - radius: 5.0, - stroke: true, - strokeColor: { r: 0.851, g: 0.604, b: 0.0 }, - strokeWidth: 1.25, - strokeOpacity: 1.0, - fillColor: { r: 1.0, g: 0.839, b: 0.439 }, - fill: true, - fillOpacity: 0.8, - sprites: false, - sprites_image: null, - position: function (d) { return d; } - }, - arg.style === undefined ? {} : arg.style - ); + this._handleMousemove = function () { + var mouse = m_this.layer().map().interactor().mouse(), + data = m_this.data(), + over = m_this.pointSearch(mouse.geo), + newFeatures = [], oldFeatures = [], lastTop = -1, top = -1; - if (arg.position !== undefined) { - defaultStyle.position = arg.position; + // Get the index of the element that was previously on top + if (m_selectedFeatures.length) { + lastTop = m_selectedFeatures[m_selectedFeatures.length - 1]; } - m_this.style(defaultStyle); - m_this.dataTime().modified(); + // There are probably faster ways of doing this: + newFeatures = over.index.filter(function (i) { + return m_selectedFeatures.indexOf(i) < 0; + }); + oldFeatures = m_selectedFeatures.filter(function (i) { + return over.index.indexOf(i) < 0; + }); - // bind to the zoom handler for point clustering - m_this.geoOn(geo.event.zoom, function (evt) { - m_this._handleZoom(evt.zoomLevel); + geo.feature.eventID += 1; + // Fire events for mouse in first. + newFeatures.forEach(function (i, idx) { + m_this.geoTrigger(geo.event.feature.mouseover, { + data: data[i], + index: i, + mouse: mouse, + eventID: geo.feature.eventID, + top: idx === newFeatures.length - 1 + }, true); }); - }; - return m_this; -}; + geo.feature.eventID += 1; + // Fire events for mouse out next + oldFeatures.forEach(function (i, idx) { + m_this.geoTrigger(geo.event.feature.mouseout, { + data: data[i], + index: i, + mouse: mouse, + eventID: geo.feature.eventID, + top: idx === oldFeatures.length - 1 + }, true); + }); -geo.event.pointFeature = $.extend({}, geo.event.feature); + geo.feature.eventID += 1; + // Fire events for mouse move last + over.index.forEach(function (i, idx) { + m_this.geoTrigger(geo.event.feature.mousemove, { + data: data[i], + index: i, + mouse: mouse, + eventID: geo.feature.eventID, + top: idx === over.index.length - 1 + }, true); + }); -/** - * Object specification for a point feature. - * - * @extends geo.feature.spec // need to make a jsdoc plugin for this to work - * @typedef geo.pointFeature.spec - * @type {object} - */ + // Replace the selected features array + m_selectedFeatures = over.index; -/** - * Create a pointFeature from an object. - * @see {@link geo.feature.create} - * @param {geo.layer} layer The layer to add the feature to - * @param {geo.pointFeature.spec} spec The object specification - * @returns {geo.pointFeature|null} - */ -geo.pointFeature.create = function (layer, renderer, spec) { - "use strict"; + // Get the index of the element that is now on top + if (m_selectedFeatures.length) { + top = m_selectedFeatures[m_selectedFeatures.length - 1]; + } - spec.type = "point"; - return geo.feature.create(layer, spec); -}; + if (lastTop !== top) { + // The element on top changed so we need to fire mouseon/mouseoff + if (lastTop !== -1) { + m_this.geoTrigger(geo.event.feature.mouseoff, { + data: data[lastTop], + index: lastTop, + mouse: mouse + }, true); + } -inherit(geo.pointFeature, geo.feature); + if (top !== -1) { + m_this.geoTrigger(geo.event.feature.mouseon, { + data: data[top], + index: top, + mouse: mouse + }, true); + } + } + }; -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class lineFeature - * - * @class - * @extends geo.feature - * @returns {geo.lineFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.lineFeature = function (arg) { - "use strict"; - if (!(this instanceof geo.lineFeature)) { - return new geo.lineFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); + //////////////////////////////////////////////////////////////////////////// + /** + * Private mouseclick handler + */ + //////////////////////////////////////////////////////////////////////////// + this._handleMouseclick = function () { + var mouse = m_this.layer().map().interactor().mouse(), + data = m_this.data(), + over = m_this.pointSearch(mouse.geo); + + geo.feature.eventID += 1; + over.index.forEach(function (i, idx) { + m_this.geoTrigger(geo.event.feature.mouseclick, { + data: data[i], + index: i, + mouse: mouse, + eventID: geo.feature.eventID, + top: idx === over.index.length - 1 + }, true); + }); + }; //////////////////////////////////////////////////////////////////////////// /** - * @private + * Private brush handler. */ //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init; + this._handleBrush = function (brush) { + var idx = m_this.boxSearch(brush.gcs.lowerLeft, brush.gcs.upperRight), + data = m_this.data(); + + geo.feature.eventID += 1; + idx.forEach(function (i, idx) { + m_this.geoTrigger(geo.event.feature.brush, { + data: data[i], + index: i, + mouse: brush.mouse, + brush: brush, + eventID: geo.feature.eventID, + top: idx === idx.length - 1 + }, true); + }); + }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set line accessor - * - * @returns {geo.pointFeature} + * Private brushend handler. */ //////////////////////////////////////////////////////////////////////////// - this.line = function (val) { - if (val === undefined) { - return m_this.style("line"); - } else { - m_this.style("line", val); - m_this.dataTime().modified(); - m_this.modified(); - } - return m_this; + this._handleBrushend = function (brush) { + var idx = m_this.boxSearch(brush.gcs.lowerLeft, brush.gcs.upperRight), + data = m_this.data(); + + geo.feature.eventID += 1; + idx.forEach(function (i, idx) { + m_this.geoTrigger(geo.event.feature.brushend, { + data: data[i], + index: i, + mouse: brush.mouse, + brush: brush, + eventID: geo.feature.eventID, + top: idx === idx.length - 1 + }, true); + }); }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set position accessor - * - * @returns {geo.pointFeature} + * Get/Set style used by the feature */ //////////////////////////////////////////////////////////////////////////// - this.position = function (val) { - if (val === undefined) { - return m_this.style("position"); + this.style = function (arg1, arg2) { + if (arg1 === undefined) { + return m_style; + } else if (typeof arg1 === "string" && arg2 === undefined) { + return m_style[arg1]; + } else if (arg2 === undefined) { + m_style = $.extend({}, m_style, arg1); + m_this.modified(); + return m_this; } else { - m_this.style("position", val); - m_this.dataTime().modified(); + m_style[arg1] = arg2; m_this.modified(); + return m_this; } - return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Returns an array of datum indices that contain the given point. - * This is a slow implementation with runtime order of the number of - * vertices. + * A uniform getter that always returns a function even for constant styles. + * Maybe extend later to support accessor-like objects. If undefined input, + * return all the styles as an object. + * + * @param {string|undefined} key + * @return {function} */ //////////////////////////////////////////////////////////////////////////// - this.pointSearch = function (p) { - var data, pt, map, line, width, indices = [], found = [], pos; - data = m_this.data(); - if (!data || !data.length) { - return { - found: [], - index: [] - }; - } - - map = m_this.layer().map(); - line = m_this.line(); - width = m_this.style.get("strokeWidth"); - pos = m_this.position(); - pt = map.gcsToDisplay(p); - - // minimum l2 distance squared from - // q -> line(u, v) - function lineDist2(q, u, v) { - var t, l2 = dist2(u, v); - - if (l2 < 1) { - // u, v are within 1 pixel - return dist2(q, u); - } - - t = ((q.x - u.x) * (v.x - u.x) + (q.y - u.y) * (v.y - u.y)) / l2; - if (t < 0) { return dist2(q, u); } - if (t > 1) { return dist2(q, v); } - return dist2( - q, - { - x: u.x + t * (v.x - u.x), - y: u.y + t * (v.y - u.y) + this.style.get = function (key) { + var tmp, out; + if (key === undefined) { + var all = {}, k; + for (k in m_style) { + if (m_style.hasOwnProperty(k)) { + all[k] = m_this.style.get(k); } - ); - } - - // l2 distance squared from u to v - function dist2(u, v) { - var dx = u.x - v.x, - dy = u.y - v.y; - return dx * dx + dy * dy; + } + return all; } - - // for each line - data.forEach(function (d, index) { - var last = null; - - try { - line(d, index).forEach(function (current, j) { - - // get the screen coordinates of the current point - var p = pos(current, j, d, index); - var s = map.gcsToDisplay(p); - var r = Math.ceil(width(p, j, d, index) / 2) + 2; - r = r * r; - - if (last) { - // test the line segment s -> last - if (lineDist2(pt, s, last) <= r) { - - // short circuit the loop here - throw "found"; - } - } - - last = s; - }); - } catch (err) { - if (err !== "found") { - throw err; - } - found.push(d); - indices.push(index); + if (key.toLowerCase().match(/color$/)) { + if (geo.util.isFunction(m_style[key])) { + tmp = geo.util.ensureFunction(m_style[key]); + out = function () { + return geo.util.convertColor( + tmp.apply(this, arguments) + ); + }; + } else { + // if the color is not a function, only convert it once + out = geo.util.ensureFunction(geo.util.convertColor(m_style[key])); } - }); - - return { - data: found, - index: indices - }; + } else { + out = geo.util.ensureFunction(m_style[key]); + } + return out; }; //////////////////////////////////////////////////////////////////////////// /** - * Returns an array of line indices that are contained in the given box. + * Get layer referenced by the feature */ //////////////////////////////////////////////////////////////////////////// - this.boxSearch = function (lowerLeft, upperRight, opts) { - var pos = m_this.position(), - idx = [], - line = m_this.line(); - - opts = opts || {}; - opts.partial = opts.partial || false; - if (opts.partial) { - throw "Unimplemented query method."; - } - - m_this.data().forEach(function (d, i) { - var inside = true; - line(d, i).forEach(function (e, j) { - if (!inside) { return; } - var p = pos(e, j, d, i); - if (!(p.x >= lowerLeft.x && - p.x <= upperRight.x && - p.y >= lowerLeft.y && - p.y <= upperRight.y) - ) { - inside = false; - } - }); - if (inside) { - idx.push(i); - } - }); - return idx; + this.layer = function () { + return m_layer; }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Get renderer used by the feature */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - - var defaultStyle = $.extend( - {}, - { - "strokeWidth": 1.0, - // Default to gold color for lines - "strokeColor": { r: 1.0, g: 0.8431372549, b: 0.0 }, - "strokeStyle": "solid", - "strokeOpacity": 1.0, - "line": function (d) { return d; }, - "position": function (d) { return d; } - }, - arg.style === undefined ? {} : arg.style - ); - - if (arg.line !== undefined) { - defaultStyle.line = arg.line; - } - - if (arg.position !== undefined) { - defaultStyle.position = arg.position; - } - - - m_this.style(defaultStyle); - - m_this.dataTime().modified(); + this.renderer = function () { + return m_renderer; }; - this._init(arg); - return this; -}; - -/** - * Create a lineFeature from an object. - * @see {@link geo.feature.create} - * @param {geo.layer} layer The layer to add the feature to - * @param {geo.lineFeature.spec} spec The object specification - * @returns {geo.lineFeature|null} - */ -geo.lineFeature.create = function (layer, spec) { - "use strict"; - - spec.type = "line"; - return geo.feature.create(layer, spec); -}; - -inherit(geo.lineFeature, geo.feature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class pathFeature - * - * @class - * @extends geo.feature - * @returns {geo.pathFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.pathFeature = function (arg) { - "use strict"; - if (!(this instanceof geo.pathFeature)) { - return new geo.pathFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); - //////////////////////////////////////////////////////////////////////////// /** - * @private + * Get/Set projection of the feature */ //////////////////////////////////////////////////////////////////////////// - var m_this = this, - m_position = arg.position === undefined ? [] : arg.position, - s_init = this._init; + this.gcs = function (val) { + if (val === undefined) { + if (m_gcs === undefined && m_renderer) { + return m_renderer.layer().map().ingcs(); + } + return m_gcs; + } else { + m_gcs = val; + m_this.modified(); + return m_this; + } + }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set positions + * Convert from the renderer's input gcs coordinates to display coordinates. * - * @returns {geo.pathFeature} + * @param {object} c The input coordinate to convert + * @param {object} c.x + * @param {object} c.y + * @param {object} [c.z=0] + * @return {object} Display space coordinates */ - //////////////////////////////////////////////////////////////////////////// - this.position = function (val) { - if (val === undefined) { - return m_position; + this.featureGcsToDisplay = function (c) { + var map = m_renderer.layer().map(); + c = map.gcsToWorld(c, map.ingcs()); + c = map.worldToDisplay(c); + if (m_renderer.baseToLocal) { + c = m_renderer.baseToLocal(c); } - // Copy incoming array of positions - m_position = val; - m_this.dataTime().modified(); - m_this.modified(); - return m_this; + return c; }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Get/Set visibility of the feature */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - - var defaultStyle = $.extend( - {}, - { - "strokeWidth": function () { return 1; }, - "strokeColor": function () { return { r: 1.0, g: 1.0, b: 1.0 }; } - }, - arg.style === undefined ? {} : arg.style - ); + this.visible = function (val) { + if (val === undefined) { + return m_visible; + } else { + m_visible = val; + m_this.modified(); - m_this.style(defaultStyle); + // bind or unbind mouse handlers on visibility change + if (m_visible) { + m_this._bindMouseHandlers(); + } else { + m_this._unbindMouseHandlers(); + } - if (m_position) { - m_this.dataTime().modified(); + return m_this; } }; - this._init(arg); - return this; -}; - -inherit(geo.pathFeature, geo.feature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class polygonFeature - * - * @class - * @extends geo.feature - * @returns {geo.polygonFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.polygonFeature = function (arg) { - "use strict"; - if (!(this instanceof geo.polygonFeature)) { - return new geo.polygonFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); - //////////////////////////////////////////////////////////////////////////// /** - * @private + * Get/Set bin of the feature + * + * Bin number is typically used for sorting the order of rendering */ //////////////////////////////////////////////////////////////////////////// - var m_this = this, - m_position, - m_polygon, - s_init = this._init, - s_data = this.data, - m_coordinates = {outer: [], inner: []}; - - if (arg.polygon === undefined) { - m_polygon = function (d) { - return d; - }; - } else { - m_polygon = arg.polygon; - } - - if (arg.position === undefined) { - m_position = function (d) { - return d; - }; - } else { - m_position = arg.position; - } + this.bin = function (val) { + if (val === undefined) { + return m_bin; + } else { + m_bin = val; + m_this.modified(); + return m_this; + } + }; //////////////////////////////////////////////////////////////////////////// /** - * Override the parent data method to keep track of changes to the - * internal coordinates. + * Get/Set timestamp of data change */ //////////////////////////////////////////////////////////////////////////// - this.data = function (arg) { - var ret = s_data(arg); - if (arg !== undefined) { - getCoordinates(); + this.dataTime = function (val) { + if (val === undefined) { + return m_dataTime; + } else { + m_dataTime = val; + m_this.modified(); + return m_this; } - return ret; }; //////////////////////////////////////////////////////////////////////////// /** - * 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. - * @private + * Get/Set timestamp of last time build happened */ //////////////////////////////////////////////////////////////////////////// - function getCoordinates() { - var posFunc = m_this.position(), - 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); - }); - - inner = (poly.inner || []).map(function (hole) { - return (hole || []).map(function (d0, k) { - return posFunc.call(m_this, d0, k, d, i); - }); - }); - return { - outer: outer, - inner: inner - }; - }); - } + this.buildTime = function (val) { + if (val === undefined) { + return m_buildTime; + } else { + m_buildTime = val; + m_this.modified(); + return m_this; + } + }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set polygon accessor - * - * @returns {geo.pointFeature} + * Get/Set timestamp of last time update happened */ //////////////////////////////////////////////////////////////////////////// - this.polygon = function (val) { + this.updateTime = function (val) { if (val === undefined) { - return m_polygon; + return m_updateTime; } else { - m_polygon = val; - m_this.dataTime().modified(); + m_updateTime = val; m_this.modified(); - getCoordinates(); + return m_this; } - return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set position accessor + * Get/Set the data array for the feature. * - * @returns {geo.pointFeature} + * @returns {Array|this} */ //////////////////////////////////////////////////////////////////////////// - this.position = function (val) { - if (val === undefined) { - return m_position; + this.data = function (data) { + if (data === undefined) { + return m_this.style("data") || []; } else { - m_position = val; + m_this.style("data", data); m_this.dataTime().modified(); m_this.modified(); - getCoordinates(); + return m_this; } - return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Point searce method for selection api. Returns markers containing the - * given point. - * @argument {Object} coordinate - * @returns {Object} + * Query if the selection API is enabled for this feature. + * @returns {bool} */ //////////////////////////////////////////////////////////////////////////// - this.pointSearch = function (coordinate) { - var found = [], indices = [], data = m_this.data(); - m_coordinates.forEach(function (coord, i) { - var inside = geo.util.pointInPolygon( - coordinate, - coord.outer, - coord.inner - ); - if (inside) { - indices.push(i); - found.push(data[i]); - } - }); - return { - index: indices, - found: found - }; + this.selectionAPI = function () { + return m_selectionAPI; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + * + * Derived class should implement this + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + if (!m_layer) { + throw "Feature requires a valid layer"; + } + m_style = $.extend({}, + {"opacity": 1.0}, arg.style === undefined ? {} : + arg.style); + m_this._bindMouseHandlers(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * Derived class should implement this + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + * + * Derived class should implement this + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + * + * Derived class should implement this + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this._unbindMouseHandlers(); + m_selectedFeatures = []; + m_style = {}; + arg = {}; + s_exit(); }; - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); + this._init(arg); + return this; +}; - var defaultStyle = $.extend( - {}, - { - "fillColor": { r: 0.0, g: 0.5, b: 0.5 }, - "fillOpacity": 1.0 - }, - arg.style === undefined ? {} : arg.style - ); +/** + * The most recent feature event triggered. + * @type {number} + */ +geo.feature.eventID = 0; - m_this.style(defaultStyle); +/** + * General object specification for feature types. + * @typedef geo.feature.spec + * @type {object} + * @property {string} type A supported feature type. + * @property {object[]} [data=[]] An array of arbitrary objects used to + * construct the feature. These objects (and their associated + * indices in the array) will be passed back to style and attribute + * accessors provided by the user. In general the number of + * "markers" drawn will be equal to the length of this array. + */ - if (m_position) { - m_this.dataTime().modified(); - } - }; +/** + * Create a feature from an object. The implementation here is + * meant to define the general interface of creating features + * from a javascript object. See documentation from individual + * feature types for specific details. In case of an error in + * the arguments this method will return null; + * @param {geo.layer} layer The layer to add the feature to + * @param {geo.feature.spec} [spec={}] The object specification + * @returns {geo.feature|null} + */ +geo.feature.create = function (layer, spec) { + "use strict"; - this._init(arg); - return this; + var type = spec.type; + + // Check arguments + if (!(layer instanceof geo.layer)) { + console.warn("Invalid layer"); + return null; + } + if (typeof spec !== "object") { + console.warn("Invalid spec"); + return null; + } + var feature = layer.createFeature(type); + if (!feature) { + console.warn("Could not create feature type '" + type + "'"); + return null; + } + + spec = spec || {}; + spec.data = spec.data || []; + return feature.style(spec); }; -inherit(geo.polygonFeature, geo.feature); +inherit(geo.feature, geo.sceneObject); ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of class planeFeature + * Create a new instance of class pointFeature * * @class - * @extends geo.polygonFeature - * @returns {geo.planeFeature} + * @param {object} arg Options object + * @param {boolean} arg.clustering Enable point clustering + * @extends geo.feature + * @returns {geo.pointFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.planeFeature = function (arg) { +geo.pointFeature = function (arg) { "use strict"; - if (!(this instanceof geo.planeFeature)) { - return new geo.planeFeature(arg); + if (!(this instanceof geo.pointFeature)) { + return new geo.pointFeature(arg); } arg = arg || {}; + geo.feature.call(this, arg); - // Defaults - arg.ul = arg.ul === undefined ? [0.0, 1.0, 0.0] : arg.ul; - arg.lr = arg.lr === undefined ? [1.0, 0.0, 0.0] : arg.lr; - arg.depth = arg.depth === undefined ? 0.0 : arg.depth; - - geo.polygonFeature.call(this, arg); - + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// var m_this = this, - m_origin = [arg.ul.x, arg.lr.y, arg.depth], - m_upperLeft = [arg.ul.x, arg.ul.y, arg.depth], - m_lowerRight = [arg.lr.x, arg.lr.y, arg.depth], - m_defaultDepth = arg.depth, - m_drawOnAsyncResourceLoad = arg.drawOnAsyncResourceLoad === undefined ? - true : false, - s_init = this._init; + s_init = this._init, + m_rangeTree = null, + m_rangeTreeTime = geo.timestamp(), + s_data = this.data, + m_maxRadius = 0, + m_clustering = arg.clustering, + m_clusterTree = null, + m_allData = [], + m_lastZoom = null, + m_ignoreData = false; // flag to ignore data() calls made locally //////////////////////////////////////////////////////////////////////////// /** - * Get/Set origin + * Get/Set clustering option * - * @returns {geo.planeFeature} + * @returns {geo.pointFeature|boolean} */ //////////////////////////////////////////////////////////////////////////// - this.origin = function (val) { + this.clustering = function (val) { if (val === undefined) { - return m_origin; - } else if (val instanceof Array) { - if (val.length > 3 || val.length < 2) { - throw "Origin point requires point in 2 or 3 dimension"; - } - m_origin = val.slice(0); - if (m_origin.length === 2) { - m_origin[2] = m_defaultDepth; - } + return m_clustering; + } + if (m_clustering && !val) { + // Throw out the cluster tree and reset the data + m_clusterTree = null; + m_clustering = false; + s_data(m_allData); + m_allData = null; + } else if (!m_clustering && val) { + // Generate the cluster tree + m_clustering = true; + m_this._clusterData(); } - m_this.dataTime().modified(); - m_this.modified(); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set pt1 - * - * @returns {geo.planeFeature} + * Generate the clustering tree from positions. This might be async in the + * future. */ //////////////////////////////////////////////////////////////////////////// - this.upperLeft = function (val) { - if (val === undefined) { - return m_upperLeft; - } else if (val instanceof Array) { - if (val.length > 3 || val.length < 2) { - throw "Upper left point requires point in 2 or 3 dimension"; - } - m_upperLeft = val.slice(0); - if (m_upperLeft.length === 2) { - m_upperLeft[2] = m_defaultDepth; - } + this._clusterData = function () { + if (!m_clustering) { + // clustering is not enabled, so this is a no-op + return; } - m_this.dataTime().modified(); - m_this.modified(); - return m_this; + + // set clustering options to default if an options argument wasn't supplied + var opts = m_clustering === true ? {radius: 0.01} : m_clustering; + + // generate the cluster tree from the raw data + var position = m_this.position(); + m_clusterTree = new geo.util.ClusterGroup( + opts, m_this.layer().width(), m_this.layer().height()); + + m_allData.forEach(function (d, i) { + + // for each point in the data set normalize the coordinate + // representation and add the point to the cluster treee + var pt = geo.util.normalizeCoordinates(position(d, i)); + pt.index = i; + m_clusterTree.addPoint(pt); + }); + + // reset the last zoom state and trigger a redraw at the current zoom level + m_lastZoom = null; + m_this._handleZoom(m_this.layer().map().zoom()); }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set origin + * Handle zoom events for clustering. This keeps track of the last + * clustering level, and only regenerates the displayed points when the + * zoom level changes. + */ + //////////////////////////////////////////////////////////////////////////// + this._handleZoom = function (zoom) { + // get the current zoom level rounded down + var z = Math.floor(zoom); + + if (!m_clustering || z === m_lastZoom) { + // short cut when there is nothing to do + return; + } + + // store the current zoom level privately + m_lastZoom = z; + + // get the raw data elements for the points at the current level + var data = m_clusterTree.points(z).map(function (d) { + return m_allData[d.index]; + }); + + // append the clusters at the current level + m_clusterTree.clusters(z).forEach(function (d) { + // mark the datum as a cluster for accessor methods + d.__cluster = true; + + // store all of the data objects for each point in the cluster as __data + d.__data = []; + d.obj.each(function (e) { + d.__data.push(m_allData[e.index]); + }); + data.push(d); + }); + + // prevent recomputing the clustering and set the new data array + m_ignoreData = true; + m_this.data(data); + m_this.layer().map().draw(); // replace with m_this.draw() when gl is fixed + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set position * - * @returns {geo.planeFeature} + * @returns {geo.pointFeature} */ //////////////////////////////////////////////////////////////////////////// - this.lowerRight = function (val) { + this.position = function (val) { if (val === undefined) { - return m_lowerRight; - } else if (val instanceof Array) { - if (val.length > 3 || val.length < 2) { - throw "Lower right point requires point in 2 or 3 dimension"; - } - m_lowerRight = val.slice(0); - if (m_lowerRight.length === 2) { - m_lowerRight[2] = m_defaultDepth; - } + return m_this.style("position"); + } else { + val = geo.util.ensureFunction(val); + m_this.style("position", function (d, i) { + if (d.__cluster) { + return d; + } else { + return val(d, i); + } + }); m_this.dataTime().modified(); + m_this.modified(); } - m_this.dataTime().modified(); - m_this.modified(); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set if draw should happen as soon as a async resource is loaded + * Update the current range tree object. Should be called whenever the + * data changes. */ //////////////////////////////////////////////////////////////////////////// - this.drawOnAsyncResourceLoad = function (val) { - if (val === undefined) { - return m_drawOnAsyncResourceLoad; - } else { - m_drawOnAsyncResourceLoad = val; - return m_this; + this._updateRangeTree = function () { + if (m_rangeTreeTime.getMTime() >= m_this.dataTime().getMTime()) { + return; } + var pts, position, + radius = m_this.style.get("radius"), + stroke = m_this.style.get("stroke"), + strokeWidth = m_this.style.get("strokeWidth"); + + position = m_this.position(); + + m_maxRadius = 0; + + // create an array of positions in geo coordinates + pts = m_this.data().map(function (d, i) { + var pt = position(d); + pt.idx = i; + + // store the maximum point radius + m_maxRadius = Math.max( + m_maxRadius, + radius(d, i) + (stroke(d, i) ? strokeWidth(d, i) : 0) + ); + + return pt; + }); + + m_rangeTree = new geo.util.RangeTree(pts); + m_rangeTreeTime.modified(); }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Returns an array of datum indices that contain the given point. + * Largely adapted from wigglemaps pointQuerier: + * + * https://github.com/dotskapes/wigglemaps/blob/cf5bed3fbfe2c3e48d31799462a80c564be1fb60/src/query/PointQuerier.js */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - var style = null; - s_init.call(m_this, arg); - style = m_this.style(); - if (style.image === undefined) { - style.image = null; + this.pointSearch = function (p) { + var min, max, data, idx = [], box, found = [], ifound = [], map, pt, + stroke = m_this.style.get("stroke"), + strokeWidth = m_this.style.get("strokeWidth"), + radius = m_this.style.get("radius"); + + if (!m_this.selectionAPI()) { + return []; } - m_this.style(style); - }; - this._init(arg); - return this; -}; + data = m_this.data(); + if (!data || !data.length) { + return { + found: [], + index: [] + }; + } -inherit(geo.planeFeature, geo.polygonFeature); + map = m_this.layer().map(); + pt = map.gcsToDisplay(p); -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class vectorFeature - * - * @class - * @extends geo.feature - * @returns {geo.vectorFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.vectorFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.vectorFeature)) { - return new geo.vectorFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); + // Get the upper right corner in geo coordinates + min = map.displayToGcs({ + x: pt.x - m_maxRadius, + y: pt.y + m_maxRadius // GCS coordinates are bottom to top + }); + + // Get the lower left corner in geo coordinates + max = map.displayToGcs({ + x: pt.x + m_maxRadius, + y: pt.y - m_maxRadius + }); + + // Find points inside the bounding box + box = new geo.util.Box(geo.util.vect(min.x, min.y), geo.util.vect(max.x, max.y)); + m_this._updateRangeTree(); + m_rangeTree.search(box).forEach(function (q) { + idx.push(q.idx); + }); + + // Filter by circular region + idx.forEach(function (i) { + var d = data[i], + p = m_this.position()(d, i), + dx, dy, rad; + + rad = radius(data[i], i); + rad += stroke(data[i], i) ? strokeWidth(data[i], i) : 0; + p = map.gcsToDisplay(p); + dx = p.x - pt.x; + dy = p.y - pt.y; + if (Math.sqrt(dx * dx + dy * dy) <= rad) { + found.push(d); + ifound.push(i); + } + }); + + return { + data: found, + index: ifound + }; + }; //////////////////////////////////////////////////////////////////////////// /** - * @private + * Returns an array of datum indices that are contained in the given box. */ //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - s_style = this.style; + this.boxSearch = function (lowerLeft, upperRight) { + var pos = m_this.position(), + idx = []; + // TODO: use the range tree + m_this.data().forEach(function (d, i) { + var p = pos(d); + if (p.x >= lowerLeft.x && + p.x <= upperRight.x && + p.y >= lowerLeft.y && + p.y <= upperRight.y + ) { + idx.push(i); + } + }); + return idx; + }; //////////////////////////////////////////////////////////////////////////// /** - * Get or set the accessor for the origin of the vector. This is the point - * that the vector base resides at. Defaults to (0, 0, 0). - * @param {geo.accessor|geo.geoPosition} [accessor] The origin accessor + * Overloaded data method that updates the internal range tree on write. */ //////////////////////////////////////////////////////////////////////////// - this.origin = function (val) { - if (val === undefined) { - return s_style('origin'); + this.data = function (data) { + if (data === undefined) { + return s_data(); + } + if (m_clustering && !m_ignoreData) { + m_allData = data; + m_this._clusterData(); } else { - s_style('origin', val); - m_this.dataTime().modified(); - m_this.modified(); + s_data(data); } + m_ignoreData = false; return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get or set the accessor for the displacement (coordinates) of the vector. - * @param {geo.accessor|geo.geoPosition} [accessor] The accessor + * Returns the bounding box for a given datum in screen coordinates as an + * object: :: + * + * { + * min: { + * x: value, + * y: value + * }, + * max: { + * x: value, + * y: value + * } + * } + * + * @returns {object} */ //////////////////////////////////////////////////////////////////////////// - this.delta = function (val) { - if (val === undefined) { - return s_style('delta'); - } else { - s_style('delta', val); - m_this.dataTime().modified(); - m_this.modified(); - } - return m_this; + this._boundingBox = function (d) { + var pt, radius; + + // get the position in geo coordinates + pt = m_this.position()(d); + + // convert to screen coordinates + pt = m_this.layer().map().gcsToDisplay(pt); + + // get the radius of the points (should we add stroke width?) + radius = m_this.style().radius(d); + + return { + min: { + x: pt.x - radius, + y: pt.y - radius + }, + max: { + x: pt.x + radius, + y: pt.y + radius + } + }; }; //////////////////////////////////////////////////////////////////////////// /** * Initialize - * @protected */ //////////////////////////////////////////////////////////////////////////// this._init = function (arg) { @@ -20248,75 +22606,76 @@ geo.vectorFeature = function (arg) { var defaultStyle = $.extend( {}, { - strokeColor: 'black', - strokeWidth: 2.0, + radius: 5.0, + stroke: true, + strokeColor: { r: 0.851, g: 0.604, b: 0.0 }, + strokeWidth: 1.25, strokeOpacity: 1.0, - // TODO: define styles for the end markers - // originStyle: 'none', - // endStyle: 'arrow', - origin: {x: 0, y: 0, z: 0}, - delta: function (d) { return d; }, - scale: null // size scaling factor (null -> renderer decides) + fillColor: { r: 1.0, g: 0.839, b: 0.439 }, + fill: true, + fillOpacity: 0.8, + sprites: false, + sprites_image: null, + position: function (d) { return d; } }, arg.style === undefined ? {} : arg.style ); - if (arg.origin !== undefined) { - defaultStyle.origin = arg.origin; + if (arg.position !== undefined) { + defaultStyle.position = arg.position; } m_this.style(defaultStyle); m_this.dataTime().modified(); + + // bind to the zoom handler for point clustering + m_this.geoOn(geo.event.zoom, function (evt) { + m_this._handleZoom(evt.zoomLevel); + }); }; + + return m_this; }; -inherit(geo.vectorFeature, geo.feature); +geo.event.pointFeature = $.extend({}, geo.event.feature); -////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of class geomFeature + * Object specification for a point feature. * - * @class - * @extends geo.feature - * @returns {geo.geomFeature} + * @extends geo.feature.spec // need to make a jsdoc plugin for this to work + * @typedef geo.pointFeature.spec + * @type {object} */ -////////////////////////////////////////////////////////////////////////////// -geo.geomFeature = function (arg) { - "use strict"; - if (!(this instanceof geo.geomFeature)) { - return new geo.geomFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); - arg.style = arg.style === undefined ? $.extend({}, { - "color": [1.0, 1.0, 1.0], - "point_sprites": false, - "point_sprites_image": null - }, arg.style) : arg.style; - - // Update style - this.style(arg.style); +/** + * Create a pointFeature from an object. + * @see {@link geo.feature.create} + * @param {geo.layer} layer The layer to add the feature to + * @param {geo.pointFeature.spec} spec The object specification + * @returns {geo.pointFeature|null} + */ +geo.pointFeature.create = function (layer, renderer, spec) { + "use strict"; - return this; + spec.type = "point"; + return geo.feature.create(layer, spec); }; -inherit(geo.geomFeature, geo.feature); +inherit(geo.pointFeature, geo.feature); ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of class graphFeature + * Create a new instance of class lineFeature * * @class * @extends geo.feature - * @returns {geo.graphFeature} + * @returns {geo.lineFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.graphFeature = function (arg) { +geo.lineFeature = function (arg) { "use strict"; - - if (!(this instanceof geo.graphFeature)) { - return new geo.graphFeature(arg); + if (!(this instanceof geo.lineFeature)) { + return new geo.lineFeature(arg); } arg = arg || {}; geo.feature.call(this, arg); @@ -20327,222 +22686,314 @@ geo.graphFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// var m_this = this, - s_draw = this.draw, - s_style = this.style, - m_nodes = null, - m_points = null, - m_children = function (d) { return d.children; }, - m_links = [], - s_init = this._init, - s_exit = this._exit; + s_init = this._init; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Get/Set line accessor + * + * @returns {geo.pointFeature} */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - - var defaultStyle = $.extend(true, {}, - { - nodes: { - radius: 5.0, - fill: true, - fillColor: { r: 1.0, g: 0.0, b: 0.0 }, - strokeColor: { r: 0, g: 0, b: 0 } - }, - links: { - strokeColor: { r: 0.0, g: 0.0, b: 0.0 } - }, - linkType: "path" /* 'path' || 'line' */ - }, - arg.style === undefined ? {} : arg.style - ); - - m_this.style(defaultStyle); - m_this.nodes(function (d) { return d; }); + this.line = function (val) { + if (val === undefined) { + return m_this.style("line"); + } else { + m_this.style("line", val); + m_this.dataTime().modified(); + m_this.modified(); + } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Call child _build methods + * Get/Set position accessor + * + * @returns {geo.pointFeature} */ //////////////////////////////////////////////////////////////////////////// - this._build = function () { - m_this.children().forEach(function (child) { - child._build(); - }); + this.position = function (val) { + if (val === undefined) { + return m_this.style("position"); + } else { + m_this.style("position", val); + m_this.dataTime().modified(); + m_this.modified(); + } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Call child _update methods + * Returns an array of datum indices that contain the given point. + * This is a slow implementation with runtime order of the number of + * vertices. */ //////////////////////////////////////////////////////////////////////////// - this._update = function () { - m_this.children().forEach(function (child) { - child._update(); - }); - }; + this.pointSearch = function (p) { + var data, pt, map, line, width, indices = [], found = [], pos; + data = m_this.data(); + if (!data || !data.length) { + return { + found: [], + index: [] + }; + } - //////////////////////////////////////////////////////////////////////////// - /** - * Custom _exit method to remove all sub-features - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.data([]); - m_links.forEach(function (link) { - link._exit(); - m_this.removeChild(link); - }); - m_links = []; - m_points._exit(); - m_this.removeChild(m_points); - s_exit(); - return m_this; - }; + map = m_this.layer().map(); + line = m_this.line(); + width = m_this.style.get("strokeWidth"); + pos = m_this.position(); + pt = m_this.featureGcsToDisplay(p); - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set style - */ - //////////////////////////////////////////////////////////////////////////// - this.style = function (arg, arg2) { - var out = s_style.call(m_this, arg, arg2); - if (out !== m_this) { - return out; + // minimum l2 distance squared from + // q -> line(u, v) + function lineDist2(q, u, v) { + var t, l2 = dist2(u, v); + + if (l2 < 1) { + // u, v are within 1 pixel + return dist2(q, u); + } + + t = ((q.x - u.x) * (v.x - u.x) + (q.y - u.y) * (v.y - u.y)) / l2; + if (t < 0) { return dist2(q, u); } + if (t > 1) { return dist2(q, v); } + return dist2( + q, + { + x: u.x + t * (v.x - u.x), + y: u.y + t * (v.y - u.y) + } + ); } - // set styles for sub-features - m_points.style(arg.nodes); - m_links.forEach(function (l) { - l.style(arg.links); + + // l2 distance squared from u to v + function dist2(u, v) { + var dx = u.x - v.x, + dy = u.y - v.y; + return dx * dx + dy * dy; + } + + // for each line + data.forEach(function (d, index) { + var last = null; + + try { + line(d, index).forEach(function (current, j) { + + // get the screen coordinates of the current point + var p = pos(current, j, d, index); + var s = m_this.featureGcsToDisplay(p); + var r = Math.ceil(width(p, j, d, index) / 2) + 2; + r = r * r; + + if (last) { + // test the line segment s -> last + if (lineDist2(pt, s, last) <= r) { + + // short circuit the loop here + throw "found"; + } + } + + last = s; + }); + } catch (err) { + if (err !== "found") { + throw err; + } + found.push(d); + indices.push(index); + } }); - return m_this; + + return { + data: found, + index: indices + }; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set links accessor. + * Returns an array of line indices that are contained in the given box. */ //////////////////////////////////////////////////////////////////////////// - this.links = function (arg) { - if (arg === undefined) { - return m_children; + this.boxSearch = function (lowerLeft, upperRight, opts) { + var pos = m_this.position(), + idx = [], + line = m_this.line(); + + opts = opts || {}; + opts.partial = opts.partial || false; + if (opts.partial) { + throw "Unimplemented query method."; } - m_children = geo.util.ensureFunction(arg); - return m_this; + m_this.data().forEach(function (d, i) { + var inside = true; + line(d, i).forEach(function (e, j) { + if (!inside) { return; } + var p = pos(e, j, d, i); + if (!(p.x >= lowerLeft.x && + p.x <= upperRight.x && + p.y >= lowerLeft.y && + p.y <= upperRight.y) + ) { + inside = false; + } + }); + if (inside) { + idx.push(i); + } + }); + return idx; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set nodes + * Initialize */ //////////////////////////////////////////////////////////////////////////// - this.nodes = function (val) { - if (val === undefined) { - return m_nodes; + this._init = function (arg) { + s_init.call(m_this, arg); + + var defaultStyle = $.extend( + {}, + { + "strokeWidth": 1.0, + // Default to gold color for lines + "strokeColor": { r: 1.0, g: 0.8431372549, b: 0.0 }, + "strokeStyle": "solid", + "strokeOpacity": 1.0, + "line": function (d) { return d; }, + "position": function (d) { return d; } + }, + arg.style === undefined ? {} : arg.style + ); + + if (arg.line !== undefined) { + defaultStyle.line = arg.line; } - m_nodes = val; - m_this.modified(); - return m_this; + + if (arg.position !== undefined) { + defaultStyle.position = arg.position; + } + + + m_this.style(defaultStyle); + + m_this.dataTime().modified(); }; + this._init(arg); + return this; +}; + +/** + * Create a lineFeature from an object. + * @see {@link geo.feature.create} + * @param {geo.layer} layer The layer to add the feature to + * @param {geo.lineFeature.spec} spec The object specification + * @returns {geo.lineFeature|null} + */ +geo.lineFeature.create = function (layer, spec) { + "use strict"; + + spec.type = "line"; + return geo.feature.create(layer, spec); +}; + +inherit(geo.lineFeature, geo.feature); + +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of class pathFeature + * + * @class + * @extends geo.feature + * @returns {geo.pathFeature} + */ +////////////////////////////////////////////////////////////////////////////// +geo.pathFeature = function (arg) { + "use strict"; + if (!(this instanceof geo.pathFeature)) { + return new geo.pathFeature(arg); + } + arg = arg || {}; + geo.feature.call(this, arg); + //////////////////////////////////////////////////////////////////////////// /** - * Get internal node feature + * @private */ //////////////////////////////////////////////////////////////////////////// - this.nodeFeature = function () { - return m_points; - }; + var m_this = this, + m_position = arg.position === undefined ? [] : arg.position, + s_init = this._init; //////////////////////////////////////////////////////////////////////////// /** - * Get internal link features + * Get/Set positions + * + * @returns {geo.pathFeature} */ //////////////////////////////////////////////////////////////////////////// - this.linkFeatures = function () { - return m_links; + this.position = function (val) { + if (val === undefined) { + return m_position; + } + // Copy incoming array of positions + m_position = val; + m_this.dataTime().modified(); + m_this.modified(); + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Build the feature for drawing + * Initialize */ //////////////////////////////////////////////////////////////////////////// - this.draw = function () { - - var layer = m_this.layer(), - data = m_this.data(), - nLinks = 0, - style; - - // get the feature style object - style = m_this.style(); - - // Bind data to the point nodes - m_points.data(data); - m_points.style(style.nodes); + this._init = function (arg) { + s_init.call(m_this, arg); - // get links from node connections - data.forEach(function (source) { - (source.children || []).forEach(function (target) { - var link; - nLinks += 1; - if (m_links.length < nLinks) { - link = geo.createFeature( - style.linkType, layer, layer.renderer() - ).style(style.links); - m_this.addChild(link); - m_links.push(link); - } - m_links[nLinks - 1].data([source, target]); - }); - }); + var defaultStyle = $.extend( + {}, + { + "strokeWidth": function () { return 1; }, + "strokeColor": function () { return { r: 1.0, g: 1.0, b: 1.0 }; } + }, + arg.style === undefined ? {} : arg.style + ); - m_links.splice(nLinks, m_links.length - nLinks).forEach(function (l) { - l._exit(); - m_this.removeChild(l); - }); + m_this.style(defaultStyle); - s_draw(); - return m_this; + if (m_position) { + m_this.dataTime().modified(); + } }; - m_points = geo.createFeature( - "point", - this.layer(), - this.layer().renderer() - ); - m_this.addChild(m_points); - - if (arg.nodes) { - this.nodes(arg.nodes); - } - this._init(arg); return this; }; -inherit(geo.graphFeature, geo.feature); +inherit(geo.pathFeature, geo.feature); ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of class contourFeature + * Create a new instance of class polygonFeature * * @class * @extends geo.feature - * @returns {geo.contourFeature} - * + * @returns {geo.polygonFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.contourFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.contourFeature)) { - return new geo.contourFeature(arg); +geo.polygonFeature = function (arg) { + "use strict"; + if (!(this instanceof geo.polygonFeature)) { + return new geo.polygonFeature(arg); } arg = arg || {}; geo.feature.call(this, arg); @@ -20553,16 +23004,26 @@ geo.contourFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// var m_this = this, - m_contour = {}, + m_position, + m_polygon, s_init = this._init, - s_data = this.data; + s_data = this.data, + m_coordinates = {outer: [], inner: []}; - if (arg.contour === undefined) { - m_contour = function (d) { + if (arg.polygon === undefined) { + m_polygon = function (d) { return d; }; } else { - m_contour = arg.contour; + m_polygon = arg.polygon; + } + + if (arg.position === undefined) { + m_position = function (d) { + return d; + }; + } else { + m_position = arg.position; } //////////////////////////////////////////////////////////////////////////// @@ -20573,87 +23034,60 @@ geo.contourFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this.data = function (arg) { var ret = s_data(arg); + if (arg !== undefined) { + getCoordinates(); + } return ret; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set contour accessor - * - * @returns {geo.pointFeature} + * 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. + * @private */ - //////////////////////////////////////////////////////////////////////////// - this.contour = function (arg1, arg2) { - if (arg1 === undefined) { - return m_contour; - } - if (typeof arg1 === 'string' && arg2 === undefined) { - return m_contour[arg1]; - } - if (arg2 === undefined) { - var contour = $.extend( - {}, - { - gridWidth: function () { - if (arg1.gridHeight) { - return Math.floor(m_this.data().length / arg1.gridHeight); - } - return Math.floor(Math.sqrt(m_this.data().length)); - }, - gridHeight: function () { - if (arg1.gridWidth) { - return Math.floor(m_this.data().length / arg1.gridWidth); - } - return Math.floor(Math.sqrt(m_this.data().length)); - }, - minColor: 'black', - minOpacity: 0, - maxColor: 'black', - maxOpacity: 0, - /* 9-step based on paraview bwr colortable */ - colorRange: [ - {r: 0.07514311, g: 0.468049805, b: 1}, - {r: 0.468487184, g: 0.588057293, b: 1}, - {r: 0.656658579, g: 0.707001303, b: 1}, - {r: 0.821573924, g: 0.837809045, b: 1}, - {r: 0.943467973, g: 0.943498599, b: 0.943398095}, - {r: 1, g: 0.788626485, b: 0.750707739}, - {r: 1, g: 0.6289553, b: 0.568237474}, - {r: 1, g: 0.472800903, b: 0.404551679}, - {r: 0.916482116, g: 0.236630659, b: 0.209939162} - ] - }, - m_contour, - arg1 - ); - m_contour = contour; - } else { - m_contour[arg1] = arg2; - } - m_this.modified(); - return m_this; - }; + //////////////////////////////////////////////////////////////////////////// + function getCoordinates() { + var posFunc = m_this.position(), + 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); + }); + + inner = (poly.inner || []).map(function (hole) { + return (hole || []).map(function (d0, k) { + return posFunc.call(m_this, d0, k, d, i); + }); + }); + return { + outer: outer, + inner: inner + }; + }); + } //////////////////////////////////////////////////////////////////////////// /** - * A uniform getter that always returns a function even for constant values. - * If undefined input, return all the contour values as an object. + * Get/Set polygon accessor * - * @param {string|undefined} key - * @return {function} + * @returns {geo.pointFeature} */ //////////////////////////////////////////////////////////////////////////// - this.contour.get = function (key) { - if (key === undefined) { - var all = {}, k; - for (k in m_contour) { - if (m_contour.hasOwnProperty(k)) { - all[k] = m_this.contour.get(k); - } - } - return all; + this.polygon = function (val) { + if (val === undefined) { + return m_polygon; + } else { + m_polygon = val; + m_this.dataTime().modified(); + m_this.modified(); + getCoordinates(); } - return geo.util.ensureFunction(m_contour[key]); + return m_this; }; //////////////////////////////////////////////////////////////////////////// @@ -20665,220 +23099,41 @@ geo.contourFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// this.position = function (val) { if (val === undefined) { - return m_this.style('position'); + return m_position; } else { - m_this.style('position', val); + m_position = val; m_this.dataTime().modified(); m_this.modified(); + getCoordinates(); } return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Create a set of vertices, values at the vertices, and opacities at the - * vertices. Create a set of triangles of indices into the vertex array. - * Create a color and opacity map corresponding to the values. - * - * @returns: an object with pos, value, opacity, elements, minValue, - * maxValue, minColor, maxColor, colorMap, factor. If there is no - * contour data that can be used, only elements is guaranteed to - * exist, and it will be a zero-length array. - */ - //////////////////////////////////////////////////////////////////////////// - this.createContours = function () { - var i, i3, j, idx, k, val, numPts, usedPts = 0, usePos, item, - idxMap = {}, - minval, maxval, range, - contour = m_this.contour, - data = m_this.data(), - posFunc = m_this.position(), posVal, - gridW = contour.get('gridWidth')(), - gridH = contour.get('gridHeight')(), - x0 = contour.get('x0')(), - y0 = contour.get('y0')(), - dx = contour.get('dx')(), - dy = contour.get('dy')(), - opacityFunc = m_this.style.get('opacity'), - opacityRange = contour.get('opacityRange')(), - rangeValues = contour.get('rangeValues')(), - valueFunc = m_this.style.get('value'), values = [], - stepped = contour.get('stepped')(), - wrapLong = contour.get('wrapLongitude')(), - calcX, skipColumn, x, origI, /* used for wrapping */ - gridWorig = gridW, /* can be different when wrapping */ - result = { - minValue: contour.get('min')(), - maxValue: contour.get('max')(), - stepped: stepped === undefined || stepped ? true : false, - wrapLongitude: wrapLong === undefined || wrapLong ? true : false, - colorMap: [], - elements: [] - }; - /* Create the min/max colors and the color array */ - result.minColor = $.extend({a: contour.get('minOpacity')() || 0}, - geo.util.convertColor(contour.get('minColor')())); - result.maxColor = $.extend({a: contour.get('maxOpacity')() || 0}, - geo.util.convertColor(contour.get('maxColor')())); - contour.get('colorRange')().forEach(function (clr, idx) { - result.colorMap.push($.extend( - {a: opacityRange && opacityRange[idx] !== undefined ? - opacityRange[idx] : 1}, geo.util.convertColor(clr))); - }); - /* Determine which values are usable */ - if (gridW * gridH > data.length) { - gridH = Math.floor(data.length) / gridW; - } - /* If we are not using the position values (we are using x0, y0, dx, dy), - * and wrapLongitude is turned on, and the position spans 180 degrees, - * duplicate one or two columns of points at opposite ends of the map. */ - usePos = (x0 === null || x0 === undefined || y0 === null || - y0 === undefined || !dx || !dy); - if (!usePos && result.wrapLongitude && (x0 < -180 || x0 > 180 || - x0 + dx * (gridW - 1) < -180 || x0 + dx * (gridW - 1) > 180) && - dx > -180 && dx < 180) { - calcX = []; - for (i = 0; i < gridW; i += 1) { - x = x0 + i * dx; - while (x < -180) { x += 360; } - while (x > 180) { x -= 360; } - if (i && Math.abs(x - calcX[calcX.length - 1]) > 180) { - if (x > calcX[calcX.length - 1]) { - calcX.push(x - 360); - calcX.push(calcX[calcX.length - 2] + 360); - } else { - calcX.push(x + 360); - calcX.push(calcX[calcX.length - 2] - 360); - } - skipColumn = i; - } - calcX.push(x); - } - gridW += 2; - if (Math.abs(Math.abs(gridWorig * dx) - 360) < 0.01) { - gridW += 1; - x = x0 + gridWorig * dx; - while (x < -180) { x += 360; } - while (x > 180) { x -= 360; } - calcX.push(x); - } - } - /* Calculate the value for point */ - numPts = gridW * gridH; - for (i = 0; i < numPts; i += 1) { - if (skipColumn === undefined) { - val = parseFloat(valueFunc(data[i])); - } else { - j = Math.floor(i / gridW); - origI = i - j * gridW; - origI += (origI > skipColumn ? -2 : 0); - if (origI >= gridWorig) { - origI -= gridWorig; - } - origI += j * gridWorig; - val = parseFloat(valueFunc(data[origI])); - } - values[i] = isNaN(val) ? null : val; - if (values[i] !== null) { - idxMap[i] = usedPts; - usedPts += 1; - if (minval === undefined) { - minval = maxval = values[i]; - } - if (values[i] < minval) { - minval = values[i]; - } - if (values[i] > maxval) { - maxval = values[i]; - } - } - } - if (!usedPts) { - return result; - } - if (!$.isNumeric(result.minValue)) { - result.minValue = minval; - } - if (!$.isNumeric(result.maxValue)) { - result.maxValue = maxval; - } - if (!rangeValues || rangeValues.length !== result.colorMap.length + 1) { - rangeValues = null; - } - if (rangeValues) { /* ensure increasing monotonicity */ - for (k = 1; k < rangeValues.length; k += 1) { - if (rangeValues[k] > rangeValues[k + 1]) { - rangeValues = null; - break; - } - } - } - if (rangeValues) { - result.minValue = rangeValues[0]; - result.maxValue = rangeValues[rangeValues.length - 1]; - } - range = result.maxValue - result.minValue; - if (!range) { - result.colorMap = result.colorMap.slice(0, 1); - range = 1; - rangeValues = null; - } - result.rangeValues = rangeValues; - result.factor = result.colorMap.length / range; - /* Create triangles */ - for (j = idx = 0; j < gridH - 1; j += 1, idx += 1) { - for (i = 0; i < gridW - 1; i += 1, idx += 1) { - if (values[idx] !== null && values[idx + 1] !== null && - values[idx + gridW] !== null && - values[idx + gridW + 1] !== null && i !== skipColumn) { - result.elements.push(idxMap[idx]); - result.elements.push(idxMap[idx + 1]); - result.elements.push(idxMap[idx + gridW]); - result.elements.push(idxMap[idx + gridW + 1]); - result.elements.push(idxMap[idx + gridW]); - result.elements.push(idxMap[idx + 1]); - } - } - } - /* Only locate the points that are in use. */ - result.pos = new Array(usedPts * 3); - result.value = new Array(usedPts); - result.opacity = new Array(usedPts); - for (j = i = i3 = 0; j < numPts; j += 1) { - val = values[j]; - if (val !== null) { - item = data[j]; - if (usePos) { - posVal = posFunc(item); - result.pos[i3] = posVal.x; - result.pos[i3 + 1] = posVal.y; - result.pos[i3 + 2] = posVal.z || 0; - } else { - if (skipColumn === undefined) { - result.pos[i3] = x0 + dx * (j % gridW); - } else { - result.pos[i3] = calcX[j % gridW]; - } - result.pos[i3 + 1] = y0 + dy * Math.floor(j / gridW); - result.pos[i3 + 2] = 0; - } - result.opacity[i] = opacityFunc(item); - if (rangeValues && val >= result.minValue && val <= result.maxValue) { - for (k = 1; k < rangeValues.length; k += 1) { - if (val <= rangeValues[k]) { - result.value[i] = k - 1 + (val - rangeValues[k - 1]) / - (rangeValues[k] - rangeValues[k - 1]); - break; - } - } - } else { - result.value[i] = (val - result.minValue) * result.factor; - } - i += 1; - i3 += 3; + * Point searce method for selection api. Returns markers containing the + * given point. + * @argument {Object} coordinate + * @returns {Object} + */ + //////////////////////////////////////////////////////////////////////////// + this.pointSearch = function (coordinate) { + var found = [], indices = [], data = m_this.data(); + m_coordinates.forEach(function (coord, i) { + var inside = geo.util.pointInPolygon( + coordinate, + coord.outer, + coord.inner + ); + if (inside) { + indices.push(i); + found.push(data[i]); } - } - return result; + }); + return { + index: indices, + found: found + }; }; //////////////////////////////////////////////////////////////////////////// @@ -20892,20 +23147,15 @@ geo.contourFeature = function (arg) { var defaultStyle = $.extend( {}, { - opacity: 1.0, - position: function (d) { - return {x: d.x, y: d.y, z: d.z}; - }, - value: function (d) { - return m_this.position()(d).z; - } + "fillColor": { r: 0.0, g: 0.5, b: 0.5 }, + "fillOpacity": 1.0 }, arg.style === undefined ? {} : arg.style ); m_this.style(defaultStyle); - if (m_contour) { + if (m_position) { m_this.dataTime().modified(); } }; @@ -20914,1607 +23164,1493 @@ geo.contourFeature = function (arg) { return this; }; -inherit(geo.contourFeature, geo.feature); - -/* Example: - -layer.createFeature('contour', { -}) -.data() -.position(function (d) { - return { x: , y: , z: }; -}) -.style({ - opacity: function (d) { - return ; - }, - value: function (d) { // defaults to position().z - return ; - } -}) -.contour({ - gridWidth: , - gridHeight: , - x0: , - y0: , - dx: , - dy: , - wrapLongitude: , - min: , - max: , - minColor: , - minOpacity: , - maxColor: , - maxOpacity: , - stepped: , - colorRange: [], - opacityRange: [], - rangeValues: [] -}) - -Notes: -* The position array is only used for position if not all of x0, y0, dx, and dy - are specified (not null or undefined). If a value array is not specified, - the position array could still be used for the value. -* If the value() of a grid point is null or undefined, that point will not be - included in the contour display. Since the values are on a grid, if this - point is in the interior of the grid, this can remove up to four squares. -* Only one of gridWidth and gridHeight needs to be specified. If both are - specified and gridWidth * gridHeight < data().length, not all the data will - be used. If neither are specified, floor(sqrt(data().length)) is used for - both. - */ +inherit(geo.polygonFeature, geo.feature); ////////////////////////////////////////////////////////////////////////////// /** - * Transform geometric data of a feature from source projection to destination - * projection. + * Create a new instance of class planeFeature * - * @namespace - */ -////////////////////////////////////////////////////////////////////////////// -geo.transform = {}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Custom transform for a feature used for OpenStreetMap + * @class + * @extends geo.polygonFeature + * @returns {geo.planeFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.transform.osmTransformFeature = function (destGcs, feature, inplace) { - /// TODO - /// Currently we make assumption that incoming feature is in 4326 - /// which may not be true. - +geo.planeFeature = function (arg) { "use strict"; - - if (!feature) { - console.log("[warning] Invalid (null) feature"); - return; - } - - if (feature.gcs() === destGcs) { - return; + if (!(this instanceof geo.planeFeature)) { + return new geo.planeFeature(arg); } + arg = arg || {}; - if (!(feature instanceof geo.pointFeature || - feature instanceof geo.lineFeature)) { - throw "Supports only point or line feature"; - } + // Defaults + arg.ul = arg.ul === undefined ? [0.0, 1.0, 0.0] : arg.ul; + arg.lr = arg.lr === undefined ? [1.0, 0.0, 0.0] : arg.lr; + arg.depth = arg.depth === undefined ? 0.0 : arg.depth; - var noOfComponents = null, - pointOffset = 0, - count = null, - inPos = null, - outPos = null, - srcGcs = feature.gcs(), - i, - yCoord; + geo.polygonFeature.call(this, arg); - inplace = !!inplace; - if (feature instanceof geo.pointFeature || - feature instanceof geo.lineFeature) { + var m_this = this, + m_origin = [arg.ul.x, arg.lr.y, arg.depth], + m_upperLeft = [arg.ul.x, arg.ul.y, arg.depth], + m_lowerRight = [arg.lr.x, arg.lr.y, arg.depth], + m_defaultDepth = arg.depth, + m_drawOnAsyncResourceLoad = arg.drawOnAsyncResourceLoad === undefined ? + true : false, + s_init = this._init; - /// If source GCS is not in 4326, transform it first into 4326 - /// before we transform it for OSM. - if (srcGcs !== "EPSG:4326") { - geo.transform.transformFeature("EPSG:4326", feature, true); + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set origin + * + * @returns {geo.planeFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.origin = function (val) { + if (val === undefined) { + return m_origin; + } else if (val instanceof Array) { + if (val.length > 3 || val.length < 2) { + throw "Origin point requires point in 2 or 3 dimension"; + } + m_origin = val.slice(0); + if (m_origin.length === 2) { + m_origin[2] = m_defaultDepth; + } } + m_this.dataTime().modified(); + m_this.modified(); + return m_this; + }; - inPos = feature.positions(); - count = inPos.length; - - if (!(inPos instanceof Array)) { - throw "Supports Array of 2D and 3D points"; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set pt1 + * + * @returns {geo.planeFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.upperLeft = function (val) { + if (val === undefined) { + return m_upperLeft; + } else if (val instanceof Array) { + if (val.length > 3 || val.length < 2) { + throw "Upper left point requires point in 2 or 3 dimension"; + } + m_upperLeft = val.slice(0); + if (m_upperLeft.length === 2) { + m_upperLeft[2] = m_defaultDepth; + } } + m_this.dataTime().modified(); + m_this.modified(); + return m_this; + }; - noOfComponents = (count % 2 === 0 ? 2 : - (count % 3 === 0 ? 3 : null)); - pointOffset = noOfComponents; - - if (noOfComponents !== 2 && noOfComponents !== 3) { - throw "Transform points require points in 2D or 3D"; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set origin + * + * @returns {geo.planeFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.lowerRight = function (val) { + if (val === undefined) { + return m_lowerRight; + } else if (val instanceof Array) { + if (val.length > 3 || val.length < 2) { + throw "Lower right point requires point in 2 or 3 dimension"; + } + m_lowerRight = val.slice(0); + if (m_lowerRight.length === 2) { + m_lowerRight[2] = m_defaultDepth; + } + m_this.dataTime().modified(); } + m_this.dataTime().modified(); + m_this.modified(); + return m_this; + }; - if (inplace) { - outPos = inPos; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set if draw should happen as soon as a async resource is loaded + */ + //////////////////////////////////////////////////////////////////////////// + this.drawOnAsyncResourceLoad = function (val) { + if (val === undefined) { + return m_drawOnAsyncResourceLoad; } else { - outPos = inPos.slice(0); - } - - for (i = 0; i < count; i += pointOffset) { - - /// Y goes from 0 (top edge is 85.0511 °N) to 2zoom − 1 - /// (bottom edge is 85.0511 °S) in a Mercator projection. - yCoord = inPos[i + 1]; - - if (yCoord > 85.0511) { - yCoord = 85.0511; - } - if (yCoord < -85.0511) { - yCoord = -85.0511; - } - outPos[i + 1] = geo.mercator.lat2y(yCoord); + m_drawOnAsyncResourceLoad = val; + return m_this; } + }; - if (inplace) { - feature.positions(outPos); - feature.gcs(destGcs); + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + var style = null; + s_init.call(m_this, arg); + style = m_this.style(); + if (style.image === undefined) { + style.image = null; } - return outPos; - } + m_this.style(style); + }; - return null; + this._init(arg); + return this; }; +inherit(geo.planeFeature, geo.polygonFeature); + ////////////////////////////////////////////////////////////////////////////// /** - * Transform a feature to destination GCS + * Create a new instance of class vectorFeature + * + * @class + * @extends geo.feature + * @returns {geo.vectorFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.transform.transformFeature = function (destGcs, feature, inplace) { - "use strict"; - - if (!feature) { - throw "Invalid (null) feature"; - } - - if (!(feature instanceof geo.pointFeature || - feature instanceof geo.lineFeature)) { - throw "Supports only point or line feature"; - } - - if (feature.gcs() === destGcs) { - return feature.positions(); - } - - if (destGcs === "EPSG:3857") { - return geo.transform.osmTransformFeature(destGcs, feature, inplace); - } - - var noOfComponents = null, - pointOffset = 0, - count = null, - inPos = null, - outPos = null, - projPoint = null, - srcGcs = feature.gcs(), - i, - projSrcGcs = new proj4.Proj(srcGcs), - projDestGcs = new proj4.Proj(destGcs); - - inplace = !!inplace; - if (feature instanceof geo.pointFeature || - feature instanceof geo.lineFeature) { - inPos = feature.positions(); - count = inPos.length; - - if (!(inPos instanceof Array)) { - throw "Supports Array of 2D and 3D points"; - } +geo.vectorFeature = function (arg) { + 'use strict'; + if (!(this instanceof geo.vectorFeature)) { + return new geo.vectorFeature(arg); + } + arg = arg || {}; + geo.feature.call(this, arg); - noOfComponents = (count % 2 === 0 ? 2 : - (count % 3 === 0 ? 3 : null)); - pointOffset = noOfComponents; + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + s_style = this.style; - if (noOfComponents !== 2 && noOfComponents !== 3) { - throw "Transform points require points in 2D or 3D"; + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the accessor for the origin of the vector. This is the point + * that the vector base resides at. Defaults to (0, 0, 0). + * @param {geo.accessor|geo.geoPosition} [accessor] The origin accessor + */ + //////////////////////////////////////////////////////////////////////////// + this.origin = function (val) { + if (val === undefined) { + return s_style('origin'); + } else { + s_style('origin', val); + m_this.dataTime().modified(); + m_this.modified(); } + return m_this; + }; - if (inplace) { - outPos = inPos; + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the accessor for the displacement (coordinates) of the vector. + * @param {geo.accessor|geo.geoPosition} [accessor] The accessor + */ + //////////////////////////////////////////////////////////////////////////// + this.delta = function (val) { + if (val === undefined) { + return s_style('delta'); } else { - outPos = []; - outPos.length = inPos.length; + s_style('delta', val); + m_this.dataTime().modified(); + m_this.modified(); } + return m_this; + }; - for (i = 0; i < count; i += pointOffset) { - if (noOfComponents === 2) { - projPoint = new proj4.Point(inPos[i], inPos[i + 1], 0.0); - } else { - projPoint = new proj4.Point(inPos[i], inPos[i + 1], inPos[i + 2]); - } - - proj4.transform(projSrcGcs, projDestGcs, projPoint); + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); - if (noOfComponents === 2) { - outPos[i] = projPoint.x; - outPos[i + 1] = projPoint.y; - } else { - outPos[i] = projPoint.x; - outPos[i + 1] = projPoint.y; - outPos[i + 2] = projPoint.z; - } - } + var defaultStyle = $.extend( + {}, + { + strokeColor: 'black', + strokeWidth: 2.0, + strokeOpacity: 1.0, + // TODO: define styles for the end markers + // originStyle: 'none', + // endStyle: 'arrow', + origin: {x: 0, y: 0, z: 0}, + delta: function (d) { return d; }, + scale: null // size scaling factor (null -> renderer decides) + }, + arg.style === undefined ? {} : arg.style + ); - if (inplace) { - feature.positions(outPos); - feature.gcs(destGcs); + if (arg.origin !== undefined) { + defaultStyle.origin = arg.origin; } - return outPos; - } - - return null; + m_this.style(defaultStyle); + m_this.dataTime().modified(); + }; }; +inherit(geo.vectorFeature, geo.feature); + ////////////////////////////////////////////////////////////////////////////// /** - * Transform geometric data of a layer from source projection to destination - * projection. + * Create a new instance of class geomFeature + * + * @class + * @extends geo.feature + * @returns {geo.geomFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.transform.transformLayer = function (destGcs, layer, baseLayer) { +geo.geomFeature = function (arg) { "use strict"; - - var features, count, i; - - if (!layer) { - throw "Requires valid layer for tranformation"; - } - - if (!baseLayer) { - throw "Requires baseLayer used by the map"; - } - - if (layer === baseLayer) { - return; + if (!(this instanceof geo.geomFeature)) { + return new geo.geomFeature(arg); } + arg = arg || {}; + geo.feature.call(this, arg); - if (layer instanceof geo.featureLayer) { - features = layer.features(); - count = features.length; - i = 0; + arg.style = arg.style === undefined ? $.extend({}, { + "color": [1.0, 1.0, 1.0], + "point_sprites": false, + "point_sprites_image": null + }, arg.style) : arg.style; - for (i = 0; i < count; i += 1) { - if (destGcs === "EPSG:3857" && baseLayer instanceof geo.osmLayer) { - geo.transform.osmTransformFeature( - destGcs, features[i], true); - } else { - geo.transform.transformFeature( - destGcs, features[i], true); - } - } + // Update style + this.style(arg.style); - layer.gcs(destGcs); - } else { - throw "Only feature layer transformation is supported"; - } + return this; }; +inherit(geo.geomFeature, geo.feature); + ////////////////////////////////////////////////////////////////////////////// /** - * Transform position coordinates from source projection to destination - * projection. + * Create a new instance of class graphFeature * - * @param {string} srcGcs GCS of the coordinates - * @param {string} destGcs Desired GCS of the transformed coordinates - * @param {object} coordinates - * @return {object|object[]} Transformed coordinates + * @class + * @extends geo.feature + * @returns {geo.graphFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.transform.transformCoordinates = function (srcGcs, destGcs, coordinates, - numberOfComponents) { +geo.graphFeature = function (arg) { "use strict"; - var i, count, offset, xCoord, yCoord, zCoord, xAcc, - yAcc, zAcc, writer, output, projPoint, - projSrcGcs = new proj4.Proj(srcGcs), - projDestGcs = new proj4.Proj(destGcs); + if (!(this instanceof geo.graphFeature)) { + return new geo.graphFeature(arg); + } + arg = arg || {}; + geo.feature.call(this, arg); - /// Default Z accessor - zAcc = function () { - return 0.0; + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_draw = this.draw, + s_style = this.style, + m_nodes = null, + m_points = null, + m_children = function (d) { return d.children; }, + m_links = [], + s_init = this._init, + s_exit = this._exit; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + + var defaultStyle = $.extend(true, {}, + { + nodes: { + radius: 5.0, + fill: true, + fillColor: { r: 1.0, g: 0.0, b: 0.0 }, + strokeColor: { r: 0, g: 0, b: 0 } + }, + links: { + strokeColor: { r: 0.0, g: 0.0, b: 0.0 } + }, + linkType: "path" /* 'path' || 'line' */ + }, + arg.style === undefined ? {} : arg.style + ); + + m_this.style(defaultStyle); + m_this.nodes(function (d) { return d; }); }; - if (destGcs === srcGcs) { - return coordinates; - } + //////////////////////////////////////////////////////////////////////////// + /** + * Call child _build methods + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + m_this.children().forEach(function (child) { + child._build(); + }); + }; - /// TODO: Can we check for EPSG code? - if (!destGcs || !srcGcs) { - throw "Invalid source or destination GCS"; - } + //////////////////////////////////////////////////////////////////////////// + /** + * Call child _update methods + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + m_this.children().forEach(function (child) { + child._update(); + }); + }; - /// Helper methods - function handleArrayCoordinates() { - if (coordinates[0] instanceof Array) { - if (coordinates[0].length === 2) { - xAcc = function (index) { - return coordinates[index][0]; - }; - yAcc = function (index) { - return coordinates[index][1]; - }; - writer = function (index, x, y) { - output[index] = [x, y]; - }; - } else if (coordinates[0].length === 3) { - xAcc = function (index) { - return coordinates[index][0]; - }; - yAcc = function (index) { - return coordinates[index][1]; - }; - zAcc = function (index) { - return coordinates[index][2]; - }; - writer = function (index, x, y, z) { - output[index] = [x, y, z]; - }; - } else { - throw "Invalid coordinates. Requires two or three components per array"; - } - } else { - if (coordinates.length === 2) { - offset = 2; + //////////////////////////////////////////////////////////////////////////// + /** + * Custom _exit method to remove all sub-features + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.data([]); + m_links.forEach(function (link) { + link._exit(); + m_this.removeChild(link); + }); + m_links = []; + m_points._exit(); + m_this.removeChild(m_points); + s_exit(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set style + */ + //////////////////////////////////////////////////////////////////////////// + this.style = function (arg, arg2) { + var out = s_style.call(m_this, arg, arg2); + if (out !== m_this) { + return out; + } + // set styles for sub-features + m_points.style(arg.nodes); + m_links.forEach(function (l) { + l.style(arg.links); + }); + return m_this; + }; - xAcc = function (index) { - return coordinates[index * offset]; - }; - yAcc = function (index) { - return coordinates[index * offset + 1]; - }; - writer = function (index, x, y) { - output[index] = x; - output[index + 1] = y; - }; - } else if (coordinates.length === 3) { - offset = 3; + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set links accessor. + */ + //////////////////////////////////////////////////////////////////////////// + this.links = function (arg) { + if (arg === undefined) { + return m_children; + } - xAcc = function (index) { - return coordinates[index * offset]; - }; - yAcc = function (index) { - return coordinates[index * offset + 1]; - }; - zAcc = function (index) { - return coordinates[index * offset + 2]; - }; - writer = function (index, x, y, z) { - output[index] = x; - output[index + 1] = y; - output[index + 2] = z; - }; - } else if (numberOfComponents) { - if (numberOfComponents === 2 || numberOfComponents || 3) { - offset = numberOfComponents; + m_children = geo.util.ensureFunction(arg); + return m_this; + }; - xAcc = function (index) { - return coordinates[index]; - }; - yAcc = function (index) { - return coordinates[index + 1]; - }; - if (numberOfComponents === 2) { - writer = function (index, x, y) { - output[index] = x; - output[index + 1] = y; - }; - } else { - zAcc = function (index) { - return coordinates[index + 2]; - }; - writer = function (index, x, y, z) { - output[index] = x; - output[index + 1] = y; - output[index + 2] = z; - }; - } - } else { - throw "Number of components should be two or three"; - } - } else { - throw "Invalid coordinates"; - } + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set nodes + */ + //////////////////////////////////////////////////////////////////////////// + this.nodes = function (val) { + if (val === undefined) { + return m_nodes; } - } + m_nodes = val; + m_this.modified(); + return m_this; + }; - /// Helper methods - function handleObjectCoordinates() { - if (coordinates[0] && - "x" in coordinates[0] && - "y" in coordinates[0]) { - xAcc = function (index) { - return coordinates[index].x; - }; - yAcc = function (index) { - return coordinates[index].y; - }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get internal node feature + */ + //////////////////////////////////////////////////////////////////////////// + this.nodeFeature = function () { + return m_points; + }; - if ("z" in coordinates[0]) { - zAcc = function (index) { - return coordinates[index].z; - }; - writer = function (index, x, y, z) { - output[i] = {x: x, y: y, z: z}; - }; - } else { - writer = function (index, x, y) { - output[index] = {x: x, y: y}; - }; - } - } else if (coordinates && - "x" in coordinates && "y" in coordinates) { - xAcc = function () { - return coordinates.x; - }; - yAcc = function () { - return coordinates.y; - }; + //////////////////////////////////////////////////////////////////////////// + /** + * Get internal link features + */ + //////////////////////////////////////////////////////////////////////////// + this.linkFeatures = function () { + return m_links; + }; - if ("z" in coordinates) { - zAcc = function () { - return coordinates.z; - }; - writer = function (index, x, y, z) { - output = {x: x, y: y, z: z}; - }; - } else { - writer = function (index, x, y) { - output = {x: x, y: y}; - }; - } - } else { - throw "Invalid coordinates"; - } - } + //////////////////////////////////////////////////////////////////////////// + /** + * Build the feature for drawing + */ + //////////////////////////////////////////////////////////////////////////// + this.draw = function () { - if (coordinates instanceof Array) { - output = []; - output.length = coordinates.length; - count = coordinates.length; + var layer = m_this.layer(), + data = m_this.data(), + nLinks = 0, + style; - if (coordinates[0] instanceof Array || - coordinates[0] instanceof Object) { - offset = 1; + // get the feature style object + style = m_this.style(); - if (coordinates[0] instanceof Array) { - handleArrayCoordinates(); - } else if (coordinates[0] instanceof Object) { - handleObjectCoordinates(); - } - } else { - handleArrayCoordinates(); - } - } else if (coordinates && coordinates instanceof Object) { - count = 1; - offset = 1; - if (coordinates && "x" in coordinates && "y" in coordinates) { - handleObjectCoordinates(); - } else { - throw "Coordinates are not valid"; - } - } + // Bind data to the point nodes + m_points.data(data); + m_points.style(style.nodes); - if (destGcs === "EPSG:3857" && srcGcs === "EPSG:4326") { - for (i = 0; i < count; i += offset) { - /// Y goes from 0 (top edge is 85.0511 °N) to 2zoom − 1 - /// (bottom edge is 85.0511 °S) in a Mercator projection. - xCoord = xAcc(i); - yCoord = yAcc(i); - zCoord = zAcc(i); + // get links from node connections + data.forEach(function (source) { + (source.children || []).forEach(function (target) { + var link; + nLinks += 1; + if (m_links.length < nLinks) { + link = geo.createFeature( + style.linkType, layer, layer.renderer() + ).style(style.links); + m_this.addChild(link); + m_links.push(link); + } + m_links[nLinks - 1].data([source, target]); + }); + }); - if (yCoord > 85.0511) { - yCoord = 85.0511; - } - if (yCoord < -85.0511) { - yCoord = -85.0511; - } + m_links.splice(nLinks, m_links.length - nLinks).forEach(function (l) { + l._exit(); + m_this.removeChild(l); + }); - writer(i, xCoord, geo.mercator.lat2y(yCoord), zCoord); - } + s_draw(); + return m_this; + }; - return output; - } else { - for (i = 0; i < count; i += offset) { - projPoint = new proj4.Point(xAcc(i), yAcc(i), zAcc(i)); - proj4.transform(projSrcGcs, projDestGcs, projPoint); - writer(i, projPoint.x, projPoint.y, projPoint.z); - return output; - } + m_points = geo.createFeature( + "point", + this.layer(), + this.layer().renderer() + ); + m_this.addChild(m_points); + + if (arg.nodes) { + this.nodes(arg.nodes); } + + this._init(arg); + return this; }; +inherit(geo.graphFeature, geo.feature); + ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of class renderer + * Create a new instance of class contourFeature * * @class - * @extends geo.object - * @returns {geo.renderer} + * @extends geo.feature + * @returns {geo.contourFeature} + * */ ////////////////////////////////////////////////////////////////////////////// -geo.renderer = function (arg) { - "use strict"; - - if (!(this instanceof geo.renderer)) { - return new geo.renderer(arg); +geo.contourFeature = function (arg) { + 'use strict'; + if (!(this instanceof geo.contourFeature)) { + return new geo.contourFeature(arg); } - geo.object.call(this); - arg = arg || {}; + geo.feature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// var m_this = this, - m_layer = arg.layer === undefined ? null : arg.layer, - m_canvas = arg.canvas === undefined ? null : arg.canvas, - m_initialized = false; + m_contour = {}, + s_init = this._init, + s_data = this.data; + + if (arg.contour === undefined) { + m_contour = function (d) { + return d; + }; + } else { + m_contour = arg.contour; + } //////////////////////////////////////////////////////////////////////////// /** - * Get layer of the renderer - * - * @returns {*} + * Override the parent data method to keep track of changes to the + * internal coordinates. */ //////////////////////////////////////////////////////////////////////////// - this.layer = function () { - return m_layer; + this.data = function (arg) { + var ret = s_data(arg); + return ret; }; //////////////////////////////////////////////////////////////////////////// /** - * Get canvas for the renderer + * Get/Set contour accessor + * + * @returns {geo.pointFeature} */ //////////////////////////////////////////////////////////////////////////// - this.canvas = function (val) { - if (val === undefined) { - return m_canvas; + this.contour = function (arg1, arg2) { + if (arg1 === undefined) { + return m_contour; + } + if (typeof arg1 === 'string' && arg2 === undefined) { + return m_contour[arg1]; + } + if (arg2 === undefined) { + var contour = $.extend( + {}, + { + gridWidth: function () { + if (arg1.gridHeight) { + return Math.floor(m_this.data().length / arg1.gridHeight); + } + return Math.floor(Math.sqrt(m_this.data().length)); + }, + gridHeight: function () { + if (arg1.gridWidth) { + return Math.floor(m_this.data().length / arg1.gridWidth); + } + return Math.floor(Math.sqrt(m_this.data().length)); + }, + minColor: 'black', + minOpacity: 0, + maxColor: 'black', + maxOpacity: 0, + /* 9-step based on paraview bwr colortable */ + colorRange: [ + {r: 0.07514311, g: 0.468049805, b: 1}, + {r: 0.468487184, g: 0.588057293, b: 1}, + {r: 0.656658579, g: 0.707001303, b: 1}, + {r: 0.821573924, g: 0.837809045, b: 1}, + {r: 0.943467973, g: 0.943498599, b: 0.943398095}, + {r: 1, g: 0.788626485, b: 0.750707739}, + {r: 1, g: 0.6289553, b: 0.568237474}, + {r: 1, g: 0.472800903, b: 0.404551679}, + {r: 0.916482116, g: 0.236630659, b: 0.209939162} + ] + }, + m_contour, + arg1 + ); + m_contour = contour; } else { - m_canvas = val; - m_this.modified(); + m_contour[arg1] = arg2; } + m_this.modified(); + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get map that this renderer belongs to + * A uniform getter that always returns a function even for constant values. + * If undefined input, return all the contour values as an object. + * + * @param {string|undefined} key + * @return {function} */ //////////////////////////////////////////////////////////////////////////// - this.map = function () { - if (m_layer) { - return m_layer.map(); - } else { - return null; + this.contour.get = function (key) { + if (key === undefined) { + var all = {}, k; + for (k in m_contour) { + if (m_contour.hasOwnProperty(k)) { + all[k] = m_this.contour.get(k); + } + } + return all; } + return geo.util.ensureFunction(m_contour[key]); }; //////////////////////////////////////////////////////////////////////////// /** - * Get base layer that belongs to this renderer + * Get/Set position accessor + * + * @returns {geo.pointFeature} */ //////////////////////////////////////////////////////////////////////////// - this.baseLayer = function () { - if (m_this.map()) { - return m_this.map().baseLayer(); + this.position = function (val) { + if (val === undefined) { + return m_this.style('position'); + } else { + m_this.style('position', val); + m_this.dataTime().modified(); + m_this.modified(); } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set if renderer has been initialized + * Create a set of vertices, values at the vertices, and opacities at the + * vertices. Create a set of triangles of indices into the vertex array. + * Create a color and opacity map corresponding to the values. + * + * @returns: an object with pos, value, opacity, elements, minValue, + * maxValue, minColor, maxColor, colorMap, factor. If there is no + * contour data that can be used, only elements is guaranteed to + * exist, and it will be a zero-length array. */ //////////////////////////////////////////////////////////////////////////// - this.initialized = function (val) { - if (val === undefined) { - return m_initialized; - } else { - m_initialized = val; - return m_this; + this.createContours = function () { + var i, i3, j, idx, k, val, numPts, usedPts = 0, usePos, item, + idxMap = {}, + minval, maxval, range, + contour = m_this.contour, + data = m_this.data(), + posFunc = m_this.position(), posVal, + gridW = contour.get('gridWidth')(), + gridH = contour.get('gridHeight')(), + x0 = contour.get('x0')(), + y0 = contour.get('y0')(), + dx = contour.get('dx')(), + dy = contour.get('dy')(), + opacityFunc = m_this.style.get('opacity'), + opacityRange = contour.get('opacityRange')(), + rangeValues = contour.get('rangeValues')(), + valueFunc = m_this.style.get('value'), values = [], + stepped = contour.get('stepped')(), + wrapLong = contour.get('wrapLongitude')(), + calcX, skipColumn, x, origI, /* used for wrapping */ + gridWorig = gridW, /* can be different when wrapping */ + result = { + minValue: contour.get('min')(), + maxValue: contour.get('max')(), + stepped: stepped === undefined || stepped ? true : false, + wrapLongitude: wrapLong === undefined || wrapLong ? true : false, + colorMap: [], + elements: [] + }; + /* Create the min/max colors and the color array */ + result.minColor = $.extend({a: contour.get('minOpacity')() || 0}, + geo.util.convertColor(contour.get('minColor')())); + result.maxColor = $.extend({a: contour.get('maxOpacity')() || 0}, + geo.util.convertColor(contour.get('maxColor')())); + contour.get('colorRange')().forEach(function (clr, idx) { + result.colorMap.push($.extend( + {a: opacityRange && opacityRange[idx] !== undefined ? + opacityRange[idx] : 1}, geo.util.convertColor(clr))); + }); + /* Determine which values are usable */ + if (gridW * gridH > data.length) { + gridH = Math.floor(data.length) / gridW; + } + /* If we are not using the position values (we are using x0, y0, dx, dy), + * and wrapLongitude is turned on, and the position spans 180 degrees, + * duplicate one or two columns of points at opposite ends of the map. */ + usePos = (x0 === null || x0 === undefined || y0 === null || + y0 === undefined || !dx || !dy); + if (!usePos && result.wrapLongitude && (x0 < -180 || x0 > 180 || + x0 + dx * (gridW - 1) < -180 || x0 + dx * (gridW - 1) > 180) && + dx > -180 && dx < 180) { + calcX = []; + for (i = 0; i < gridW; i += 1) { + x = x0 + i * dx; + while (x < -180) { x += 360; } + while (x > 180) { x -= 360; } + if (i && Math.abs(x - calcX[calcX.length - 1]) > 180) { + if (x > calcX[calcX.length - 1]) { + calcX.push(x - 360); + calcX.push(calcX[calcX.length - 2] + 360); + } else { + calcX.push(x + 360); + calcX.push(calcX[calcX.length - 2] - 360); + } + skipColumn = i; + } + calcX.push(x); + } + gridW += 2; + if (Math.abs(Math.abs(gridWorig * dx) - 360) < 0.01) { + gridW += 1; + x = x0 + gridWorig * dx; + while (x < -180) { x += 360; } + while (x > 180) { x -= 360; } + calcX.push(x); + } + } + /* Calculate the value for point */ + numPts = gridW * gridH; + for (i = 0; i < numPts; i += 1) { + if (skipColumn === undefined) { + val = parseFloat(valueFunc(data[i])); + } else { + j = Math.floor(i / gridW); + origI = i - j * gridW; + origI += (origI > skipColumn ? -2 : 0); + if (origI >= gridWorig) { + origI -= gridWorig; + } + origI += j * gridWorig; + val = parseFloat(valueFunc(data[origI])); + } + values[i] = isNaN(val) ? null : val; + if (values[i] !== null) { + idxMap[i] = usedPts; + usedPts += 1; + if (minval === undefined) { + minval = maxval = values[i]; + } + if (values[i] < minval) { + minval = values[i]; + } + if (values[i] > maxval) { + maxval = values[i]; + } + } + } + if (!usedPts) { + return result; + } + if (!$.isNumeric(result.minValue)) { + result.minValue = minval; + } + if (!$.isNumeric(result.maxValue)) { + result.maxValue = maxval; + } + if (!rangeValues || rangeValues.length !== result.colorMap.length + 1) { + rangeValues = null; + } + if (rangeValues) { /* ensure increasing monotonicity */ + for (k = 1; k < rangeValues.length; k += 1) { + if (rangeValues[k] > rangeValues[k + 1]) { + rangeValues = null; + break; + } + } + } + if (rangeValues) { + result.minValue = rangeValues[0]; + result.maxValue = rangeValues[rangeValues.length - 1]; + } + range = result.maxValue - result.minValue; + if (!range) { + result.colorMap = result.colorMap.slice(0, 1); + range = 1; + rangeValues = null; + } + result.rangeValues = rangeValues; + result.factor = result.colorMap.length / range; + /* Create triangles */ + for (j = idx = 0; j < gridH - 1; j += 1, idx += 1) { + for (i = 0; i < gridW - 1; i += 1, idx += 1) { + if (values[idx] !== null && values[idx + 1] !== null && + values[idx + gridW] !== null && + values[idx + gridW + 1] !== null && i !== skipColumn) { + result.elements.push(idxMap[idx]); + result.elements.push(idxMap[idx + 1]); + result.elements.push(idxMap[idx + gridW]); + result.elements.push(idxMap[idx + gridW + 1]); + result.elements.push(idxMap[idx + gridW]); + result.elements.push(idxMap[idx + 1]); + } + } } + /* Only locate the points that are in use. */ + result.pos = new Array(usedPts * 3); + result.value = new Array(usedPts); + result.opacity = new Array(usedPts); + for (j = i = i3 = 0; j < numPts; j += 1) { + val = values[j]; + if (val !== null) { + item = data[j]; + if (usePos) { + posVal = posFunc(item); + result.pos[i3] = posVal.x; + result.pos[i3 + 1] = posVal.y; + result.pos[i3 + 2] = posVal.z || 0; + } else { + if (skipColumn === undefined) { + result.pos[i3] = x0 + dx * (j % gridW); + } else { + result.pos[i3] = calcX[j % gridW]; + } + result.pos[i3 + 1] = y0 + dy * Math.floor(j / gridW); + result.pos[i3 + 2] = 0; + } + result.opacity[i] = opacityFunc(item); + if (rangeValues && val >= result.minValue && val <= result.maxValue) { + for (k = 1; k < rangeValues.length; k += 1) { + if (val <= rangeValues[k]) { + result.value[i] = k - 1 + (val - rangeValues[k - 1]) / + (rangeValues[k] - rangeValues[k - 1]); + break; + } + } + } else { + result.value[i] = (val - result.minValue) * result.factor; + } + i += 1; + i3 += 3; + } + } + return result; }; //////////////////////////////////////////////////////////////////////////// /** - * Get render API used by the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.api = function () { - throw "Should be implemented by derivied classes"; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Reset to default - */ - //////////////////////////////////////////////////////////////////////////// - this.reset = function () { - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert array of points from world to GCS coordinate space - */ - //////////////////////////////////////////////////////////////////////////// - this.worldToGcs = function () { - throw "Should be implemented by derivied classes"; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert array of points from display to GCS space - */ - //////////////////////////////////////////////////////////////////////////// - this.displayToGcs = function () { - throw "Should be implemented by derivied classes"; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert array of points from display to GCS space - */ - //////////////////////////////////////////////////////////////////////////// - this.gcsToDisplay = function () { - throw "Should be implemented by derivied classes"; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert array of points from world to display space - */ - //////////////////////////////////////////////////////////////////////////// - this.worldToDisplay = function () { - throw "Should be implemented by derivied classes"; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert array of points from display to world space - */ - //////////////////////////////////////////////////////////////////////////// - this.displayToWorld = function () { - throw "Should be implemented by derivied classes"; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get mouse coodinates related to canvas - * - * @param {object} event - * @returns {object} + * Initialize */ //////////////////////////////////////////////////////////////////////////// - this.relMouseCoords = function (event) { - var totalOffsetX = 0, - totalOffsetY = 0, - canvasX = 0, - canvasY = 0, - currentElement = m_this.canvas(); + this._init = function (arg) { + s_init.call(m_this, arg); - do { - totalOffsetX += currentElement.offsetLeft - currentElement.scrollLeft; - totalOffsetY += currentElement.offsetTop - currentElement.scrollTop; - currentElement = currentElement.offsetParent; - } while (currentElement); + var defaultStyle = $.extend( + {}, + { + opacity: 1.0, + position: function (d) { + return {x: d.x, y: d.y, z: d.z}; + }, + value: function (d) { + return m_this.position()(d).z; + } + }, + arg.style === undefined ? {} : arg.style + ); - canvasX = event.pageX - totalOffsetX; - canvasY = event.pageY - totalOffsetY; + m_this.style(defaultStyle); - return { - x: canvasX, - y: canvasY - }; + if (m_contour) { + m_this.dataTime().modified(); + } }; - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function () { - }; + this._init(arg); + return this; +}; - //////////////////////////////////////////////////////////////////////////// - /** - * Handle resize event - */ - //////////////////////////////////////////////////////////////////////////// - this._resize = function () { - }; +inherit(geo.contourFeature, geo.feature); - //////////////////////////////////////////////////////////////////////////// - /** - * Render - */ - //////////////////////////////////////////////////////////////////////////// - this._render = function () { - }; +/* Example: - return this; -}; +layer.createFeature('contour', { +}) +.data() +.position(function (d) { + return { x: , y: , z: }; +}) +.style({ + opacity: function (d) { + return ; + }, + value: function (d) { // defaults to position().z + return ; + } +}) +.contour({ + gridWidth: , + gridHeight: , + x0: , + y0: , + dx: , + dy: , + wrapLongitude: , + min: , + max: , + minColor: , + minOpacity: , + maxColor: , + maxOpacity: , + stepped: , + colorRange: [], + opacityRange: [], + rangeValues: [] +}) -inherit(geo.renderer, geo.object); +Notes: +* The position array is only used for position if not all of x0, y0, dx, and dy + are specified (not null or undefined). If a value array is not specified, + the position array could still be used for the value. +* If the value() of a grid point is null or undefined, that point will not be + included in the contour display. Since the values are on a grid, if this + point is in the interior of the grid, this can remove up to four squares. +* Only one of gridWidth and gridHeight needs to be specified. If both are + specified and gridWidth * gridHeight < data().length, not all the data will + be used. If neither are specified, floor(sqrt(data().length)) is used for + both. + */ ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of osmLayer + * Create a new instance of class renderer * * @class - * @extends geo.featureLayer - * - * @param {Object} arg - arg can contain following keys: baseUrl, - * imageFormat (such as png or jpeg), and displayLast - * (to decide whether or not render tiles from last zoom level). + * @extends geo.object + * @returns {geo.renderer} */ ////////////////////////////////////////////////////////////////////////////// -geo.osmLayer = function (arg) { - 'use strict'; +geo.renderer = function (arg) { + "use strict"; - if (!(this instanceof geo.osmLayer)) { - return new geo.osmLayer(arg); + if (!(this instanceof geo.renderer)) { + return new geo.renderer(arg); } + geo.object.call(this); - // set a default attribution if no other is provided arg = arg || {}; - if (arg && arg.attribution === undefined) { - arg.attribution = - '© OpenStreetMap contributors'; - } - geo.featureLayer.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * Private member variables - * - * @private - */ - //////////////////////////////////////////////////////////////////////////// var m_this = this, - s_exit = this._exit, - m_tiles = {}, - m_hiddenBinNumber = -1, - m_lastVisibleBinNumber = -1, - m_visibleBinNumber = 1000, - m_pendingNewTiles = [], - m_pendingInactiveTiles = [], - m_numberOfCachedTiles = 0, - m_tileCacheSize = 100, - m_baseUrl = 'http://tile.openstreetmap.org/', - m_mapOpacity = 1.0, - m_imageFormat = 'png', - m_updateTimerId = null, - m_lastVisibleZoom = null, - m_visibleTilesRange = {}, - s_init = this._init, - m_pendingNewTilesStat = {}, - s_update = this._update, - m_updateDefer = null, - m_zoom = null, - m_tileUrl, - m_tileUrlFromTemplate, - m_crossOrigin = 'anonymous'; - - if (arg && arg.baseUrl !== undefined) { - m_baseUrl = arg.baseUrl; - } - - if (m_baseUrl.charAt(m_baseUrl.length - 1) !== '/') { - m_baseUrl += '/'; - } - - if (arg && arg.mapOpacity !== undefined) { - m_mapOpacity = arg.mapOpacity; - } - if (arg && arg.imageFormat !== undefined) { - m_imageFormat = arg.imageFormat; - } - - if (arg && arg.displayLast !== undefined && arg.displayLast) { - m_lastVisibleBinNumber = 999; - } - - if (arg && arg.useCredentials !== undefined && arg.useCredentials) { - m_crossOrigin = 'use-credentials'; - } + m_layer = arg.layer === undefined ? null : arg.layer, + m_canvas = arg.canvas === undefined ? null : arg.canvas, + m_initialized = false; //////////////////////////////////////////////////////////////////////////// /** - * Returns a url string containing the requested tile. This default - * version uses the open street map standard, but the user can - * change the default behavior. + * Get layer of the renderer * - * @param {integer} zoom The zoom level - * @param {integer} x The tile from the xth row - * @param {integer} y The tile from the yth column - * @private + * @returns {*} */ //////////////////////////////////////////////////////////////////////////// - m_tileUrl = function (zoom, x, y) { - return m_baseUrl + zoom + '/' + x + - '/' + y + '.' + m_imageFormat; + this.layer = function () { + return m_layer; }; //////////////////////////////////////////////////////////////////////////// /** - * Returns an OSM tile server formatting function from a standard format - * string: replaces , , and . - * - * @param {string} base The tile format string - * @private + * Get canvas for the renderer */ //////////////////////////////////////////////////////////////////////////// - m_tileUrlFromTemplate = function (base) { - return function (zoom, x, y) { - return base.replace('', zoom) - .replace('', x) - .replace('', y); - }; + this.canvas = function (val) { + if (val === undefined) { + return m_canvas; + } else { + m_canvas = val; + m_this.modified(); + } }; //////////////////////////////////////////////////////////////////////////// /** - * Check if a tile is visible in current view - * - * @private + * Get map that this renderer belongs to */ //////////////////////////////////////////////////////////////////////////// - function isTileVisible(tile) { - if (tile.zoom in m_visibleTilesRange) { - if (tile.index_x >= m_visibleTilesRange[tile.zoom].startX && - tile.index_x <= m_visibleTilesRange[tile.zoom].endX && - tile.index_y >= m_visibleTilesRange[tile.zoom].startY && - tile.index_y <= m_visibleTilesRange[tile.zoom].endY) { - return true; - } + this.map = function () { + if (m_layer) { + return m_layer.map(); + } else { + return null; } - return false; - } + }; //////////////////////////////////////////////////////////////////////////// /** - * Draw new tiles and remove the old ones - * - * @private + * Get base layer that belongs to this renderer */ //////////////////////////////////////////////////////////////////////////// - function drawTiles() { - m_this._removeTiles(); - m_this.draw(); - delete m_pendingNewTilesStat[m_updateTimerId]; - } + this.baseLayer = function () { + if (m_this.map()) { + return m_this.map().baseLayer(); + } + }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set tile cache size + * Get/Set if renderer has been initialized */ //////////////////////////////////////////////////////////////////////////// - this.tileCacheSize = function (val) { + this.initialized = function (val) { if (val === undefined) { - return m_tileCacheSize; + return m_initialized; + } else { + m_initialized = val; + return m_this; } - m_tileCacheSize = val; - m_this.modified(); }; //////////////////////////////////////////////////////////////////////////// /** - * Get/Set the tile url formatting function. + * Get render API used by the renderer */ //////////////////////////////////////////////////////////////////////////// - this.tileUrl = function (val) { - if (val === undefined) { - return m_tileUrl; - } else if (typeof val === 'string') { - m_tileUrl = m_tileUrlFromTemplate(val); - } else { - m_tileUrl = val; - } - m_this.modified(); - return m_this; + this.api = function () { + throw "Should be implemented by derivied classes"; }; //////////////////////////////////////////////////////////////////////////// /** - * Transform a point or array of points in latitude-longitude-altitude - * space to local space of the layer - * - * @param {*} input - * Input can be of following types: - * - * 3. [x1,y1, x2, y2] - * 4. [[x,y]] - * 5. {x:val: y:val, z:val}, - * 6. [{x:val: y:val}] - * - * returns {x:lon, y:lat}, [{x:lon, y:lat}], [x1,y1, x2, y2], [[x,y]] + * Reset to default */ //////////////////////////////////////////////////////////////////////////// - this.toLocal = function (input) { - var i, output, delta; - - /// Now handle different data types - if (input instanceof Array && input.length > 0) { - output = []; - output.length = input.length; - - if (input[0] instanceof Array) { - delta = input % 3 === 0 ? 3 : 2; - - if (delta === 2) { - for (i = 0; i < input.length; i += delta) { - output[i] = input[i]; - output[i + 1] = geo.mercator.lat2y(input[i + 1]); - } - } else { - for (i = 0; i < input.length; i += delta) { - output[i] = input[i]; - output[i + 1] = geo.mercator.lat2y(input[i + 1]); - output[i + 2] = input[i + 2]; - } - } - } else if (input[0] instanceof Object && - 'x' in input[0] && 'y' in input[0] && 'z' in input[0]) { - /// Input is array of object - output[i] = { x: input[i].x, y: geo.mercator.lat2y(input[i].y), - z: input[i].z }; - } else if (input[0] instanceof Object && - 'x' in input[0] && 'y' in input[0] && 'z' in input[0]) { - /// Input is array of object - output[i] = { x: input[i].x, y: geo.mercator.lat2y(input[i].y)}; - } else if (input.length >= 2) { - output = input.slice(0); - output[1] = geo.mercator.lat2y(input[1]); - } - } else { - output = {}; - output.x = input.x; - output.y = geo.mercator.lat2y(input.y); - } - - return output; + this.reset = function () { + return true; }; //////////////////////////////////////////////////////////////////////////// /** - * Transform a point or array of points in local space to - * latitude-longitude space - * - * @input Input An object, array, of array of objects/array representing 2D - * point in space. [x,y], [[x,y]], [{x:val: y:val}], {x:val, y:val} + * Initialize */ //////////////////////////////////////////////////////////////////////////// - this.fromLocal = function (input) { - var i, output; - - if (input instanceof Array && input.length > 0) { - output = []; - output.length = input.length; - - if (input[0] instanceof Object) { - for (i = 0; i < input.length; i += 1) { - output[i] = {}; - output[i].x = input[i].x; - output[i].y = geo.mercator.y2lat(input[i].y); - } - } else if (input[0] instanceof Array) { - for (i = 0; i < input.length; i += 1) { - output[i] = input[i]; - output[i][1] = geo.mercator.y2lat(input[i][1]); - } - } else { - for (i = 0; i < input.length; i += 1) { - output[i] = input[i]; - output[i + 1] = geo.mercator.y2lat(input[i + 1]); - } - } - } else { - output = {}; - output.x = input.x; - output.y = geo.mercator.y2lat(input.y); - } - - return output; + this._init = function () { }; //////////////////////////////////////////////////////////////////////////// /** - * Check if a tile exists in the cache - * - * @param {number} zoom The zoom value for the map [1-17] - * @param {number} x X axis tile index - * @param {number} y Y axis tile index + * Handle resize event */ //////////////////////////////////////////////////////////////////////////// - this._hasTile = function (zoom, x, y) { - if (!m_tiles[zoom]) { - return false; - } - if (!m_tiles[zoom][x]) { - return false; - } - if (!m_tiles[zoom][x][y]) { - return false; - } - return true; + this._resize = function () { }; //////////////////////////////////////////////////////////////////////////// /** - * Create a new tile - * @param {number} x X axis tile index - * @param {number} y Y axis tile index + * Render */ //////////////////////////////////////////////////////////////////////////// - this._addTile = function (request, zoom, x, y) { - if (!m_tiles[zoom]) { - m_tiles[zoom] = {}; - } - if (!m_tiles[zoom][x]) { - m_tiles[zoom][x] = {}; - } - if (m_tiles[zoom][x][y]) { - return; - } + this._render = function () { + }; + + return this; +}; + +inherit(geo.renderer, geo.object); - /// Compute corner points - var noOfTilesX = Math.max(1, Math.pow(2, zoom)), - noOfTilesY = Math.max(1, Math.pow(2, zoom)), - /// Convert into mercator - totalLatDegrees = 360.0, - lonPerTile = 360.0 / noOfTilesX, - latPerTile = totalLatDegrees / noOfTilesY, - llx = -180.0 + x * lonPerTile, - lly = -totalLatDegrees * 0.5 + y * latPerTile, - urx = -180.0 + (x + 1) * lonPerTile, - ury = -totalLatDegrees * 0.5 + (y + 1) * latPerTile, - tile = new Image(); +(function () { + 'use strict'; - tile.LOADING = true; - tile.LOADED = false; - tile.REMOVED = false; - tile.REMOVING = false; - tile.INVALID = false; + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of osmLayer + * + * @class + * @extends geo.featureLayer + * + * @param {Object} arg - arg can contain following keys: baseUrl, + * imageFormat (such as png or jpeg), and displayLast + * (to decide whether or not render tiles from last zoom level). + */ + ////////////////////////////////////////////////////////////////////////////// + geo.osmLayer = function (arg) { - tile.crossOrigin = m_crossOrigin; - tile.zoom = zoom; - tile.index_x = x; - tile.index_y = y; - tile.llx = llx; - tile.lly = lly; - tile.urx = urx; - tile.ury = ury; - tile.lastused = new Date(); + if (!(this instanceof geo.osmLayer)) { + return new geo.osmLayer(arg); + } + if (arg.mapOpacity !== undefined && arg.opacity === undefined) { + arg.opacity = arg.mapOpacity; + } + geo.tileLayer.call(this, arg); - tile.src = m_tileUrl(zoom, x, Math.pow(2, zoom) - 1 - y); + /* mapOpacity is just another name for the layer opacity. */ + this.mapOpacity = this.opacity; - m_tiles[zoom][x][y] = tile; - m_pendingNewTiles.push(tile); - m_numberOfCachedTiles += 1; - return tile; + /** + * Returns an instantiated imageTile object with the given indices. This + * method always returns a new tile object. Use `_getTileCached` + * to use the caching layer. + * @param {Object} index The tile index + * @param {Number} index.x + * @param {Number} index.y + * @param {Number} index.level + * @param {Object} source The tile index used for constructing the url + * @param {Number} source.x + * @param {Number} source.y + * @param {Number} source.level + * @returns {geo.tile} + */ + this._getTile = function (index, source) { + var urlParams = source || index; + return geo.imageTile({ + index: index, + size: {x: this._options.tileWidth, y: this._options.tileHeight}, + url: this._options.url(urlParams.x, urlParams.y, urlParams.level || 0, + this._options.subdomains) + }); + }.bind(this); }; - //////////////////////////////////////////////////////////////////////////// + // Compute the circumference of the earth / 2 in meters for osm layer image bounds + var cEarth = Math.PI * geo.util.radiusEarth; + /** - * Clear tiles that are no longer required + * This object contains the default options used to initialize the osmLayer. */ - //////////////////////////////////////////////////////////////////////////// - /* jshint -W089 */ - this._removeTiles = function () { - var i, x, y, tile, zoom, currZoom = m_zoom, - lastZoom = m_lastVisibleZoom; - - if (!m_tiles) { - return m_this; - } + geo.osmLayer.defaults = $.extend({}, geo.tileLayer.defaults, { + minX: -cEarth, + maxX: cEarth, + minY: -cEarth, + maxY: cEarth, + minLevel: 0, + maxLevel: 18, + tileOverlap: 0, + tileWidth: 256, + tileHeight: 256, + tileOffset : function (level) { + var s = Math.pow(2, level - 1) * 256; + return {x: s, y: s}; + }, + wrapX: true, + wrapY: false, + url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + attribution: 'Tile data © ' + + 'OpenStreetMap contributors' + }); - for (zoom in m_tiles) { - for (x in m_tiles[zoom]) { - for (y in m_tiles[zoom][x]) { - tile = m_tiles[zoom][x][y]; - if (tile) { - tile.REMOVING = true; - m_pendingInactiveTiles.push(tile); - } - } - } - } + inherit(geo.osmLayer, geo.tileLayer); - /// First remove the tiles if we have cached more than max cached limit - m_pendingInactiveTiles.sort(function (a, b) { - return a.lastused - b.lastused; - }); + geo.registerLayer('osm', geo.osmLayer); +})(); - i = 0; +geo.domRenderer = function (arg) { + 'use strict'; - /// Get rid of tiles if we have reached our threshold. However, - /// If the tile is required for current zoom, then do nothing. - /// Also do not delete the tile if it is from the previous zoom - while (m_numberOfCachedTiles > m_tileCacheSize && - i < m_pendingInactiveTiles.length) { - tile = m_pendingInactiveTiles[i]; + if (!(this instanceof geo.domRenderer)) { + return new geo.domRenderer(arg); + } + geo.renderer.call(this, arg); - if (isTileVisible(tile)) { - i += 1; - } else { - m_this.deleteFeature(tile.feature); - delete m_tiles[tile.zoom][tile.index_x][tile.index_y]; - m_pendingInactiveTiles.splice(i, 1); - m_numberOfCachedTiles -= 1; - } - } + arg = arg || {}; - for (i = 0; i < m_pendingInactiveTiles.length; i += 1) { - tile = m_pendingInactiveTiles[i]; - tile.REMOVING = false; - tile.REMOVED = false; - if (tile.zoom !== currZoom && tile.zoom === lastZoom) { - tile.feature.bin(m_lastVisibleBinNumber); - } else if (tile.zoom !== currZoom) { - tile.feature.bin(m_hiddenBinNumber); - } else { - tile.lastused = new Date(); - tile.feature.bin(m_visibleBinNumber); - } - tile.feature._update(); - } - m_pendingInactiveTiles = []; + var m_this = this; - return m_this; + this.api = function () { + return 'dom'; }; - /* jshint +W089 */ - //////////////////////////////////////////////////////////////////////////// - /** - * Create / delete tiles as necessary - */ - //////////////////////////////////////////////////////////////////////////// - this._addTiles = function (request) { - var feature, ren = m_this.renderer(), - /// First get corner points - /// In display coordinates the origin is on top left corner (0, 0) - llx = 0.0, lly = m_this.height(), urx = m_this.width(), ury = 0.0, - temp = null, tile = null, tile1x = null, tile1y = null, tile2x = null, - tile2y = null, invJ = null, i = 0, j = 0, lastStartX, lastStartY, - lastEndX, lastEndY, currStartX, currStartY, currEndX, currEndY, - worldPt1 = ren.displayToWorld([llx, lly]), - worldPt2 = ren.displayToWorld([urx, ury]), - worldDeltaY = null, displayDeltaY = null, - worldDelta = null, displayDelta = null, - noOfTilesRequired = null, worldDeltaPerTile = null, - minDistWorldDeltaPerTile = null, distWorldDeltaPerTile; - - worldPt1[0] = Math.max(worldPt1[0], -180.0); - worldPt1[0] = Math.min(worldPt1[0], 180.0); - worldPt1[1] = Math.max(worldPt1[1], -180.0); - worldPt1[1] = Math.min(worldPt1[1], 180.0); - - worldPt2[0] = Math.max(worldPt2[0], -180.0); - worldPt2[0] = Math.min(worldPt2[0], 180.0); - worldPt2[1] = Math.max(worldPt2[1], -180.0); - worldPt2[1] = Math.min(worldPt2[1], 180.0); - - /// Compute tile zoom - worldDelta = Math.abs(worldPt2[0] - worldPt1[0]); - worldDeltaY = Math.abs(worldPt2[1] - worldPt1[1]); - - displayDelta = urx - llx; - displayDeltaY = lly - ury; - - /// Reuse variables - if (displayDeltaY > displayDelta) { - displayDelta = displayDeltaY; - worldDelta = worldDeltaY; - } - - noOfTilesRequired = Math.round(displayDelta / 256.0); - worldDeltaPerTile = worldDelta / noOfTilesRequired; - - /// Minimize per pixel distortion - minDistWorldDeltaPerTile = Number.POSITIVE_INFINITY; - for (i = 20; i >= 2; i = i - 1) { - distWorldDeltaPerTile = Math.abs(360.0 / Math.pow(2, i) - worldDeltaPerTile); - if (distWorldDeltaPerTile < minDistWorldDeltaPerTile) { - minDistWorldDeltaPerTile = distWorldDeltaPerTile; - m_zoom = i; - } - } - - /// Compute tilex and tiley - tile1x = geo.mercator.long2tilex(worldPt1[0], m_zoom); - tile1y = geo.mercator.lat2tiley(worldPt1[1], m_zoom); - - tile2x = geo.mercator.long2tilex(worldPt2[0], m_zoom); - tile2y = geo.mercator.lat2tiley(worldPt2[1], m_zoom); - - /// Clamp tilex and tiley - tile1x = Math.max(tile1x, 0); - tile1x = Math.min(Math.pow(2, m_zoom) - 1, tile1x); - tile1y = Math.max(tile1y, 0); - tile1y = Math.min(Math.pow(2, m_zoom) - 1, tile1y); - - tile2x = Math.max(tile2x, 0); - tile2x = Math.min(Math.pow(2, m_zoom) - 1, tile2x); - tile2y = Math.max(tile2y, 0); - tile2y = Math.min(Math.pow(2, m_zoom) - 1, tile2y); - - /// Check and update variables appropriately if view - /// direction is flipped. This should not happen but - /// just in case. - if (tile1x > tile2x) { - temp = tile1x; - tile1x = tile2x; - tile2x = temp; - } - if (tile2y > tile1y) { - temp = tile1y; - tile1y = tile2y; - tile2y = temp; - } - - /// Compute current tile indices - currStartX = tile1x; - currEndX = tile2x; - currStartY = (Math.pow(2, m_zoom) - 1 - tile1y); - currEndY = (Math.pow(2, m_zoom) - 1 - tile2y); - if (currEndY < currStartY) { - temp = currStartY; - currStartY = currEndY; - currEndY = temp; - } - - /// Compute last tile indices - lastStartX = geo.mercator.long2tilex(worldPt1[0], - m_lastVisibleZoom); - lastStartY = geo.mercator.lat2tiley(worldPt1[1], - m_lastVisibleZoom); - lastEndX = geo.mercator.long2tilex(worldPt2[0], - m_lastVisibleZoom); - lastEndY = geo.mercator.lat2tiley(worldPt2[1], - m_lastVisibleZoom); - lastStartY = Math.pow(2, m_lastVisibleZoom) - 1 - lastStartY; - lastEndY = Math.pow(2, m_lastVisibleZoom) - 1 - lastEndY; - - if (lastEndY < lastStartY) { - temp = lastStartY; - lastStartY = lastEndY; - lastEndY = temp; - } - - m_visibleTilesRange = {}; - m_visibleTilesRange[m_zoom] = { startX: currStartX, endX: currEndX, - startY: currStartY, endY: currEndY }; - - m_visibleTilesRange[m_lastVisibleZoom] = - { startX: lastStartX, endX: lastEndX, - startY: lastStartY, endY: lastEndY }; - m_pendingNewTilesStat[m_updateTimerId] = { total: - ((tile2x - tile1x + 1) * (tile1y - tile2y + 1)), count: 0 }; - - for (i = tile1x; i <= tile2x; i += 1) { - for (j = tile2y; j <= tile1y; j += 1) { - invJ = (Math.pow(2, m_zoom) - 1 - j); - if (!m_this._hasTile(m_zoom, i, invJ)) { - tile = m_this._addTile(request, m_zoom, i, invJ); - } else { - tile = m_tiles[m_zoom][i][invJ]; - tile.feature.bin(m_visibleBinNumber); - if (tile.LOADED && m_updateTimerId in m_pendingNewTilesStat) { - m_pendingNewTilesStat[m_updateTimerId].count += 1; - } - tile.feature._update(); - } - tile.lastused = new Date(); - tile.updateTimerId = m_updateTimerId; - } + this._init = function () { + var layer = m_this.layer().node(); + + if (!m_this.canvas() && layer && layer.length) { + // The renderer and the UI Layer share the same canvas + // at least for now. This renderer is essentially a noop renderer + // designed for backwards compatibility + m_this.canvas(layer[0]); } + }; - // define a function here to set tile properties after it is loaded - function tileOnLoad(tile) { - var defer = $.Deferred(); - m_this.addDeferred(defer); + this._init(arg); + return this; +}; - return function () { - if (tile.INVALID) { - return; - } - tile.LOADING = false; - tile.LOADED = true; - if ((tile.REMOVING || tile.REMOVED) && - tile.feature && - tile.zoom !== m_zoom) { - tile.feature.bin(m_hiddenBinNumber); - tile.REMOVING = false; - tile.REMOVED = true; - } else { - tile.REMOVED = false; - tile.lastused = new Date(); - tile.feature.bin(m_visibleBinNumber); - } +inherit(geo.domRenderer, geo.renderer); - if (tile.updateTimerId === m_updateTimerId && - m_updateTimerId in m_pendingNewTilesStat) { - tile.feature.bin(m_visibleBinNumber); - m_pendingNewTilesStat[m_updateTimerId].count += 1; - } else { - tile.REMOVED = true; - tile.feature.bin(m_hiddenBinNumber); - } - tile.feature._update(); +geo.registerRenderer('dom', geo.domRenderer); - if (m_updateTimerId in m_pendingNewTilesStat && - m_pendingNewTilesStat[m_updateTimerId].count >= - m_pendingNewTilesStat[m_updateTimerId].total) { - drawTiles(); - } - defer.resolve(); - }; - } +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of class choroplethFeature + * + * @class + * @extends geo.feature + * @returns {geo.choroplethFeature} + * + */ +////////////////////////////////////////////////////////////////////////////// +geo.choroplethFeature = function (arg) { + 'use strict'; + if (!(this instanceof geo.choroplethFeature)) { + return new geo.choroplethFeature(arg); + } + arg = arg || {}; + geo.feature.call(this, arg); - /// And now finally add them - for (i = 0; i < m_pendingNewTiles.length; i += 1) { - tile = m_pendingNewTiles[i]; - feature = m_this.createFeature( - 'plane', {drawOnAsyncResourceLoad: false, onload: tileOnLoad(tile)}) - .origin([tile.llx, tile.lly]) - .upperLeft([tile.llx, tile.ury]) - .lowerRight([tile.urx, tile.lly]) - .gcs('EPSG:3857') - .style({image: tile, opacity: m_mapOpacity}); - tile.feature = feature; - tile.feature._update(); - } - m_pendingNewTiles = []; + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + m_choropleth = $ + .extend({}, + { + /* 9-step based on paraview bwr colortable */ + colorRange: [ + {r: 0.07514311, g: 0.468049805, b: 1}, + {r: 0.468487184, g: 0.588057293, b: 1}, + {r: 0.656658579, g: 0.707001303, b: 1}, + {r: 0.821573924, g: 0.837809045, b: 1}, + {r: 0.943467973, g: 0.943498599, b: 0.943398095}, + {r: 1, g: 0.788626485, b: 0.750707739}, + {r: 1, g: 0.6289553, b: 0.568237474}, + {r: 1, g: 0.472800903, b: 0.404551679}, + {r: 0.916482116, g: 0.236630659, b: 0.209939162} + ], + scale: d3.scale.quantize(), + accessors: { + //accessor for ID on geodata feature + geoId: function (geoFeature) { + return geoFeature.properties.GEO_ID; + }, + //accessor for ID on scalar element + scalarId: function (scalarElement) { + return scalarElement.id; + }, + //accessor for value on scalar element + scalarValue: function (scalarElement) { + return scalarElement.value; + } + } + }, + arg.choropleth); + + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set choropleth scalar data + * + * @returns {geo.feature.choropleth} + */ + //////////////////////////////////////////////////////////////////////////// + this.scalar = function (data, aggregator) { + var scalarId, scalarValue; - if (m_updateTimerId in m_pendingNewTilesStat && - m_pendingNewTilesStat[m_updateTimerId].count >= - m_pendingNewTilesStat[m_updateTimerId].total) { - drawTiles(); + if (data === undefined) { + return m_this.choropleth.get('scalar')(); + } else { + scalarId = m_this.choropleth.get('accessors')().scalarId; + scalarValue = m_this.choropleth.get('accessors')().scalarValue; + m_choropleth.scalar = data; + m_choropleth.scalarAggregator = aggregator || d3.mean; + // we make internal dictionary from array for faster lookup + // when matching geojson features to scalar values, + // note that we also allow for multiple scalar elements + // for the same geo feature + m_choropleth.scalar._dictionary = data + .reduce(function (accumeDictionary, scalarElement) { + var id, value; + + id = scalarId(scalarElement); + value = scalarValue(scalarElement); + + accumeDictionary[id] = + accumeDictionary[id] ? + accumeDictionary[id].push(value) : [value]; + + return accumeDictionary; + }, {}); + m_this.dataTime().modified(); } + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Update OSM tiles as needed + * Get/Set choropleth accessor + * + * @returns {geo.feature.choropleth} */ //////////////////////////////////////////////////////////////////////////// - function updateOSMTiles(request) { - if (request === undefined) { - request = {}; - } + this.choropleth = function (arg1, arg2) { + var choropleth; - if (!m_zoom) { - m_zoom = m_this.map().zoom(); + if (arg1 === undefined) { + return m_choropleth; } - - if (!m_lastVisibleZoom) { - m_lastVisibleZoom = m_zoom; + if (typeof arg1 === 'string' && arg2 === undefined) { + return m_choropleth[arg1]; } - - /// Add tiles that are currently visible - m_this._addTiles(request); - - /// Update the zoom - if (m_lastVisibleZoom !== m_zoom) { - m_lastVisibleZoom = m_zoom; + if (arg2 === undefined) { + choropleth = $.extend( + {}, + m_choropleth, + arg1 + ); + m_choropleth = choropleth; + } else { + m_choropleth[arg1] = arg2; //if you pass in accessor for prop } - - m_this.updateTime().modified(); - } + m_this.modified(); + return m_this; + }; //////////////////////////////////////////////////////////////////////////// /** - * Create / delete tiles as necessary + * A uniform getter that always returns a function even for constant values. + * If undefined input, return all the choropleth values as an object. + * + * @param {string|undefined} key + * @return {function} */ //////////////////////////////////////////////////////////////////////////// - this._updateTiles = function (request) { - var defer = $.Deferred(); - m_this.addDeferred(defer); - - if (m_updateTimerId !== null) { - clearTimeout(m_updateTimerId); - m_updateDefer.resolve(); - m_updateDefer = defer; - if (m_updateTimerId in m_pendingNewTilesStat) { - delete m_pendingNewTilesStat[m_updateTimerId]; + this.choropleth.get = function (key) { + var all = {}, k; + if (key === undefined) { + for (k in m_choropleth) { + if (m_choropleth.hasOwnProperty(k)) { + all[k] = m_this.choropleth.get(k); + } } - /// Set timeout for 60 ms. 60 ms seems to playing well - /// with the events. Also, 60ms corresponds to 15 FPS. - m_updateTimerId = setTimeout(function () { - updateOSMTiles(request); - m_updateDefer.resolve(); - }, 100); - } else { - m_updateDefer = defer; - m_updateTimerId = setTimeout(function () { - updateOSMTiles(request); - m_updateDefer.resolve(); - }, 0); + return all; } - return m_this; + return geo.util.ensureFunction(m_choropleth[key]); }; - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function () { - s_init.call(m_this); - m_this.gcs('EPSG:3857'); - m_this.map().zoomRange({ - min: 0, - max: 18 - }); - return m_this; + //////////////////////////////////////////////////////////////////////////// + /** + * A method that adds a polygon feature to the current layer. + * + * @param {array} coordinateArray + * @param {geo.color} fillColor + * @return {geo.feature} + */ + //////////////////////////////////////////////////////////////////////////// + this._addPolygonFeature = function (feature, fillColor) { + var newFeature = m_this.layer() + .createFeature('polygon', {}); + + if (feature.geometry.type === 'Polygon') { + newFeature.data([{ + type: 'Polygon', + coordinates: feature.geometry.coordinates + }]); + } else if (feature.geometry.type === 'MultiPolygon') { + newFeature.data(feature.geometry.coordinates.map(function (coordinateMap) { + return { + type: 'Polygon', + coordinates: coordinateMap + }; + })); + } + + newFeature + .polygon(function (d) { + return { + 'outer': d.coordinates[0], + 'inner': d.coordinates[1] // undefined but ok + }; + }) + .position(function (d) { + return { + x: d[0], + y: d[1] + }; + }) + .style({ + 'fillColor': fillColor + }); + + return newFeature; }; //////////////////////////////////////////////////////////////////////////// /** - * Update layer + * A method that adds polygons from a given feature to the current layer. + * + * @param {} geoJsonFeature + * @param geo.color + * @return [{geo.feature}] */ //////////////////////////////////////////////////////////////////////////// - this._update = function (request) { - /// Update tiles (create new / delete old etc...) - m_this._updateTiles(request); - - /// Now call base class update - s_update.call(m_this, request); + this._featureToPolygons = function (feature, fillValue) { + return m_this + ._addPolygonFeature(feature, fillValue); }; //////////////////////////////////////////////////////////////////////////// /** - * Update baseUrl for map tiles. Map all tiles as needing to be refreshed. - * If no argument is given the tiles will be forced refreshed. + * A method that sets a choropleth scale's domain and range. * - * @param baseUrl: the new baseUrl for the map. + * @param {undefined | function({})} valueAccessor + * @return {geo.feature.choropleth} */ //////////////////////////////////////////////////////////////////////////// - /* jshint -W089 */ - this.updateBaseUrl = function (baseUrl) { - if (baseUrl && baseUrl.charAt(m_baseUrl.length - 1) !== '/') { - baseUrl += '/'; - } - if (baseUrl !== m_baseUrl) { + this._generateScale = function (valueAccessor) { + var extent = + d3.extent(m_this.scalar(), valueAccessor || undefined); - if (baseUrl !== undefined) { - m_baseUrl = baseUrl; - } - - var tile, x, y, zoom; - for (zoom in m_tiles) { - for (x in m_tiles[zoom]) { - for (y in m_tiles[zoom][x]) { - tile = m_tiles[zoom][x][y]; - tile.INVALID = true; - m_this.deleteFeature(tile.feature); - } - } - } - m_tiles = {}; - m_pendingNewTiles = []; - m_pendingInactiveTiles = []; - m_numberOfCachedTiles = 0; - m_visibleTilesRange = {}; - m_pendingNewTilesStat = {}; + m_this.choropleth() + .scale + .domain(extent) + .range(m_this.choropleth().colorRange); - if (m_updateTimerId !== null) { - clearTimeout(m_updateTimerId); - m_updateTimerId = null; - } - this._update(); - } + return m_this; }; //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set map opacity - * - * @returns {geo.osmLayer} + /**sr + * Generate scale for choropleth.data(), make polygons from features. + * @returns: [ [geo.feature.polygon, ...] , ... ] */ //////////////////////////////////////////////////////////////////////////// - this.mapOpacity = function (val) { - if (val === undefined) { - return m_mapOpacity; - } else if (val !== m_mapOpacity) { - m_mapOpacity = val; - var zoom, x, y, tile; - for (zoom in m_tiles) { - for (x in m_tiles[zoom]) { - for (y in m_tiles[zoom][x]) { - tile = m_tiles[zoom][x][y]; - tile.feature.style().opacity = val; - tile.feature._update(); - } - } - } - m_this.modified(); - } - return m_this; + this.createChoropleth = function () { + var choropleth = m_this.choropleth, + data = m_this.data(), + scalars = m_this.scalar(), + valueFunc = choropleth.get('accessors')().scalarValue, + getFeatureId = choropleth.get('accessors')().geoId; + + m_this._generateScale(valueFunc); + + return data + .map(function (feature) { + var id = getFeatureId(feature); + var valueArray = scalars._dictionary[id]; + var accumulatedScalarValue = choropleth().scalarAggregator(valueArray); + // take average of this array of values + // which allows for non-bijective correspondance + // between geo data and scalar data + var fillColor = + m_this + .choropleth() + .scale(accumulatedScalarValue); + + return m_this + ._featureToPolygons(feature, fillColor); + }); }; //////////////////////////////////////////////////////////////////////////// /** - * Exit + * Initialize */ //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_tiles = {}; - m_pendingNewTiles = []; - m_pendingInactiveTiles = []; - m_numberOfCachedTiles = 0; - m_visibleTilesRange = {}; - m_pendingNewTilesStat = {}; - s_exit(); - }; - - if (arg && arg.tileUrl !== undefined) { - this.tileUrl(arg.tileUrl); - } + this._init = function (arg) { + s_init.call(m_this, arg); + if (m_choropleth) { + m_this.dataTime().modified(); + } + }; + this._init(arg); return this; }; -inherit(geo.osmLayer, geo.featureLayer); +inherit(geo.choroplethFeature, geo.feature); -geo.registerLayer('osm', geo.osmLayer); +/* Example: + */ /** * @namespace */ geo.gl = {}; -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class vglRenderer - * - * @class - * @extends geo.renderer - * @param canvas - * @returns {geo.gl.renderer} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.renderer = function (arg) { - 'use strict'; - - if (!(this instanceof geo.gl.renderer)) { - return new geo.gl.renderer(arg); - } - geo.renderer.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * Get context specific renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.contextRenderer = function () { - throw 'Should be implemented by derived classes'; - }; - - return this; -}; - -inherit(geo.gl.renderer, geo.renderer); - ////////////////////////////////////////////////////////////////////////////// /** * Create a new instance of lineFeature @@ -22771,7 +24907,7 @@ geo.gl.lineFeature = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this.verticesPerFeature = function () { - return this.featureVertices().length; + return m_this.featureVertices().length; }; //////////////////////////////////////////////////////////////////////////// @@ -23521,7 +25657,7 @@ geo.gl.geomFeature = function (arg) { m_mapper.setGeometryData(m_geom); } - this.setMapper(m_mapper); + m_this.setMapper(m_mapper); if (style.point_sprites !== undefined && style.point_sprites && style.point_sprites_image !== undefined && @@ -23642,8 +25778,11 @@ geo.gl.planeFeature = function (arg) { image = null, texture = null; + //DWM:: we are expecting the features to be in the same coordinate system + //DWM:: as the camera -- should it be otherwise? /// TODO If for some reason base layer changes its gcs at run time /// then we need to trigger an event to rebuild every feature + /* or = geo.transform.transformCoordinates(m_this.gcs(), m_this.layer().map().gcs(), or); @@ -23653,6 +25792,7 @@ geo.gl.planeFeature = function (arg) { lr = geo.transform.transformCoordinates(m_this.gcs(), m_this.layer().map().gcs(), lr); + */ m_this.buildTime().modified(); @@ -24406,7 +26546,7 @@ geo.registerFeature('vgl', 'contour', geo.gl.contourFeature); * Create a new instance of class vglRenderer * * @class - * @extends geo.gl.renderer + * @extends geo.renderer * @param canvas * @returns {geo.gl.vglRenderer} */ @@ -24418,14 +26558,14 @@ geo.gl.vglRenderer = function (arg) { return new geo.gl.vglRenderer(arg); } arg = arg || {}; - geo.gl.renderer.call(this, arg); + geo.renderer.call(this, arg); var m_this = this, m_contextRenderer = null, m_viewer = null, m_width = 0, m_height = 0, - m_initialized = false, + m_renderAnimFrameRef = null, s_init = this._init; /// TODO: Move this API to the base class @@ -24447,160 +26587,6 @@ geo.gl.vglRenderer = function (arg) { return m_height; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Convert input data in display space to world space - * - * @param {object} input {x:val, y:val}, [{x:val, y:val}], - * [{x:val, y:val}], [x1,y1], [[x,y]] - * - * @returns {object} {x:val, y:val, z:val, w:val}, [{x:val, y:val, z:val, w:val}], - [[x, y, z, w]], [x1,y1,z1,w] - */ - //////////////////////////////////////////////////////////////////////////// - this.displayToWorld = function (input) { - var i, - delta, - ren = m_this.contextRenderer(), - cam = ren.camera(), - fdp = ren.focusDisplayPoint(), - output, - temp, - point; - - /// Handle if the input is an array [...] - if (input instanceof Array && input.length > 0) { - output = []; - /// Input is array of object {x:val, y:val} - if (input[0] instanceof Object) { - delta = 1; - for (i = 0; i < input.length; i += delta) { - point = input[i]; - temp = ren.displayToWorld(vec4.fromValues( - point.x, point.y, fdp[2], 1.0), - cam.viewMatrix(), cam.projectionMatrix(), - m_width, m_height); - output.push({x: temp[0], y: temp[1], z: temp[2], w: temp[3]}); - } - /// Input is array of 2d array [[x,y], [x,y]] - } else if (input[0] instanceof Array) { - delta = 1; - for (i = 0; i < input.length; i += delta) { - point = input[i]; - temp = ren.displayToWorld(vec4.fromValues( - point[0], point[1], fdp[2], 1.0), - cam.viewMatrix(), cam.projectionMatrix(), - m_width, m_height); - output.push(temp); - } - /// Input is flat array [x1,y1,x2,y2] - } else { - delta = input.length % 3 === 0 ? 3 : 2; - for (i = 0; i < input.length; i += delta) { - temp = ren.displayToWorld(vec4.fromValues( - input[i], - input[i + 1], - fdp[2], - 1.0), cam.viewMatrix(), cam.projectionMatrix(), - m_width, m_height); - output.push(temp[0]); - output.push(temp[1]); - output.push(temp[2]); - output.push(temp[3]); - } - } - /// Input is object {x:val, y:val} - } else if (input instanceof Object) { - output = {}; - temp = ren.displayToWorld(vec4.fromValues( - input.x, input.y, fdp[2], 1.0), - cam.viewMatrix(), cam.projectionMatrix(), - m_width, m_height); - output = {x: temp[0], y: temp[1], z: temp[2], w: temp[3]}; - } else { - throw 'Display to world conversion requires array of 2D/3D points'; - } - return output; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert input data in world space to display space - * - * @param {object} input {x:val, y:val} or {x:val, y:val, z:val} or [{x:val, y:val}] - * [{x:val, y:val, z:val}] or [[x,y]] or [[x,y,z]] or [x1,y1,z1, x2, y2, z2] - * - * @returns {object} {x:val, y:val} or [{x:val, y:val}] or [[x,y]] or - * [x1,y1, x2, y2] - */ - //////////////////////////////////////////////////////////////////////////// - this.worldToDisplay = function (input) { - var i, temp, delta, - ren = m_this.contextRenderer(), cam = ren.camera(), - fp = cam.focalPoint(), output = []; - - /// Input is an array - if (input instanceof Array && input.length > 0) { - output = []; - - /// Input is an array of objects - if (input[0] instanceof Object) { - delta = 1; - for (i = 0; i < input.length; i += delta) { - temp = ren.worldToDisplay(vec4.fromValues( - input[i].x, input[i].y, fp[2], 1.0), cam.viewMatrix(), - cam.projectionMatrix(), - m_width, m_height); - output[i] = { x: temp[0], y: temp[1], z: temp[2] }; - } - } else if (input[0] instanceof Array) { - /// Input is an array of array - delta = 1; - for (i = 0; i < input.length; i += delta) { - temp = ren.worldToDisplay( - vec4.fromValues(input[i][0], input[i][1], fp[2], 1.0), - cam.viewMatrix(), cam.projectionMatrix(), m_width, m_height); - output[i].push(temp); - } - } else { - /// Input is a flat array of 2 or 3 dimension - delta = input.length % 3 === 0 ? 3 : 2; - if (delta === 2) { - for (i = 0; i < input.length; i += delta) { - temp = ren.worldToDisplay(vec4.fromValues( - input[i], input[i + 1], fp[2], 1.0), cam.viewMatrix(), - cam.projectionMatrix(), - m_width, m_height); - output.push(temp[0]); - output.push(temp[1]); - output.push(temp[2]); - } - } else { - for (i = 0; i < input.length; i += delta) { - temp = ren.worldToDisplay(vec4.fromValues( - input[i], input[i + 1], input[i + 2], 1.0), cam.viewMatrix(), - cam.projectionMatrix(), - m_width, m_height); - output.push(temp[0]); - output.push(temp[1]); - output.push(temp[2]); - } - } - } - } else if (input instanceof Object) { - temp = ren.worldToDisplay(vec4.fromValues( - input.x, input.y, fp[2], 1.0), cam.viewMatrix(), - cam.projectionMatrix(), - m_width, m_height); - - output = {x: temp[0], y: temp[1], z: temp[2]}; - } else { - throw 'World to display conversion requires array of 2D/3D points'; - } - - return output; - }; - //////////////////////////////////////////////////////////////////////////// /** * Get context specific renderer @@ -24633,7 +26619,6 @@ geo.gl.vglRenderer = function (arg) { var canvas = $(document.createElement('canvas')); canvas.attr('class', 'webgl-canvas'); - m_this.canvas(canvas); $(m_this.layer().node().get(0)).append(canvas); m_viewer = vgl.viewer(canvas.get(0), arg.options); m_viewer.init(); @@ -24643,6 +26628,11 @@ geo.gl.vglRenderer = function (arg) { if (m_viewer.renderWindow().renderers().length > 0) { m_contextRenderer.setLayer(m_viewer.renderWindow().renderers().length); } + m_this.canvas(canvas); + /* Initialize the size of the renderer */ + var map = m_this.layer().map(), + mapSize = map.size(); + m_this._resize(0, 0, mapSize.width, mapSize.height); return m_this; }; @@ -24653,229 +26643,263 @@ geo.gl.vglRenderer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this._resize = function (x, y, w, h) { - var vglRenderer = m_this.contextRenderer(), - map = m_this.layer().map(), - camera = vglRenderer.camera(), - baseContextRenderer, - baseCamera, - renderWindow = m_viewer.renderWindow(), - layer = m_this.layer(), - baseLayer = layer.map().baseLayer(), - focalPoint, - position, - zoom, - newZ, - mapCenter; + var renderWindow = m_viewer.renderWindow(); m_width = w; m_height = h; m_this.canvas().attr('width', w); m_this.canvas().attr('height', h); renderWindow.positionAndResize(x, y, w, h); + + m_this._updateRendererCamera(); m_this._render(); - // Ignore if the base layer is not set yet - if (!baseLayer || m_initialized) { - return; + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render + */ + //////////////////////////////////////////////////////////////////////////// + this._render = function () { + if (m_renderAnimFrameRef === null) { + m_renderAnimFrameRef = window.requestAnimationFrame(function () { + m_viewer.render(); + m_renderAnimFrameRef = null; + }); } - m_initialized = true; + return m_this; + }; + + this._updateRendererCamera = function () { + var renderWindow = m_viewer.renderWindow(), + map = m_this.layer().map(), + camera = map.camera(), + view = camera.view, + proj = camera.projectionMatrix; + if (proj[15]) { + /* we want positive z to be closer to the camera, but webGL does the + * converse, so reverse the z coordinates. */ + proj = mat4.scale(mat4.create(), proj, [1, 1, -1]); + } + /* A similar kluge as in the base camera class worldToDisplay4. With this, + * we can show z values from 0 to 1. */ + proj = mat4.translate(mat4.create(), proj, + [0, 0, camera.constructor.bounds.far]); + + renderWindow.renderers().forEach(function (renderer) { + var cam = renderer.camera(); + cam.setViewMatrix(view); + cam.setProjectionMatrix(proj); + if (proj[1] || proj[2] || proj[3] || proj[4] || proj[6] || proj[7] || + proj[8] || proj[9] || proj[11] || proj[15] !== 1 || + (parseFloat(map.zoom().toFixed(6)) !== + parseFloat(map.zoom().toFixed(0)))) { + /* Don't align texels */ + cam.viewAlignment = function () { + return null; + }; + } else { + /* Set information for texel alignment. The rounding factors should + * probably be divided by window.devicePixelRatio. */ + cam.viewAlignment = function () { + var align = { + roundx: 2.0 / camera.viewport.width, + roundy: 2.0 / camera.viewport.height + }; + align.dx = (camera.viewport.width % 2) ? align.roundx * 0.5 : 0; + align.dy = (camera.viewport.height % 2) ? align.roundy * 0.5 : 0; + return align; + }; + } + }); + }; + + // Connect to interactor events + // Connect to pan event + m_this.layer().geoOn(geo.event.pan, function (evt) { + void(evt); + m_this._updateRendererCamera(); + }); + + // Connect to zoom event + m_this.layer().geoOn(geo.event.zoom, function (evt) { + void(evt); + m_this._updateRendererCamera(); + }); + + // Connect to parallelprojection event + m_this.layer().geoOn(geo.event.parallelprojection, function (evt) { + var vglRenderer = m_this.contextRenderer(), + camera, + layer = m_this.layer(); - // skip handling if the renderer is unconnected - if (!vglRenderer || !vglRenderer.camera()) { - console.log('Zoom event triggered on unconnected vgl renderer.'); + if (evt.geo && evt.geo._triggeredBy !== layer) { + if (!vglRenderer || !vglRenderer.camera()) { + console.log('Parallel projection event triggered on unconnected VGL ' + + 'renderer.'); + } + camera = vglRenderer.camera(); + camera.setEnableParallelProjection(evt.parallelProjection); + m_this._updateRendererCamera(); } + }); - position = camera.position(); - zoom = map.zoom(); - newZ = camera.zoomToHeight(zoom, w, h); + return this; +}; - // Assuming that baselayer will be a GL layer - if (layer !== baseLayer) { - baseContextRenderer = baseLayer.renderer().contextRenderer(); - baseCamera = baseContextRenderer.camera(); - position = baseCamera.position(); - focalPoint = baseCamera.focalPoint(); - camera.setPosition(position[0], position[1], position[2]); - camera.setFocalPoint(focalPoint[0], focalPoint[1], focalPoint[2]); - } else { - mapCenter = layer.toLocal(layer.map().center()); - focalPoint = camera.focalPoint(); - camera.setPosition(mapCenter.x, mapCenter.y, newZ); - camera.setFocalPoint(mapCenter.x, mapCenter.y, focalPoint[2]); +inherit(geo.gl.vglRenderer, geo.renderer); + +geo.registerRenderer('vgl', geo.gl.vglRenderer); + +geo.gl.tileLayer = function () { + 'use strict'; + var m_this = this; + + this._drawTile = function (tile) { + var bounds = this._tileBounds(tile), + level = tile.index.level || 0, + to = this._options.tileOffset(level); + var ul = this.fromLocal(this.fromLevel({ + x: bounds.left - to.x, y: bounds.top - to.y + }, level), 0); + var lr = this.fromLocal(this.fromLevel({ + x: bounds.right - to.x, y: bounds.bottom - to.y + }, level), 0); + /* Use a small z-value for layering the tile levels. */ + tile.feature = m_this.createFeature( + 'plane', {drawOnAsyncResourceLoad: true}) + .origin([ul.x, lr.y, level * 1e-7]) + .upperLeft([ul.x, ul.y, level * 1e-7]) + .lowerRight([lr.x, lr.y, level * 1e-7]) + .style({image: tile._image}); + tile.feature._update(); + m_this.draw(); + }; + + /* Remove the tile feature. */ + this._remove = function (tile) { + if (tile.feature) { + m_this.deleteFeature(tile.feature); + tile.feature = null; + m_this.draw(); + } + }; + + /* These functions don't need to do anything. */ + this._getSubLayer = function () {}; + this._updateSubLayer = function () {}; +}; + +geo.registerLayerAdjustment('vgl', 'tile', geo.gl.tileLayer); + +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of choroplethFeature + * + * @class + * @extends geo.choroplethFeature + * @returns {geo.gl.choroplethFeature} + */ +////////////////////////////////////////////////////////////////////////////// +geo.gl.choroplethFeature = function (arg) { + 'use strict'; + + if (!(this instanceof geo.gl.choroplethFeature)) { + return new geo.gl.choroplethFeature(arg); + } + arg = arg || {}; + geo.choroplethFeature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + m_gl_polygons = null, + s_exit = this._exit, + s_init = this._init, + s_update = this._update; + + /* Create the choropleth. This calls the base class to generate the contours, + * into the various gl uniforms and buffers. + */ + function createGLChoropleth() { + return m_this.createChoropleth(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + m_this.buildTime().modified(); + return (m_gl_polygons = createGLChoropleth()); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + s_update.call(m_this); + if (m_this.dataTime().getMTime() >= m_this.buildTime().getMTime() || + m_this.updateTime().getMTime() <= m_this.getMTime()) { + m_this._wipePolygons(); + m_this._build(); } - camera.setParallelExtents({zoom: zoom}); - - m_this._updateRendererCamera(); - - return m_this; + m_this.updateTime().modified(); }; //////////////////////////////////////////////////////////////////////////// /** - * Render + * Destroy Polygon Sub-Features */ //////////////////////////////////////////////////////////////////////////// - this._render = function () { - m_viewer.render(); - return m_this; + this._wipePolygons = function () { + if (m_gl_polygons) { + m_gl_polygons.map(function (polygon) { + return polygon._exit(); + }); + } + m_gl_polygons = null; }; - this._updateRendererCamera = function () { - var vglRenderer = m_this.contextRenderer(), - renderWindow = m_viewer.renderWindow(), - camera = vglRenderer.camera(), - pos, fp, cr, pe; - - vglRenderer.resetCameraClippingRange(); - pos = camera.position(); - fp = camera.focalPoint(); - cr = camera.clippingRange(); - pe = camera.parallelExtents(); - renderWindow.renderers().forEach(function (renderer) { - var cam = renderer.camera(); - - if (cam !== camera) { - cam.setPosition(pos[0], pos[1], pos[2]); - cam.setFocalPoint(fp[0], fp[1], fp[2]); - cam.setClippingRange(cr[0], cr[1]); - cam.setParallelExtents(pe); - renderer.render(); - } - }); + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this._wipePolygons(); + s_exit(); }; - // Connect to interactor events - // Connect to pan event - m_this.layer().geoOn(geo.event.pan, function (evt) { - var vglRenderer = m_this.contextRenderer(), - camera, - focusPoint, - centerDisplay, - centerGeo, - newCenterDisplay, - newCenterGeo, - renderWindow, - layer = m_this.layer(); - - if (evt.geo && evt.geo._triggeredBy !== layer) { - // skip handling if the renderer is unconnected - if (!vglRenderer || !vglRenderer.camera()) { - console.log('Pan event triggered on unconnected VGL renderer.'); - } - - renderWindow = m_viewer.renderWindow(); - camera = vglRenderer.camera(); - focusPoint = renderWindow.focusDisplayPoint(); - - // Calculate the center in display coordinates - centerDisplay = [m_width / 2, m_height / 2, 0]; - - // Calculate the center in world coordinates - centerGeo = renderWindow.displayToWorld( - centerDisplay[0], - centerDisplay[1], - focusPoint, - vglRenderer - ); - - newCenterDisplay = [ - centerDisplay[0] + evt.screenDelta.x, - centerDisplay[1] + evt.screenDelta.y - ]; - - newCenterGeo = renderWindow.displayToWorld( - newCenterDisplay[0], - newCenterDisplay[1], - focusPoint, - vglRenderer - ); - - camera.pan( - centerGeo[0] - newCenterGeo[0], - centerGeo[1] - newCenterGeo[1], - centerGeo[2] - newCenterGeo[2] - ); - - evt.center = { - x: newCenterGeo[0], - y: newCenterGeo[1], - z: newCenterGeo[2] - }; - - m_this._updateRendererCamera(); - } - }); - - // Connect to zoom event - m_this.layer().geoOn(geo.event.zoom, function (evt) { - var vglRenderer = m_this.contextRenderer(), - camera, - renderWindow, - layer = m_this.layer(), - center, - dir, - focusPoint, - position, - newZ; - - if (evt.geo && evt.geo._triggeredBy !== layer) { - // skip handling if the renderer is unconnected - if (!vglRenderer || !vglRenderer.camera()) { - console.log('Zoom event triggered on unconnected vgl renderer.'); - } - - renderWindow = m_viewer.renderWindow(); - camera = vglRenderer.camera(); - focusPoint = camera.focalPoint(); - position = camera.position(); - var windowSize = renderWindow.windowSize(); - newZ = camera.zoomToHeight(evt.zoomLevel, windowSize[0], windowSize[1]); - - evt.pan = null; - if (evt.screenPosition) { - center = renderWindow.displayToWorld( - evt.screenPosition.x, - evt.screenPosition.y, - focusPoint, - vglRenderer - ); - dir = [center[0] - position[0], center[1] - position[1], center[2] - position[2]]; - evt.center = layer.fromLocal({ - x: position[0] + dir[0] * (1 - newZ / position[2]), - y: position[1] + dir[1] * (1 - newZ / position[2]) - }); - } - - camera.setPosition(position[0], position[1], newZ); - camera.setParallelExtents({zoom: evt.zoomLevel}); - - m_this._updateRendererCamera(); - } - }); - - // Connect to parallelprojection event - m_this.layer().geoOn(geo.event.parallelprojection, function (evt) { - var vglRenderer = m_this.contextRenderer(), - camera, - layer = m_this.layer(); - - if (evt.geo && evt.geo._triggeredBy !== layer) { - if (!vglRenderer || !vglRenderer.camera()) { - console.log('Parallel projection event triggered on unconnected VGL ' + - 'renderer.'); - } - camera = vglRenderer.camera(); - camera.setEnableParallelProjection(evt.parallelProjection); - m_this._updateRendererCamera(); - } - }); - + this._init(arg); return this; }; -inherit(geo.gl.vglRenderer, geo.gl.renderer); +inherit(geo.gl.choroplethFeature, geo.choroplethFeature); -geo.registerRenderer('vgl', geo.gl.vglRenderer); +// Now register it +geo.registerFeature('vgl', 'choropleth', geo.gl.choroplethFeature); /** @namespace */ geo.d3 = {}; @@ -24970,545 +26994,740 @@ inherit(geo.d3.object, geo.sceneObject); ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of pointFeature + * Create a new instance of class d3Renderer * * @class - * @extends geo.pointFeature - * @extends geo.d3.object - * @returns {geo.d3.pointFeature} + * @extends geo.renderer + * @returns {geo.d3.d3Renderer} */ ////////////////////////////////////////////////////////////////////////////// -geo.d3.pointFeature = function (arg) { +geo.d3.d3Renderer = function (arg) { 'use strict'; - if (!(this instanceof geo.d3.pointFeature)) { - return new geo.d3.pointFeature(arg); + + if (!(this instanceof geo.d3.d3Renderer)) { + return new geo.d3.d3Renderer(arg); } + geo.renderer.call(this, arg); + + var s_exit = this._exit; + + geo.d3.object.call(this, arg); + arg = arg || {}; - geo.pointFeature.call(this, arg); - geo.d3.object.call(this); + + var m_this = this, + m_sticky = null, + m_features = {}, + m_corners = null, + m_width = null, + m_height = null, + m_scale = 1, + m_dx = 0, + m_dy = 0, + m_svg = null, + m_defs = null; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set attributes to a d3 selection. + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function setAttrs(select, attrs) { + var key; + for (key in attrs) { + if (attrs.hasOwnProperty(key)) { + select.attr(key, attrs[key]); + } + } + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Meta functions for converting from geojs styles to d3. + * @private + */ + //////////////////////////////////////////////////////////////////////////// + this._convertColor = function (f, g) { + f = geo.util.ensureFunction(f); + g = g || function () { return true; }; + return function () { + var c = 'none'; + if (g.apply(m_this, arguments)) { + c = f.apply(m_this, arguments); + if (c.hasOwnProperty('r') && + c.hasOwnProperty('g') && + c.hasOwnProperty('b')) { + c = d3.rgb(255 * c.r, 255 * c.g, 255 * c.b); + } + } + return c; + }; + }; + + this._convertPosition = function (f) { + f = geo.util.ensureFunction(f); + return function () { + return m_this.layer().map().worldToDisplay(f.apply(m_this, arguments)); + }; + }; + + this._convertScale = function (f) { + f = geo.util.ensureFunction(f); + return function () { + return f.apply(m_this, arguments) / m_scale; + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set styles to a d3 selection. Ignores unkown style keys. + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function setStyles(select, styles) { + /* jshint validthis:true */ + var key, k, f; + function fillFunc() { + if (styles.fill.apply(m_this, arguments)) { + return null; + } else { + return 'none'; + } + } + function strokeFunc() { + if (styles.stroke.apply(m_this, arguments)) { + return null; + } else { + return 'none'; + } + } + for (key in styles) { + if (styles.hasOwnProperty(key)) { + f = null; + k = null; + if (key === 'strokeColor') { + k = 'stroke'; + f = m_this._convertColor(styles[key], styles.stroke); + } else if (key === 'stroke' && styles[key]) { + k = 'stroke'; + f = strokeFunc; + } else if (key === 'strokeWidth') { + k = 'stroke-width'; + f = m_this._convertScale(styles[key]); + } else if (key === 'strokeOpacity') { + k = 'stroke-opacity'; + f = styles[key]; + } else if (key === 'fillColor') { + k = 'fill'; + f = m_this._convertColor(styles[key], styles.fill); + } else if (key === 'fill' && !styles.hasOwnProperty('fillColor')) { + k = 'fill'; + f = fillFunc; + } else if (key === 'fillOpacity') { + k = 'fill-opacity'; + f = styles[key]; + } + if (k) { + select.style(k, f); + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the svg group element associated with this renderer instance, or of a + * group within the render instance. + * + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function getGroup(parentId) { + if (parentId) { + return m_svg.select('.group-' + parentId); + } + return m_svg.select('.group-' + m_this._d3id()); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Set the initial lat-lon coordinates of the map view. + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function initCorners() { + var layer = m_this.layer(), + map = layer.map(), + width = m_this.layer().map().size().width, + height = m_this.layer().map().size().height; + + m_width = width; + m_height = height; + if (!m_width || !m_height) { + throw 'Map layer has size 0'; + } + m_corners = { + upperLeft: map.displayToGcs({'x': 0, 'y': 0}, null), + lowerRight: map.displayToGcs({'x': width, 'y': height}, null) + }; + } //////////////////////////////////////////////////////////////////////////// /** + * Set the translation, scale, and zoom for the current view. + * @note rotation not yet supported * @private */ //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - s_update = this._update, - m_buildTime = geo.timestamp(), - m_style = {}, - m_sticky; + this._setTransform = function () { + if (!m_corners) { + initCorners(); + } + + if (!m_sticky) { + return; + } + + var layer = m_this.layer(), + map = layer.map(), + upperLeft = map.gcsToDisplay(m_corners.upperLeft, null), + lowerRight = map.gcsToDisplay(m_corners.lowerRight, null), + group = getGroup(), + canvas = m_this.canvas(), + dx, dy, scale; + + if (canvas.attr('scale') !== null) { + scale = canvas.attr('scale') || 1; + dx = (canvas.attr('dx') || 0) * scale; + dy = (canvas.attr('dy') || 0) * scale; + dx += map.size().width / 2; + dy += map.size().height / 2; + } else { + // calculate the translation + dx = upperLeft.x; + dy = upperLeft.y; + scale = (lowerRight.y - upperLeft.y) / m_height; + } + + // set the group transform property + group.attr('transform', 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'); + + // set internal variables + m_scale = scale; + m_dx = dx; + m_dy = dy; + }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Convert from screen pixel coordinates to the local coordinate system + * in the SVG group element taking into account the transform. + * @private */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - m_sticky = m_this.layer().sticky(); - return m_this; + this.baseToLocal = function (pt) { + return { + x: (pt.x - m_dx) / m_scale, + y: (pt.y - m_dy) / m_scale + }; }; //////////////////////////////////////////////////////////////////////////// /** - * Build - * - * @override + * Convert from the local coordinate system in the SVG group element + * to screen pixel coordinates. + * @private */ //////////////////////////////////////////////////////////////////////////// - this._build = function () { - var data = m_this.data(), - s_style = m_this.style.get(), - m_renderer = m_this.renderer(), - pos_func = m_this.position(); - - // call super-method - s_update.call(m_this); - - // default to empty data array - if (!data) { data = []; } - - // fill in d3 renderer style object defaults - m_style.id = m_this._d3id(); - m_style.data = data; - m_style.append = 'circle'; - m_style.attributes = { - r: m_renderer._convertScale(s_style.radius), - cx: function (d) { - return m_renderer.worldToDisplay(pos_func(d)).x; - }, - cy: function (d) { - return m_renderer.worldToDisplay(pos_func(d)).y; - } + this.localToBase = function (pt) { + return { + x: pt.x * m_scale + m_dx, + y: pt.y * m_scale + m_dy }; - m_style.style = s_style; - m_style.classes = ['d3PointFeature']; - - // pass to renderer to draw - m_this.renderer()._drawFeatures(m_style); - - // update time stamps - m_buildTime.modified(); - m_this.updateTime().modified(); - return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Update - * - * @override + * Initialize */ //////////////////////////////////////////////////////////////////////////// - this._update = function () { - s_update.call(m_this); + this._init = function (arg) { + if (!m_this.canvas()) { + var canvas; + arg.widget = arg.widget || false; - if (m_this.getMTime() >= m_buildTime.getMTime()) { - m_this._build(); - } + if ('d3Parent' in arg) { + m_svg = d3.select(arg.d3Parent).append('svg'); + } else { + m_svg = d3.select(m_this.layer().node().get(0)).append('svg'); + } - return m_this; - }; + // create a global svg definitions element + m_defs = m_svg.append('defs'); - this._init(arg); - return this; -}; + var shadow = m_defs + .append('filter') + .attr('id', 'geo-highlight') + .attr('x', '-100%') + .attr('y', '-100%') + .attr('width', '300%') + .attr('height', '300%'); + shadow + .append('feMorphology') + .attr('operator', 'dilate') + .attr('radius', 2) + .attr('in', 'SourceAlpha') + .attr('result', 'dilateOut'); + shadow + .append('feGaussianBlur') + .attr('stdDeviation', 5) + .attr('in', 'dilateOut') + .attr('result', 'blurOut'); + shadow + .append('feColorMatrix') + .attr('type', 'matrix') + .attr('values', '-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0') + .attr('in', 'blurOut') + .attr('result', 'invertOut'); + shadow + .append('feBlend') + .attr('in', 'SourceGraphic') + .attr('in2', 'invertOut') + .attr('mode', 'normal'); -inherit(geo.d3.pointFeature, geo.pointFeature); + if (!arg.widget) { + canvas = m_svg.append('g'); + } -// Now register it -geo.registerFeature('d3', 'point', geo.d3.pointFeature); + shadow = m_defs.append('filter') + .attr('id', 'geo-blur') + .attr('x', '-100%') + .attr('y', '-100%') + .attr('width', '300%') + .attr('height', '300%'); -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class lineFeature - * - * @class - * @extends geo.lineFeature - * @extends geo.d3.object - * @returns {geo.d3.lineFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.d3.lineFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.d3.lineFeature)) { - return new geo.d3.lineFeature(arg); - } - arg = arg || {}; - geo.lineFeature.call(this, arg); - geo.d3.object.call(this); + shadow + .append('feGaussianBlur') + .attr('stdDeviation', 20) + .attr('in', 'SourceGraphic'); + + m_sticky = m_this.layer().sticky(); + m_svg.attr('class', m_this._d3id()); + m_svg.attr('width', m_this.layer().node().width()); + m_svg.attr('height', m_this.layer().node().height()); + + if (!arg.widget) { + canvas.attr('class', 'group-' + m_this._d3id()); + + m_this.canvas(canvas); + } else { + m_this.canvas(m_svg); + } + } + m_this._setTransform(); + }; //////////////////////////////////////////////////////////////////////////// /** - * @private + * Get API used by the renderer */ //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - m_buildTime = geo.timestamp(), - s_update = this._update; + this.api = function () { + return 'd3'; + }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Return the current scaling factor to build features that shouldn't + * change size during zooms. For example: + * + * selection.append('circle') + * .attr('r', r0 / renderer.scaleFactor()); + * + * This will create a circle element with radius r0 independent of the + * current zoom level. */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - return m_this; + this.scaleFactor = function () { + return m_scale; }; //////////////////////////////////////////////////////////////////////////// /** - * Build - * - * @override + * Handle resize event */ //////////////////////////////////////////////////////////////////////////// - this._build = function () { - var data = m_this.data() || [], - s_style = m_this.style(), - m_renderer = m_this.renderer(), - pos_func = m_this.position(), - line = d3.svg.line() - .x(function (d) { return m_renderer.worldToDisplay(d).x; }) - .y(function (d) { return m_renderer.worldToDisplay(d).y; }); - - s_update.call(m_this); - s_style.fill = function () { return false; }; - - data.forEach(function (item, idx) { - var m_style; - var ln = m_this.line()(item, idx); - - var style = {}, key; - function wrapStyle(func) { - if (geo.util.isFunction(func)) { - return function () { - return func(ln[0], 0, item, idx); - }; - } else { - return func; - } - } - for (key in s_style) { - if (s_style.hasOwnProperty(key)) { - style[key] = wrapStyle(s_style[key]); - } - } - - // item is an object representing a single line - // m_this.line()(item) is an array of coordinates - m_style = { - data: [ln.map(function (d, i) { return pos_func(d, i, item, idx);})], - append: 'path', - attributes: { - d: line - }, - id: m_this._d3id() + idx, - classes: ['d3LineFeature', 'd3SubLine-' + idx], - style: style - }; - - m_renderer._drawFeatures(m_style); - }); - - m_buildTime.modified(); - m_this.updateTime().modified(); - return m_this; + this._resize = function (x, y, w, h) { + if (!m_corners) { + initCorners(); + } + m_svg.attr('width', w); + m_svg.attr('height', h); + m_this._setTransform(); + m_this.layer().geoTrigger(geo.event.d3Rescale, { scale: m_scale }, true); }; //////////////////////////////////////////////////////////////////////////// /** - * Update - * - * @override + * Update noop for geo.d3.object api. */ //////////////////////////////////////////////////////////////////////////// this._update = function () { - s_update.call(m_this); - - if (m_this.getMTime() >= m_buildTime.getMTime()) { - m_this._build(); - } - - return m_this; }; - this._init(arg); - return this; -}; - -inherit(geo.d3.lineFeature, geo.lineFeature); - -geo.registerFeature('d3', 'line', geo.d3.lineFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class pathFeature - * - * @class - * @extends geo.pathFeature - * @extends geo.d3.object - * @returns {geo.d3.pathFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.d3.pathFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.d3.pathFeature)) { - return new geo.d3.pathFeature(arg); - } - arg = arg || {}; - geo.pathFeature.call(this, arg); - geo.d3.object.call(this); - //////////////////////////////////////////////////////////////////////////// /** - * @private + * Exit */ //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - m_buildTime = geo.timestamp(), - s_update = this._update, - m_style = {}; - - m_style.style = {}; + this._exit = function () { + m_features = {}; + m_this.canvas().remove(); + s_exit(); + }; //////////////////////////////////////////////////////////////////////////// /** - * Initialize + * Get the definitions dom element for the layer + * @protected */ //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - return m_this; + this._definitions = function () { + return m_defs; }; //////////////////////////////////////////////////////////////////////////// /** - * Build + * Create a new feature element from an object that describes the feature + * attributes. To be called from feature classes only. * - * @override + * Input: + * { + * id: A unique string identifying the feature. + * data: Array of data objects used in a d3 data method. + * index: A function that returns a unique id for each data element. + * style: An object containing element CSS styles. + * attributes: An object containing element attributes. + * classes: An array of classes to add to the elements. + * append: The element type as used in d3 append methods. + * parentId: If set, the group ID of the parent element. + * } */ //////////////////////////////////////////////////////////////////////////// - this._build = function () { - var data = m_this.data() || [], - s_style = m_this.style(), - m_renderer = m_this.renderer(), - tmp, diag; - s_update.call(m_this); - - diag = function (d) { - var p = { - source: d.source, - target: d.target - }; - return d3.svg.diagonal()(p); - }; - tmp = []; - data.forEach(function (d, i) { - var src, trg; - if (i < data.length - 1) { - src = d; - trg = data[i + 1]; - tmp.push({ - source: m_renderer.worldToDisplay(src), - target: m_renderer.worldToDisplay(trg) - }); - } - }); - m_style.data = tmp; - m_style.attributes = { - d: diag + this._drawFeatures = function (arg) { + m_features[arg.id] = { + data: arg.data, + index: arg.dataIndex, + style: arg.style, + attributes: arg.attributes, + classes: arg.classes, + append: arg.append, + parentId: arg.parentId }; + return m_this.__render(arg.id, arg.parentId); + }; - m_style.id = m_this._d3id(); - m_style.append = 'path'; - m_style.classes = ['d3PathFeature']; - m_style.style = $.extend({ - 'fill': function () { return false; }, - 'fillColor': function () { return { r: 0, g: 0, b: 0 }; } - }, s_style); + //////////////////////////////////////////////////////////////////////////// + /** + * Updates a feature by performing a d3 data join. If no input id is + * provided then this method will update all features. + */ + //////////////////////////////////////////////////////////////////////////// + this.__render = function (id, parentId) { + var key; + if (id === undefined) { + for (key in m_features) { + if (m_features.hasOwnProperty(key)) { + m_this.__render(key); + } + } + return m_this; + } + var data = m_features[id].data, + index = m_features[id].index, + style = m_features[id].style, + attributes = m_features[id].attributes, + classes = m_features[id].classes, + append = m_features[id].append, + selection = m_this.select(id, parentId).data(data, index); + selection.enter().append(append); + selection.exit().remove(); + setAttrs(selection, attributes); + selection.attr('class', classes.concat([id]).join(' ')); + setStyles(selection, style); + return m_this; + }; - m_this.renderer()._drawFeatures(m_style); + //////////////////////////////////////////////////////////////////////////// + /** + * Returns a d3 selection for the given feature id. + */ + //////////////////////////////////////////////////////////////////////////// + this.select = function (id, parentId) { + return getGroup(parentId).selectAll('.' + id); + }; - m_buildTime.modified(); - m_this.updateTime().modified(); + //////////////////////////////////////////////////////////////////////////// + /** + * Removes a feature from the layer. + */ + //////////////////////////////////////////////////////////////////////////// + this._removeFeature = function (id) { + m_this.select(id).remove(); + delete m_features[id]; return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Update - * - * @override - */ + * Override draw method to do nothing. + */ //////////////////////////////////////////////////////////////////////////// - this._update = function () { - s_update.call(m_this); + this.draw = function () { + }; - if (m_this.dataTime().getMTime() >= m_buildTime.getMTime()) { - m_this._build(); - } + // connect to pan event + this.layer().geoOn(geo.event.pan, m_this._setTransform); - return m_this; - }; + // connect to zoom event + this.layer().geoOn(geo.event.zoom, function () { + m_this._setTransform(); + m_this.__render(); + m_this.layer().geoTrigger(geo.event.d3Rescale, { scale: m_scale }, true); + }); + + this.layer().geoOn(geo.event.resize, function (event) { + m_this._resize(event.x, event.y, event.width, event.height); + }); this._init(arg); return this; }; -inherit(geo.d3.pathFeature, geo.pathFeature); +inherit(geo.d3.d3Renderer, geo.renderer); -geo.registerFeature('d3', 'path', geo.d3.pathFeature); +geo.registerRenderer('d3', geo.d3.d3Renderer); -/** - * @class - * @extends geo.graphFeature - */ -geo.d3.graphFeature = function (arg) { +geo.d3.tileLayer = function () { 'use strict'; + var m_this = this, + s_update = this._update, + s_init = this._init; - var m_this = this; + this._drawTile = function (tile) { + var bounds = m_this._tileBounds(tile), + parentNode = m_this._getSubLayer(tile.index.level); + tile.feature = m_this.createFeature( + 'plane', {drawOnAsyncResourceLoad: true}) + .origin([bounds.left, bounds.top]) + .upperLeft([bounds.left, bounds.top]) + .lowerRight([bounds.right, bounds.bottom]) + .style({ + image: tile._url, + opacity: 1, + reference: tile.toString(), + parentId: parentNode.attr('data-tile-layer-id') + }); + tile.feature._update(); + m_this.draw(); + }; - if (!(this instanceof geo.d3.graphFeature)) { - return new geo.d3.graphFeature(arg); - } - geo.graphFeature.call(this, arg); + /** + * Return the DOM eleement containing a level specific + * layer. This will create the element if it doesn't + * already exist. + * @param {number} level The zoom level of the layer to fetch + * @return {DOM} + */ + this._getSubLayer = function (level) { + var node = m_this.canvas().select( + 'g[data-tile-layer="' + level.toFixed() + '"]'); + if (node.empty()) { + node = m_this.canvas().append('g'); + var id = geo.d3.uniqueID(); + node.classed('group-' + id, true); + node.classed('geo-tile-layer', true); + node.attr('data-tile-layer', level.toFixed()); + node.attr('data-tile-layer-id', id); + } + return node; + }; - //////////////////////////////////////////////////////////////////////////// /** - * Returns a d3 selection for the graph elements - */ - //////////////////////////////////////////////////////////////////////////// - this.select = function () { - var renderer = m_this.renderer(), - selection = {}, - node = m_this.nodeFeature(), - links = m_this.linkFeatures(); - selection.nodes = renderer.select(node._d3id()); - selection.links = links.map(function (link) { - return renderer.select(link._d3id()); + * Set sublayer transforms to align them with the given zoom level. + * @param {number} level The target zoom level + */ + this._updateSubLayers = function (level) { + $.each(m_this.canvas().selectAll('.geo-tile-layer')[0], function (idx, el) { + var layer = parseInt($(el).attr('data-tile-layer')); + el = m_this._getSubLayer(layer); + var scale = Math.pow(2, level - layer); + el.attr('transform', 'matrix(' + [scale, 0, 0, scale, 0, 0].join() + ')'); }); - return selection; }; - return this; -}; + /* Initialize the tile layer. This creates a series of sublayers so that + * the different layers will stack in the proper order. + */ + this._init = function () { + var sublayer; -inherit(geo.d3.graphFeature, geo.graphFeature); + s_init.apply(m_this, arguments); + for (sublayer = 0; sublayer <= m_this._options.maxLevel; sublayer += 1) { + m_this._getSubLayer(sublayer); + } + }; -geo.registerFeature('d3', 'graph', geo.d3.graphFeature); + /* When update is called, apply the transform to our renderer. */ + this._update = function () { + s_update.apply(m_this, arguments); + m_this.renderer()._setTransform(); + }; + + /* Remove both the tile feature and an internal image element. */ + this._remove = function (tile) { + if (tile.feature) { + m_this.deleteFeature(tile.feature); + tile.feature = null; + } + if (tile.image) { + $(tile.image).remove(); + } + }; +}; + +geo.registerLayerAdjustment('d3', 'tile', geo.d3.tileLayer); ////////////////////////////////////////////////////////////////////////////// /** - * Create a plane feature given a lower left corner point - * and and upper right corner point - * - * *CURRENTLY BROKEN* + * Create a new instance of pointFeature * * @class - * @extends geo.planeFeature - * @param lowerleft - * @param upperright - * @returns {geo.d3.planeFeature} + * @extends geo.pointFeature + * @extends geo.d3.object + * @returns {geo.d3.pointFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.d3.planeFeature = function (arg) { +geo.d3.pointFeature = function (arg) { 'use strict'; - if (!(this instanceof geo.d3.planeFeature)) { - return new geo.d3.planeFeature(arg); + if (!(this instanceof geo.d3.pointFeature)) { + return new geo.d3.pointFeature(arg); } - geo.planeFeature.call(this, arg); + arg = arg || {}; + geo.pointFeature.call(this, arg); geo.d3.object.call(this); + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// var m_this = this, - m_style = {}, - s_update = this._update, s_init = this._init, - m_buildTime = geo.timestamp(); + s_update = this._update, + m_buildTime = geo.timestamp(), + m_style = {}, + m_sticky; - ////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// /** - * Normalize a coordinate as an object {x: ..., y: ...} - * - * @private - * @returns {Object} + * Initialize */ - ////////////////////////////////////////////////////////////////////////////// - function normalize(pt) { - if (Array.isArray(pt)) { - return { - x: pt[0], - y: pt[1] - }; - } - return pt; - } + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + m_sticky = m_this.layer().sticky(); + return m_this; + }; - ////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// /** - * Build the feature object and pass to the renderer for drawing. + * Build * - * @private - * @returns {geo.d3.planeFeature} + * @override */ - ////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// this._build = function () { - var origin = normalize(m_this.origin()), - ul = normalize(m_this.upperLeft()), - lr = normalize(m_this.lowerRight()), - renderer = m_this.renderer(), - s = m_this.style(); + var data = m_this.data(), + s_style = m_this.style.get(), + m_renderer = m_this.renderer(), + pos_func = m_this.position(); - delete s.fill_color; - delete s.color; - delete s.opacity; - if (!s.screenCoordinates) { - origin = renderer.worldToDisplay(origin); - ul = renderer.worldToDisplay(ul); - lr = renderer.worldToDisplay(lr); - } + // call super-method + s_update.call(m_this); + + // default to empty data array + if (!data) { data = []; } + + // fill in d3 renderer style object defaults m_style.id = m_this._d3id(); - m_style.style = s; + m_style.data = data; + m_style.append = 'circle'; m_style.attributes = { - x: ul.x, - y: ul.y, - width: lr.x - origin.x, - height: origin.y - ul.y + r: m_renderer._convertScale(s_style.radius), + cx: function (d) { + return m_this.featureGcsToDisplay(pos_func(d)).x; + }, + cy: function (d) { + return m_this.featureGcsToDisplay(pos_func(d)).y; + } }; - m_style.append = 'rect'; - m_style.data = [0]; - m_style.classes = ['d3PlaneFeature']; + m_style.style = s_style; + m_style.classes = ['d3PointFeature']; - renderer._drawFeatures(m_style); + // pass to renderer to draw + m_this.renderer()._drawFeatures(m_style); + + // update time stamps m_buildTime.modified(); + m_this.updateTime().modified(); return m_this; }; - ////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// /** - * Redraw the plane feature if necessary. + * Update * - * @private - * @returns {geo.d3.planeFeature} + * @override */ - ////////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// this._update = function () { s_update.call(m_this); - if (m_this.dataTime().getMTime() >= m_buildTime.getMTime()) { + if (m_this.getMTime() >= m_buildTime.getMTime()) { m_this._build(); } - return m_this; - }; - ////////////////////////////////////////////////////////////////////////////// - /** - * Initializes the plane feature style (over-riding the parent default). - * - * @private - * @returns {geo.d3.planeFeature} - */ - ////////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg || {}); - m_this.style({ - stroke: function () { return false; }, - fill: function () { return true; }, - fillColor: function () { return {r: 0.3, g: 0.3, b: 0.3}; }, - fillOpacity: function () { return 0.5; } - }); return m_this; }; - this._init(); + this._init(arg); return this; }; -inherit(geo.d3.planeFeature, geo.planeFeature); +inherit(geo.d3.pointFeature, geo.pointFeature); -geo.registerFeature('d3', 'plane', geo.d3.planeFeature); +// Now register it +geo.registerFeature('d3', 'point', geo.d3.pointFeature); ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of vectorFeature + * Create a new instance of class lineFeature * * @class - * @extends geo.vectorFeature + * @extends geo.lineFeature * @extends geo.d3.object - * @returns {geo.d3.vectorFeature} + * @returns {geo.d3.lineFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.d3.vectorFeature = function (arg) { +geo.d3.lineFeature = function (arg) { 'use strict'; - if (!(this instanceof geo.d3.vectorFeature)) { - return new geo.d3.vectorFeature(arg); + if (!(this instanceof geo.d3.lineFeature)) { + return new geo.d3.lineFeature(arg); } arg = arg || {}; - geo.vectorFeature.call(this, arg); + geo.lineFeature.call(this, arg); geo.d3.object.call(this); //////////////////////////////////////////////////////////////////////////// @@ -25518,151 +27737,74 @@ geo.d3.vectorFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// var m_this = this, s_init = this._init, - s_exit = this._exit, - s_update = this._update, m_buildTime = geo.timestamp(), - m_style = {}, - m_sticky; - - //////////////////////////////////////////////////////////////////////////// - /** - * Generate a unique ID for a marker definition - * @private - * @param {object} d Unused datum (for d3 compat) - * @param {number} i The marker index - */ - //////////////////////////////////////////////////////////////////////////// - function markerID(d, i) { - return m_this._d3id() + '_marker_' + i; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Add marker styles for vector arrows. - * @private - * @param {object[]} data The vector data array - * @param {function} stroke The stroke accessor - * @param {function} opacity The opacity accessor - */ - //////////////////////////////////////////////////////////////////////////// - function updateMarkers(data, stroke, opacity) { - - var renderer = m_this.renderer(); - var sel = m_this.renderer()._definitions() - .selectAll('marker.geo-vector') - .data(data); - sel.enter() - .append('marker') - .attr('id', markerID) - .attr('class', 'geo-vector') - .attr('viewBox', '0 0 10 10') - .attr('refX', '1') - .attr('refY', '5') - .attr('markerWidth', '5') - .attr('markerHeight', '5') - .attr('orient', 'auto') - .append('path') - .attr('d', 'M 0 0 L 10 5 L 0 10 z'); - - sel.exit().remove(); - - sel.style('stroke', renderer._convertColor(stroke)) - .style('fill', renderer._convertColor(stroke)) - .style('opacity', opacity); - } + s_update = this._update; //////////////////////////////////////////////////////////////////////////// /** * Initialize - * @protected */ //////////////////////////////////////////////////////////////////////////// this._init = function (arg) { s_init.call(m_this, arg); - m_sticky = m_this.layer().sticky(); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** * Build - * @protected + * + * @override */ //////////////////////////////////////////////////////////////////////////// this._build = function () { - var data = m_this.data(), - s_style = m_this.style.get(), + var data = m_this.data() || [], + s_style = m_this.style(), m_renderer = m_this.renderer(), - orig_func = m_this.origin(), - size_func = m_this.delta(), - cache = [], - scale = m_this.style('scale'), - max = Number.NEGATIVE_INFINITY; + pos_func = m_this.position(), + line = d3.svg.line() + .x(function (d) { return m_this.featureGcsToDisplay(d).x; }) + .y(function (d) { return m_this.featureGcsToDisplay(d).y; }); - // call super-method s_update.call(m_this); - - // default to empty data array - if (!data) { data = []; } - - // cache the georeferencing - cache = data.map(function (d, i) { - var origin = m_renderer.worldToDisplay(orig_func(d, i)), - delta = size_func(d, i); - max = Math.max(max, delta.x * delta.x + delta.y * delta.y); - return { - x1: origin.x, - y1: origin.y, - dx: delta.x, - dy: -delta.y - }; - }); - - max = Math.sqrt(max); - if (!scale) { - scale = 75 / max; - } - - function getScale() { - return scale / m_renderer.scaleFactor(); - } - - // fill in d3 renderer style object defaults - m_style.id = m_this._d3id(); - m_style.data = data; - m_style.append = 'line'; - m_style.attributes = { - x1: function (d, i) { - return cache[i].x1; - }, - y1: function (d, i) { - return cache[i].y1; - }, - x2: function (d, i) { - return cache[i].x1 + getScale() * cache[i].dx; - }, - y2: function (d, i) { - return cache[i].y1 + getScale() * cache[i].dy; - }, - 'marker-end': function (d, i) { - return 'url(#' + markerID(d, i) + ')'; + s_style.fill = function () { return false; }; + + data.forEach(function (item, idx) { + var m_style; + var ln = m_this.line()(item, idx); + + var style = {}, key; + function wrapStyle(func) { + if (geo.util.isFunction(func)) { + return function () { + return func(ln[0], 0, item, idx); + }; + } else { + return func; + } + } + for (key in s_style) { + if (s_style.hasOwnProperty(key)) { + style[key] = wrapStyle(s_style[key]); + } } - }; - m_style.style = { - stroke: function () { return true; }, - strokeColor: s_style.strokeColor, - strokeWidth: s_style.strokeWidth, - strokeOpacity: s_style.strokeOpacity - }; - m_style.classes = ['d3VectorFeature']; - // Add markers to the defition list - updateMarkers(data, s_style.strokeColor, s_style.strokeOpacity); + // item is an object representing a single line + // m_this.line()(item) is an array of coordinates + m_style = { + data: [ln.map(function (d, i) { return pos_func(d, i, item, idx);})], + append: 'path', + attributes: { + d: line + }, + id: m_this._d3id() + idx, + classes: ['d3LineFeature', 'd3SubLine-' + idx], + style: style + }; - // pass to renderer to draw - m_this.renderer()._drawFeatures(m_style); + m_renderer._drawFeatures(m_style); + }); - // update time stamps m_buildTime.modified(); m_this.updateTime().modified(); return m_this; @@ -25671,7 +27813,8 @@ geo.d3.vectorFeature = function (arg) { //////////////////////////////////////////////////////////////////////////// /** * Update - * @protected + * + * @override */ //////////////////////////////////////////////////////////////////////////// this._update = function () { @@ -25679,584 +27822,532 @@ geo.d3.vectorFeature = function (arg) { if (m_this.getMTime() >= m_buildTime.getMTime()) { m_this._build(); - } else { - updateMarkers( - m_style.data, - m_style.style.strokeColor, - m_style.style.strokeOpacity - ); } return m_this; }; - //////////////////////////////////////////////////////////////////////////// - /** - * Exit - * @protected - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - s_exit.call(m_this); - m_style = {}; - updateMarkers([], null, null); - }; - this._init(arg); return this; }; -inherit(geo.d3.vectorFeature, geo.vectorFeature); +inherit(geo.d3.lineFeature, geo.lineFeature); -// Now register it -geo.registerFeature('d3', 'vector', geo.d3.vectorFeature); +geo.registerFeature('d3', 'line', geo.d3.lineFeature); ////////////////////////////////////////////////////////////////////////////// /** - * Create a new instance of class d3Renderer + * Create a new instance of class pathFeature * * @class - * @extends geo.renderer - * @returns {geo.d3.d3Renderer} + * @extends geo.pathFeature + * @extends geo.d3.object + * @returns {geo.d3.pathFeature} */ ////////////////////////////////////////////////////////////////////////////// -geo.d3.d3Renderer = function (arg) { +geo.d3.pathFeature = function (arg) { 'use strict'; - - if (!(this instanceof geo.d3.d3Renderer)) { - return new geo.d3.d3Renderer(arg); + if (!(this instanceof geo.d3.pathFeature)) { + return new geo.d3.pathFeature(arg); } - geo.renderer.call(this, arg); - - var s_exit = this._exit; - - geo.d3.object.call(this, arg); - arg = arg || {}; - - var m_this = this, - m_sticky = null, - m_features = {}, - m_corners = null, - m_width = null, - m_height = null, - m_scale = 1, - m_dx = 0, - m_dy = 0, - m_svg = null, - m_defs = null; + geo.pathFeature.call(this, arg); + geo.d3.object.call(this); //////////////////////////////////////////////////////////////////////////// /** - * Set attributes to a d3 selection. * @private */ //////////////////////////////////////////////////////////////////////////// - function setAttrs(select, attrs) { - var key; - for (key in attrs) { - if (attrs.hasOwnProperty(key)) { - select.attr(key, attrs[key]); - } - } - } + var m_this = this, + s_init = this._init, + m_buildTime = geo.timestamp(), + s_update = this._update, + m_style = {}; + + m_style.style = {}; //////////////////////////////////////////////////////////////////////////// /** - * Meta functions for converting from geojs styles to d3. - * @private + * Initialize */ //////////////////////////////////////////////////////////////////////////// - this._convertColor = function (f, g) { - f = geo.util.ensureFunction(f); - g = g || function () { return true; }; - return function () { - var c = 'none'; - if (g.apply(this, arguments)) { - c = f.apply(this, arguments); - if (c.hasOwnProperty('r') && - c.hasOwnProperty('g') && - c.hasOwnProperty('b')) { - c = d3.rgb(255 * c.r, 255 * c.g, 255 * c.b); - } - } - return c; - }; - }; - - this._convertPosition = function (f) { - f = geo.util.ensureFunction(f); - return function () { - return m_this.worldToDisplay(f.apply(this, arguments)); - }; - }; - - this._convertScale = function (f) { - f = geo.util.ensureFunction(f); - return function () { - return f.apply(this, arguments) / m_scale; - }; + this._init = function (arg) { + s_init.call(m_this, arg); + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Set styles to a d3 selection. Ignores unkown style keys. - * @private + * Build + * + * @override */ //////////////////////////////////////////////////////////////////////////// - function setStyles(select, styles) { - /* jshint validthis:true */ - var key, k, f; - function fillFunc() { - if (styles.fill.apply(this, arguments)) { - return null; - } else { - return 'none'; - } - } - function strokeFunc() { - if (styles.stroke.apply(this, arguments)) { - return null; - } else { - return 'none'; - } - } - for (key in styles) { - if (styles.hasOwnProperty(key)) { - f = null; - k = null; - if (key === 'strokeColor') { - k = 'stroke'; - f = m_this._convertColor(styles[key], styles.stroke); - } else if (key === 'stroke' && styles[key]) { - k = 'stroke'; - f = strokeFunc; - } else if (key === 'strokeWidth') { - k = 'stroke-width'; - f = m_this._convertScale(styles[key]); - } else if (key === 'strokeOpacity') { - k = 'stroke-opacity'; - f = styles[key]; - } else if (key === 'fillColor') { - k = 'fill'; - f = m_this._convertColor(styles[key], styles.fill); - } else if (key === 'fill' && !styles.hasOwnProperty('fillColor')) { - k = 'fill'; - f = fillFunc; - } else if (key === 'fillOpacity') { - k = 'fill-opacity'; - f = styles[key]; - } - if (k) { - select.style(k, f); - } - } - } - } + this._build = function () { + var data = m_this.data() || [], + s_style = m_this.style(), + tmp, diag; + s_update.call(m_this); - //////////////////////////////////////////////////////////////////////////// - /** - * Get the map instance or return null if not connected to a map. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function getMap() { - var layer = m_this.layer(); - if (!layer) { - return null; - } - return layer.map(); - } + diag = function (d) { + var p = { + source: d.source, + target: d.target + }; + return d3.svg.diagonal()(p); + }; + tmp = []; + data.forEach(function (d, i) { + var src, trg; + if (i < data.length - 1) { + src = d; + trg = data[i + 1]; + tmp.push({ + source: m_this.featureGcsToDisplay(src), + target: m_this.featureGcsToDisplay(trg) + }); + } + }); + m_style.data = tmp; + m_style.attributes = { + d: diag + }; - //////////////////////////////////////////////////////////////////////////// - /** - * Get the svg group element associated with this renderer instance. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function getGroup() { - return m_svg.select('.group-' + m_this._d3id()); - } + m_style.id = m_this._d3id(); + m_style.append = 'path'; + m_style.classes = ['d3PathFeature']; + m_style.style = $.extend({ + 'fill': function () { return false; }, + 'fillColor': function () { return { r: 0, g: 0, b: 0 }; } + }, s_style); - //////////////////////////////////////////////////////////////////////////// - /** - * Set the initial lat-lon coordinates of the map view. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function initCorners() { - var layer = m_this.layer(), - map = layer.map(), - width = m_this.layer().width(), - height = m_this.layer().height(); + m_this.renderer()._drawFeatures(m_style); - m_width = width; - m_height = height; - if (!m_width || !m_height) { - throw 'Map layer has size 0'; - } - m_corners = { - 'upperLeft': map.displayToGcs({'x': 0, 'y': 0}), - 'lowerRight': map.displayToGcs({'x': width, 'y': height}) - }; - } + m_buildTime.modified(); + m_this.updateTime().modified(); + return m_this; + }; //////////////////////////////////////////////////////////////////////////// /** - * Set the translation, scale, and zoom for the current view. - * @note rotation not yet supported - * @private + * Update + * + * @override */ //////////////////////////////////////////////////////////////////////////// - function setTransform() { - - if (!m_corners) { - initCorners(); - } + this._update = function () { + s_update.call(m_this); - if (!m_sticky) { - return; + if (m_this.dataTime().getMTime() >= m_buildTime.getMTime()) { + m_this._build(); } - var layer = m_this.layer(), - map = layer.map(), - upperLeft = map.gcsToDisplay(m_corners.upperLeft), - lowerRight = map.gcsToDisplay(m_corners.lowerRight), - group = getGroup(), - dx, dy, scale; + return m_this; + }; - // calculate the translation - dx = upperLeft.x; - dy = upperLeft.y; + this._init(arg); + return this; +}; - // calculate the scale - scale = (lowerRight.y - upperLeft.y) / m_height; +inherit(geo.d3.pathFeature, geo.pathFeature); - // set the group transform property - group.attr('transform', 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'); +geo.registerFeature('d3', 'path', geo.d3.pathFeature); - // set internal variables - m_scale = scale; - m_dx = dx; - m_dy = dy; - } +/** + * @class + * @extends geo.graphFeature + */ +geo.d3.graphFeature = function (arg) { + 'use strict'; - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from screen pixel coordinates to the local coordinate system - * in the SVG group element taking into account the transform. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function baseToLocal(pt) { - return { - x: (pt.x - m_dx) / m_scale, - y: (pt.y - m_dy) / m_scale - }; - } + var m_this = this; - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from the local coordinate system in the SVG group element - * to screen pixel coordinates. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function localToBase(pt) { - return { - x: pt.x * m_scale + m_dx, - y: pt.y * m_scale + m_dy - }; + if (!(this instanceof geo.d3.graphFeature)) { + return new geo.d3.graphFeature(arg); } + geo.graphFeature.call(this, arg); //////////////////////////////////////////////////////////////////////////// /** - * Initialize - */ + * Returns a d3 selection for the graph elements + */ //////////////////////////////////////////////////////////////////////////// - this._init = function () { - if (!m_this.canvas()) { - var canvas; - m_svg = d3.select(m_this.layer().node().get(0)).append('svg'); - - // create a global svg definitions element - m_defs = m_svg.append('defs'); - - var shadow = m_defs - .append('filter') - .attr('id', 'geo-highlight') - .attr('x', '-100%') - .attr('y', '-100%') - .attr('width', '300%') - .attr('height', '300%'); - shadow - .append('feMorphology') - .attr('operator', 'dilate') - .attr('radius', 2) - .attr('in', 'SourceAlpha') - .attr('result', 'dilateOut'); - shadow - .append('feGaussianBlur') - .attr('stdDeviation', 5) - .attr('in', 'dilateOut') - .attr('result', 'blurOut'); - shadow - .append('feColorMatrix') - .attr('type', 'matrix') - .attr('values', '-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0') - .attr('in', 'blurOut') - .attr('result', 'invertOut'); - shadow - .append('feBlend') - .attr('in', 'SourceGraphic') - .attr('in2', 'invertOut') - .attr('mode', 'normal'); - canvas = m_svg.append('g'); + this.select = function () { + var renderer = m_this.renderer(), + selection = {}, + node = m_this.nodeFeature(), + links = m_this.linkFeatures(); + selection.nodes = renderer.select(node._d3id()); + selection.links = links.map(function (link) { + return renderer.select(link._d3id()); + }); + return selection; + }; - shadow = m_defs.append('filter') - .attr('id', 'geo-blur') - .attr('x', '-100%') - .attr('y', '-100%') - .attr('width', '300%') - .attr('height', '300%'); + return this; +}; - shadow - .append('feGaussianBlur') - .attr('stdDeviation', 20) - .attr('in', 'SourceGraphic'); +inherit(geo.d3.graphFeature, geo.graphFeature); - m_sticky = m_this.layer().sticky(); - m_svg.attr('class', m_this._d3id()); - m_svg.attr('width', m_this.layer().node().width()); - m_svg.attr('height', m_this.layer().node().height()); +geo.registerFeature('d3', 'graph', geo.d3.graphFeature); - canvas.attr('class', 'group-' + m_this._d3id()); +////////////////////////////////////////////////////////////////////////////// +/** + * Create a plane feature given a lower left corner point + * and and upper right corner point + * + * @class + * @extends geo.planeFeature + * @param lowerleft + * @param upperright + * @returns {geo.d3.planeFeature} + */ +////////////////////////////////////////////////////////////////////////////// +geo.d3.planeFeature = function (arg) { + 'use strict'; + if (!(this instanceof geo.d3.planeFeature)) { + return new geo.d3.planeFeature(arg); + } + geo.planeFeature.call(this, arg); + geo.d3.object.call(this); - m_this.canvas(canvas); - } - }; + var m_this = this, + m_style = {}, + s_update = this._update, + s_init = this._init, + m_buildTime = geo.timestamp(); - //////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// /** - * Convert from coordinates in the svg group element to lat/lon. - * Supports objects or arrays of objects. + * Normalize a coordinate as an object {x: ..., y: ...} + * + * @private + * @returns {Object} */ - //////////////////////////////////////////////////////////////////////////// - this.displayToWorld = function (pt) { - var map = getMap(); - if (!map) { - throw 'Cannot project until this layer is connected to a map.'; - } + ////////////////////////////////////////////////////////////////////////////// + function normalize(pt) { if (Array.isArray(pt)) { - pt = pt.map(function (x) { - return map.displayToGcs(localToBase(x)); - }); - } else { - pt = map.displayToGcs(localToBase(pt)); + return { + x: pt[0], + y: pt[1] + }; } return pt; - }; + } - //////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// /** - * Convert from lat/lon to pixel coordinates in the svg group element. - * Supports objects or arrays of objects. + * Build the feature object and pass to the renderer for drawing. + * + * @private + * @returns {geo.d3.planeFeature} */ - //////////////////////////////////////////////////////////////////////////// - this.worldToDisplay = function (pt) { - var map = getMap(); - if (!map) { - throw 'Cannot project until this layer is connected to a map.'; + ////////////////////////////////////////////////////////////////////////////// + this._build = function () { + var ul = normalize(m_this.upperLeft()), + lr = normalize(m_this.lowerRight()), + renderer = m_this.renderer(), + s = m_this.style(); + + delete s.fill_color; + delete s.color; + delete s.opacity; + /* + if (!s.screenCoordinates) { + origin = renderer.layer().map().worldToDisplay(origin); + ul = renderer.layer().map().worldToDisplay(ul); + lr = renderer.layer().map().worldToDisplay(lr); } - var v; - if (Array.isArray(pt)) { - v = pt.map(function (x) { - return baseToLocal(map.gcsToDisplay(x)); - }); + */ + m_style.id = m_this._d3id(); + m_style.style = s; + m_style.attributes = { + x: ul.x, + y: ul.y, + width: Math.abs(lr.x - ul.x), + height: Math.abs(lr.y - ul.y), + reference: s.reference + }; + if (s.image) { + m_style.append = 'image'; + m_style.attributes['xlink:href'] = s.image; } else { - v = baseToLocal(map.gcsToDisplay(pt)); + m_style.append = 'rect'; } - return v; + m_style.data = [0]; + m_style.classes = ['d3PlaneFeature']; + if (s.parentId) { + m_style.parentId = s.parentId; + } + + renderer._drawFeatures(m_style); + m_buildTime.modified(); + return m_this; }; - //////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// /** - * Get API used by the renderer + * Redraw the plane feature if necessary. + * + * @private + * @returns {geo.d3.planeFeature} */ - //////////////////////////////////////////////////////////////////////////// - this.api = function () { - return 'd3'; + ////////////////////////////////////////////////////////////////////////////// + this._update = function () { + s_update.call(m_this); + + if (m_this.dataTime().getMTime() >= m_buildTime.getMTime()) { + m_this._build(); + } + return m_this; }; - //////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// /** - * Return the current scaling factor to build features that shouldn't - * change size during zooms. For example: - * - * selection.append('circle') - * .attr('r', r0 / renderer.scaleFactor()); + * Initializes the plane feature style (over-riding the parent default). * - * This will create a circle element with radius r0 independent of the - * current zoom level. + * @private + * @returns {geo.d3.planeFeature} */ - //////////////////////////////////////////////////////////////////////////// - this.scaleFactor = function () { - return m_scale; + ////////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg || {}); + m_this.style({ + stroke: function () { return false; }, + fill: function () { return true; }, + fillColor: function () { return {r: 0.3, g: 0.3, b: 0.3}; }, + fillOpacity: function () { return 0.5; } + }); + return m_this; }; + this._init(); + return this; +}; + +inherit(geo.d3.planeFeature, geo.planeFeature); + +geo.registerFeature('d3', 'plane', geo.d3.planeFeature); + +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of vectorFeature + * + * @class + * @extends geo.vectorFeature + * @extends geo.d3.object + * @returns {geo.d3.vectorFeature} + */ +////////////////////////////////////////////////////////////////////////////// +geo.d3.vectorFeature = function (arg) { + 'use strict'; + if (!(this instanceof geo.d3.vectorFeature)) { + return new geo.d3.vectorFeature(arg); + } + arg = arg || {}; + geo.vectorFeature.call(this, arg); + geo.d3.object.call(this); + //////////////////////////////////////////////////////////////////////////// /** - * Handle resize event + * @private */ //////////////////////////////////////////////////////////////////////////// - this._resize = function (x, y, w, h) { - if (!m_corners) { - initCorners(); - } - m_svg.attr('width', w); - m_svg.attr('height', h); - setTransform(); - m_this.layer().geoTrigger(geo.event.d3Rescale, { scale: m_scale }, true); - }; + var m_this = this, + s_init = this._init, + s_exit = this._exit, + s_update = this._update, + m_buildTime = geo.timestamp(), + m_style = {}, + m_sticky; //////////////////////////////////////////////////////////////////////////// /** - * Update noop for geo.d3.object api. + * Generate a unique ID for a marker definition + * @private + * @param {object} d Unused datum (for d3 compat) + * @param {number} i The marker index */ //////////////////////////////////////////////////////////////////////////// - this._update = function () { - }; + function markerID(d, i) { + return m_this._d3id() + '_marker_' + i; + } //////////////////////////////////////////////////////////////////////////// /** - * Exit + * Add marker styles for vector arrows. + * @private + * @param {object[]} data The vector data array + * @param {function} stroke The stroke accessor + * @param {function} opacity The opacity accessor */ //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_features = {}; - m_this.canvas().remove(); - s_exit(); - }; + function updateMarkers(data, stroke, opacity) { + + var renderer = m_this.renderer(); + var sel = m_this.renderer()._definitions() + .selectAll('marker.geo-vector') + .data(data); + sel.enter() + .append('marker') + .attr('id', markerID) + .attr('class', 'geo-vector') + .attr('viewBox', '0 0 10 10') + .attr('refX', '1') + .attr('refY', '5') + .attr('markerWidth', '5') + .attr('markerHeight', '5') + .attr('orient', 'auto') + .append('path') + .attr('d', 'M 0 0 L 10 5 L 0 10 z'); + + sel.exit().remove(); + + sel.style('stroke', renderer._convertColor(stroke)) + .style('fill', renderer._convertColor(stroke)) + .style('opacity', opacity); + } //////////////////////////////////////////////////////////////////////////// /** - * Get the definitions dom element for the layer + * Initialize * @protected */ //////////////////////////////////////////////////////////////////////////// - this._definitions = function () { - return m_defs; + this._init = function (arg) { + s_init.call(m_this, arg); + m_sticky = m_this.layer().sticky(); + return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Create a new feature element from an object that describes the feature - * attributes. To be called from feature classes only. - * - * Input: - * { - * id: A unique string identifying the feature. - * data: Array of data objects used in a d3 data method. - * index: A function that returns a unique id for each data element. - * style: An object containing element CSS styles. - * attributes: An object containing element attributes. - * classes: An array of classes to add to the elements. - * append: The element type as used in d3 append methods. - * } + * Build + * @protected */ //////////////////////////////////////////////////////////////////////////// - this._drawFeatures = function (arg) { - m_features[arg.id] = { - data: arg.data, - index: arg.dataIndex, - style: arg.style, - attributes: arg.attributes, - classes: arg.classes, - append: arg.append - }; - return m_this.__render(arg.id); - }; + this._build = function () { + var data = m_this.data(), + s_style = m_this.style.get(), + m_renderer = m_this.renderer(), + orig_func = m_this.origin(), + size_func = m_this.delta(), + cache = [], + scale = m_this.style('scale'), + max = Number.NEGATIVE_INFINITY; - //////////////////////////////////////////////////////////////////////////// - /** - * Updates a feature by performing a d3 data join. If no input id is - * provided then this method will update all features. - */ - //////////////////////////////////////////////////////////////////////////// - this.__render = function (id) { - var key; - if (id === undefined) { - for (key in m_features) { - if (m_features.hasOwnProperty(key)) { - m_this.__render(key); - } - } - return m_this; + // call super-method + s_update.call(m_this); + + // default to empty data array + if (!data) { data = []; } + + // cache the georeferencing + cache = data.map(function (d, i) { + var origin = m_this.featureGcsToDisplay(orig_func(d, i)), + delta = size_func(d, i); + max = Math.max(max, delta.x * delta.x + delta.y * delta.y); + return { + x1: origin.x, + y1: origin.y, + dx: delta.x, + dy: -delta.y + }; + }); + + max = Math.sqrt(max); + if (!scale) { + scale = 75 / max; } - var data = m_features[id].data, - index = m_features[id].index, - style = m_features[id].style, - attributes = m_features[id].attributes, - classes = m_features[id].classes, - append = m_features[id].append, - selection = m_this.select(id).data(data, index); - selection.enter().append(append); - selection.exit().remove(); - setAttrs(selection, attributes); - selection.attr('class', classes.concat([id]).join(' ')); - setStyles(selection, style); + + function getScale() { + return scale / m_renderer.scaleFactor(); + } + + // fill in d3 renderer style object defaults + m_style.id = m_this._d3id(); + m_style.data = data; + m_style.append = 'line'; + m_style.attributes = { + x1: function (d, i) { + return cache[i].x1; + }, + y1: function (d, i) { + return cache[i].y1; + }, + x2: function (d, i) { + return cache[i].x1 + getScale() * cache[i].dx; + }, + y2: function (d, i) { + return cache[i].y1 + getScale() * cache[i].dy; + }, + 'marker-end': function (d, i) { + return 'url(#' + markerID(d, i) + ')'; + } + }; + m_style.style = { + stroke: function () { return true; }, + strokeColor: s_style.strokeColor, + strokeWidth: s_style.strokeWidth, + strokeOpacity: s_style.strokeOpacity + }; + m_style.classes = ['d3VectorFeature']; + + // Add markers to the defition list + updateMarkers(data, s_style.strokeColor, s_style.strokeOpacity); + + // pass to renderer to draw + m_this.renderer()._drawFeatures(m_style); + + // update time stamps + m_buildTime.modified(); + m_this.updateTime().modified(); return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Returns a d3 selection for the given feature id. - */ + * Update + * @protected + */ //////////////////////////////////////////////////////////////////////////// - this.select = function (id) { - return getGroup().selectAll('.' + id); - }; + this._update = function () { + s_update.call(m_this); + + if (m_this.getMTime() >= m_buildTime.getMTime()) { + m_this._build(); + } else { + updateMarkers( + m_style.data, + m_style.style.strokeColor, + m_style.style.strokeOpacity + ); + } - //////////////////////////////////////////////////////////////////////////// - /** - * Removes a feature from the layer. - */ - //////////////////////////////////////////////////////////////////////////// - this._removeFeature = function (id) { - m_this.select(id).remove(); - delete m_features[id]; return m_this; }; //////////////////////////////////////////////////////////////////////////// /** - * Override draw method to do nothing. - */ + * Exit + * @protected + */ //////////////////////////////////////////////////////////////////////////// - this.draw = function () { + this._exit = function () { + s_exit.call(m_this); + m_style = {}; + updateMarkers([], null, null); }; - // connect to pan event - this.layer().geoOn(geo.event.pan, setTransform); - - // connect to zoom event - this.layer().geoOn(geo.event.zoom, function () { - setTransform(); - m_this.__render(); - m_this.layer().geoTrigger(geo.event.d3Rescale, { scale: m_scale }, true); - }); - - this.layer().geoOn(geo.event.resize, function (event) { - m_this._resize(event.x, event.y, event.width, event.height); - }); - this._init(arg); return this; }; -inherit(geo.d3.d3Renderer, geo.renderer); +inherit(geo.d3.vectorFeature, geo.vectorFeature); -geo.registerRenderer('d3', geo.d3.d3Renderer); +// Now register it +geo.registerFeature('d3', 'vector', geo.d3.vectorFeature); ////////////////////////////////////////////////////////////////////////////// /** @@ -26277,8 +28368,8 @@ geo.gui = {}; geo.gui.uiLayer = function (arg) { 'use strict'; - // The widget stays fixed on the screen. (only available in d3 at the moment) - arg.renderer = 'd3'; + // The widget stays fixed on the screen. + arg.renderer = 'dom'; arg.sticky = false; if (!(this instanceof geo.gui.uiLayer)) { @@ -26297,12 +28388,14 @@ geo.gui.uiLayer = function (arg) { */ //////////////////////////////////////////////////////////////////////////// this.createWidget = function (widgetName, arg) { + var newWidget = geo.createWidget(widgetName, m_this, arg); - var newWidget = geo.createWidget( - widgetName, m_this, m_this.renderer(), arg); + // We only want top level widgets to be a child of the uiLayer + if (!(arg && 'parent' in arg)) { + m_this.addChild(newWidget); + } - m_this.addChild(newWidget); - newWidget._init(); + newWidget._init(arg); m_this.modified(); return newWidget; }; @@ -26354,7 +28447,14 @@ geo.gui.widget = function (arg) { var m_this = this, s_exit = this._exit, - m_layer = arg.layer; + m_layer = arg.layer, + m_canvas = null; + + arg.position = arg.position === undefined ? { left: 0, top: 0 } : arg.position; + + if (arg.parent !== undefined && !(arg.parent instanceof geo.gui.widget)) { + throw 'Parent must be of type geo.gui.widget'; + } this._init = function () { m_this.modified(); @@ -26364,6 +28464,9 @@ geo.gui.widget = function (arg) { m_this.children().forEach(function (child) { m_this._deleteFeature(child); }); + + m_this.layer().geoOff(geo.event.pan, m_this.repositionEvent); + m_this.parentCanvas().removeChild(m_this.canvas()); s_exit(); }; @@ -26403,15 +28506,255 @@ geo.gui.widget = function (arg) { this.layer = function () { return m_layer; }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create the canvas this widget will operate on. + */ + //////////////////////////////////////////////////////////////////////////// + this._createCanvas = function () { + throw 'Must be defined in derived classes'; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set the canvas for the widget + */ + //////////////////////////////////////////////////////////////////////////// + this.canvas = function (val) { + if (val === undefined) { + return m_canvas; + } else { + m_canvas = val; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Appends a child to the widget + * The widget determines how to append itself to a parent, the parent can either + * be another widget, or the UI Layer. + */ + //////////////////////////////////////////////////////////////////////////// + this._appendChild = function () { + m_this.parentCanvas().appendChild(m_this.canvas()); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the parent canvas (top level widgets define their layer as their parent canvas) + */ + //////////////////////////////////////////////////////////////////////////// + this.parentCanvas = function () { + if (m_this.parent === undefined) { + return m_this.layer().canvas(); + } else { + return m_this.parent().canvas(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Gets the CSS positioning that a widget should be placed at. + * { top: 0, left: 0 } by default. + */ + //////////////////////////////////////////////////////////////////////////// + this.position = function () { + var position; + + if (arg && + arg.hasOwnProperty('position') && + arg.position.hasOwnProperty('x') && + arg.position.hasOwnProperty('y')) { + + position = m_this.layer().map().gcsToDisplay(arg.position); + + return { + left: position.x, + top: position.y + }; + } + + return arg.position; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Repositions a widget based on the argument passed, or calling position on + * the widget itself. + * @param {object} position A position with the form: + * { top: m, left: n } + */ + //////////////////////////////////////////////////////////////////////////// + this.reposition = function (position) { + position = position || m_this.position(); + m_this.canvas().style.position = 'absolute'; + + for (var cssAttr in position) { + if (position.hasOwnProperty(cssAttr)) { + m_this.canvas().style[cssAttr] = position[cssAttr] + 'px'; + } + } + }; + + this.repositionEvent = function () { + return m_this.reposition(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Determines whether or not the widget is completely within the viewport. + */ + //////////////////////////////////////////////////////////////////////////// + this.isInViewport = function () { + var position = m_this.position(); + var layer = m_this.layer(); + + return ((position.left >= 0 && position.top >= 0) && + (position.left <= layer.width() && position.top <= layer.height())); + }; + + if (arg && + arg.hasOwnProperty('position') && + arg.position.hasOwnProperty('x') && + arg.position.hasOwnProperty('y')) { + this.layer().geoOn(geo.event.pan, m_this.repositionEvent); + } }; inherit(geo.gui.widget, geo.sceneObject); +geo.gui.domWidget = function (arg) { + 'use strict'; + if (!(this instanceof geo.gui.domWidget)) { + return new geo.gui.domWidget(arg); + } + + geo.gui.widget.call(this, arg); + + var m_this = this, + m_default_canvas = 'div'; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initializes DOM Widget. + * Sets the canvas for the widget, does parent/child relationship management, + * appends it to it's parent and handles any positioning logic. + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + if (arg.hasOwnProperty('parent')) { + arg.parent.addChild(m_this); + } + + m_this._createCanvas(); + m_this._appendChild(); + + m_this.canvas().addEventListener('mousedown', function (e) { + e.stopPropagation(); + }); + + m_this.reposition(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Creates the widget canvas. + * This is just a simple DOM element (based on args.el, or defaults to a div) + */ + //////////////////////////////////////////////////////////////////////////// + this._createCanvas = function () { + m_this.canvas(document.createElement(arg.el || m_default_canvas)); + }; + + return this; +}; + +inherit(geo.gui.domWidget, geo.gui.widget); + +geo.registerWidget('dom', 'dom', geo.gui.domWidget); + +////////////////////////////////////////////////////////////////////////////// +/** + * Create a new instance of class geo.gui.svgWidget + * + * Due to the nature of d3 creating DOM elements as it inserts them, calls to appendChild + * don't appear in this widget. + * + * The canvas of an svgWidget always refers to the actual svg element. + * The parentCanvas can refer to another widgets svg element, dom element, or the + * UI layers dom element. + * See {@link geo.gui.widget#parentCanvas}. + * + * @class + * @extends geo.gui.domWidget + * @returns {geo.gui.svgWidget} + * + */ +////////////////////////////////////////////////////////////////////////////// +geo.gui.svgWidget = function (arg) { + 'use strict'; + if (!(this instanceof geo.gui.svgWidget)) { + return new geo.gui.svgWidget(arg); + } + + geo.gui.domWidget.call(this, arg); + + var m_this = this, + m_renderer = null; + + this._init = function (arg) { + var d3Parent; + if (arg.hasOwnProperty('parent')) { + arg.parent.addChild(m_this); + + // Tell the renderer there is an SVG element as a parent + d3Parent = arg.parent.canvas(); + } + + m_this._createCanvas(d3Parent); + + m_this.canvas().addEventListener('mousedown', function (e) { + e.stopPropagation(); + }); + + m_this.reposition(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Creates the canvas for the svg widget. + * This directly uses the {@link geo.d3.d3Renderer} as a helper to do all of the heavy + * lifting. + */ + //////////////////////////////////////////////////////////////////////////// + this._createCanvas = function (d3Parent) { + var rendererOpts = { + layer: m_this.layer(), + widget: true + }; + + if (d3Parent) { + rendererOpts.d3Parent = d3Parent; + } + + m_renderer = geo.d3.d3Renderer(rendererOpts); + + m_this.canvas(m_renderer.canvas()[0][0]); + }; + + return this; +}; + +inherit(geo.gui.svgWidget, geo.gui.domWidget); + +geo.registerWidget('dom', 'svg', geo.gui.svgWidget); + ////////////////////////////////////////////////////////////////////////////// /** * Create a new instance of class sliderWidget * * @class - * @extends {geo.gui.widget} + * @extends {geo.gui.svgWidget} * @returns {geo.gui.sliderWidget} */ ////////////////////////////////////////////////////////////////////////////// @@ -26420,10 +28763,12 @@ geo.gui.sliderWidget = function (arg) { if (!(this instanceof geo.gui.sliderWidget)) { return new geo.gui.sliderWidget(arg); } - geo.gui.widget.call(this, arg); + geo.gui.svgWidget.call(this, arg); var m_this = this, s_exit = this._exit, + s_createCanvas = this._createCanvas, + s_appendChild = this._appendChild, m_xscale, m_yscale, m_plus, @@ -26452,20 +28797,20 @@ geo.gui.sliderWidget = function (arg) { black: '#505050' }; -////////////////////////////////////////////////////////////////////////////// -/** - * Add an icon from a path string. Returns a d3 group element. - * - * @function - * @argument {String} icon svg path string - * @argument {Array} base where to append the element (d3 selection) - * @argument {Number} cx Center x-coordinate - * @argument {Number} cy Center y-coordinate - * @argument {Number} size Icon size in pixels - * @returns {object} - * @private - */ -////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /** + * Add an icon from a path string. Returns a d3 group element. + * + * @function + * @argument {String} icon svg path string + * @argument {Array} base where to append the element (d3 selection) + * @argument {Number} cx Center x-coordinate + * @argument {Number} cy Center y-coordinate + * @argument {Number} size Icon size in pixels + * @returns {object} + * @private + */ + ////////////////////////////////////////////////////////////////////////////// function put_icon(icon, base, cx, cy, size) { var g = base.append('g'); @@ -26485,17 +28830,22 @@ geo.gui.sliderWidget = function (arg) { return g; } -////////////////////////////////////////////////////////////////////////////// -/** - * Initialize the slider widget in the map. - * - * @function - * @returns {geo.gui.sliderWidget} - * @private - */ -////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /** + * Initialize the slider widget in the map. + * + * @function + * @returns {geo.gui.sliderWidget} + * @private + */ + ////////////////////////////////////////////////////////////////////////////// this._init = function () { - var svg = m_this.layer().renderer().canvas(), + s_createCanvas(); + s_appendChild(); + + m_this.reposition(); + + var svg = d3.select(m_this.canvas()), x0 = 40, y0 = 40 + m_width, map = m_this.layer().map(); @@ -26693,31 +29043,31 @@ geo.gui.sliderWidget = function (arg) { m_this._update(); }; -////////////////////////////////////////////////////////////////////////////// -/** - * Removes the slider element from the map and unbinds all handlers. - * - * @function - * @returns {geo.gui.sliderWidget} - * @private - */ -////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /** + * Removes the slider element from the map and unbinds all handlers. + * + * @function + * @returns {geo.gui.sliderWidget} + * @private + */ + ////////////////////////////////////////////////////////////////////////////// this._exit = function () { m_group.remove(); m_this.layer().geoOff(geo.event.zoom); s_exit(); }; -////////////////////////////////////////////////////////////////////////////// -/** - * Update the slider widget state in reponse to map changes. I.e. zoom - * range changes. - * - * @function - * @returns {geo.gui.sliderWidget} - * @private - */ -////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////// + /** + * Update the slider widget state in reponse to map changes. I.e. zoom + * range changes. + * + * @function + * @returns {geo.gui.sliderWidget} + * @private + */ + ////////////////////////////////////////////////////////////////////////////// this._update = function (obj) { var map = m_this.layer().map(), zoomRange = map.zoomRange(), @@ -26734,16 +29084,16 @@ geo.gui.sliderWidget = function (arg) { }; }; -inherit(geo.gui.sliderWidget, geo.gui.widget); +inherit(geo.gui.sliderWidget, geo.gui.svgWidget); -geo.registerWidget('d3', 'slider', geo.gui.sliderWidget); +geo.registerWidget('dom', 'slider', geo.gui.sliderWidget); ////////////////////////////////////////////////////////////////////////////// /** * Create a new instance of class legendWidget * * @class - * @extends geo.gui.widget + * @extends geo.gui.svgWidget * @returns {geo.gui.legendWidget} */ ////////////////////////////////////////////////////////////////////////////// @@ -26752,7 +29102,7 @@ geo.gui.legendWidget = function (arg) { if (!(this instanceof geo.gui.legendWidget)) { return new geo.gui.legendWidget(arg); } - geo.gui.widget.call(this, arg); + geo.gui.svgWidget.call(this, arg); /** @private */ var m_this = this, @@ -26761,7 +29111,9 @@ geo.gui.legendWidget = function (arg) { m_group = null, m_border = null, m_spacing = 20, // distance in pixels between lines - m_padding = 12; // padding in pixels inside the border + m_padding = 12, // padding in pixels inside the border + s_createCanvas = this._createCanvas, + s_appendChild = this._appendChild; ////////////////////////////////////////////////////////////////////////////// /** @@ -26813,8 +29165,8 @@ geo.gui.legendWidget = function (arg) { ////////////////////////////////////////////////////////////////////////////// this.size = function () { var width = 1, height; - var test = m_this.layer().renderer().canvas().append('text') - .style('opacity', 1e-6); + var test = d3.select(m_this.canvas()).append('text') + .style('opacity', 1e-6); m_categories.forEach(function (d) { test.text(d.name); @@ -26839,12 +29191,12 @@ geo.gui.legendWidget = function (arg) { m_this._init(); function applyColor(selection) { selection.style('fill', function (d) { - if (d.style.fill || d.style.fill === undefined) { - return d.style.fillColor; - } else { - return 'none'; - } - }) + if (d.style.fill || d.style.fill === undefined) { + return d.style.fillColor; + } else { + return 'none'; + } + }) .style('fill-opacity', function (d) { if (d.style.fillOpacity === undefined) { return 1; @@ -26878,41 +29230,41 @@ geo.gui.legendWidget = function (arg) { var scale = m_this._scale(); var labels = m_group.selectAll('g.geo-label') - .data(m_categories, function (d) { return d.name; }); + .data(m_categories, function (d) { return d.name; }); var g = labels.enter().append('g') - .attr('class', 'geo-label') - .attr('transform', function (d, i) { - return 'translate(0,' + scale.y(i) + ')'; - }); + .attr('class', 'geo-label') + .attr('transform', function (d, i) { + return 'translate(0,' + scale.y(i) + ')'; + }); applyColor(g.filter(function (d) { - return d.type !== 'point' && d.type !== 'line'; - }).append('rect') - .attr('x', 0) - .attr('y', -6) - .attr('rx', 5) - .attr('ry', 5) - .attr('width', 40) - .attr('height', 12) - ); + return d.type !== 'point' && d.type !== 'line'; + }).append('rect') + .attr('x', 0) + .attr('y', -6) + .attr('rx', 5) + .attr('ry', 5) + .attr('width', 40) + .attr('height', 12) + ); applyColor(g.filter(function (d) { - return d.type === 'point'; - }).append('circle') - .attr('cx', 20) - .attr('cy', 0) - .attr('r', 6) - ); + return d.type === 'point'; + }).append('circle') + .attr('cx', 20) + .attr('cy', 0) + .attr('r', 6) + ); applyColor(g.filter(function (d) { - return d.type === 'line'; - }).append('line') - .attr('x1', 0) - .attr('y1', 0) - .attr('x2', 40) - .attr('y2', 0) - ); + return d.type === 'line'; + }).append('line') + .attr('x1', 0) + .attr('y1', 0) + .attr('x2', 40) + .attr('y2', 0) + ); g.append('text') .attr('x', '50px') @@ -26922,6 +29274,8 @@ geo.gui.legendWidget = function (arg) { return d.name; }); + m_this.reposition(); + return m_this; }; @@ -26950,23 +29304,33 @@ geo.gui.legendWidget = function (arg) { */ ////////////////////////////////////////////////////////////////////////////// this._init = function () { - var w = m_this.size().width + 2 * m_padding, - h = m_this.size().height + 2 * m_padding, - nw = m_this.layer().map().node().width(), - margin = 20; + // adding categories redraws the entire thing by calling _init, see + // the m_top.remove() line below + if (!m_top) { + s_createCanvas(); + s_appendChild(); + } + + // total size = interior size + 2 * padding + 2 * width of the border + var w = m_this.size().width + 2 * m_padding + 4, + h = m_this.size().height + 2 * m_padding + 4; + + // @todo - removing after creating to maintain the appendChild structure if (m_top) { m_top.remove(); } - m_top = m_this.layer().renderer().canvas().append('g') - .attr('transform', 'translate(' + (nw - w - margin) + ',' + margin + ')'); + + d3.select(m_this.canvas()).attr('width', w).attr('height', h); + + m_top = d3.select(m_this.canvas()).append('g'); m_group = m_top .append('g') - .attr('transform', 'translate(' + [m_padding - 1.5, m_padding] + ')'); + .attr('transform', 'translate(' + [m_padding + 2, m_padding + 2] + ')'); m_border = m_group.append('rect') .attr('x', -m_padding) .attr('y', -m_padding) - .attr('width', w) - .attr('height', h) + .attr('width', w - 4) + .attr('height', h - 4) .attr('rx', 3) .attr('ry', 3) .style({ @@ -26989,17 +29353,19 @@ geo.gui.legendWidget = function (arg) { .duration(250) .style('fill-opacity', 0.75); }); + + m_this.reposition(); }; this.geoOn(geo.event.resize, function () { - this.draw(); + m_this.draw(); }); }; -inherit(geo.gui.legendWidget, geo.gui.widget); +inherit(geo.gui.legendWidget, geo.gui.svgWidget); -geo.registerWidget('d3', 'legend', geo.gui.legendWidget); +geo.registerWidget('dom', 'legend', geo.gui.legendWidget); /*jscs:disable validateIndentation*/ (function ($, geo, d3) { @@ -27259,7 +29625,7 @@ geo.registerWidget('d3', 'legend', geo.gui.legendWidget); * Describes layers added to the map * @property {boolean} [autoresize=true] * Resize the map on window.resize (initialization only) - * @property {string} [tileUrl] + * @property {string} [url] * The open street map tile server spec default: * http://tile.openstreetmap.org/<zoom>/<x>/<y>.png */ @@ -27270,7 +29636,7 @@ geo.registerWidget('d3', 'legend', geo.gui.legendWidget); height: null, layers: [], data: [], - tileUrl: 'http://tile.openstreetmap.org///.png', + url: 'http://tile.openstreetmap.org/{z}/{x}/{y}.png', attribution: undefined, // These options are for future use, but shouldn't @@ -27304,7 +29670,7 @@ geo.registerWidget('d3', 'legend', geo.gui.legendWidget); this.options.baseLayer, { renderer: this.options.baseRenderer, - tileUrl: this.options.tileUrl, + url: this.options.url, attribution: this.options.attribution } ); @@ -27401,9 +29767,8 @@ geo.registerWidget('d3', 'legend', geo.gui.legendWidget); * @instance * @param {string} url The url format string of an OSM tile server. */ - tileUrl: function (url) { - this._baseLayer.tileUrl(url); - this._baseLayer.updateBaseUrl(); + url: function (url) { + this._baseLayer.url(url); return this; }, diff --git a/geo.min.js b/geo.min.js index 6262699bce..7c5426102f 100644 --- a/geo.min.js +++ b/geo.min.js @@ -1,11 +1,12 @@ -function inherit(C,P){"use strict";var F=function(){};F.prototype=P.prototype,C.prototype=new F,C.uber=P.prototype,C.prototype.constructor=C}var geo={};if(window.geo=geo,geo.renderers={},geo.features={},geo.fileReaders={},geo.inherit=function(C,P){"use strict";var F=inherit.func();F.prototype=P.prototype,C.prototype=new F,C.prototype.constructor=C},geo.inherit.func=function(){"use strict";return function(){}},window.inherit=geo.inherit,geo.registerFileReader=function(name,func){"use strict";void 0===geo.fileReaders&&(geo.fileReaders={}),geo.fileReaders[name]=func},geo.createFileReader=function(name,opts){"use strict";return geo.fileReaders.hasOwnProperty(name)?geo.fileReaders[name](opts):null},geo.registerRenderer=function(name,func){"use strict";void 0===geo.renderers&&(geo.renderers={}),geo.renderers[name]=func},geo.createRenderer=function(name,layer,canvas,options){"use strict";if(geo.renderers.hasOwnProperty(name)){var ren=geo.renderers[name]({layer:layer,canvas:canvas,options:options});return ren._init(),ren}return null},geo.registerFeature=function(category,name,func){"use strict";void 0===geo.features&&(geo.features={}),category in geo.features||(geo.features[category]={}),geo.features[category][name]=func},geo.createFeature=function(name,layer,renderer,arg){"use strict";var category=renderer.api(),options={layer:layer,renderer:renderer};return category in geo.features&&name in geo.features[category]?(void 0!==arg&&$.extend(!0,options,arg),geo.features[category][name](options)):null},geo.registerLayer=function(name,func){"use strict";void 0===geo.layers&&(geo.layers={}),geo.layers[name]=func},geo.createLayer=function(name,map,arg){"use strict";var options={map:map,renderer:"vgl"},layer=null;return name in geo.layers?(void 0!==arg&&$.extend(!0,options,arg),layer=geo.layers[name](options),layer._init(),layer):null},geo.registerWidget=function(category,name,func){"use strict";void 0===geo.widgets&&(geo.widgets={}),category in geo.widgets||(geo.widgets[category]={}),geo.widgets[category][name]=func},geo.createWidget=function(name,layer,renderer,arg){"use strict";var category=renderer.api(),options={layer:layer,renderer:renderer};return category in geo.widgets&&name in geo.widgets[category]?(void 0!==arg&&$.extend(!0,options,arg),geo.widgets[category][name](options)):null},window.requestAnimationFrame||(window.requestAnimationFrame=function(func){"use strict";window.setTimeout(func,15)}),Math.log2||(Math.log2=function(){"use strict";return Math.log.apply(Math,arguments)/Math.LN2}),geo.version="0.5.0","undefined"==typeof ogs)var ogs={};ogs.namespace=function(ns_string){"use strict";var i,parts=ns_string.split("."),parent=ogs;for("ogs"===parts[0]&&(parts=parts.slice(1)),i=0;ithis.computeBoundsTimestamp().getMTime()&&this.m_parent.boundsDirtyTimestamp.modified(),this.computeBounds(),visitor.mode()===visitor.TraverseAllChildren)for(i=0;ithis.boundsDirtyTimestamp().getMTime()))for(i=0;ii;i+=1)istep=2*i,jstep=2*i+1,childBounds[istep]bounds[jstep]&&(bounds[jstep]=childBounds[jstep]);this.setBounds(bounds[0],bounds[1],bounds[2],bounds[3],bounds[4],bounds[5])}},this},inherit(vgl.groupNode,vgl.node),vgl.actor=function(){"use strict";if(!(this instanceof vgl.actor))return new vgl.actor;vgl.node.call(this);var m_this=this,m_transformMatrix=mat4.create(),m_referenceFrame=vgl.boundingObject.ReferenceFrame.Relative,m_mapper=null;return this.matrix=function(){return m_transformMatrix},this.setMatrix=function(tmatrix){tmatrix!==m_transformMatrix&&(m_transformMatrix=tmatrix,m_this.modified())},this.referenceFrame=function(){return m_referenceFrame},this.setReferenceFrame=function(referenceFrame){return referenceFrame!==m_referenceFrame?(m_referenceFrame=referenceFrame,m_this.modified(),!0):!1},this.mapper=function(){return m_mapper},this.setMapper=function(mapper){mapper!==m_mapper&&(m_mapper=mapper,m_this.boundsModified())},this.accept=function(visitor){visitor=visitor},this.ascend=function(visitor){visitor=visitor},this.computeLocalToWorldMatrix=function(matrix,visitor){matrix=matrix,visitor=visitor},this.computeWorldToLocalMatrix=function(matrix,visitor){matrix=matrix,visitor=visitor},this.computeBounds=function(){if(null===m_mapper||void 0===m_mapper)return void m_this.resetBounds();var mapperBounds,minPt,maxPt,newBounds,computeBoundsTimestamp=m_this.computeBoundsTimestamp();(m_this.boundsDirtyTimestamp().getMTime()>computeBoundsTimestamp.getMTime()||m_mapper.boundsDirtyTimestamp().getMTime()>computeBoundsTimestamp.getMTime())&&(m_mapper.computeBounds(),mapperBounds=m_mapper.bounds(),minPt=[mapperBounds[0],mapperBounds[2],mapperBounds[4]],maxPt=[mapperBounds[1],mapperBounds[3],mapperBounds[5]],vec3.transformMat4(minPt,minPt,m_transformMatrix),vec3.transformMat4(maxPt,maxPt,m_transformMatrix),newBounds=[minPt[0]>maxPt[0]?maxPt[0]:minPt[0],minPt[0]>maxPt[0]?minPt[0]:maxPt[0],minPt[1]>maxPt[1]?maxPt[1]:minPt[1],minPt[1]>maxPt[1]?minPt[1]:maxPt[1],minPt[2]>maxPt[2]?maxPt[2]:minPt[2],minPt[2]>maxPt[2]?minPt[2]:maxPt[2]],m_this.setBounds(newBounds[0],newBounds[1],newBounds[2],newBounds[3],newBounds[4],newBounds[5]),computeBoundsTimestamp.modified())},m_this},inherit(vgl.actor,vgl.node),vgl.freezeObject=function(obj){"use strict";var freezedObject=Object.freeze(obj);return"undefined"==typeof freezedObject&&(freezedObject=function(o){return o}),freezedObject},vgl.defaultValue=function(a,b){"use strict";return"undefined"!=typeof a?a:b},vgl.defaultValue.EMPTY_OBJECT=vgl.freezeObject({}),vgl.graphicsObject=function(type){"use strict";if(type=type,!(this instanceof vgl.graphicsObject))return new vgl.graphicsObject;vgl.object.call(this);var m_this=this;return this._setup=function(renderState){return renderState=renderState,!1},this._cleanup=function(renderState){return renderState=renderState,!1},this.bind=function(renderState){return renderState=renderState,!1},this.undoBind=function(renderState){return renderState=renderState,!1},this.render=function(renderState){return renderState=renderState,!1},this.remove=function(renderState){m_this._cleanup(renderState)},m_this},inherit(vgl.graphicsObject,vgl.object),vgl.geojsonReader=function(){"use strict";return this instanceof vgl.geojsonReader?(this.readScalars=function(coordinates,geom,size_estimate,idx){var array=null,s=null,r=null,g=null,b=null;"values"===this.m_scalarFormat&&4===coordinates.length?(s=coordinates[3],array=geom.sourceData(vgl.vertexAttributeKeys.Scalar),array||(array=new vgl.sourceDataSf,this.m_scalarRange&&array.setScalarRange(this.m_scalarRange[0],this.m_scalarRange[1]),void 0!==size_estimate&&(array.data().length=size_estimate),geom.addSource(array)),void 0===size_estimate?array.pushBack(s):array.insertAt(idx,s)):"rgb"===this.m_scalarFormat&&6===coordinates.length&&(array=geom.sourceData(vgl.vertexAttributeKeys.Color),array||(array=new vgl.sourceDataC3fv,void 0!==size_estimate&&(array.length=3*size_estimate),geom.addSource(array)),r=coordinates[3],g=coordinates[4],b=coordinates[5],void 0===size_estimate?array.pushBack([r,g,b]):array.insertAt(idx,[r,g,b]))},this.readPoint=function(coordinates){var geom=new vgl.geometryData,vglpoints=new vgl.points,vglcoords=new vgl.sourceDataP3fv,indices=new Uint16Array(1),x=null,y=null,z=null,i=null;for(geom.addSource(vglcoords),i=0;1>i;i+=1)indices[i]=i,x=coordinates[0],y=coordinates[1],z=0,coordinates.length>2&&(z=coordinates[2]),vglcoords.pushBack([x,y,z]),this.readScalars(coordinates,geom);return vglpoints.setIndices(indices),geom.addPrimitive(vglpoints),geom.setName("aPoint"),geom},this.readMultiPoint=function(coordinates){var i,geom=new vgl.geometryData,vglpoints=new vgl.points,vglcoords=new vgl.sourceDataP3fv,indices=new Uint16Array(coordinates.length),pntcnt=0,estpntcnt=coordinates.length,x=null,y=null,z=null;for(vglcoords.data().length=3*estpntcnt,i=0;i2&&(z=coordinates[i][2]),vglcoords.insertAt(pntcnt,[x,y,z]),this.readScalars(coordinates[i],geom,estpntcnt,pntcnt),pntcnt+=1;return vglpoints.setIndices(indices),geom.addPrimitive(vglpoints),geom.addSource(vglcoords),geom.setName("manyPoints"),geom},this.readLineString=function(coordinates){var geom=new vgl.geometryData,vglline=new vgl.lineStrip,vglcoords=new vgl.sourceDataP3fv,indices=[],i=null,x=null,y=null,z=null;for(vglline.setIndicesPerPrimitive(coordinates.length),i=0;i2&&(z=coordinates[i][2]),vglcoords.pushBack([x,y,z]),this.readScalars(coordinates[i],geom);return vglline.setIndices(indices),geom.addPrimitive(vglline),geom.addSource(vglcoords),geom.setName("aLineString"),geom},this.readMultiLineString=function(coordinates){var geom=new vgl.geometryData,vglcoords=new vgl.sourceDataP3fv,pntcnt=0,estpntcnt=2*coordinates.length,i=null,j=null,x=null,y=null,z=null,indices=null,vglline=null,thisLineLength=null;for(vglcoords.data().length=3*estpntcnt,j=0;ji;i+=1)indices.push(pntcnt),x=coordinates[j][i][0],y=coordinates[j][i][1],z=0,coordinates[j][i].length>2&&(z=coordinates[j][i][2]),vglcoords.insertAt(pntcnt,[x,y,z]),this.readScalars(coordinates[j][i],geom,2*estpntcnt,pntcnt),pntcnt+=1;vglline.setIndices(indices),geom.addPrimitive(vglline)}return geom.setName("aMultiLineString"),geom.addSource(vglcoords),geom},this.readPolygon=function(coordinates){var geom=new vgl.geometryData,vglcoords=new vgl.sourceDataP3fv,x=null,y=null,z=null,thisPolyLength=coordinates[0].length,vl=1,i=null,indices=null,vgltriangle=null;for(i=0;thisPolyLength>i;i+=1)x=coordinates[0][i][0],y=coordinates[0][i][1],z=0,coordinates[0][i].length>2&&(z=coordinates[0][i][2]),vglcoords.pushBack([x,y,z]),this.readScalars(coordinates[0][i],geom),i>1&&(indices=new Uint16Array([0,vl,i]),vgltriangle=new vgl.triangles,vgltriangle.setIndices(indices),geom.addPrimitive(vgltriangle),vl=i);return geom.setName("POLY"),geom.addSource(vglcoords),geom},this.readMultiPolygon=function(coordinates){var geom=new vgl.geometryData,vglcoords=new vgl.sourceDataP3fv,ccount=0,numPolys=coordinates.length,pntcnt=0,estpntcnt=3*numPolys,vgltriangle=new vgl.triangles,indexes=[],i=null,j=null,x=null,y=null,z=null,thisPolyLength=null,vf=null,vl=null,flip=null,flipped=!1,tcount=0;for(vglcoords.data().length=3*numPolys,j=0;numPolys>j;j+=1)for(thisPolyLength=coordinates[j][0].length,vf=ccount,vl=ccount+1,flip=[!1,!1,!1],i=0;thisPolyLength>i;i+=1)x=coordinates[j][0][i][0],y=coordinates[j][0][i][1],z=0,coordinates[j][0][i].length>2&&(z=coordinates[j][0][i][2]),flipped=!1,x>180&&(flipped=!0,x-=360),0===i?flip[0]=flipped:flip[1+(i-1)%2]=flipped,vglcoords.insertAt(pntcnt,[x,y,z]),this.readScalars(coordinates[j][0][i],geom,estpntcnt,pntcnt),pntcnt+=1,i>1&&(flip[0]===flip[1]&&flip[1]===flip[2]&&(indexes[3*tcount+0]=vf,indexes[3*tcount+1]=vl,indexes[3*tcount+2]=ccount,tcount+=1),vl=ccount),ccount+=1;return vgltriangle.setIndices(indexes),geom.addPrimitive(vgltriangle),geom.setName("aMultiPoly"),geom.addSource(vglcoords),geom},this.readGJObjectInt=function(object){if(!object.hasOwnProperty("type"))return null;object.properties&&object.properties.ScalarFormat&&"values"===object.properties.ScalarFormat&&(this.m_scalarFormat="values",object.properties.ScalarRange&&(this.m_scalarRange=object.properties.ScalarRange)),object.properties&&object.properties.ScalarFormat&&"rgb"===object.properties.ScalarFormat&&(this.m_scalarFormat="rgb");var ret,type=object.type,next=null,nextset=null,i=null;switch(type){case"Point":ret=this.readPoint(object.coordinates);break;case"MultiPoint":ret=this.readMultiPoint(object.coordinates);break;case"LineString":ret=this.readLineString(object.coordinates);break;case"MultiLineString":ret=this.readMultiLineString(object.coordinates);break;case"Polygon":ret=this.readPolygon(object.coordinates);break;case"MultiPolygon":ret=this.readMultiPolygon(object.coordinates);break;case"GeometryCollection":for(nextset=[],i=0;im_max)&&(m_max=value),(null===m_min||m_min>value)&&(m_min=value),this.data()[this.data().length]=value},this.insertAt=function(index,value){(null===m_max||value>m_max)&&(m_max=value),(null===m_min||m_min>value)&&(m_min=value),this.data()[index]=value},this.scalarRange=function(){return null===m_fixedmin||null===m_fixedmax?[m_min,m_max]:[m_fixedmin,m_fixedmax]},this.setScalarRange=function(min,max){m_fixedmin=min,m_fixedmax=max},this},inherit(vgl.sourceDataSf,vgl.sourceData),vgl.sourceDataDf=function(arg){"use strict";return this instanceof vgl.sourceDataDf?(vgl.sourceData.call(this,arg),this.addAttribute(vgl.vertexAttributeKeys.Scalar,vgl.GL.FLOAT,4,0,4,1,!1),this.pushBack=function(value){this.data()[this.data().length]=value},this.insertAt=function(index,value){this.data()[index]=value},this):new vgl.sourceDataDf(arg)},inherit(vgl.sourceDataDf,vgl.sourceData),vgl.geometryData=function(){"use strict";if(!(this instanceof vgl.geometryData))return new vgl.geometryData;vgl.data.call(this);var m_name="",m_primitives=[],m_sources=[],m_bounds=[0,0,0,0,0,0],m_computeBoundsTimestamp=vgl.timestamp(),m_boundsDirtyTimestamp=vgl.timestamp();return this.type=function(){return vgl.data.geometry},this.name=function(){return m_name},this.setName=function(name){m_name=name},this.addSource=function(source,sourceName){return void 0!==sourceName&&source.setName(sourceName),-1===m_sources.indexOf(source)?(m_sources.push(source),source.hasKey(vgl.vertexAttributeKeys.Position)&&m_boundsDirtyTimestamp.modified(),!0):!1},this.source=function(index){return indexm_computeBoundsTimestamp.getMTime()&&this.computeBounds(),m_bounds},this.boundsDirty=function(dirty){return dirty&&m_boundsDirtyTimestamp.modified(),m_boundsDirtyTimestamp.getMTime()>m_computeBoundsTimestamp.getMTime()},this.resetBounds=function(){m_bounds[0]=0,m_bounds[1]=0,m_bounds[2]=0,m_bounds[3]=0,m_bounds[4]=0,m_bounds[5]=0},this.setBounds=function(minX,maxX,minY,maxY,minZ,maxZ){return m_bounds[0]=minX,m_bounds[1]=maxX,m_bounds[2]=minY,m_bounds[3]=maxY,m_bounds[4]=minZ,m_bounds[5]=maxZ,m_computeBoundsTimestamp.modified(),!0},this.computeBounds=function(){if(m_boundsDirtyTimestamp.getMTime()>m_computeBoundsTimestamp.getMTime()){var j,ib,jb,maxv,minv,vertexIndex,attr=vgl.vertexAttributeKeys.Position,sourceData=this.sourceData(attr),data=sourceData.data(),numberOfComponents=sourceData.attributeNumberOfComponents(attr),stride=sourceData.attributeStride(attr),offset=sourceData.attributeOffset(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),count=data.length,value=null;for(stride/=sizeOfDataType,offset/=sizeOfDataType,this.resetBounds(),j=0;numberOfComponents>j;j+=1){for(ib=2*j,jb=2*j+1,maxv=minv=count?m_bounds[jb]=data[offset+j]:0,vertexIndex=offset+stride+j;count>vertexIndex;vertexIndex+=stride)value=data[vertexIndex],value>maxv&&(maxv=value),minv>value&&(minv=value);m_bounds[ib]=minv,m_bounds[jb]=maxv}m_computeBoundsTimestamp.modified()}},this.findClosestVertex=function(point){var vi,vPos,dx,dy,dz,dist,i,attr=vgl.vertexAttributeKeys.Position,sourceData=this.sourceData(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),numberOfComponents=sourceData.attributeNumberOfComponents(attr),data=sourceData.data(),stride=sourceData.attributeStride(attr)/sizeOfDataType,offset=sourceData.attributeOffset(attr)/sizeOfDataType,minDist=Number.MAX_VALUE,minIndex=null;for(3!==numberOfComponents&&console.log("[warning] Find closest vertex assumes threecomponent vertex "),point.z||(point={x:point.x,y:point.y,z:0}),vi=offset,i=0;vidist&&(minDist=dist,minIndex=i);return minIndex},this.getPosition=function(index){var attr=vgl.vertexAttributeKeys.Position,sourceData=this.sourceData(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),numberOfComponents=sourceData.attributeNumberOfComponents(attr),data=sourceData.data(),stride=sourceData.attributeStride(attr)/sizeOfDataType,offset=sourceData.attributeOffset(attr)/sizeOfDataType;return 3!==numberOfComponents&&console.log("[warning] getPosition assumes three component data"),[data[offset+index*stride],data[offset+index*stride+1],data[offset+index*stride+2]]},this.getScalar=function(index){var numberOfComponents,sizeOfDataType,data,stride,offset,attr=vgl.vertexAttributeKeys.Scalar,sourceData=this.sourceData(attr);return sourceData?(numberOfComponents=sourceData.attributeNumberOfComponents(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),data=sourceData.data(),stride=sourceData.attributeStride(attr)/sizeOfDataType,offset=sourceData.attributeOffset(attr)/sizeOfDataType,index*stride+offset>=data.length&&console.log("access out of bounds in getScalar"),data[index*stride+offset]):null},this},inherit(vgl.geometryData,vgl.data),vgl.mapper=function(arg){"use strict";function deleteVertexBufferObjects(renderState){var i;for(i=0;ii;i+=1){for(bufferId=m_context.createBuffer(),m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,bufferId),data=m_geomData.source(i).data(),data instanceof Float32Array||(data=new Float32Array(data)),m_context.bufferData(vgl.GL.ARRAY_BUFFER,data,m_dynamicDraw?vgl.GL.DYNAMIC_DRAW:vgl.GL.STATIC_DRAW),keys=m_geomData.source(i).keys(),ks=[],j=0;jk;k+=1)bufferId=m_context.createBuffer(),m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,bufferId),m_context.bufferData(vgl.GL.ARRAY_BUFFER,m_geomData.primitive(k).indices(),vgl.GL.STATIC_DRAW),m_buffers[i]=bufferId,i+=1;m_glCompileTimestamp.modified()}}function cleanUpDrawObjects(renderState){renderState=renderState,m_bufferVertexAttributeMap={},m_buffers=[]}function setupDrawObjects(renderState){deleteVertexBufferObjects(renderState),cleanUpDrawObjects(renderState),createVertexBufferObjects(renderState),m_dirty=!1}if(!(this instanceof vgl.mapper))return new vgl.mapper(arg);vgl.boundingObject.call(this),arg=arg||{};var m_dirty=!0,m_color=[0,1,1],m_geomData=null,m_buffers=[],m_bufferVertexAttributeMap={},m_dynamicDraw=void 0===arg.dynamicDraw?!1:arg.dynamicDraw,m_glCompileTimestamp=vgl.timestamp(),m_context=null;return this.computeBounds=function(){if(null===m_geomData||"undefined"==typeof m_geomData)return void this.resetBounds();var computeBoundsTimestamp=this.computeBoundsTimestamp(),boundsDirtyTimestamp=this.boundsDirtyTimestamp(),geomBounds=null;boundsDirtyTimestamp.getMTime()>computeBoundsTimestamp.getMTime()&&(geomBounds=m_geomData.bounds(),this.setBounds(geomBounds[0],geomBounds[1],geomBounds[2],geomBounds[3],geomBounds[4],geomBounds[5]),computeBoundsTimestamp.modified())},this.color=function(){return m_color},this.setColor=function(r,g,b){m_color[0]=r,m_color[1]=g,m_color[2]=b,this.modified()},this.geometryData=function(){return m_geomData},this.setGeometryData=function(geom){m_geomData!==geom&&(m_geomData=geom,this.modified(),this.boundsDirtyTimestamp().modified())},this.updateSourceBuffer=function(sourceName,values,renderState){if(renderState&&(m_context=renderState.m_context),!m_context)return!1;for(var bufferIndex=-1,i=0;ibufferIndex||bufferIndex>=m_buffers.length?!1:(values||(values=m_geomData.source(i).dataToFloat32Array()),m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,m_buffers[bufferIndex]),values instanceof Float32Array?m_context.bufferSubData(vgl.GL.ARRAY_BUFFER,0,values):m_context.bufferSubData(vgl.GL.ARRAY_BUFFER,0,new Float32Array(values)),!0)},this.getSourceBuffer=function(sourceName){var source=m_geomData.sourceByName(sourceName);return source?source.dataToFloat32Array():new Float32Array},this.render=function(renderState){(this.getMTime()>m_glCompileTimestamp.getMTime()||renderState.m_contextChanged)&&setupDrawObjects(renderState),m_context=renderState.m_context,m_context.vertexAttrib3fv(vgl.vertexAttributeKeys.Color,this.color());var i,bufferIndex=0,j=0,noOfPrimitives=null,primitive=null;for(i in m_bufferVertexAttributeMap)if(m_bufferVertexAttributeMap.hasOwnProperty(i)){for(m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,m_buffers[bufferIndex]),j=0;jj;j+=1){switch(m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,m_buffers[bufferIndex]),bufferIndex+=1,primitive=m_geomData.primitive(j),primitive.primitiveType()){case vgl.GL.POINTS:m_context.drawArrays(vgl.GL.POINTS,0,primitive.numberOfIndices());break;case vgl.GL.LINES:m_context.drawArrays(vgl.GL.LINES,0,primitive.numberOfIndices());break;case vgl.GL.LINE_STRIP:m_context.drawArrays(vgl.GL.LINE_STRIP,0,primitive.numberOfIndices());break;case vgl.GL.TRIANGLES:m_context.drawArrays(vgl.GL.TRIANGLES,0,primitive.numberOfIndices());break;case vgl.GL.TRIANGLE_STRIP:m_context.drawArrays(vgl.GL.TRIANGLE_STRIP,0,primitive.numberOfIndices())}m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,null)}},this},inherit(vgl.mapper,vgl.boundingObject),vgl.groupMapper=function(){"use strict";if(!(this instanceof vgl.groupMapper))return new vgl.groupMapper;vgl.mapper.call(this);var m_createMappersTimestamp=vgl.timestamp(),m_mappers=[],m_geomDataArray=[];return this.geometryData=function(index){return void 0!==index&&index0?m_geomDataArray[0]:null},this.setGeometryData=function(geom){(1!==m_geomDataArray.length||m_geomDataArray[0]!==geom)&&(m_geomDataArray=[],m_geomDataArray.push(geom),this.modified())},this.geometryDataArray=function(){return m_geomDataArray},this.setGeometryDataArray=function(geoms){if(geoms instanceof Array){if(m_geomDataArray!==geoms)return m_geomDataArray=[],m_geomDataArray=geoms,this.modified(),!0}else console.log("[error] Requies array of geometry data");return!1},this.computeBounds=function(){if(null===m_geomDataArray||void 0===m_geomDataArray)return void this.resetBounds();var computeBoundsTimestamp=this.computeBoundsTimestamp(),boundsDirtyTimestamp=this.boundsDirtyTimestamp(),m_bounds=this.bounds(),geomBounds=null,i=null;if(boundsDirtyTimestamp.getMTime()>computeBoundsTimestamp.getMTime()){for(i=0;igeomBounds[0]&&(m_bounds[0]=geomBounds[0]),m_bounds[1]geomBounds[2]&&(m_bounds[2]=geomBounds[2]),m_bounds[3]geomBounds[4]&&(m_bounds[4]=geomBounds[4]),m_bounds[5]m_createMappersTimestamp.getMTime()){for(i=0;i0&&m_this.m_resetScene&&(m_this.resetCamera(),m_this.m_resetScene=!1),i=0;i=0&&sortedActors.push([actor.material().binNumber(),actor]);for(sortedActors.sort(function(a,b){return a[0]-b[0]}),i=0;idiagonals[1]?diagonals[0]>diagonals[2]?diagonals[0]/2:diagonals[2]/2:diagonals[1]>diagonals[2]?diagonals[1]/2:diagonals[2]/2,angle=aspect>=1?2*Math.atan(Math.tan(.5*angle)/aspect):2*Math.atan(Math.tan(.5*angle)*aspect),distance=radius/Math.sin(.5*angle),vup=m_this.m_camera.viewUpDirection(),Math.abs(vec3.dot(vup,vn))>.999&&m_this.m_camera.setViewUpDirection(-vup[2],vup[0],vup[1]),m_this.m_camera.setFocalPoint(center[0],center[1],center[2]),m_this.m_camera.setPosition(center[0]+distance*-vn[0],center[1]+distance*-vn[1],center[2]+distance*-vn[2]),m_this.resetCameraClippingRange(visibleBounds)},this.hasValidBounds=function(bounds){return bounds[0]===Number.MAX_VALUE||bounds[1]===-Number.MAX_VALUE||bounds[2]===Number.MAX_VALUE||bounds[3]===-Number.MAX_VALUE||bounds[4]===Number.MAX_VALUE||bounds[5]===-Number.MAX_VALUE?!1:!0},this.resetCameraClippingRange=function(bounds){if("undefined"==typeof bounds&&(m_this.m_camera.computeBounds(),bounds=m_this.m_camera.bounds()),m_this.hasValidBounds(bounds)){var vn=m_this.m_camera.viewPlaneNormal(),position=m_this.m_camera.position(),a=-vn[0],b=-vn[1],c=-vn[2],d=-(a*position[0]+b*position[1]+c*position[2]),range=vec2.create(),dist=null,i=null,j=null,k=null;if(m_this.m_resetClippingRange){for(range[0]=a*bounds[0]+b*bounds[2]+c*bounds[4]+d,range[1]=1e-18,k=0;2>k;k+=1)for(j=0;2>j;j+=1)for(i=0;2>i;i+=1)dist=a*bounds[i]+b*bounds[2+j]+c*bounds[4+k]+d,range[0]=distrange[1]?dist:range[1];range[0]<0&&(range[0]=0),range[0]=.99*range[0]-.5*(range[1]-range[0]),range[1]=1.01*range[1]+.5*(range[1]-range[0]),range[0]=range[0]>=range[1]?.01*range[1]:range[0],m_this.m_nearClippingPlaneTolerance||(m_this.m_nearClippingPlaneTolerance=.01,m_this.m_depthBits&&m_this.m_depthBits>16&&(m_this.m_nearClippingPlaneTolerance=.001)),range[0]x||0>y||0>=width||0>=height)return void console.log("[error] Invalid position and resize values",x,y,width,height);if(m_this.m_resizable&&(m_this.m_width=width,m_this.m_height=height,m_this.m_camera.setViewAspect(width/height),m_this.m_camera.setParallelExtents({width:width,height:height}),m_this.modified()),m_this.m_renderPasses)for(i=0;im_width||0===m_renderers[i].width()||m_renderers[i].height()>m_height||0===m_renderers[i].height())&&m_renderers[i].resize(m_x,m_y,m_width,m_height);return!0}catch(e){}return m_context||console("[ERROR] Unable to initialize WebGL. Your browser may not support it."),!1},this.context=function(){return m_context},this._cleanup=function(renderState){var i;for(i=0;iwidth?(m_currPos.x=0,m_outsideCanvas=!0):m_currPos.x=coords.x,coords.y<0||coords.y>height?(m_currPos.y=0,m_outsideCanvas=!0):m_currPos.y=coords.y,m_outsideCanvas!==!0?(fp=cam.focalPoint(),fwp=vec4.fromValues(fp[0],fp[1],fp[2],1),fdp=ren.worldToDisplay(fwp,cam.viewMatrix(),cam.projectionMatrix(),width,height),dp1=vec4.fromValues(m_currPos.x,m_currPos.y,fdp[2],1),dp2=vec4.fromValues(m_lastPos.x,m_lastPos.y,fdp[2],1),wp1=ren.displayToWorld(dp1,cam.viewMatrix(),cam.projectionMatrix(),width,height),wp2=ren.displayToWorld(dp2,cam.viewMatrix(),cam.projectionMatrix(),width,height),dx=wp1[0]-wp2[0],dy=wp1[1]-wp2[1],dz=wp1[2]-wp2[2],m_midMouseBtnDown&&(cam.pan(-dx,-dy,-dz),m_that.viewer().render()),m_leftMouseBtnDown&&(cam.rotate(m_lastPos.x-m_currPos.x,m_lastPos.y-m_currPos.y),ren.resetCameraClippingRange(),m_that.viewer().render()),m_rightMouseBtnDown&&(m_zTrans=2*(m_currPos.y-m_lastPos.y)/height,m_zTrans>0?cam.zoom(1-Math.abs(m_zTrans)):cam.zoom(1+Math.abs(m_zTrans)),ren.resetCameraClippingRange(),m_that.viewer().render()),m_lastPos.x=m_currPos.x,m_lastPos.y=m_currPos.y,!1):void 0},this.handleMouseDown=function(event){var coords;return 0===event.button&&(m_leftMouseBtnDown=!0),1===event.button&&(m_midMouseBtnDown=!0),2===event.button&&(m_rightMouseBtnDown=!0),coords=m_that.viewer().relMouseCoords(event),coords.x<0?m_lastPos.x=0:m_lastPos.x=coords.x,coords.y<0?m_lastPos.y=0:m_lastPos.y=coords.y,!1},this.handleMouseUp=function(event){return 0===event.button&&(m_leftMouseBtnDown=!1),1===event.button&&(m_midMouseBtnDown=!1),2===event.button&&(m_rightMouseBtnDown=!1),!1},this.handleMouseWheel=function(event){var ren=m_that.viewer().renderWindow().activeRenderer(),cam=ren.camera();return event.originalEvent.wheelDelta<0?cam.zoom(.9):cam.zoom(1.1),ren.resetCameraClippingRange(),m_that.viewer().render(),!0},this},inherit(vgl.trackballInteractorStyle,vgl.interactorStyle),vgl.pvwInteractorStyle=function(){"use strict";function render(){m_renderer.resetCameraClippingRange(),m_that.viewer().render()}if(!(this instanceof vgl.pvwInteractorStyle))return new vgl.pvwInteractorStyle;vgl.trackballInteractorStyle.call(this);var m_width,m_height,m_renderer,m_camera,m_outsideCanvas,m_coords,m_currentMousePos,m_focalPoint,m_focusWorldPt,m_focusDisplayPt,m_displayPt1,m_displayPt2,m_worldPt1,m_worldPt2,m_dx,m_dy,m_dz,m_zTrans,m_that=this,m_leftMouseButtonDown=!1,m_rightMouseButtonDown=!1,m_middleMouseButtonDown=!1,m_mouseLastPos={x:0,y:0};return this.handleMouseMove=function(event){var rens=[],i=null,secCameras=[],deltaxy=null;for(m_width=m_that.viewer().renderWindow().windowSize()[0],m_height=m_that.viewer().renderWindow().windowSize()[1],m_renderer=m_that.viewer().renderWindow().activeRenderer(),m_camera=m_renderer.camera(),m_outsideCanvas=!1,m_coords=m_that.viewer().relMouseCoords(event),m_currentMousePos={x:0,y:0},rens=m_that.viewer().renderWindow().renderers(),i=0;im_width?(m_currentMousePos.x=0,m_outsideCanvas=!0):m_currentMousePos.x=m_coords.x,m_coords.y<0||m_coords.y>m_height?(m_currentMousePos.y=0,m_outsideCanvas=!0):m_currentMousePos.y=m_coords.y,m_outsideCanvas!==!0){if(m_focalPoint=m_camera.focalPoint(),m_focusWorldPt=vec4.fromValues(m_focalPoint[0],m_focalPoint[1],m_focalPoint[2],1),m_focusDisplayPt=m_renderer.worldToDisplay(m_focusWorldPt,m_camera.viewMatrix(),m_camera.projectionMatrix(),m_width,m_height),m_displayPt1=vec4.fromValues(m_currentMousePos.x,m_currentMousePos.y,m_focusDisplayPt[2],1),m_displayPt2=vec4.fromValues(m_mouseLastPos.x,m_mouseLastPos.y,m_focusDisplayPt[2],1),m_worldPt1=m_renderer.displayToWorld(m_displayPt1,m_camera.viewMatrix(),m_camera.projectionMatrix(),m_width,m_height),m_worldPt2=m_renderer.displayToWorld(m_displayPt2,m_camera.viewMatrix(),m_camera.projectionMatrix(),m_width,m_height),m_dx=m_worldPt1[0]-m_worldPt2[0],m_dy=m_worldPt1[1]-m_worldPt2[1],m_dz=m_worldPt1[2]-m_worldPt2[2],m_middleMouseButtonDown&&(m_camera.pan(-m_dx,-m_dy,-m_dz),render()),m_leftMouseButtonDown){for(deltaxy=[m_mouseLastPos.x-m_currentMousePos.x,m_mouseLastPos.y-m_currentMousePos.y],m_camera.rotate(deltaxy[0],deltaxy[1]),i=0;i0?m_camera.zoom(1-Math.abs(m_zTrans)):m_camera.zoom(1+Math.abs(m_zTrans)),render()),m_mouseLastPos.x=m_currentMousePos.x,m_mouseLastPos.y=m_currentMousePos.y,!1}},this.handleMouseDown=function(event){return 0===event.button&&(m_leftMouseButtonDown=!0),1===event.button&&(m_middleMouseButtonDown=!0),2===event.button&&(m_rightMouseButtonDown=!0),m_coords=m_that.viewer().relMouseCoords(event),m_coords.x<0?m_mouseLastPos.x=0:m_mouseLastPos.x=m_coords.x,m_coords.y<0?m_mouseLastPos.y=0:m_mouseLastPos.y=m_coords.y,!1},this.handleMouseUp=function(event){return 0===event.button&&(m_leftMouseButtonDown=!1),1===event.button&&(m_middleMouseButtonDown=!1),2===event.button&&(m_rightMouseButtonDown=!1),!1},this},inherit(vgl.pvwInteractorStyle,vgl.trackballInteractorStyle),vgl.viewer=function(canvas,options){"use strict";if(!(this instanceof vgl.viewer))return new vgl.viewer(canvas,options);vgl.object.call(this);var m_that=this,m_canvas=canvas,m_ready=!0,m_interactorStyle=null,m_renderer=vgl.renderer(options),m_renderWindow=vgl.renderWindow(m_canvas);return this.canvas=function(){return m_canvas},this.renderWindow=function(){return m_renderWindow},this.init=function(){null!==m_renderWindow?m_renderWindow._setup():console.log("[ERROR] No render window attached")},this.exit=function(renderState){null!==m_renderWindow?m_renderWindow._cleanup(renderState):console.log("[ERROR] No render window attached")},this.interactorStyle=function(){return m_interactorStyle},this.setInteractorStyle=function(style){style!==m_interactorStyle&&(m_interactorStyle=style,m_interactorStyle.setViewer(this),this.modified())},this.handleMouseDown=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);2===event.button&&fixedEvent.preventDefault(),fixedEvent.state="down",fixedEvent.type=vgl.event.mousePress,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseUp=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.state="up",fixedEvent.type=vgl.event.mouseRelease,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseMove=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.mouseMove,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseWheel=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.mouseWheel,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseOut=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.mouseOut,$(m_that).trigger(fixedEvent)}return!0},this.handleKeyPress=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.keyPress,$(m_that).trigger(fixedEvent)}return!0},this.handleContextMenu=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.contextMenu,$(m_that).trigger(fixedEvent)}return!1},this.handleClick=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.click,$(m_that).trigger(fixedEvent)}return!1},this.handleDoubleClick=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.dblClick,$(m_that).trigger(fixedEvent)}return!1},this.relMouseCoords=function(event){if(void 0===event.pageX||void 0===event.pageY)throw"Missing attributes pageX and pageY on the event";var totalOffsetX=0,totalOffsetY=0,canvasX=0,canvasY=0,currentElement=m_canvas;do totalOffsetX+=currentElement.offsetLeft-currentElement.scrollLeft,totalOffsetY+=currentElement.offsetTop-currentElement.scrollTop,currentElement=currentElement.offsetParent;while(currentElement);return canvasX=event.pageX-totalOffsetX,canvasY=event.pageY-totalOffsetY,{x:canvasX,y:canvasY}},this.render=function(){m_renderWindow.render()},this.bindEventHandlers=function(){$(m_canvas).on("mousedown",this.handleMouseDown),$(m_canvas).on("mouseup",this.handleMouseUp),$(m_canvas).on("mousemove",this.handleMouseMove),$(m_canvas).on("mousewheel",this.handleMouseWheel),$(m_canvas).on("contextmenu",this.handleContextMenu)},this.unbindEventHandlers=function(){$(m_canvas).off("mousedown",this.handleMouseDown),$(m_canvas).off("mouseup",this.handleMouseUp),$(m_canvas).off("mousemove",this.handleMouseMove),$(m_canvas).off("mousewheel",this.handleMouseWheel),$(m_canvas).off("contextmenu",this.handleContextMenu)},this._init=function(){this.bindEventHandlers(),m_renderWindow.addRenderer(m_renderer)},this._init(),this},inherit(vgl.viewer,vgl.object),vgl.shader=function(type){"use strict";if(!(this instanceof vgl.shader))return new vgl.shader(type);vgl.object.call(this);var m_shaderHandle=null,m_compileTimestamp=vgl.timestamp(),m_shaderType=type,m_shaderSource="";this.shaderHandle=function(){return m_shaderHandle},this.shaderType=function(){return m_shaderType},this.shaderSource=function(){return m_shaderSource},this.setShaderSource=function(source){m_shaderSource=source,this.modified()},this.compile=function(renderState){return this.getMTime()-1)return!1;var i;for(i=0;i-1?!1:(m_uniforms.push(uniform),void m_this.modified())},this.addVertexAttribute=function(attr,key){m_vertexAttributes[key]=attr,m_this.modified()},this.uniformLocation=function(name){return m_uniformNameToLocation[name]},this.attributeLocation=function(name){return m_vertexAttributeNameToLocation[name]},this.uniform=function(name){var i;for(i=0;i=this.getMTime())){for(m_this._setup(renderState),i=0;im_setupTimestamp.getMTime()&&this.setup(renderState),activateTextureUnit(renderState),renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,this.m_textureHandle)},this.undoBind=function(renderState){renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,null)},this.image=function(){return this.m_image},this.setImage=function(image){return null!==image?(this.m_image=image,this.updateDimensions(),this.modified(),!0):!1},this.nearestPixel=function(){return this.m_nearestPixel},this.setNearestPixel=function(nearest){return nearest=nearest?!0:!1,nearest!==this.m_nearestPixel?(this.m_nearestPixel=nearest,this.modified(),!0):!1},this.textureUnit=function(){return this.m_textureUnit},this.setTextureUnit=function(unit){return this.m_textureUnit===unit?!1:(this.m_textureUnit=unit,this.modified(),!0)},this.width=function(){return this.m_width},this.setWidth=function(width){return m_that.m_width!==width?(m_that.m_width=width,m_that.modified(),!0):!1},this.height=function(){return m_that.m_height},this.setHeight=function(height){return m_that.m_height!==height?(m_that.m_height=height,m_that.modified(),!0):!1},this.depth=function(){return this.m_depth},this.setDepth=function(depth){return null===this.m_image?!1:(this.m_depth=depth,this.modified(),!0)},this.textureHandle=function(){return this.m_textureHandle},this.internalFormat=function(){return this.m_internalFormat},this.setInternalFormat=function(internalFormat){return this.m_internalFormat!==internalFormat?(this.m_internalFormat=internalFormat,this.modified(),!0):!1},this.pixelFormat=function(){return this.m_pixelFormat},this.setPixelFormat=function(pixelFormat){return null===this.m_image?!1:(this.m_pixelFormat=pixelFormat,this.modified(),!0)},this.pixelDataType=function(){return this.m_pixelDataType},this.setPixelDataType=function(pixelDataType){return null===this.m_image?!1:(this.m_pixelDataType=pixelDataType,this.modified(),!0)},this.computeInternalFormatUsingImage=function(){this.m_internalFormat=vgl.GL.RGBA,this.m_pixelFormat=vgl.GL.RGBA,this.m_pixelDataType=vgl.GL.UNSIGNED_BYTE},this.updateDimensions=function(){null!==this.m_image&&(this.m_width=this.m_image.width,this.m_height=this.m_image.height,this.m_depth=0)},this},inherit(vgl.texture,vgl.materialAttribute),vgl.lookupTable=function(){"use strict";if(!(this instanceof vgl.lookupTable))return new vgl.lookupTable;vgl.texture.call(this);var m_setupTimestamp=vgl.timestamp(),m_range=[0,0];return this.m_colorTable=[.07514311,.468049805,1,1,.247872569,.498782363,1,1,.339526309,.528909511,1,1,.409505078,.558608486,1,1,.468487184,.588057293,1,1,.520796675,.617435078,1,1,.568724526,.646924167,1,1,.613686735,.676713218,1,1,.656658579,.707001303,1,1,.698372844,.738002964,1,1,.739424025,.769954435,1,1,.780330104,.803121429,1,1,.821573924,.837809045,1,1,.863634967,.874374691,1,1,.907017747,.913245283,1,1,.936129275,.938743558,.983038586,1,.943467973,.943498599,.943398095,1,.990146732,.928791426,.917447482,1,1,.88332677,.861943246,1,1,.833985467,.803839606,1,1,.788626485,.750707739,1,1,.746206642,.701389973,1,1,.70590052,.654994046,1,1,.667019783,.610806959,1,1,.6289553,.568237474,1,1,.591130233,.526775617,1,1,.552955184,.485962266,1,1,.513776083,.445364274,1,1,.472800903,.404551679,1,1,.428977855,.363073592,1,1,.380759558,.320428137,1,.961891484,.313155629,.265499262,1,.916482116,.236630659,.209939162,1].map(function(x){ -return 255*x}),this.setup=function(renderState){0===this.textureUnit()?renderState.m_context.activeTexture(vgl.GL.TEXTURE0):1===this.textureUnit()&&renderState.m_context.activeTexture(vgl.GL.TEXTURE1),renderState.m_context.deleteTexture(this.m_textureHandle),this.m_textureHandle=renderState.m_context.createTexture(),renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,this.m_textureHandle),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_MIN_FILTER,vgl.GL.LINEAR),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_MAG_FILTER,vgl.GL.LINEAR),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_WRAP_S,vgl.GL.CLAMP_TO_EDGE),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_WRAP_T,vgl.GL.CLAMP_TO_EDGE),renderState.m_context.pixelStorei(vgl.GL.UNPACK_ALIGNMENT,1),this.m_width=this.m_colorTable.length/4,this.m_height=1,this.m_depth=0,renderState.m_context.texImage2D(vgl.GL.TEXTURE_2D,0,vgl.GL.RGBA,this.m_width,this.m_height,this.m_depth,vgl.GL.RGBA,vgl.GL.UNSIGNED_BYTE,new Uint8Array(this.m_colorTable)),renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,null),m_setupTimestamp.modified()},this.colorTable=function(){return this.m_colorTable},this.setColorTable=function(colors){return this.m_colorTable===colors?!1:(this.m_colorTable=colors,this.modified(),!0)},this.range=function(){return m_range},this.setRange=function(range){return m_range===range?!1:(m_range=range,this.modified(),!0)},this.updateRange=function(range){range instanceof Array||console.log("[error] Invalid data type for range. Requires array [min,max]"),range[0]m_range[1]&&(m_range[1]=range[1],this.modified())},this},inherit(vgl.lookupTable,vgl.texture),vgl.uniform=function(type,name){"use strict";if(!(this instanceof vgl.uniform))return new vgl.uniform;this.getTypeNumberOfComponents=function(type){switch(type){case vgl.GL.FLOAT:case vgl.GL.INT:case vgl.GL.BOOL:return 1;case vgl.GL.FLOAT_VEC2:case vgl.GL.INT_VEC2:case vgl.GL.BOOL_VEC2:return 2;case vgl.GL.FLOAT_VEC3:case vgl.GL.INT_VEC3:case vgl.GL.BOOL_VEC3:return 3;case vgl.GL.FLOAT_VEC4:case vgl.GL.INT_VEC4:case vgl.GL.BOOL_VEC4:return 4;case vgl.GL.FLOAT_MAT3:return 9;case vgl.GL.FLOAT_MAT4:return 16;default:return 0}};var m_type=type,m_name=name,m_dataArray=[];return m_dataArray.length=this.getTypeNumberOfComponents(m_type),this.name=function(){return m_name},this.type=function(){return m_type},this.get=function(){return m_dataArray},this.set=function(value){var i=0;if(16===m_dataArray.length)for(i=0;16>i;i+=1)m_dataArray[i]=value[i];else if(9===m_dataArray.length)for(i=0;9>i;i+=1)m_dataArray[i]=value[i];else if(4===m_dataArray.length)for(i=0;4>i;i+=1)m_dataArray[i]=value[i];else if(3===m_dataArray.length)for(i=0;3>i;i+=1)m_dataArray[i]=value[i];else if(2===m_dataArray.length)for(i=0;2>i;i+=1)m_dataArray[i]=value[i];else m_dataArray[0]=value},this.callGL=function(renderState,location){if(!(this.m_numberElements<1))switch(m_type){case vgl.GL.BOOL:case vgl.GL.INT:renderState.m_context.uniform1iv(location,m_dataArray);break;case vgl.GL.FLOAT:renderState.m_context.uniform1fv(location,m_dataArray);break;case vgl.GL.FLOAT_VEC2:renderState.m_context.uniform2fv(location,m_dataArray);break;case vgl.GL.FLOAT_VEC3:renderState.m_context.uniform3fv(location,m_dataArray);break;case vgl.GL.FLOAT_VEC4:renderState.m_context.uniform4fv(location,m_dataArray);break;case vgl.GL.FLOAT_MAT3:renderState.m_context.uniformMatrix3fv(location,vgl.GL.FALSE,m_dataArray);break;case vgl.GL.FLOAT_MAT4:renderState.m_context.uniformMatrix4fv(location,vgl.GL.FALSE,m_dataArray)}},this.update=function(renderState,program){renderState=renderState,program=program},this},vgl.modelViewUniform=function(name){"use strict";return this instanceof vgl.modelViewUniform?(0===name.length&&(name="modelViewMatrix"),vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program,this.set(renderState.m_modelViewMatrix)},this):new vgl.modelViewUniform(name)},inherit(vgl.modelViewUniform,vgl.uniform),vgl.modelViewOriginUniform=function(name,origin){"use strict";if(!(this instanceof vgl.modelViewOriginUniform))return new vgl.modelViewOriginUniform(name,origin);0===name.length&&(name="modelViewMatrix");var m_origin=vec3.fromValues(origin[0],origin[1],origin[2]);return vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program;var view=mat4.create();if(mat4.translate(view,renderState.m_modelViewMatrix,m_origin),renderState.m_modelViewAlignment){var align=renderState.m_modelViewAlignment;view[12]=Math.round(view[12]/align.round)*align.round+align.dx,view[13]=Math.round(view[13]/align.round)*align.round+align.dy}this.set(view)},this},inherit(vgl.modelViewOriginUniform,vgl.uniform),vgl.projectionUniform=function(name){"use strict";return this instanceof vgl.projectionUniform?(0===name.length&&(name="projectionMatrix"),vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program,this.set(renderState.m_projectionMatrix)},this):new vgl.projectionUniform(name)},inherit(vgl.projectionUniform,vgl.uniform),vgl.floatUniform=function(name,value){"use strict";return this instanceof vgl.floatUniform?(0===name.length&&(name="floatUniform"),value=void 0===value?1:value,vgl.uniform.call(this,vgl.GL.FLOAT,name),void this.set(value)):new vgl.floatUniform(name,value)},inherit(vgl.floatUniform,vgl.uniform),vgl.normalMatrixUniform=function(name){"use strict";return this instanceof vgl.normalMatrixUniform?(0===name.length&&(name="normalMatrix"),vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program,this.set(renderState.m_normalMatrix)},this):new vgl.normalMatrixUniform(name)},inherit(vgl.normalMatrixUniform,vgl.uniform),vgl.vertexAttributeKeys={Position:0,Normal:1,TextureCoordinate:2,Color:3,Scalar:4,CountAttributeIndex:5},vgl.vertexAttributeKeysIndexed={Zero:0,One:1,Two:2,Three:3,Four:4,Five:5,Six:6,Seven:7,Eight:8,Nine:9},vgl.vertexAttribute=function(name){"use strict";if(!(this instanceof vgl.vertexAttribute))return new vgl.vertexAttribute(name);var m_name=name;this.name=function(){return m_name},this.bindVertexData=function(renderState,key){var geometryData=renderState.m_mapper.geometryData(),sourceData=geometryData.sourceData(key),program=renderState.m_material.shaderProgram();renderState.m_context.vertexAttribPointer(program.attributeLocation(m_name),sourceData.attributeNumberOfComponents(key),sourceData.attributeDataType(key),sourceData.normalized(key),sourceData.attributeStride(key),sourceData.attributeOffset(key)),renderState.m_context.enableVertexAttribArray(program.attributeLocation(m_name))},this.undoBindVertexData=function(renderState,key){key=key;var program=renderState.m_material.shaderProgram();renderState.m_context.disableVertexAttribArray(program.attributeLocation(m_name))}},vgl.source=function(){"use strict";return this instanceof vgl.source?(vgl.object.call(this),this.create=function(){},this):new vgl.source},inherit(vgl.source,vgl.object),vgl.planeSource=function(){"use strict";if(!(this instanceof vgl.planeSource))return new vgl.planeSource;vgl.source.call(this);var m_origin=[0,0,0],m_point1=[1,0,0],m_point2=[0,1,0],m_normal=[0,0,1],m_xresolution=1,m_yresolution=1,m_geom=null;this.setOrigin=function(x,y,z){m_origin[0]=x,m_origin[1]=y,m_origin[2]=z},this.setPoint1=function(x,y,z){m_point1[0]=x,m_point1[1]=y,m_point1[2]=z},this.setPoint2=function(x,y,z){m_point2[0]=x,m_point2[1]=y,m_point2[2]=z},this.create=function(){m_geom=new vgl.geometryData;var i,j,k,ii,numPts,numPolys,sourceTexCoords,x=[],tc=[],v1=[],v2=[],pts=[],posIndex=0,normIndex=0,colorIndex=0,texCoordIndex=0,positions=[],normals=[],colors=[],texCoords=[],indices=[],tristrip=null,sourcePositions=null,sourceColors=null;for(x.length=3,tc.length=2,v1.length=3,v2.length=3,pts.length=3,i=0;3>i;i+=1)v1[i]=m_point1[i]-m_origin[i],v2[i]=m_point2[i]-m_origin[i];for(numPts=(m_xresolution+1)*(m_yresolution+1),numPolys=m_xresolution*m_yresolution*2,positions.length=3*numPts,normals.length=3*numPts,texCoords.length=2*numPts,indices.length=numPts,k=0,i=0;m_yresolution+1>i;i+=1)for(tc[1]=i/m_yresolution,j=0;m_xresolution+1>j;j+=1){for(tc[0]=j/m_xresolution,ii=0;3>ii;ii+=1)x[ii]=m_origin[ii]+tc[0]*v1[ii]+tc[1]*v2[ii];positions[posIndex++]=x[0],positions[posIndex++]=x[1],positions[posIndex++]=x[2],colors[colorIndex++]=1,colors[colorIndex++]=1,colors[colorIndex++]=1,normals[normIndex++]=m_normal[0],normals[normIndex++]=m_normal[1],normals[normIndex++]=m_normal[2],texCoords[texCoordIndex++]=tc[0],texCoords[texCoordIndex++]=tc[1]}for(i=0;m_yresolution>i;i+=1)for(j=0;m_xresolution>j;j+=1)pts[0]=j+i*(m_xresolution+1),pts[1]=pts[0]+1,pts[2]=pts[0]+m_xresolution+2,pts[3]=pts[0]+m_xresolution+1;for(i=0;numPts>i;i+=1)indices[i]=i;return tristrip=new vgl.triangleStrip,tristrip.setIndices(indices),sourcePositions=vgl.sourceDataP3fv(),sourcePositions.pushBack(positions),sourceColors=vgl.sourceDataC3fv(),sourceColors.pushBack(colors),sourceTexCoords=vgl.sourceDataT2fv(),sourceTexCoords.pushBack(texCoords),m_geom.addSource(sourcePositions),m_geom.addSource(sourceColors),m_geom.addSource(sourceTexCoords),m_geom.addPrimitive(tristrip),m_geom}},inherit(vgl.planeSource,vgl.source),vgl.pointSource=function(){"use strict";if(!(this instanceof vgl.pointSource))return new vgl.pointSource;vgl.source.call(this);var m_this=this,m_positions=[],m_colors=[],m_textureCoords=[],m_size=[],m_geom=null;this.getPositions=function(){return m_positions},this.setPositions=function(positions){positions instanceof Array?m_positions=positions:console.log("[ERROR] Invalid data type for positions. Array is required."),m_this.modified()},this.getColors=function(){return m_colors},this.setColors=function(colors){colors instanceof Array?m_colors=colors:console.log("[ERROR] Invalid data type for colors. Array is required."),m_this.modified()},this.getSize=function(){return m_size},this.setSize=function(size){m_size=size,this.modified()},this.setTextureCoordinates=function(texcoords){texcoords instanceof Array?m_textureCoords=texcoords:console.log("[ERROR] Invalid data type for texture coordinates. Array is required."),m_this.modified()},this.create=function(){if(m_geom=new vgl.geometryData,m_positions.length%3!==0)return void console.log("[ERROR] Invalid length of the points array");var pointsPrimitive,sourcePositions,sourceColors,sourceTexCoords,sourceSize,numPts=m_positions.length/3,i=0,indices=[];for(indices.length=numPts,i=0;numPts>i;i+=1)indices[i]=i;if(sourceSize=vgl.sourceDataDf(),numPts!==m_size.length)for(i=0;numPts>i;i+=1)sourceSize.pushBack(m_size);else sourceSize.setData(m_size);return m_geom.addSource(sourceSize),pointsPrimitive=new vgl.points,pointsPrimitive.setIndices(indices),sourcePositions=vgl.sourceDataP3fv(),sourcePositions.pushBack(m_positions),m_geom.addSource(sourcePositions),m_colors.length>0&&m_colors.length===m_positions.length?(sourceColors=vgl.sourceDataC3fv(),sourceColors.pushBack(m_colors),m_geom.addSource(sourceColors)):m_colors.length>0&&m_colors.length!==m_positions.length&&console.log("[ERROR] Number of colors are different than number of points"),m_textureCoords.length>0&&m_textureCoords.length===m_positions.length?(sourceTexCoords=vgl.sourceDataT2fv(),sourceTexCoords.pushBack(m_textureCoords),m_geom.addSource(sourceTexCoords)):m_textureCoords.length>0&&m_textureCoords.length/2!==m_positions.length/3&&console.log("[ERROR] Number of texture coordinates are different than number of points"),m_geom.addPrimitive(pointsPrimitive),m_geom}},inherit(vgl.pointSource,vgl.source),vgl.lineSource=function(positions,colors){"use strict";if(!(this instanceof vgl.lineSource))return new vgl.lineSource;vgl.source.call(this);var m_positions=positions,m_colors=colors;this.setPositions=function(positions){return positions instanceof Array?(m_positions=positions,this.modified(),!0):(console.log("[ERROR] Invalid data type for positions. Array is required."),!1)},this.setColors=function(colors){return colors instanceof Array?(m_colors=colors,this.modified(),!0):(console.log("[ERROR] Invalid data type for colors. Array is required."),!1)},this.create=function(){if(!m_positions)return void console.log("[error] Invalid positions");if(m_positions.length%3!==0)return void console.log("[error] Line source requires 3d points");if(m_positions.length%3!==0)return void console.log("[ERROR] Invalid length of the points array");var i,linesPrimitive,sourcePositions,sourceColors,m_geom=new vgl.geometryData,numPts=m_positions.length/3,indices=[];for(indices.length=numPts,i=0;numPts>i;i+=1)indices[i]=i;return linesPrimitive=new vgl.lines,linesPrimitive.setIndices(indices),sourcePositions=vgl.sourceDataP3fv(),sourcePositions.pushBack(m_positions),m_geom.addSource(sourcePositions),m_colors&&m_colors.length>0&&m_colors.length===m_positions.length?(sourceColors=vgl.sourceDataC3fv(),sourceColors.pushBack(m_colors),m_geom.addSource(sourceColors)):m_colors&&m_colors.length>0&&m_colors.length!==m_positions.length&&console.log("[error] Number of colors are different than number of points"),m_geom.addPrimitive(linesPrimitive),m_geom}},inherit(vgl.lineSource,vgl.source),vgl.utils=function(){"use strict";return this instanceof vgl.utils?(vgl.object.call(this),this):new vgl.utils},inherit(vgl.utils,vgl.object),vgl.utils.computePowerOfTwo=function(value,pow){"use strict";for(pow=pow||1;value>pow;)pow*=2;return pow},vgl.utils.createTextureVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 textureCoord;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying highp vec3 iTextureCoord;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);"," iTextureCoord = textureCoord;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createTextureFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying highp vec3 iTextureCoord;","uniform sampler2D sampler2d;","uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4(texture2D(sampler2d, vec2(iTextureCoord.s, iTextureCoord.t)).xyz, opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createRgbaTextureFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying highp vec3 iTextureCoord;","uniform sampler2D sampler2d;","uniform mediump float opacity;","void main(void) {"," mediump vec4 color = vec4(texture2D(sampler2d, vec2(iTextureCoord.s, iTextureCoord.t)).xyzw);"," color.w *= opacity;"," gl_FragColor = color;","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 vertexColor;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying mediump vec3 iVertexColor;","varying highp vec3 iTextureCoord;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);"," iVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createPointVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 vertexColor;","attribute float vertexSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying mediump vec3 iVertexColor;","varying highp vec3 iTextureCoord;","void main(void)","{","gl_PointSize = vertexSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);"," iVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createVertexShaderSolidColor=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createVertexShaderColorMap=function(context,min,max){"use strict";context=context,min=min,max=max;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute float vertexScalar;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform float lutMin;","uniform float lutMax;","varying mediump float iVertexScalar;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);","iVertexScalar = (vertexScalar-lutMin)/(lutMax-lutMin);","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying mediump vec3 iVertexColor;","uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4(iVertexColor, opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createPhongVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute highp vec3 vertexPosition;","attribute mediump vec3 vertexNormal;","attribute mediump vec3 vertexColor;","uniform highp mat4 projectionMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 normalMatrix;","varying highp vec4 varPosition;","varying mediump vec3 varNormal;","varying mediump vec3 varVertexColor;","void main(void)","{","varPosition = modelViewMatrix * vec4(vertexPosition, 1.0);","gl_Position = projectionMatrix * varPosition;","varNormal = vec3(normalMatrix * vec4(vertexNormal, 0.0));","varVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createPhongFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["uniform mediump float opacity;","precision mediump float;","varying vec3 varNormal;","varying vec4 varPosition;","varying mediump vec3 varVertexColor;","const vec3 lightPos = vec3(0.0, 0.0,10000.0);","const vec3 ambientColor = vec3(0.01, 0.01, 0.01);","const vec3 specColor = vec3(0.0, 0.0, 0.0);","void main() {","vec3 normal = normalize(varNormal);","vec3 lightDir = normalize(lightPos);","vec3 reflectDir = -reflect(lightDir, normal);","vec3 viewDir = normalize(-varPosition.xyz);","float lambertian = max(dot(lightDir, normal), 0.0);","vec3 color = vec3(0.0);","if(lambertian > 0.0) {"," color = lambertian * varVertexColor;","}","gl_FragColor = vec4(color * opacity, 1.0 - opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createFragmentShaderSolidColor=function(context,color){"use strict";var fragmentShaderSource=["uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4("+color[0]+","+color[1]+","+color[2]+", opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createFragmentShaderColorMap=function(context){"use strict";context=context;var fragmentShaderSource=["varying mediump float iVertexScalar;","uniform sampler2D sampler2d;","uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4(texture2D(sampler2d, vec2(iVertexScalar, 0.0)).xyz, opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createPointSpritesVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 vertexColor;","uniform mediump vec2 pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform float height;","varying mediump vec3 iVertexColor;","varying highp float iVertexScalar;","void main(void)","{","mediump float realPointSize = pointSize.y;","if (pointSize.x > pointSize.y) {"," realPointSize = pointSize.x;}","gl_PointSize = realPointSize ;","iVertexScalar = vertexPosition.z;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition.xy, height, 1.0);"," iVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createPointSpritesFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying mediump vec3 iVertexColor;","varying highp float iVertexScalar;","uniform sampler2D opacityLookup;","uniform highp float lutMin;","uniform highp float lutMax;","uniform sampler2D scalarsToColors;","uniform int useScalarsToColors;","uniform int useVertexColors;","uniform mediump vec2 pointSize;","uniform mediump float vertexColorWeight;","void main(void) {","mediump vec2 realTexCoord;","if (pointSize.x > pointSize.y) {"," realTexCoord = vec2(1.0, pointSize.y/pointSize.x) * gl_PointCoord;","} else {"," realTexCoord = vec2(pointSize.x/pointSize.y, 1.0) * gl_PointCoord;","}","highp float texOpacity = texture2D(opacityLookup, realTexCoord).w;","if (useScalarsToColors == 1) {"," gl_FragColor = vec4(texture2D(scalarsToColors, vec2((iVertexScalar - lutMin)/(lutMax - lutMin), 0.0)).xyz, texOpacity);","} else if (useVertexColors == 1) {"," gl_FragColor = vec4(iVertexColor, texOpacity);","} else {"," gl_FragColor = vec4(texture2D(opacityLookup, realTexCoord).xyz, texOpacity);","}}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createTextureMaterial=function(isRgba,origin){"use strict";var modelViewUniform,mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createTextureVertexShader(vgl.GL),fragmentShader=null,posVertAttr=new vgl.vertexAttribute("vertexPosition"),texCoordVertAttr=new vgl.vertexAttribute("textureCoord"),pointsizeUniform=new vgl.floatUniform("pointSize",5),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.INT,"sampler2d"),opacityUniform=null;return modelViewUniform=void 0!==origin?new vgl.modelViewOriginUniform("modelViewMatrix",origin):new vgl.modelViewUniform("modelViewMatrix"),samplerUniform.set(0),prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(texCoordVertAttr,vgl.vertexAttributeKeys.TextureCoordinate),prog.addUniform(pointsizeUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),fragmentShader=isRgba?vgl.utils.createRgbaTextureFragmentShader(vgl.GL):vgl.utils.createTextureFragmentShader(vgl.GL),opacityUniform=new vgl.floatUniform("opacity",1),prog.addUniform(opacityUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createGeometryMaterial=function(){"use strict";var mat=new vgl.material,prog=new vgl.shaderProgram,pointSize=5,opacity=1,vertexShader=vgl.utils.createVertexShader(vgl.GL),fragmentShader=vgl.utils.createFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),pointsizeUniform=new vgl.floatUniform("pointSize",pointSize),opacityUniform=new vgl.floatUniform("opacity",opacity),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat},vgl.utils.createPointGeometryMaterial=function(opacity){"use strict";opacity=void 0===opacity?1:opacity;var mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createPointVertexShader(vgl.GL),fragmentShader=vgl.utils.createFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),sizeVertAttr=new vgl.vertexAttribute("vertexSize"),opacityUniform=new vgl.floatUniform("opacity",opacity),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addVertexAttribute(sizeVertAttr,vgl.vertexAttributeKeys.Scalar),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createPhongMaterial=function(){"use strict";var mat=new vgl.material,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createPhongVertexShader(vgl.GL),fragmentShader=vgl.utils.createPhongFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),normalVertAttr=new vgl.vertexAttribute("vertexNormal"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),opacityUniform=new vgl.floatUniform("opacity",1),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),normalUniform=new vgl.normalMatrixUniform("normalMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(normalVertAttr,vgl.vertexAttributeKeys.Normal),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addUniform(normalUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat},vgl.utils.createColorMaterial=function(){"use strict";var mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createVertexShader(vgl.GL),fragmentShader=vgl.utils.createFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),texCoordVertAttr=new vgl.vertexAttribute("textureCoord"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),pointsizeUniform=new vgl.floatUniform("pointSize",5),opacityUniform=new vgl.floatUniform("opacity",1),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addVertexAttribute(texCoordVertAttr,vgl.vertexAttributeKeys.TextureCoordinate),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createColorMappedMaterial=function(lut){"use strict";lut||(lut=new vgl.lookupTable);var scalarRange=lut.range(),mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createVertexShaderColorMap(vgl.GL,scalarRange[0],scalarRange[1]),fragmentShader=vgl.utils.createFragmentShaderColorMap(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),scalarVertAttr=new vgl.vertexAttribute("vertexScalar"),pointsizeUniform=new vgl.floatUniform("pointSize",5),opacityUniform=new vgl.floatUniform("opacity",1),lutMinUniform=new vgl.floatUniform("lutMin",scalarRange[0]),lutMaxUniform=new vgl.floatUniform("lutMax",scalarRange[1]),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.FLOAT,"sampler2d"),lookupTable=lut;return samplerUniform.set(0),prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(scalarVertAttr,vgl.vertexAttributeKeys.Scalar),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(lutMinUniform),prog.addUniform(lutMaxUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat.addAttribute(lookupTable),mat},vgl.utils.updateColorMappedMaterial=function(mat,lut){"use strict";if(!mat)return void console.log("[warning] Invalid material. Nothing to update.");if(!lut)return void console.log("[warning] Invalid lookup table. Nothing to update.");var lutMin=mat.shaderProgram().uniform("lutMin"),lutMax=mat.shaderProgram().uniform("lutMax");lutMin.set(lut.range()[0]),lutMax.set(lut.range()[1]),mat.setAttribute(lut)},vgl.utils.createSolidColorMaterial=function(color){"use strict";color||(color=[1,1,1]);var mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createVertexShaderSolidColor(vgl.GL),fragmentShader=vgl.utils.createFragmentShaderSolidColor(vgl.GL,color),posVertAttr=new vgl.vertexAttribute("vertexPosition"),pointsizeUniform=new vgl.floatUniform("pointSize",5),opacityUniform=new vgl.floatUniform("opacity",1),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createPointSpritesMaterial=function(image,lut){"use strict";var scalarRange=void 0===lut?[0,1]:lut.range(),mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createPointSpritesVertexShader(vgl.GL),fragmentShader=vgl.utils.createPointSpritesFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),heightUniform=new vgl.floatUniform("height",0),vertexColorWeightUniform=new vgl.floatUniform("vertexColorWeight",0),lutMinUniform=new vgl.floatUniform("lutMin",scalarRange[0]),lutMaxUniform=new vgl.floatUniform("lutMax",scalarRange[1]),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.INT,"opacityLookup"),scalarsToColors=new vgl.uniform(vgl.GL.INT,"scalarsToColors"),useScalarsToColors=new vgl.uniform(vgl.GL.INT,"useScalarsToColors"),useVertexColors=new vgl.uniform(vgl.GL.INT,"useVertexColors"),pointSize=new vgl.uniform(vgl.GL.FLOAT_VEC2,"pointSize"),texture=new vgl.texture;return samplerUniform.set(0),scalarsToColors.set(1),useScalarsToColors.set(0),useVertexColors.set(0),pointSize.set([1,1]),prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addUniform(heightUniform),prog.addUniform(vertexColorWeightUniform), -prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addUniform(samplerUniform),prog.addUniform(useVertexColors),prog.addUniform(useScalarsToColors),prog.addUniform(pointSize),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),lut&&(prog.addUniform(scalarsToColors),useScalarsToColors.set(1),prog.addUniform(lutMinUniform),prog.addUniform(lutMaxUniform),lut.setTextureUnit(1),mat.addAttribute(lut)),texture.setImage(image),texture.setTextureUnit(0),mat.addAttribute(texture),mat},vgl.utils.createPlane=function(originX,originY,originZ,point1X,point1Y,point1Z,point2X,point2Y,point2Z){"use strict";var mapper=new vgl.mapper,planeSource=new vgl.planeSource,mat=vgl.utils.createGeometryMaterial(),actor=new vgl.actor;return planeSource.setOrigin(originX,originY,originZ),planeSource.setPoint1(point1X,point1Y,point1Z),planeSource.setPoint2(point2X,point2Y,point2Z),mapper.setGeometryData(planeSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createTexturePlane=function(originX,originY,originZ,point1X,point1Y,point1Z,point2X,point2Y,point2Z,isRgba){"use strict";var mapper=new vgl.mapper,planeSource=new vgl.planeSource,mat=vgl.utils.createTextureMaterial(isRgba,[originX,originY,originZ]),actor=new vgl.actor;return planeSource.setPoint1(point1X-originX,point1Y-originY,point1Z-originZ),planeSource.setPoint2(point2X-originX,point2Y-originY,point2Z-originZ),mapper.setGeometryData(planeSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createPoints=function(positions,size,colors,texcoords,opacity){"use strict";if(!positions)return console.log("[ERROR] Cannot create points without positions"),null;opacity=void 0===opacity?1:opacity;var mapper=new vgl.mapper,pointSource=new vgl.pointSource,mat=vgl.utils.createPointGeometryMaterial(opacity),actor=new vgl.actor;return pointSource.setPositions(positions),colors&&pointSource.setColors(colors),texcoords&&pointSource.setTextureCoordinates(texcoords),size?pointSource.setSize(size):pointSource.setSize(1),mapper.setGeometryData(pointSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createPointSprites=function(image,positions,colors,texcoords){"use strict";if(!image)return console.log("[ERROR] Point sprites requires an image"),null;if(!positions)return console.log("[ERROR] Cannot create points without positions"),null;var mapper=new vgl.mapper,pointSource=new vgl.pointSource,mat=vgl.utils.createPointSpritesMaterial(image),actor=new vgl.actor;return pointSource.setPositions(positions),colors&&pointSource.setColors(colors),texcoords&&pointSource.setTextureCoordinates(texcoords),mapper.setGeometryData(pointSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createLines=function(positions,colors){"use strict";if(!positions)return console.log("[ERROR] Cannot create points without positions"),null;var mapper=new vgl.mapper,lineSource=new vgl.lineSource,mat=vgl.utils.createGeometryMaterial(),actor=new vgl.actor;return lineSource.setPositions(positions),colors&&lineSource.setColors(colors),mapper.setGeometryData(lineSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createColorLegend=function(varname,lookupTable,origin,width,height,countMajor,countMinor){"use strict";function createLabels(varname,positions,range){if(!positions)return void console.log("[error] Create labels requires positions (x,y,z) array");if(positions.length%3!==0)return void console.log("[error] Create labels require positions array contain 3d points");if(!range)return void console.log("[error] Create labels requires Valid range");var i,actor=null,size=vgl.utils.computePowerOfTwo(48),index=0,actors=[],origin=[],pt1=[],pt2=[],delta=positions[6]-positions[0],axisLabelOffset=4;for(origin.length=3,pt1.length=3,pt2.length=3,i=0;2>i;i+=1)index=i*(positions.length-3),origin[0]=positions[index]-delta,origin[1]=positions[index+1]-2*delta,origin[2]=positions[index+2],pt1[0]=positions[index]+delta,pt1[1]=origin[1],pt1[2]=origin[2],pt2[0]=origin[0],pt2[1]=positions[1],pt2[2]=origin[2],actor=vgl.utils.createTexturePlane(origin[0],origin[1],origin[2],pt1[0],pt1[1],pt1[2],pt2[0],pt2[1],pt2[2],!0),actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute),actor.material().setBinNumber(vgl.material.RenderBin.Overlay),actor.material().addAttribute(vgl.utils.create2DTexture(range[i].toFixed(2).toString(),12,null)),actors.push(actor);return origin[0]=.5*(positions[0]+positions[positions.length-3]-size),origin[1]=positions[1]+axisLabelOffset,origin[2]=positions[2],pt1[0]=origin[0]+size,pt1[1]=origin[1],pt1[2]=origin[2],pt2[0]=origin[0],pt2[1]=origin[1]+size,pt2[2]=origin[2],actor=vgl.utils.createTexturePlane(origin[0],origin[1],origin[2],pt1[0],pt1[1],pt1[2],pt2[0],pt2[1],pt2[2],!0),actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute),actor.material().setBinNumber(vgl.material.RenderBin.Overlay),actor.material().addAttribute(vgl.utils.create2DTexture(varname,24,null)),actors.push(actor),actors}function createTicksAndLabels(varname,lut,originX,originY,originZ,pt1X,pt1Y,pt1Z,pt2X,pt2Y,pt2Z,countMajor,countMinor,heightMajor,heightMinor){heightMinor=heightMinor;var width=pt2X-pt1X,index=null,delta=width/countMajor,positions=[],actors=[];for(index=0;countMajor>=index;index+=1)positions.push(pt1X+delta*index),positions.push(pt1Y),positions.push(pt1Z),positions.push(pt1X+delta*index),positions.push(pt1Y+heightMajor),positions.push(pt1Z);return actors=actors.concat(createLabels(varname,positions,lut.range()))}if(!lookupTable)return console.log("[error] Invalid lookup table"),[];var pt1X=origin[0]+width,pt1Y=origin[1],pt1Z=0,pt2X=origin[0],pt2Y=origin[1]+height,pt2Z=0,actors=[],actor=null,mat=null,group=vgl.groupNode();return actor=vgl.utils.createTexturePlane(origin[0],origin[1],origin[2],pt1X,pt1Y,pt1Z,pt2X,pt2Y,pt2Z,!0),mat=actor.material(),mat.addAttribute(lookupTable),actor.setMaterial(mat),group.addChild(actor),actor.material().setBinNumber(vgl.material.RenderBin.Overlay),actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute),actors.push(actor),actors=actors.concat(createTicksAndLabels(varname,lookupTable,origin[0],origin[1],origin[1],pt2X,pt1Y,pt1Z,pt1X,pt1Y,pt1Z,countMajor,countMinor,5,3))},vgl.utils.create2DTexture=function(textToWrite,textSize,color,font,alignment,baseline,bold){"use strict";var canvas=document.getElementById("textRendering"),ctx=null,texture=vgl.texture();return font=font||"sans-serif",alignment=alignment||"center",baseline=baseline||"bottom","undefined"==typeof bold&&(bold=!0),canvas||(canvas=document.createElement("canvas")),ctx=canvas.getContext("2d"),canvas.setAttribute("id","textRendering"),canvas.style.display="none",canvas.height=vgl.utils.computePowerOfTwo(8*textSize),canvas.width=canvas.height,ctx.fillStyle="rgba(0, 0, 0, 0)",ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height),ctx.fillStyle="rgba(200, 85, 10, 1.0)",ctx.textAlign=alignment,ctx.textBaseline=baseline,ctx.font=4*textSize+"px "+font,bold&&(ctx.font="bold "+ctx.font),ctx.fillText(textToWrite,canvas.width/2,canvas.height/2,canvas.width),texture.setImage(canvas),texture.updateDimensions(),texture},vgl.picker=function(){"use strict";if(!(this instanceof vgl.picker))return new vgl.picker;vgl.object.call(this);var m_actors=[];return this.getActors=function(){return m_actors},this.pick=function(selectionX,selectionY,renderer){if(void 0===selectionX)return 0;if(void 0===selectionY)return 0;if(void 0===renderer)return 0;m_actors=[];var actors,count,i,bb,tmin,tmax,tymin,tymax,tzmin,tzmax,actor,camera=renderer.camera(),width=renderer.width(),height=renderer.height(),fpoint=camera.focalPoint(),focusWorldPt=vec4.fromValues(fpoint[0],fpoint[1],fpoint[2],1),focusDisplayPt=renderer.worldToDisplay(focusWorldPt,camera.viewMatrix(),camera.projectionMatrix(),width,height),displayPt=vec4.fromValues(selectionX,selectionY,focusDisplayPt[2],1),worldPt=renderer.displayToWorld(displayPt,camera.viewMatrix(),camera.projectionMatrix(),width,height),cameraPos=camera.position(),ray=[];for(i=0;3>i;i+=1)ray[i]=worldPt[i]-cameraPos[i];for(actors=renderer.sceneRoot().children(),count=0,i=0;i=0?(tmin=(bb[0]-cameraPos[0])/ray[0],tmax=(bb[1]-cameraPos[0])/ray[0]):(tmin=(bb[1]-cameraPos[0])/ray[0],tmax=(bb[0]-cameraPos[0])/ray[0]),ray[1]>=0?(tymin=(bb[2]-cameraPos[1])/ray[1],tymax=(bb[3]-cameraPos[1])/ray[1]):(tymin=(bb[3]-cameraPos[1])/ray[1],tymax=(bb[2]-cameraPos[1])/ray[1]),tmin>tymax||tymin>tmax)continue;if(tymin>tmin&&(tmin=tymin),tmax>tymax&&(tmax=tymax),ray[2]>=0?(tzmin=(bb[4]-cameraPos[2])/ray[2],tzmax=(bb[5]-cameraPos[2])/ray[2]):(tzmin=(bb[5]-cameraPos[2])/ray[2],tzmax=(bb[4]-cameraPos[2])/ray[2]),tmin>tzmax||tzmin>tmax)continue;tzmin>tmin&&(tmin=tzmin),tmax>tzmax&&(tmax=tzmax),m_actors[count]=actor,count+=1}return count},this},inherit(vgl.picker,vgl.object),vgl.shapefileReader=function(){"use strict";if(!(this instanceof vgl.shapefileReader))return new vgl.shapefileReader;var m_that=this,SHP_NULL=0,SHP_POINT=1,SHP_POLYGON=5,SHP_POLYLINE=3;return this.int8=function(data,offset){return data.charCodeAt(offset)},this.bint32=function(data,offset){return((255&data.charCodeAt(offset))<<24)+((255&data.charCodeAt(offset+1))<<16)+((255&data.charCodeAt(offset+2))<<8)+(255&data.charCodeAt(offset+3))},this.lint32=function(data,offset){return((255&data.charCodeAt(offset+3))<<24)+((255&data.charCodeAt(offset+2))<<16)+((255&data.charCodeAt(offset+1))<<8)+(255&data.charCodeAt(offset))},this.bint16=function(data,offset){return((255&data.charCodeAt(offset))<<8)+(255&data.charCodeAt(offset+1))},this.lint16=function(data,offset){return((255&data.charCodeAt(offset+1))<<8)+(255&data.charCodeAt(offset))},this.ldbl64=function(data,offset){var b0=255&data.charCodeAt(offset),b1=255&data.charCodeAt(offset+1),b2=255&data.charCodeAt(offset+2),b3=255&data.charCodeAt(offset+3),b4=255&data.charCodeAt(offset+4),b5=255&data.charCodeAt(offset+5),b6=255&data.charCodeAt(offset+6),b7=255&data.charCodeAt(offset+7),sign=1-2*(b7>>7),exp=((127&b7)<<4)+((240&b6)>>4)-1023,frac=(15&b6)*Math.pow(2,48)+b5*Math.pow(2,40)+b4*Math.pow(2,32)+b3*Math.pow(2,24)+b2*Math.pow(2,16)+b1*Math.pow(2,8)+b0;return sign*(1+frac*Math.pow(2,-52))*Math.pow(2,exp)},this.lfloat32=function(data,offset){var b0=255&data.charCodeAt(offset),b1=255&data.charCodeAt(offset+1),b2=255&data.charCodeAt(offset+2),b3=255&data.charCodeAt(offset+3),sign=1-2*(b3>>7),exp=((127&b3)<<1)+((254&b2)>>7)-127,frac=(127&b2)*Math.pow(2,16)+b1*Math.pow(2,8)+b0;return sign*(1+frac*Math.pow(2,-23))*Math.pow(2,exp)},this.str=function(data,offset,length){for(var chars=[],index=offset;offset+length>index;){var c=data[index];if(0===c.charCodeAt(0))break;chars.push(c),index+=1}return chars.join("")},this.readHeader=function(data){var code=this.bint32(data,0),length=this.bint32(data,24),version=this.lint32(data,28),shapetype=this.lint32(data,32);return{code:code,length:length,version:version,shapetype:shapetype}},this.loadShx=function(data){for(var indices=[],appendIndex=function(offset){return indices.push(2*m_that.bint32(data,offset)),offset+8},offset=100;offsetheader_offset;)headers.push(readHeader(header_offset)),header_offset+=HEADER_LENGTH;for(var records=[],record_offset=header_size;header_size+num_entries*record_size>record_offset;){var declare=m_that.str(data,record_offset,1);if("*"===declare)record_offset+=record_size;else{record_offset+=1;for(var record={},i=0;i=start;i-=1){var x=m_that.ldbl64(data,offset+16*i),y=m_that.ldbl64(data,offset+16*i+8);ring.push([x,y])}return ring},readRecord=function(offset){var num_parts,num_points,parts_start,points_start,i,start,end,ring,rings,record_offset=offset+8,geom_type=m_that.lint32(data,record_offset);if(geom_type===SHP_NULL)console.log("NULL Shape");else if(geom_type===SHP_POINT){var x=m_that.ldbl64(data,record_offset+4),y=m_that.ldbl64(data,record_offset+12);features.push({type:"Point",attr:{},geom:[[x,y]]})}else if(geom_type===SHP_POLYGON){for(num_parts=m_that.lint32(data,record_offset+36),num_points=m_that.lint32(data,record_offset+40),parts_start=offset+52,points_start=offset+52+4*num_parts,rings=[],i=0;num_parts>i;i+=1)start=m_that.lint32(data,parts_start+4*i),end=num_parts>i+1?m_that.lint32(data,parts_start+4*(i+1)):num_points,ring=readRing(points_start,start,end),rings.push(ring);features.push({type:"Polygon",attr:{},geom:[rings]})}else{if(geom_type!==SHP_POLYLINE)throw"Not Implemented: "+geom_type;for(num_parts=m_that.lint32(data,record_offset+36),num_points=m_that.lint32(data,record_offset+40),parts_start=offset+52,points_start=offset+52+4*num_parts,rings=[],i=0;num_parts>i;i+=1)start=m_that.lint32(data,parts_start+4*i),end=num_parts>i+1?m_that.lint32(data,parts_start+4*(i+1)):num_points,ring=readRing(points_start,start,end),rings.push(ring);features.push({type:"Polyline",attr:{},geom:[rings]})}},attr=this.loadDBF(dbf_data);for(i=0;i=m_base64Str.length)return END_OF_INPUT;if(nextCharacter=m_base64Str.charAt(m_base64Count),m_base64Count+=1,m_reverseBase64Chars[nextCharacter])return m_reverseBase64Chars[nextCharacter];if("A"===nextCharacter)return 0}return END_OF_INPUT},this.decode64=function(str){var result="",inBuffer=new Array(4),done=!1;for(m_base64Str=str,m_base64Count=0;!done&&(inBuffer[0]=this.readReverseBase64())!==END_OF_INPUT&&(inBuffer[1]=this.readReverseBase64())!==END_OF_INPUT;)inBuffer[2]=this.readReverseBase64(),inBuffer[3]=this.readReverseBase64(),result+=this.ntos(inBuffer[0]<<2&255|inBuffer[1]>>4),inBuffer[2]!==END_OF_INPUT?(result+=this.ntos(inBuffer[1]<<4&255|inBuffer[2]>>2),inBuffer[3]!==END_OF_INPUT?result+=this.ntos(inBuffer[2]<<6&255|inBuffer[3]):done=!0):done=!0;return result},this.readNumber=function(ss){var v=ss[m_pos++]+(ss[m_pos++]<<8)+(ss[m_pos++]<<16)+(ss[m_pos++]<<24);return v},this.readF3Array=function(numberOfPoints,ss){var i,size=4*numberOfPoints*3,test=new Int8Array(size),points=null;for(i=0;size>i;i+=1)test[i]=ss[m_pos],m_pos+=1;return points=new Float32Array(test.buffer)},this.readColorArray=function(numberOfPoints,ss,vglcolors){var i,idx=0,tmp=new Array(3*numberOfPoints);for(i=0;numberOfPoints>i;i+=1)tmp[idx++]=ss[m_pos++]/255,tmp[idx++]=ss[m_pos++]/255,tmp[idx++]=ss[m_pos++]/255,m_pos++;vglcolors.insert(tmp)},this.parseObject=function(vtkObject){var size,actor,colorMapData,shaderProg,opacityUniform,lookupTable,colorTable,windowSize,width,height,position,geom=new vgl.geometryData,mapper=vgl.mapper(),ss=[],type=null,data=null,matrix=null,material=null;for(data=atob(vtkObject.data),i=0;ii;i+=1)p[idx++]=points[3*i],p[idx++]=points[3*i+1],p[idx++]=points[3*i+2];for(vglpoints.insert(p),geom.addSource(vglpoints),vglcolors=new vgl.sourceDataC3fv,this.readColorArray(numberOfPoints,ss,vglcolors),geom.addSource(vglcolors),vgllines=new vgl.lines,geom.addPrimitive(vgllines),numberOfIndex=this.readNumber(ss),temp=new Int8Array(2*numberOfIndex),i=0;2*numberOfIndex>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;for(index=new Uint16Array(temp.buffer),vgllines.setIndices(index),vgllines.setPrimitiveType(vgl.GL.LINES),size=64,temp=new Int8Array(size),i=0;size>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;return m=new Float32Array(temp.buffer),mat4.copy(matrix,m),matrix},this.parseMeshData=function(geom,ss){var numberOfIndex,numberOfPoints,points,temp,index,size,m,i,tcoord,vglpoints=null,vglcolors=null,normals=null,matrix=mat4.create(),vgltriangles=null,pn=null,idx=0;for(numberOfPoints=this.readNumber(ss),pn=new Array(6*numberOfPoints),vglpoints=new vgl.sourceDataP3N3f,points=this.readF3Array(numberOfPoints,ss),normals=this.readF3Array(numberOfPoints,ss),i=0;numberOfPoints>i;i+=1)pn[idx++]=points[3*i],pn[idx++]=points[3*i+1],pn[idx++]=points[3*i+2],pn[idx++]=normals[3*i],pn[idx++]=normals[3*i+1],pn[idx++]=normals[3*i+2];for(vglpoints.insert(pn),geom.addSource(vglpoints),vglcolors=new vgl.sourceDataC3fv,this.readColorArray(numberOfPoints,ss,vglcolors),geom.addSource(vglcolors),temp=[],vgltriangles=new vgl.triangles,numberOfIndex=this.readNumber(ss),temp=new Int8Array(2*numberOfIndex),i=0;2*numberOfIndex>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;for(index=new Uint16Array(temp.buffer),vgltriangles.setIndices(index),geom.addPrimitive(vgltriangles),size=64,temp=new Int8Array(size),i=0;size>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;return m=new Float32Array(temp.buffer),mat4.copy(matrix,m),tcoord=null,matrix},this.parsePointData=function(geom,ss){var numberOfPoints,points,indices,temp,size,m,matrix=mat4.create(),vglpoints=null,vglcolors=null,vglVertexes=null,p=null,idx=0;for(numberOfPoints=this.readNumber(ss),p=new Array(3*numberOfPoints),vglpoints=new vgl.sourceDataP3fv,points=this.readF3Array(numberOfPoints,ss),indices=new Uint16Array(numberOfPoints),i=0;numberOfPoints>i;i+=1)indices[i]=i,p[idx++]=points[3*i],p[idx++]=points[3*i+1],p[idx++]=points[3*i+2];for(vglpoints.insert(p),geom.addSource(vglpoints),vglcolors=new vgl.sourceDataC3fv,this.readColorArray(numberOfPoints,ss,vglcolors),geom.addSource(vglcolors),vglVertexes=new vgl.points,vglVertexes.setIndices(indices),geom.addPrimitive(vglVertexes),size=64,temp=new Int8Array(size),i=0;size>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;return m=new Float32Array(temp.buffer),mat4.copy(matrix,m),matrix},this.parseColorMapData=function(geom,ss,numColors){var tmpArray,size,xrgb,i,c,obj={};for(obj.numOfColors=numColors,size=8,tmpArray=new Int8Array(size),i=0;size>i;i+=1)tmpArray[i]=ss[m_pos],m_pos+=1;for(obj.position=new Float32Array(tmpArray.buffer),size=8,tmpArray=new Int8Array(size),i=0;size>i;i+=1)tmpArray[i]=ss[m_pos],m_pos+=1;for(obj.size=new Float32Array(tmpArray.buffer),obj.colors=[],c=0;ci;i+=1)tmpArray[i]=ss[m_pos],m_pos+=1;xrgb=[new Float32Array(tmpArray.buffer)[0],ss[m_pos++],ss[m_pos++],ss[m_pos++]],obj.colors[c]=xrgb}for(obj.orientation=ss[m_pos++],obj.numOfLabels=ss[m_pos++],obj.title="";m_pos=0;layer-=1)renderer=this.getRenderer(layer),this.parseSceneMetadata(renderer,layer);return m_viewer},this.createViewer=function(node){var interactorStyle;return null===m_viewer&&(m_node=node,m_viewer=vgl.viewer(node),m_viewer.init(),m_viewer.renderWindow().removeRenderer(m_viewer.renderWindow().activeRenderer()),m_viewer.renderWindow().addRenderer(new vgl.depthPeelRenderer),m_vtkRenderedList[0]=m_viewer.renderWindow().activeRenderer(),m_viewer.renderWindow().resize(node.width,node.height),interactorStyle=vgl.pvwInteractorStyle(),m_viewer.setInteractorStyle(interactorStyle)),m_viewer},this.deleteViewer=function(){m_vtkRenderedList={},m_viewer=null},this.updateCanvas=function(node){return m_node=node,m_viewer.renderWindow().resize(node.width,node.height),m_viewer},this.numObjects=function(){return m_vtkObjectCount},this.getRenderer=function(layer){var renderer;return renderer=m_vtkRenderedList[layer],(null===renderer||"undefined"==typeof renderer)&&(renderer=new vgl.renderer,renderer.setResetScene(!1),renderer.setResetClippingRange(!1),m_viewer.renderWindow().addRenderer(renderer),0!==layer&&renderer.camera().setClearMask(vgl.GL.DepthBufferBit),m_vtkRenderedList[layer]=renderer),renderer},this.setVtkScene=function(scene){m_vtkScene=scene},this},vgl.DataBuffers=function(initialSize){"use strict";if(!(this instanceof vgl.DataBuffers))return new vgl.DataBuffers(initialSize);var size,data={};size=initialSize||0===initialSize?initialSize:256;var current=0,copyArray=function(dst,src,start,count){dst||console.log("ack"),start||(start=0),count||(count=src.length);for(var i=0;count>i;i+=1)dst[start+i]=src[i]},resize=function(min_expand){var new_size=size;for(min_expand>2*new_size&&(new_size=min_expand);min_expand>new_size;)new_size*=2;size=new_size;for(var name in data)if(data.hasOwnProperty(name)){var newArray=new Float32Array(new_size*data[name].len),oldArray=data[name].array;copyArray(newArray,oldArray),data[name].array=newArray,data[name].dirty=!0}};this.create=function(name,len){if(!len)throw"Length of buffer must be a positive integer";var array=new Float32Array(size*len);return data[name]={array:array,len:len,dirty:!1},data[name].array},this.alloc=function(num){current+num>=size&&resize(current+num);var start=current;return current+=num,start},this.get=function(name){return data[name].array},this.write=function(name,array,start,count){copyArray(data[name].array,array,start*data[name].len,count*data[name].len),data[name].dirty=!0},this.repeat=function(name,elem,start,count){for(var i=0;count>i;i+=1)copyArray(data[name].array,elem,(start+i)*data[name].len,data[name].len);data[name].dirty=!0},this.count=function(){return current},this.data=function(name){return data[name].array}},function(){"use strict";function setNumeric(){var i;for(i=0;in?!1:(outer.forEach(function(vert,i){var j=(n+i-1)%n,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;intersect&&(inside=!inside)}),(inner||[]).forEach(function(hole){inside=inside&&!geo.util.pointInPolygon(point,hole)}),inside)},isFunction:function(f){return"function"==typeof f},ensureFunction:function(f){return geo.util.isFunction(f)?f:function(){return f}},randomString:function(n){var s,i,r;for(n=n||8,s="",i=0;n>i;i+=1)r=Math.floor(Math.random()*chars.length),s+=chars.substring(r,r+1);return s},convertColor:function(color){return void 0!==color.r&&void 0!==color.g&&void 0!==color.b?color:("string"==typeof color&&(geo.util.cssColors.hasOwnProperty(color)?color=geo.util.cssColors[color]:"#"===color.charAt(0)&&(color=parseInt(color.slice(1),16))),isFinite(color)&&(color={r:((16711680&color)>>16)/255,g:((65280&color)>>8)/255,b:(255&color)/255}),color)},normalizeCoordinates:function(p){return p=p||{},Array.isArray(p)?{x:p[0],y:p[1],z:p[2]||0}:{x:setNumeric(p.x,p.longitude,p.lng,p.lon,0),y:setNumeric(p.y,p.latitude,p.lat,0),z:setNumeric(p.z,p.elevation,p.elev,p.height,0)}}},geo.util.cssColors={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074}}(),function(){"use strict";function vect(x,y){return new Vector2D(x,y)}var RangeNode=function(elem,start,end,current){this.data=elem[current],this.left=null,this.right=null,start!==current&&(this.left=new RangeNode(elem,start,current-1,parseInt((start+(current-1))/2,10))),end!==current&&(this.right=new RangeNode(elem,current+1,end,parseInt((end+(current+1))/2,10))),this.elem=elem,this.start=start,this.end=end,this.subtree=null,this.search=rangeNodeSearch},rangeNodeSearch=function(result,box){var m_this=this,xrange=function(b){return b.x_in(m_this.elem[m_this.start])&&b.x_in(m_this.elem[m_this.end])},yrange=function(b,start,end){return b.y_in(m_this.subtree[start])&&b.y_in(m_this.subtree[end])},subquery=function(result,box,start,end,current){if(yrange(box,start,end))for(var i=start;end>=i;i++)result.push(m_this.subtree[i]);else box.y_in(m_this.subtree[current])&&result.push(m_this.subtree[current]),box.y_left(m_this.subtree[current])?current!==end&&subquery(result,box,current+1,end,parseInt((end+(current+1))/2,10)):box.x_right(m_this.subtree[current])?current!==start&&subquery(result,box,start,current-1,parseInt((start+(current-1))/2,10)):(current!==end&&subquery(result,box,current+1,end,parseInt((end+(current+1))/2,10)), -current!==start&&subquery(result,box,start,current-1,parseInt((start+(current-1))/2,10)))};return xrange(box)?(this.subtree||(this.subtree=this.elem.slice(this.start,this.end+1),this.subtree.sort(function(a,b){return a.y-b.y})),void subquery(result,box,0,this.subtree.length-1,parseInt((this.subtree.length-1)/2,10))):(box.contains(this.data)&&result.push(this.data),void(box.x_left(this.data)?this.right&&this.right.search(result,box):box.x_right(this.data)?this.left&&this.left.search(result,box):(this.left&&this.left.search(result,box),this.right&&this.right.search(result,box))))},RangeTree=function(elem){elem.sort(function(a,b){return a.x-b.x}),elem.length>0?this.root=new RangeNode(elem,0,elem.length-1,parseInt((elem.length-1)/2,10)):this.root=null,this.search=function(_box){if(!this.root)return[];var box=_box.clone(),result=[];return this.root.search(result,box),result}},Box=function(v1,v2){this.min=v1.clone(),this.max=v2.clone(),this.contains=function(p){return v1.x<=p.x&&v2.x>=p.x&&v1.y<=p.y&&v2.y>=p.y},this.x_in=function(p){return v1.x<=p.x&&v2.x>=p.x},this.x_left=function(p){return v1.x>=p.x},this.x_right=function(p){return v2.x<=p.x},this.y_in=function(p){return v1.y<=p.y&&v2.y>=p.y},this.y_left=function(p){return v1.y>=p.y},this.y_right=function(p){return v2.y<=p.y},this.area=function(){return(this.max.x-this.min.x)*(this.max.y-this.min.y)},this.height=function(){return this.max.y-this.min.y},this.width=function(){return this.max.x-this.min.x},this.vertex=function(index){switch(index){case 0:return this.min.clone();case 1:return new vect(this.max.x,this.min.y);case 2:return this.max.clone();case 3:return new vect(this.min.x,this.max.y);default:throw"Index out of bounds: "+index}},this.intersects=function(box){for(var i=0;4>i;i++)for(var j=0;4>j;j++)if(vect.intersects(this.vertex(i),this.vertex((i+1)%4),box.vertex(j),box.vertex((j+1)%4)))return!0;return this.contains(box.min)&&this.contains(box.max)&&this.contains(new vect(box.min.x,box.max.y))&&this.contains(new vect(box.max.x,box.min.y))?!0:box.contains(this.min)&&box.contains(this.max)&&box.contains(new vect(this.min.x,this.max.y))&&box.contains(new vect(this.max.x,this.min.y))?!0:!1},this.union=function(b){this.min.x=Math.min(this.min.x,b.min.x),this.min.y=Math.min(this.min.y,b.min.y),this.max.x=Math.max(this.max.x,b.max.x),this.max.y=Math.max(this.max.y,b.max.y)},this.centroid=function(){return new vect((this.max.x+this.min.x)/2,(this.max.y+this.min.y)/2)},this.clone=function(){return new Box(v1,v2)}},Vector2D=function(x,y){this.x=x,this.y=y,this.add=function(v){return this.x+=v.x,this.y+=v.y,this},this.sub=function(v){return this.x-=v.x,this.y-=v.y,this},this.scale=function(s){return this.x*=s,this.y*=s,this},this.length=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},this.normalize=function(){var scale=this.length();return 0===scale?this:(this.x/=scale,this.y/=scale,this)},this.div=function(v){return this.x/=v.x,this.y/=v.y,this},this.floor=function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},this.zero=function(tol){return tol=tol||0,this.length()<=tol},this.dot=function(v){return this.x*v.x+this.y*v.y},this.cross=function(v){return this.x*v.y-this.y*v.x},this.rotate=function(omega){var cos=Math.cos(omega),sin=Math.sin(omega);return xp=cos*this.x-sin*this.y,yp=sin*this.x+cos*this.y,this.x=xp,this.y=yp,this},this.clone=function(){return new Vector2D(this.x,this.y)},this.array=function(){return[this.x,this.y]}};vect.scale=function(v,s){return v.clone().scale(s)},vect.add=function(v1,v2){return v1.clone().add(v2)},vect.sub=function(v1,v2){return v1.clone().sub(v2)},vect.dist=function(v1,v2){return v1.clone().sub(v2).length()},vect.dir=function(v1,v2){return v1.clone().sub(v2).normalize()},vect.dot=function(v1,v2){return v1.x*v2.x+v1.y*v2.y},vect.cross=function(v1,v2){return v1.x*v2.y-v1.y*v2.x},vect.left=function(a,b,c,tol){tol||(tol=0);var v1=vect.sub(b,a),v2=vect.sub(c,a);return vect.cross(v1,v2)>=-tol},vect.intersects=function(a,b,c,d,tol){return tol||(tol=0),vect.left(a,b,c,tol)!=vect.left(a,b,d,tol)&&vect.left(c,d,b,tol)!=vect.left(c,d,a,tol)},vect.intersect2dt=function(a,b,c,d){var denom=a.x*(d.y-c.y)+b.x*(c.y-d.y)+d.x*(b.y-a.y)+c.x*(a.y-b.y);if(0===denom)return 1/0;var num_t=(a.x*(d.y-c.y)+c.x*(a.y-d.y)+d.x*(c.y-a.y),-(a.x*(c.y-b.y)+b.x*(a.y-c.y)+c.x*(b.y-a.y))),t=num_t/denom;return t},vect.intersect2dpos=function(a,b,c,d){var denom=a.x*(d.y-c.y)+b.x*(c.y-d.y)+d.x*(b.y-a.y)+c.x*(a.y-b.y);if(0===denom)return 1/0;var num_s=a.x*(d.y-c.y)+c.x*(a.y-d.y)+d.x*(c.y-a.y),s=num_s/denom,dir=vect.sub(b,a);return dir.scale(s),vect.add(a,dir)},vect.rotate=function(v,omega){var cos=Math.cos(omega),sin=Math.sin(omega);xp=cos*v.x-sin*v.y,yp=sin*v.x+cos*v.y;var c=new vect(xp,yp);return c},vect.normalize=function(v){return v.clone().normalize()},geo.util.RangeTree=RangeTree,geo.util.Box=Box,geo.util.vect=vect}(),function(){"use strict";var L={};L.Util={stamp:function(obj){return obj._leaflet_id=obj._leaflet_id||++L.Util.lastId,obj._leaflet_id},lastId:0},geo.util.DistanceGrid=function(cellSize){this._cellSize=cellSize,this._sqCellSize=cellSize*cellSize,this._grid={},this._objectPoint={}},geo.util.DistanceGrid.prototype={addObject:function(obj,point){var x=this._getCoord(point.x),y=this._getCoord(point.y),grid=this._grid,row=grid[y]=grid[y]||{},cell=row[x]=row[x]||[],stamp=L.Util.stamp(obj);point.obj=obj,this._objectPoint[stamp]=point,cell.push(obj)},updateObject:function(obj,point){this.removeObject(obj),this.addObject(obj,point)},removeObject:function(obj,point){var i,len,x=this._getCoord(point.x),y=this._getCoord(point.y),grid=this._grid,row=grid[y]=grid[y]||{},cell=row[x]=row[x]||[];for(delete this._objectPoint[L.Util.stamp(obj)],i=0,len=cell.length;len>i;i++)if(cell[i]===obj)return cell.splice(i,1),1===len&&delete row[x],!0},eachObject:function(fn,context){var i,j,k,len,row,cell,removed,grid=this._grid;for(i in grid){row=grid[i];for(j in row)for(cell=row[j],k=0,len=cell.length;len>k;k++)removed=fn.call(context,cell[k]),removed&&(k--,len--)}},getNearObject:function(point){var i,j,k,row,cell,len,obj,dist,x=this._getCoord(point.x),y=this._getCoord(point.y),objectPoint=this._objectPoint,closestDistSq=this._sqCellSize,closest=null;for(i=y-1;y+1>=i;i++)if(row=this._grid[i])for(j=x-1;x+1>=j;j++)if(cell=row[j])for(k=0,len=cell.length;len>k;k++)obj=cell[k],dist=this._sqDist(objectPoint[L.Util.stamp(obj)],point),closestDistSq>dist&&(closestDistSq=dist,closest=obj);return closest},contents:function(){return $.map(this._objectPoint,function(val){return val})},_getCoord:function(x){return Math.floor(x/this._cellSize)},_sqDist:function(p,p2){var dx=p2.x-p.x,dy=p2.y-p.y;return dx*dx+dy*dy}}}(),function(){"use strict";function ClusterTree(group,zoom,children){this._group=group,this._zoom=zoom,this._points=[],this._clusters=[],this._count=0,this._parent=null,this._coord=null;var that=this;(children||[]).forEach(function(c){that._add(c)})}function C(opts,width,height){this._opts=$.extend({maxZoom:18,radius:.05},opts),this._opts.width=this._opts.width||width||256,this._opts.height=this._opts.height||height||256,this._clusters={},this._points={};var zoom,scl;for(zoom=this._opts.maxZoom;zoom>=0;zoom-=1)scl=this._scaleAtLevel(zoom,this._opts.width,this._opts.height),this._clusters[zoom]=new geo.util.DistanceGrid(scl),this._points[zoom]=new geo.util.DistanceGrid(scl);this._topClusterLevel=new ClusterTree(this,-1)}ClusterTree.prototype._add=function(pt){var inc=1;pt instanceof ClusterTree?(this._clusters.push(pt),inc=pt._count):this._points.push(pt),pt._parent=this,this._increment(inc)},ClusterTree.prototype._increment=function(inc){this._coord=null,this._count+=inc,this._parent&&this._parent._increment(inc)},ClusterTree.prototype.count=function(){return this._count},ClusterTree.prototype.each=function(func){var i;for(i=0;i=0;zoom-=1){if(closest=this._clusters[zoom].getNearObject(point))return void closest._add(point);if(closest=this._points[zoom].getNearObject(point)){if(parent=closest._parent)for(z=parent._points.length-1;z>=0;z-=1)if(parent._points[z]===closest){parent._points.splice(z,1),parent._increment(-1);break}for(parent||$.noop(),newCluster=new ClusterTree(this,zoom,[closest,point]),this._clusters[zoom].addObject(newCluster,newCluster.coords()),lastParent=newCluster,z=zoom-1;z>parent._zoom;z-=1)lastParent=new ClusterTree(this,z,[lastParent]),this._clusters[z].addObject(lastParent,lastParent.coords());for(parent._add(lastParent),z=zoom;z>=0&&this._points[z].removeObject(closest,closest);z-=1);return}this._points[zoom].addObject(point,point)}this._topClusterLevel._add(point)},C.prototype.points=function(zoom){return zoom=Math.min(Math.max(Math.floor(zoom),0),this._opts.maxZoom-1),this._points[Math.floor(zoom)].contents()},C.prototype.clusters=function(zoom){return zoom=Math.min(Math.max(Math.floor(zoom),0),this._opts.maxZoom-1),this._clusters[Math.floor(zoom)].contents()},geo.util.ClusterGroup=C}(),geo.object=function(){"use strict";if(!(this instanceof geo.object))return new geo.object;var m_this=this,m_eventHandlers={},m_idleHandlers=[],m_deferredCount=0;return this.onIdle=function(handler){return m_deferredCount?m_idleHandlers.push(handler):handler(),m_this},this.addDeferred=function(defer){return m_deferredCount+=1,defer.done(function(){m_deferredCount-=1,m_deferredCount||m_idleHandlers.splice(0,m_idleHandlers.length).forEach(function(handler){handler()})}),m_this},this.geoOn=function(event,handler){return Array.isArray(event)?(event.forEach(function(e){m_this.geoOn(e,handler)}),m_this):(m_eventHandlers.hasOwnProperty(event)||(m_eventHandlers[event]=[]),m_eventHandlers[event].push(handler),m_this)},this.geoTrigger=function(event,args){return Array.isArray(event)?(event.forEach(function(e){m_this.geoTrigger(e,args)}),m_this):(m_eventHandlers.hasOwnProperty(event)&&m_eventHandlers[event].forEach(function(handler){handler.call(m_this,args)}),m_this)},this.geoOff=function(event,arg){if(void 0===event&&(m_eventHandlers={},m_idleHandlers=[],m_deferredCount=0),Array.isArray(event))return event.forEach(function(e){m_this.geoOff(e,arg)}),m_this;if(arg){if(Array.isArray(arg))return arg.forEach(function(handler){m_this.geoOff(event,handler)}),m_this}else m_eventHandlers[event]=[];return m_eventHandlers.hasOwnProperty(event)&&(m_eventHandlers[event]=m_eventHandlers[event].filter(function(f){return f!==arg})),m_this},this._exit=function(){m_this.geoOff()},vgl.object.call(this),this},inherit(geo.object,vgl.object),geo.sceneObject=function(arg){"use strict";if(!(this instanceof geo.sceneObject))return new geo.sceneObject;geo.object.call(this,arg);var m_this=this,m_parent=null,m_children=[],s_exit=this._exit,s_trigger=this.geoTrigger,s_addDeferred=this.addDeferred,s_onIdle=this.onIdle;return this.addDeferred=function(defer){m_parent?m_parent.addDeferred(defer):s_addDeferred(defer)},this.onIdle=function(handler){m_parent?m_parent.onIdle(handler):s_onIdle(handler)},this.parent=function(arg){return void 0===arg?m_parent:(m_parent=arg,m_this)},this.addChild=function(child){return Array.isArray(child)?(child.forEach(m_this.addChild),m_this):(child.parent(m_this),m_children.push(child),m_this)},this.removeChild=function(child){return Array.isArray(child)?(child.forEach(m_this.removeChild),m_this):(m_children=m_children.filter(function(c){return c!==child}),m_this)},this.children=function(){return m_children.slice()},this.draw=function(arg){return m_this.children().forEach(function(child){child.draw(arg)}),m_this},this.geoTrigger=function(event,args,childrenOnly){var geoArgs;return args=args||{},geoArgs=args.geo||{},args.geo=geoArgs,geoArgs.stopPropagation?m_this:!childrenOnly&&m_parent&&geoArgs._triggeredBy!==m_parent?(geoArgs._triggeredBy=m_this,m_parent.geoTrigger(event,args),m_this):(s_trigger.call(m_this,event,args),geoArgs.stopPropagation?m_this:(m_children.forEach(function(child){geoArgs._triggeredBy=m_this,child.geoTrigger(event,args)}),m_this))},this._exit=function(){m_this.children=[],delete m_this.parent,s_exit()},this},inherit(geo.sceneObject,geo.object),geo.timestamp=function(){"use strict";return this instanceof geo.timestamp?void vgl.timestamp.call(this):new geo.timestamp},inherit(geo.timestamp,vgl.timestamp),geo.ellipsoid=function(x,y,z){"use strict";if(!(this instanceof geo.ellipsoid))return new geo.ellipsoid(x,y,z);if(x=vgl.defaultValue(x,0),y=vgl.defaultValue(y,0),z=vgl.defaultValue(z,0),0>x||0>y||0>z)return console.log("[error] Al radii components must be greater than zero");var m_this=this,m_radii=new vec3.fromValues(x,y,z),m_radiiSquared=new vec3.fromValues(x*x,y*y,z*z),m_minimumRadius=Math.min(x,y,z),m_maximumRadius=Math.max(x,y,z);return this.radii=function(){return m_radii},this.radiiSquared=function(){return m_radiiSquared},this.maximumRadius=function(){return m_maximumRadius},this.minimumRadius=function(){return m_minimumRadius},this.computeGeodeticSurfaceNormal=function(lat,lon){if("undefined"==typeof lat||"undefined"==typeof lon)throw"[error] Valid latitude and longitude is required";var cosLatitude=Math.cos(lat),result=vec3.create();return result[0]=cosLatitude*Math.cos(lon),result[1]=cosLatitude*Math.sin(lon),result[2]=Math.sin(lat),vec3.normalize(result,result),result},this.transformPoint=function(lat,lon,elev){lat*=Math.PI/180,lon*=Math.PI/180;var n=m_this.computeGeodeticSurfaceNormal(lat,lon),k=vec3.create(),gamma=Math.sqrt(vec3.dot(n,k)),result=vec3.create();return vec3.multiply(k,m_radiiSquared,n),vec3.scale(k,k,1/gamma),vec3.scale(n,n,elev),vec3.add(result,n,k),result},this.transformGeometry=function(geom){if(!geom)throw"[error] Failed to transform to cartesian. Invalid geometry.";var sourceData=geom.sourceData(vgl.vertexAttributeKeys.Position),sourceDataArray=sourceData.data(),noOfComponents=sourceData.attributeNumberOfComponents(vgl.vertexAttributeKeys.Position),stride=sourceData.attributeStride(vgl.vertexAttributeKeys.Position),offset=sourceData.attributeOffset(vgl.vertexAttributeKeys.Position),sizeOfDataType=sourceData.sizeOfAttributeDataType(vgl.vertexAttributeKeys.Position),index=null,count=sourceDataArray.length*(1/noOfComponents),gamma=null,n=null,j=0,k=vec3.create(),result=vec3.create();if(stride/=sizeOfDataType,offset/=sizeOfDataType,3!==noOfComponents)throw"[error] Requires positions with three components";for(j=0;count>j;j+=1)index=j*stride+offset,sourceDataArray[index]=sourceDataArray[index]*(Math.PI/180),sourceDataArray[index+1]=sourceDataArray[index+1]*(Math.PI/180),n=m_this.computeGeodeticSurfaceNormal(sourceDataArray[index+1],sourceDataArray[index]),vec3.multiply(k,m_radiiSquared,n),gamma=Math.sqrt(vec3.dot(n,k)),vec3.scale(k,k,1/gamma),vec3.scale(n,n,sourceDataArray[index+2]),vec3.add(result,n,k),sourceDataArray[index]=result[0],sourceDataArray[index+1]=result[1],sourceDataArray[index+2]=result[2]},m_this},geo.ellipsoid.WGS84=vgl.freezeObject(geo.ellipsoid(6378137,6378137,6356752.314245179)),geo.ellipsoid.UNIT_SPHERE=vgl.freezeObject(geo.ellipsoid(1,1,1)),geo.mercator={r_major:6378137},geo.mercator.r_minor=function(spherical){"use strict";var r_minor;return spherical=void 0!==spherical?spherical:!1,r_minor=spherical?6378137:6356752.314245179},geo.mercator.f=function(spherical){"use strict";return(geo.mercator.r_major-geo.mercator.r_minor(spherical))/geo.mercator.r_major},geo.mercator.long2tilex=function(lon,z){"use strict";var rad=(lon+180)/360,f=Math.floor(rad*Math.pow(2,z));return f},geo.mercator.lat2tiley=function(lat,z){"use strict";var rad=lat*Math.PI/180;return Math.floor((1-rad/Math.PI)/2*Math.pow(2,z))},geo.mercator.long2tilex2=function(lon,z){"use strict";var rad=(lon+180)/360,f=rad*Math.pow(2,z),ret=Math.floor(f),frac=f-ret;return[ret,frac]},geo.mercator.lat2tiley2=function(lat,z){"use strict";var rad=lat*Math.PI/180,f=(1-Math.log(Math.tan(rad)+1/Math.cos(rad))/Math.PI)/2*Math.pow(2,z),ret=Math.floor(f),frac=f-ret;return[ret,frac]},geo.mercator.tilex2long=function(x,z){"use strict";return x/Math.pow(2,z)*360-180},geo.mercator.tiley2lat=function(y,z){"use strict";var n=Math.PI-2*Math.PI*y/Math.pow(2,z);return 180/Math.PI*Math.atan(.5*(Math.exp(n)-Math.exp(-n)))},geo.mercator.y2lat=function(a){"use strict";return 180/Math.PI*(2*Math.atan(Math.exp(a*Math.PI/180))-Math.PI/2)},geo.mercator.lat2y=function(a){"use strict";return 180/Math.PI*Math.log(Math.tan(Math.PI/4+a*(Math.PI/180)/2))},geo.mercator.deg2rad=function(d){"use strict";var r=d*(Math.PI/180);return r},geo.mercator.rad2deg=function(r){"use strict";var d=r/(Math.PI/180);return d},geo.mercator.ll2m=function(lon,lat,spherical){"use strict";lat>89.5&&(lat=89.5),-89.5>lat&&(lat=-89.5);var x=this.r_major*this.deg2rad(lon),temp=this.r_minor(spherical)/this.r_major,es=1-temp*temp,eccent=Math.sqrt(es),phi=this.deg2rad(lat),sinphi=Math.sin(phi),con=eccent*sinphi,com=.5*eccent,con2=Math.pow((1-con)/(1+con),com),ts=Math.tan(.5*(.5*Math.PI-phi))/con2,y=-this.r_major*Math.log(ts),ret={x:x,y:y};return ret},geo.mercator.m2ll=function(x,y,spherical){"use strict";var lon=this.rad2deg(x/this.r_major),temp=this.r_minor(spherical)/this.r_major,e=Math.sqrt(1-temp*temp),lat=this.rad2deg(this.pjPhi2(Math.exp(-(y/this.r_major)),e)),ret={lon:lon,lat:lat};return ret},geo.mercator.pjPhi2=function(ts,e){"use strict";var con,dphi,N_ITER=15,HALFPI=Math.PI/2,TOL=1e-10,i=N_ITER,eccnth=.5*e,Phi=HALFPI-2*Math.atan(ts);do con=e*Math.sin(Phi),dphi=HALFPI-2*Math.atan(ts*Math.pow((1-con)/(1+con),eccnth))-Phi,Phi+=dphi,i-=1;while(Math.abs(dphi)>TOL&&i);return Phi},geo.layer=function(arg){"use strict";if(!(this instanceof geo.layer))return new geo.layer(arg);arg=arg||{},geo.sceneObject.call(this,arg);var m_this=this,s_exit=this._exit,m_style=void 0===arg.style?{opacity:.5,color:[.8,.8,.8],visible:!0,bin:100}:arg.style,m_id=void 0===arg.id?geo.layer.newLayerId():arg.id,m_name="",m_gcs="EPSG:4326",m_timeRange=null,m_source=arg.source||null,m_map=void 0===arg.map?null:arg.map,m_isReference=!1,m_x=0,m_y=0,m_width=0,m_height=0,m_node=null,m_canvas=null,m_renderer=null,m_initialized=!1,m_rendererName=void 0===arg.renderer?"vgl":arg.renderer,m_dataTime=geo.timestamp(),m_updateTime=geo.timestamp(),m_drawTime=geo.timestamp(),m_sticky=void 0===arg.sticky?!0:arg.sticky,m_active=void 0===arg.active?!0:arg.active,m_attribution=arg.attribution||null;return this.sticky=function(){return m_sticky},this.active=function(){return m_active},this.node=function(){return m_node},this.id=function(val){return void 0===val?m_id:(m_id=geo.newLayerId(),m_this.modified(),m_this)},this.name=function(val){return void 0===val?m_name:(m_name=val,m_this.modified(),m_this)},this.opacity=function(val){return void 0===val?m_style.opacity:(m_style.opacity=val,m_this.modified(),m_this)},this.visible=function(val){return void 0===val?m_style.visible:(m_style.visible=val,m_this.modified(),m_this)},this.bin=function(val){return void 0===val?m_style.bin:(m_style.bin=val,m_this.modified(),m_this)},this.gcs=function(val){return void 0===val?m_gcs:(m_gcs=val,m_this.modified(),m_this)},this.transform=function(val){return geo.transform.transformLayer(val,m_this,m_map.baseLayer()),m_this},this.timeRange=function(val){return void 0===val?m_timeRange:(m_timeRange=val,m_this.modified(),m_this)},this.source=function(val){return void 0===val?m_source:(m_source=val,m_this.modified(),m_this)},this.map=function(val){return void 0===val?m_map:(m_map=val,m_map.node().append(m_node),m_this.modified(),m_this)},this.renderer=function(){return m_renderer},this.canvas=function(){return m_canvas},this.viewport=function(){return[m_x,m_y,m_width,m_height]},this.dataTime=function(){return m_dataTime},this.updateTime=function(){return m_updateTime},this.drawTime=function(){return m_drawTime},this.query=function(){},this.referenceLayer=function(val){return void 0!==val?(m_isReference=val,m_this.modified(),m_this):m_isReference},this.initialized=function(val){return void 0!==val?(m_initialized=val,m_this):m_initialized},this.toLocal=function(input){return input},this.fromLocal=function(input){return input},this.attribution=function(arg){return void 0!==arg?(m_attribution=arg,m_this.map().updateAttribution(),m_this):m_attribution},this._init=function(){if(m_initialized)return m_this;m_node=$(document.createElement("div")),m_node.attr("id",m_name),m_node.css("position","absolute"),m_map&&m_map.node().append(m_node);var options=$.extend({},arg);return delete options.map,m_canvas?m_renderer=geo.createRenderer(m_rendererName,m_this,m_canvas,options):(m_renderer=geo.createRenderer(m_rendererName,m_this,void 0,options),m_canvas=m_renderer.canvas()),m_this.active()||m_node.css("pointerEvents","none"),m_initialized=!0,m_this},this._exit=function(){m_renderer._exit(),m_node.off(),m_node.remove(),m_node=null,arg={},m_canvas=null,m_renderer=null,s_exit()},this._update=function(){},this._resize=function(x,y,w,h){return m_x=x,m_y=y,m_width=w,m_height=h,m_node.width(w),m_node.height(h),m_this.modified(),m_this.geoTrigger(geo.event.resize,{x:x,y:y,width:m_width,height:m_height}),m_this},this.width=function(){return m_width},this.height=function(){return m_height},this},geo.layer.newLayerId=function(){"use strict";var currentId=1;return function(){var id=currentId;return currentId+=1,id}}(),geo.layer.create=function(map,spec){"use strict";if(spec=spec||{},spec.type="feature","feature"!==spec.type)return console.warn("Unsupported layer type"),null;if(spec.renderer=spec.renderer||"vgl","d3"!==spec.renderer&&"vgl"!==spec.renderer)return console.warn("Invalid renderer"),null;var layer=map.createLayer(spec.type,spec);return layer?(spec.features.forEach(function(f){f.data=f.data||spec.data,f.feature=geo.feature.create(layer,f)}),layer):(console.warn("Unable to create a layer"),null)},inherit(geo.layer,geo.sceneObject),geo.featureLayer=function(arg){"use strict";if(!(this instanceof geo.featureLayer))return new geo.featureLayer(arg);geo.layer.call(this,arg);var m_this=this,m_features=[],s_init=this._init,s_exit=this._exit,s_update=this._update,s_draw=this.draw;return this.createFeature=function(featureName,arg){var newFeature=geo.createFeature(featureName,m_this,m_this.renderer(),arg);return m_this.addChild(newFeature),m_features.push(newFeature),m_this.features(m_features),m_this.modified(),newFeature},this.deleteFeature=function(feature){var i;for(i=0;im_this.updateTime().getMTime())for(i=0;i=deltaT?30:deltaT;var sf=springForce(),speed=calcSpeed(v),vx=v.x/speed,vy=v.y/speed;return speed*=Math.exp(-m_options.momentum.drag*deltaT),calcSpeed(sf)*deltaT+speed0?(vx*=speed,vy*=speed):(vx=0,vy=0),{x:vx-sf.x*deltaT,y:vy-sf.y*deltaT})}function springForce(){var xplus,xminus,yplus,yminus;if(!m_options.spring.enabled)return{x:0,y:0};var ul=m_this.map().gcsToDisplay({x:-180,y:82}),lr=m_this.map().gcsToDisplay({x:180,y:-82}),c=m_options.spring.springConstant,width=m_this.map().node().width(),height=m_this.map().node().height();return xplus=c*Math.max(0,ul.x),xminus=c*Math.max(0,width-lr.x),yplus=c*Math.max(0,ul.y)/2,yminus=c*Math.max(0,height-lr.y)/2,{x:xplus-xminus,y:yplus-yminus}}if(!(this instanceof geo.mapInteractor))return new geo.mapInteractor(args);geo.object.call(this);var m_mouse,m_keyboard,m_state,$node,m_options=args||{},m_this=this,m_wheelQueue={x:0,y:0},m_throttleTime=10,m_wait=!1,m_disableThrottle=!0,m_selectionLayer=null,m_selectionPlane=null;return m_options=$.extend(!0,{panMoveButton:"left",panMoveModifiers:{},zoomMoveButton:"right",zoomMoveModifiers:{},panWheelEnabled:!1,panWheelModifiers:{},zoomWheelEnabled:!0,zoomWheelModifiers:{},wheelScaleX:1,wheelScaleY:1,zoomScale:1,selectionButton:"left",selectionModifiers:{shift:!0},momentum:{enabled:!0,maxSpeed:2.5,minSpeed:.01,drag:.01},spring:{enabled:!1,springConstant:5e-5}},m_options),m_mouse={page:{x:0,y:0},map:{x:0,y:0},buttons:{left:!1,right:!1,middle:!1},modifiers:{alt:!1,ctrl:!1,shift:!1,meta:!1},time:new Date,deltaTime:1,velocity:{x:0,y:0}},m_keyboard={},m_state={},this._connectEvents=function(){return m_options.map?(m_this._disconnectEvents(),$node=$(m_options.map.node()),$node.on("mousemove.geojs",m_this._handleMouseMove),$node.on("mousedown.geojs",m_this._handleMouseDown),$node.on("mouseup.geojs",m_this._handleMouseUp),$node.on("wheel.geojs",m_this._handleMouseWheel),("right"===m_options.panMoveButton||"right"===m_options.zoomMoveButton)&&$node.on("contextmenu.geojs",function(){return!1}),m_this):m_this},this._disconnectEvents=function(){return $node&&($node.off(".geojs"),$node=null),m_this},this.map=function(val){return void 0!==val?(m_options.map=val,m_this._connectEvents(),m_this):m_options.map},this.options=function(opts){return void 0===opts?$.extend({},m_options):($.extend(m_options,opts),m_this)},this._getMousePosition=function(evt){var dt,t,offset=$node.offset();t=(new Date).valueOf(),dt=t-m_mouse.time,m_mouse.time=t,m_mouse.deltaTime=dt,m_mouse.velocity={x:(evt.pageX-m_mouse.page.x)/dt,y:(evt.pageY-m_mouse.page.y)/dt},m_mouse.page={x:evt.pageX,y:evt.pageY},m_mouse.map={x:evt.pageX-offset.left,y:evt.pageY-offset.top};try{m_mouse.geo=m_this.map().displayToGcs(m_mouse.map)}catch(e){m_mouse.geo=null}},this._getMouseButton=function(evt){1===evt.which?m_mouse.buttons.left="mouseup"!==evt.type:3===evt.which?m_mouse.buttons.right="mouseup"!==evt.type:2===evt.which&&(m_mouse.buttons.middle="mouseup"!==evt.type)},this._getMouseModifiers=function(evt){m_mouse.modifiers.alt=evt.altKey,m_mouse.modifiers.ctrl=evt.ctrlKey,m_mouse.modifiers.meta=evt.metaKey,m_mouse.modifiers.shift=evt.shiftKey},this._getSelection=function(){var origin=m_state.origin,mouse=m_this.mouse(),map=m_this.map(),display={},gcs={};return display.upperLeft={x:Math.min(origin.map.x,mouse.map.x),y:Math.min(origin.map.y,mouse.map.y)},display.lowerRight={x:Math.max(origin.map.x,mouse.map.x),y:Math.max(origin.map.y,mouse.map.y)},display.upperRight={x:display.lowerRight.x,y:display.upperLeft.y},display.lowerLeft={x:display.upperLeft.x,y:display.lowerRight.y},gcs.upperLeft=map.displayToGcs(display.upperLeft),gcs.lowerRight=map.displayToGcs(display.lowerRight),gcs.upperRight=map.displayToGcs(display.upperRight),gcs.lowerLeft=map.displayToGcs(display.lowerLeft),m_selectionPlane.origin([display.lowerLeft.x,display.lowerLeft.y,0]),m_selectionPlane.upperLeft([display.upperLeft.x,display.upperLeft.y,0]),m_selectionPlane.lowerRight([display.lowerRight.x,display.lowerRight.y,0]),m_selectionPlane.draw(),{display:display,gcs:gcs,mouse:mouse,origin:$.extend({},m_state.origin)}},this.cancel=function(action){var out;return out=action?m_state.action===action:!!m_state.action,out&&(m_state={}),out},this._handleMouseDown=function(evt){var action=null;"momentum"===m_state.action&&(m_state={}),m_this._getMousePosition(evt),m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),eventMatch(m_options.panMoveButton,m_options.panMoveModifiers)?action="pan":eventMatch(m_options.zoomMoveButton,m_options.zoomMoveModifiers)?action="zoom":eventMatch(m_options.selectionButton,m_options.selectionModifiers)&&(action="select"),m_mouse.velocity={x:0,y:0},action&&(m_state={action:action,origin:$.extend(!0,{},m_mouse),delta:{x:0,y:0}},"select"===action&&(m_selectionLayer&&(m_selectionLayer.clear(),m_this.map().deleteLayer(m_selectionLayer),m_selectionLayer=null),m_selectionLayer=m_this.map().createLayer("feature",{renderer:"d3"}),m_selectionPlane=m_selectionLayer.createFeature("plane"),m_selectionPlane.style({screenCoordinates:!0,fillOpacity:function(){return.25}}),m_this.map().geoTrigger(geo.event.brushstart,m_this._getSelection())),$(document).on("mousemove.geojs",m_this._handleMouseMoveDocument),$(document).on("mouseup.geojs",m_this._handleMouseUpDocument))},this._handleMouseMove=function(evt){m_state.action||(m_this._getMousePosition(evt),m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),m_this.map().geoTrigger(geo.event.mousemove,m_this.mouse()))},this._handleMouseMoveDocument=function(evt){var dx,dy,selectionObj;return m_this._getMousePosition(evt),m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),m_state.action?void(doRespond()&&(dx=m_mouse.map.x-m_state.origin.map.x-m_state.delta.x,dy=m_mouse.map.y-m_state.origin.map.y-m_state.delta.y, -m_state.delta.x+=dx,m_state.delta.y+=dy,"pan"===m_state.action?m_this.map().pan({x:dx,y:dy}):"zoom"===m_state.action?m_this.map().zoom(m_this.map().zoom()-dy*m_options.zoomScale/120):"select"===m_state.action&&(selectionObj=m_this._getSelection(),m_this.map().geoTrigger(geo.event.brush,selectionObj)),evt.preventDefault())):void console.log("WARNING: Invalid state in mapInteractor.")},this._handleMouseUpDocument=function(evt){var selectionObj,oldAction;m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),$(document).off(".geojs"),m_mouse.buttons.right&&evt.preventDefault(),"select"===m_state.action&&(selectionObj=m_this._getSelection(),m_selectionLayer.clear(),m_this.map().deleteLayer(m_selectionLayer),m_selectionLayer=null,m_selectionPlane=null,m_this.map().geoTrigger(geo.event.brushend,selectionObj)),oldAction=m_state.action,m_state={},m_options.momentum.enabled&&"pan"===oldAction&&m_this.springBack(!0)},this._handleMouseUp=function(evt){m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),m_this.map().geoTrigger(geo.event.mouseclick,m_this.mouse())},this._handleMouseWheel=function(evt){var zoomFactor,direction;return evt.deltaFactor=1,1===evt.originalEvent.deltaMode?evt.deltaFactor=12:2===evt.originalEvent.deltaMode&&(evt.deltaFactor=$(window).height()),evt.deltaX=evt.originalEvent.deltaX||0,evt.deltaY=evt.originalEvent.deltaY||0,m_this._getMouseModifiers(evt),evt.deltaX=evt.deltaX*m_options.wheelScaleX*evt.deltaFactor/120,evt.deltaY=evt.deltaY*m_options.wheelScaleY*evt.deltaFactor/120,evt.preventDefault(),doRespond()?(evt.deltaX+=m_wheelQueue.x,evt.deltaY+=m_wheelQueue.y,m_wheelQueue={x:0,y:0},void(m_options.panWheelEnabled&&eventMatch("wheel",m_options.panWheelModifiers)?m_this.map().pan({x:evt.deltaX,y:-evt.deltaY}):m_options.zoomWheelEnabled&&eventMatch("wheel",m_options.zoomWheelModifiers)&&(zoomFactor=-evt.deltaY,direction=m_mouse.map,m_this.map().zoom(m_this.map().zoom()+zoomFactor,direction)))):(m_wheelQueue.x+=evt.deltaX,void(m_wheelQueue.y+=evt.deltaY))},this.springBack=function(initialVelocity){"momentum"!==m_state.action&&(initialVelocity||(m_mouse.velocity={x:0,y:0}),m_state.action="momentum",m_state.origin=m_this.mouse(),m_state.start=new Date,m_state.handler=function(){var v,s,last,dt;if(dt=Math.min(m_mouse.deltaTime,30),"momentum"===m_state.action&&m_this.map()&&!m_this.map().transition()){if(last=m_state.start.valueOf(),m_state.start=new Date,v=modifyVelocity(m_mouse.velocity,m_state.start-last),!v)return void(m_state={});s=calcSpeed(v),s>m_options.momentum.maxSpeed&&(s=m_options.momentum.maxSpeed/s,v.x=v.x*s,v.y=v.y*s),isFinite(v.x)&&isFinite(v.y)||(v.x=0,v.y=0),m_mouse.velocity.x=v.x,m_mouse.velocity.y=v.y,m_this.map().pan({x:m_mouse.velocity.x*dt,y:m_mouse.velocity.y*dt}),m_state.handler&&window.requestAnimationFrame(m_state.handler)}},m_state.handler&&window.requestAnimationFrame(m_state.handler))},this._handleDoubleClick=function(){},this.destroy=function(){m_this._disconnectEvents(),m_this.map(null)},this.mouse=function(){return $.extend(!0,{},m_mouse)},this.keyboard=function(){return $.extend(!0,{},m_keyboard)},this.state=function(){return $.extend(!0,{},m_state)},this.simulateEvent=function(type,options){var evt,page,offset,which;return m_this.map()?(page=options.page||{},options.map&&(offset=$node.offset(),page.x=options.map.x+offset.left,page.y=options.map.y+offset.top),"left"===options.button?which=1:"right"===options.button?which=3:"middle"===options.button&&(which=2),options.modifiers=options.modifiers||[],options.wheelDelta=options.wheelDelta||{},evt=$.Event(type,{pageX:page.x,pageY:page.y,which:which,altKey:options.modifiers.indexOf("alt")>=0,ctrlKey:options.modifiers.indexOf("ctrl")>=0,metaKey:options.modifiers.indexOf("meta")>=0,shiftKey:options.modifiers.indexOf("shift")>=0,originalEvent:{deltaX:options.wheelDelta.x,deltaY:options.wheelDelta.y,deltaMode:options.wheelMode}}),void $node.trigger(evt)):m_this},this._connectEvents(),this},inherit(geo.mapInteractor,geo.object),geo.clock=function(opts){"use strict";if(!(this instanceof geo.clock))return new geo.clock(opts);opts=opts||{},geo.object.call(this,opts);var m_this=this,m_now=new Date(0),m_start=null,m_end=null,m_step=null,m_rate=null,m_loop=Number.POSITIVE_INFINITY,m_currentLoop=0,m_state="stop",m_currentAnimation=null,m_object=null;this.object=function(arg){return void 0===arg?m_object:(m_object=arg,m_this)},this._attached=function(){return m_object instanceof geo.object},this.now=function(arg){var previous=m_now;return void 0===arg?m_now:(m_now=arg,m_now!==previous&&m_this._attached()&&m_this.object().geoTrigger(geo.event.clock.change,{previous:previous,current:m_now,clock:m_this}),m_this)},this.start=function(arg){return void 0===arg?m_start:(m_start=arg,m_this)},this.end=function(arg){return void 0===arg?m_end:(m_end=arg,m_this)},this.step=function(arg){return void 0===arg?m_step:(m_step=arg,m_this)},this.loop=function(arg){return void 0===arg?m_loop:(m_loop=arg,m_this)},this.state=function(arg,step){return void 0===arg?m_state:["stop","play","pause"].indexOf(arg)<0?(console.log("WARNING: Ignored invalid state: "+arg),m_this):("play"===arg&&"stop"===m_state&&(m_currentLoop=0,m_this.now(m_this.start())),"play"===arg&&"play"!==m_state&&(m_state=arg,m_this._animate(step||1)),m_state=arg,m_this)},this.framerate=function(arg){return void 0===arg?m_rate:(m_rate=arg,m_this)},this.stepForward=function(){return m_this.state("pause"),m_this._setNextFrame(1),m_this},this.stepBackward=function(){return m_this.state("pause"),m_this._setNextFrame(-1),m_this},this._setNextFrame=function(step){var next=new Date(m_this.now().valueOf()+step*m_this.step());return next>=m_this.end()||next<=m_this.start()?m_this.loop()<=m_currentLoop?void m_this.state("stop"):(m_currentLoop+=1,void(step>=0?m_this.now(m_this.start()):m_this.now(m_this.end()))):void m_this.now(next)},this._animate=function(step){function frame(){myAnimation===m_currentAnimation&&(m_this._setNextFrame(step),"play"===m_this.state()?m_this.framerate()?window.setTimeout(frame,1e3/m_this.framerate()):window.requestAnimationFrame(frame):m_this._attached()&&m_this.object().geoTrigger(geo.event.clock[m_this.state()],{current:m_this.now(),clock:m_this}))}var myAnimation={};m_currentAnimation=myAnimation,m_this._attached()&&m_this.object().geoTrigger(geo.event.clock.play,{current:m_this.now(),clock:m_this}),m_this.framerate()?window.setTimeout(frame,1e3/m_this.framerate()):window.requestAnimationFrame(frame)}},inherit(geo.clock,geo.object),geo.fileReader=function(arg){"use strict";function newFileReader(done,progress){var reader=new FileReader;return progress&&(reader.onprogress=progress),reader.onloadend=function(){reader.result||done(reader.error),done(reader.result)},reader}if(!(this instanceof geo.fileReader))return new geo.fileReader(arg);if(geo.object.call(this),arg=arg||{},!(arg.layer instanceof geo.featureLayer))throw"fileReader must be given a feature layer";var m_layer=arg.layer;return this.layer=function(){return m_layer},this.canRead=function(){return!1},this.read=function(file,done){done(!1)},this._getString=function(file,done,progress){var reader=newFileReader(done,progress);reader.readAsText(file)},this._getArrayBuffer=function(file,done,progress){var reader=newFileReader(done,progress);reader.readAsText(file)},this},inherit(geo.fileReader,geo.object),geo.jsonReader=function(arg){"use strict";if(!(this instanceof geo.jsonReader))return new geo.jsonReader(arg);var m_this=this,m_style=arg.style||{};m_style=$.extend({strokeWidth:2,strokeColor:{r:0,g:0,b:0},strokeOpacity:1,fillColor:{r:1,g:0,b:0},fillOpacity:1},m_style),geo.fileReader.call(this,arg),this.canRead=function(file){if(file instanceof File)return"application/json"===file.type||file.name.match(/\.json$/);if("string"==typeof file){try{JSON.parse(file)}catch(e){return!1}return!0}try{if(Array.isArray(m_this._featureArray(file)))return!0}catch(e){}return!1},this._readObject=function(file,done,progress){function onDone(fileString){"string"!=typeof fileString&&done(!1);try{object=JSON.parse(fileString),done(object)}catch(e){object||$.ajax({type:"GET",url:fileString,dataType:"text"}).done(function(data){object=JSON.parse(data),done(object)}).fail(function(){done(!1)})}}var object;file instanceof File?m_this._getString(file,onDone,progress):"string"==typeof file?onDone(file):done(file)},this._featureArray=function(spec){if("FeatureCollection"===spec.type)return spec.features||[];if("GeometryCollection"===spec.type)throw"GeometryCollection not yet implemented.";if(Array.isArray(spec.coordinates))return spec;throw"Unsupported collection type: "+spec.type},this._featureType=function(spec){var geometry=spec.geometry||{};return"Point"===geometry.type||"MultiPoint"===geometry.type?"point":"LineString"===geometry.type?"line":"Polygon"===geometry.type?"polygon":"MultiPolygon"===geometry.type?"multipolygon":null},this._getCoordinates=function(spec){var elv,geometry=spec.geometry||{},coordinates=geometry.coordinates||[];return(2===coordinates.length||3===coordinates.length)&&isFinite(coordinates[0])&&isFinite(coordinates[1])?(isFinite(coordinates[2])&&(elv=coordinates[2]),[{x:coordinates[0],y:coordinates[1],z:elv}]):(Array.isArray(coordinates[0][0])&&(coordinates=coordinates[0]),coordinates.map(function(c){return{x:c[0],y:c[1],z:c[2]}}))},this._getStyle=function(spec){return spec.properties},this.read=function(file,done,progress){function _done(object){var features,allFeatures=[];features=m_this._featureArray(object),features.forEach(function(feature){var type=m_this._featureType(feature),coordinates=m_this._getCoordinates(feature),style=m_this._getStyle(feature);type?"line"===type?(style.fill=style.fill||!1,allFeatures.push(m_this._addFeature(type,[coordinates],style,feature.properties))):"point"===type?(style.stroke=style.stroke||!1,allFeatures.push(m_this._addFeature(type,coordinates,style,feature.properties))):"polygon"===type?(style.fill=void 0===style.fill?!0:style.fill,style.fillOpacity=void 0===style.fillOpacity?.25:style.fillOpacity,allFeatures.push(m_this._addFeature("line",[coordinates],style,feature.properties))):"multipolygon"===type&&(style.fill=void 0===style.fill?!0:style.fill,style.fillOpacity=void 0===style.fillOpacity?.25:style.fillOpacity,coordinates=feature.geometry.coordinates.map(function(c){return c[0].map(function(el){return{x:el[0],y:el[1],z:el[2]}})}),allFeatures.push(m_this._addFeature("line",coordinates,style,feature.properties))):console.log("unsupported feature type: "+feature.geometry.type)}),done&&done(allFeatures)}m_this._readObject(file,_done,progress)},this._buildData=function(coordinates,properties,style){return coordinates.map(function(coord){return{coordinates:coord,properties:properties,style:style}})},this._addFeature=function(type,coordinates,style,properties){var _style=$.extend({},m_style,style),feature=m_this.layer().createFeature(type).data(m_this._buildData(coordinates,properties,style)).style(_style);return"line"===type?feature.line(function(d){return d.coordinates}):feature.position(function(d){return d.coordinates}),feature}},inherit(geo.jsonReader,geo.fileReader),geo.registerFileReader("jsonReader",geo.jsonReader),geo.map=function(arg){"use strict";function resizeSelf(){m_this.resize(0,0,m_node.width(),m_node.height())}if(!(this instanceof geo.map))return new geo.map(arg);arg=arg||{},geo.sceneObject.call(this,arg),arg.layers=void 0===arg.layers?[]:arg.layers;var m_this=this,s_exit=this._exit,m_x=0,m_y=0,m_node=$(arg.node),m_width=arg.width||m_node.width(),m_height=arg.height||m_node.height(),m_gcs=void 0===arg.gcs?"EPSG:4326":arg.gcs,m_uigcs=void 0===arg.uigcs?"EPSG:4326":arg.uigcs,m_center={x:0,y:0},m_zoom=void 0===arg.zoom?4:arg.zoom,m_baseLayer=null,m_fileReader=null,m_interactor=null,m_validZoomRange={min:0,max:16},m_transition=null,m_queuedTransition=null,m_clock=null,m_parallelProjection=arg.parallelProjection?!0:!1,m_discreteZoom=arg.discreteZoom?!0:!1,m_bounds={};return arg.center=geo.util.normalizeCoordinates(arg.center),arg.autoResize=void 0===arg.autoResize?!0:arg.autoResize,arg.clampBounds=void 0===arg.clampBounds?!0:arg.clampBounds,this.gcs=function(arg){return void 0===arg?m_gcs:(m_gcs=arg,m_this)},this.uigcs=function(){return m_uigcs},this.node=function(){return m_node},this.zoom=function(val,direction,ignoreDiscreteZoom){var base,evt,recenter=!1;return void 0===val?m_zoom:(m_discreteZoom&&val!==Math.round(val)&&!ignoreDiscreteZoom&&(m_zoom!==Math.round(m_zoom)||Math.abs(val-m_zoom)<.01?val=Math.round(m_zoom):m_zoom>val?val=Math.min(Math.round(val),m_zoom-1):val>m_zoom&&(val=Math.max(Math.round(val),m_zoom+1))),val=Math.min(m_validZoomRange.max,Math.max(val,m_validZoomRange.min)),val===m_zoom?m_this:(base=m_this.baseLayer(),evt={geo:{},zoomLevel:val,screenPosition:direction,eventType:geo.event.zoom},base&&base.geoTrigger(geo.event.zoom,evt,!0),recenter=evt.center,evt.geo.preventDefault||(m_zoom=val,m_this._updateBounds(),m_this.children().forEach(function(child){child.geoTrigger(geo.event.zoom,evt,!0)}),m_this.modified()),evt.center?m_this.center(recenter):m_this.pan({x:0,y:0}),m_this))},this.pan=function(delta,force){var evt,pt,corner1,corner2,base=m_this.baseLayer();return arg.clampBounds&&!force&&m_width&&m_height&&(pt=m_this.displayToGcs({x:delta.x,y:delta.y}),corner1=m_this.gcsToDisplay({x:-180,y:82}),corner2=m_this.gcsToDisplay({x:180,y:-82}),corner1.x>0&&corner2.x0&&corner2.y0||input instanceof Object))throw"Conversion method latLonToDisplay does not handle "+input;return world=m_baseLayer.toLocal(input),output=m_baseLayer.renderer().worldToDisplay(world)},this.displayToGcs=function(input){var output;if(!(input instanceof Array&&input.length>0||input instanceof Object))throw"Conversion method displayToGcs does not handle "+input;return output=m_baseLayer.renderer().displayToWorld(input),output=m_baseLayer.fromLocal(output)},this.query=function(){},this.baseLayer=function(baseLayer){var save;return void 0!==baseLayer?(m_gcs!==baseLayer.gcs()&&m_this.gcs(baseLayer.gcs()),m_baseLayer=baseLayer,m_baseLayer.referenceLayer(!0),arg.center&&m_this.center(arg.center,!0),save=m_zoom,m_zoom=null,m_this.zoom(save),m_this._updateBounds(),m_this.pan({x:0,y:0}),m_this):m_baseLayer},this.draw=function(){var i,layers=m_this.children();for(m_this.geoTrigger(geo.event.draw,{type:geo.event.draw,target:m_this}),m_this._update(),i=0;i=m_transition.end.time||next)return next||(m_this.center(m_transition.end.center),m_this.zoom(m_transition.end.zoom)),m_transition=null,m_this.geoTrigger(geo.event.transitionend,defaultOpts),done&&done(),void(next&&(m_queuedTransition=null,m_this.transition(next)));var z=m_transition.ease((time-m_transition.start.time)/defaultOpts.duration),p=m_transition.interp(z);m_transition.zCoord&&(p[2]=z2zoom(p[2])),m_this.center({x:p[0],y:p[1]}),m_this.zoom(p[2],void 0,!0),window.requestAnimationFrame(anim)}if(void 0===opts)return m_transition;if(m_transition)return m_queuedTransition=opts,m_this;var defaultOpts={center:m_this.center(),zoom:m_this.zoom(),duration:1e3,ease:function(t){return t},interp:defaultInterp,done:null,zCoord:!0};return opts.center&&(opts.center=geo.util.normalizeCoordinates(opts.center)),$.extend(defaultOpts,opts),m_transition={start:{center:m_this.center(),zoom:m_this.zoom()},end:{center:defaultOpts.center,zoom:m_discreteZoom?Math.round(defaultOpts.zoom):defaultOpts.zoom},ease:defaultOpts.ease,zCoord:defaultOpts.zCoord,done:defaultOpts.done,duration:defaultOpts.duration},defaultOpts.zCoord?m_transition.interp=defaultOpts.interp([m_transition.start.center.x,m_transition.start.center.y,zoom2z(m_transition.start.zoom)],[m_transition.end.center.x,m_transition.end.center.y,zoom2z(m_transition.end.zoom)]):m_transition.interp=defaultOpts.interp([m_transition.start.center.x,m_transition.start.center.y,m_transition.start.zoom],[m_transition.end.center.x,m_transition.end.center.y,m_transition.end.zoom]),m_this.geoTrigger(geo.event.transitionstart,defaultOpts),defaultOpts.cancelNavigation?(m_this.geoTrigger(geo.event.transitionend,defaultOpts),m_this):(defaultOpts.cancelAnimation?(defaultOpts.duration=0,anim(0)):window.requestAnimationFrame(anim),m_this)},this._updateBounds=function(){m_bounds.lowerLeft=m_this.displayToGcs({x:0,y:m_height}),m_bounds.lowerRight=m_this.displayToGcs({x:m_width,y:m_height}),m_bounds.upperLeft=m_this.displayToGcs({x:0,y:0}),m_bounds.upperRight=m_this.displayToGcs({x:m_width,y:0})},this.bounds=function(bds){var nav;return void 0===bds?m_bounds:(nav=m_this.zoomAndCenterFromBounds(bds),m_this.zoom(nav.zoom),m_this.center(nav.center),m_bounds)},this.zoomAndCenterFromBounds=function(bds){var ll,ur,dx,dy,zx,zy,center,zoom;if(ll=geo.util.normalizeCoordinates(bds.lowerLeft||{}),ur=geo.util.normalizeCoordinates(bds.upperRight||{}),ll.x>=ur.x||ll.y>=ur.y)throw new Error("Invalid bounds provided");return center={x:(ll.x+ur.x)/2,y:(ll.y+ur.y)/2},dx=m_bounds.upperRight.x-m_bounds.lowerLeft.x,dy=m_bounds.upperRight.y-m_bounds.lowerLeft.y,zx=m_zoom-Math.log2((ur.x-ll.x)/dx),zy=m_zoom-Math.log2((ur.y-ll.y)/dy),zoom=Math.min(zx,zy),m_discreteZoom&&(zoom=Math.floor(zoom)),{zoom:zoom,center:center}},this.discreteZoom=function(discreteZoom){return void 0===discreteZoom?m_discreteZoom:(discreteZoom=discreteZoom?!0:!1,m_discreteZoom!==discreteZoom&&(m_discreteZoom=discreteZoom,m_discreteZoom&&m_this.zoom(Math.round(m_this.zoom()))),m_this)},this.updateAttribution=function(){m_this.node().find(".geo-attribution").remove();var $a=$("
").addClass("geo-attribution").css({position:"absolute",right:"0px",bottom:"0px","padding-right":"5px",cursor:"auto",font:'11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif',"z-index":"1001",background:"rgba(255,255,255,0.7)",clear:"both",display:"block","pointer-events":"auto"}).on("mousedown",function(evt){evt.stopPropagation()});return m_this.children().forEach(function(layer){var content=layer.attribution();content&&$("").addClass("geo-attribution-layer").css({"padding-left":"5px"}).html(content).appendTo($a)}),$a.appendTo(m_this.node()),m_this},this.interactor(arg.interactor||geo.mapInteractor()),this.clock(arg.clock||geo.clock()),arg.autoResize&&$(window).resize(resizeSelf),m_this.geoOn([geo.event.layerAdd,geo.event.layerRemove],m_this.updateAttribution),this},geo.map.create=function(spec){"use strict";var map=geo.map(spec);return map?(spec.data=spec.data||[],spec.layers=spec.layers||[],spec.layers.forEach(function(l){l.data=l.data||spec.data,l.layer=geo.layer.create(map,l)}),map):(console.warn("Could not create map."),null)},inherit(geo.map,geo.sceneObject),geo.feature=function(arg){"use strict";if(!(this instanceof geo.feature))return new geo.feature(arg);geo.sceneObject.call(this),arg=arg||{};var m_this=this,s_exit=this._exit,m_selectionAPI=void 0===arg.selectionAPI?!1:arg.selectionAPI,m_style={},m_layer=void 0===arg.layer?null:arg.layer,m_gcs=void 0===arg.gcs?"EPSG:4326":arg.gcs,m_visible=void 0===arg.visible?!0:arg.visible,m_bin=void 0===arg.bin?0:arg.bin,m_renderer=void 0===arg.renderer?null:arg.renderer,m_dataTime=geo.timestamp(),m_buildTime=geo.timestamp(),m_updateTime=geo.timestamp(),m_selectedFeatures=[];return this._bindMouseHandlers=function(){m_selectionAPI&&(m_this._unbindMouseHandlers(),m_this.geoOn(geo.event.mousemove,m_this._handleMousemove),m_this.geoOn(geo.event.mouseclick,m_this._handleMouseclick),m_this.geoOn(geo.event.brushend,m_this._handleBrushend),m_this.geoOn(geo.event.brush,m_this._handleBrush))},this._unbindMouseHandlers=function(){m_this.geoOff(geo.event.mousemove,m_this._handleMousemove),m_this.geoOff(geo.event.mouseclick,m_this._handleMouseclick),m_this.geoOff(geo.event.brushend,m_this._handleBrushend),m_this.geoOff(geo.event.brush,m_this._handleBrush)},this.pointSearch=function(){return{index:[],found:[]}},this._handleMousemove=function(){var mouse=m_this.layer().map().interactor().mouse(),data=m_this.data(),over=m_this.pointSearch(mouse.geo),newFeatures=[],oldFeatures=[],lastTop=-1,top=-1;m_selectedFeatures.length&&(lastTop=m_selectedFeatures[m_selectedFeatures.length-1]),newFeatures=over.index.filter(function(i){return m_selectedFeatures.indexOf(i)<0}),oldFeatures=m_selectedFeatures.filter(function(i){return over.index.indexOf(i)<0}),geo.feature.eventID+=1,newFeatures.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mouseover,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===newFeatures.length-1},!0)}),geo.feature.eventID+=1,oldFeatures.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mouseout,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===oldFeatures.length-1},!0)}),geo.feature.eventID+=1,over.index.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mousemove,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===over.index.length-1},!0)}),m_selectedFeatures=over.index,m_selectedFeatures.length&&(top=m_selectedFeatures[m_selectedFeatures.length-1]),lastTop!==top&&(-1!==lastTop&&m_this.geoTrigger(geo.event.feature.mouseoff,{data:data[lastTop],index:lastTop,mouse:mouse},!0),-1!==top&&m_this.geoTrigger(geo.event.feature.mouseon,{data:data[top],index:top,mouse:mouse},!0))},this._handleMouseclick=function(){var mouse=m_this.layer().map().interactor().mouse(),data=m_this.data(),over=m_this.pointSearch(mouse.geo);geo.feature.eventID+=1,over.index.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mouseclick,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===over.index.length-1},!0)})},this._handleBrush=function(brush){var idx=m_this.boxSearch(brush.gcs.lowerLeft,brush.gcs.upperRight),data=m_this.data();geo.feature.eventID+=1,idx.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.brush,{data:data[i],index:i,mouse:brush.mouse,brush:brush,eventID:geo.feature.eventID,top:idx===idx.length-1},!0)})},this._handleBrushend=function(brush){var idx=m_this.boxSearch(brush.gcs.lowerLeft,brush.gcs.upperRight),data=m_this.data();geo.feature.eventID+=1,idx.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.brushend,{data:data[i],index:i,mouse:brush.mouse,brush:brush,eventID:geo.feature.eventID,top:idx===idx.length-1},!0)})},this.style=function(arg1,arg2){return void 0===arg1?m_style:"string"==typeof arg1&&void 0===arg2?m_style[arg1]:void 0===arg2?(m_style=$.extend({},m_style,arg1),m_this.modified(),m_this):(m_style[arg1]=arg2,m_this.modified(),m_this)},this.style.get=function(key){var tmp,out;if(void 0===key){var k,all={};for(k in m_style)m_style.hasOwnProperty(k)&&(all[k]=m_this.style.get(k));return all}return key.toLowerCase().match(/color$/)?geo.util.isFunction(m_style[key])?(tmp=geo.util.ensureFunction(m_style[key]),out=function(){return geo.util.convertColor(tmp.apply(this,arguments))}):out=geo.util.ensureFunction(geo.util.convertColor(m_style[key])):out=geo.util.ensureFunction(m_style[key]),out},this.layer=function(){return m_layer},this.renderer=function(){return m_renderer},this.drawables=function(){return m_this._drawables()},this.gcs=function(val){return void 0===val?m_gcs:(m_gcs=val,m_this.modified(),m_this)},this.visible=function(val){return void 0===val?m_visible:(m_visible=val,m_this.modified(),m_visible?m_this._bindMouseHandlers():m_this._unbindMouseHandlers(),m_this)},this.bin=function(val){return void 0===val?m_bin:(m_bin=val,m_this.modified(),m_this)},this.dataTime=function(val){return void 0===val?m_dataTime:(m_dataTime=val,m_this.modified(),m_this)},this.buildTime=function(val){return void 0===val?m_buildTime:(m_buildTime=val,m_this.modified(),m_this)},this.updateTime=function(val){return void 0===val?m_updateTime:(m_updateTime=val,m_this.modified(),m_this)},this.data=function(data){return void 0===data?m_this.style("data")||[]:(m_this.style("data",data),m_this.dataTime().modified(),m_this.modified(),m_this)},this.selectionAPI=function(){return m_selectionAPI},this._init=function(arg){if(!m_layer)throw"Feature requires a valid layer";m_style=$.extend({},{opacity:1},void 0===arg.style?{}:arg.style),m_this._bindMouseHandlers()},this._build=function(){},this._drawables=function(){},this._update=function(){},this._exit=function(){m_this._unbindMouseHandlers(),m_selectedFeatures=[],m_style={},arg={},s_exit()},this._init(arg),this},geo.feature.eventID=0,geo.feature.create=function(layer,spec){"use strict";var type=spec.type;if(!layer instanceof geo.layer)return console.warn("Invalid layer"),null;if("object"!=typeof spec)return console.warn("Invalid spec"),null;var feature=layer.createFeature(type);return feature?(spec=spec||{},spec.data=spec.data||[],feature.style(spec)):(console.warn("Could not create feature type '"+type+"'"),null)},inherit(geo.feature,geo.sceneObject),geo.pointFeature=function(arg){"use strict";if(!(this instanceof geo.pointFeature))return new geo.pointFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_init=this._init,m_rangeTree=null,m_rangeTreeTime=geo.timestamp(),s_data=this.data,m_maxRadius=0,m_clustering=arg.clustering,m_clusterTree=null,m_allData=[],m_lastZoom=null,m_ignoreData=!1;return this.clustering=function(val){return void 0===val?m_clustering:(m_clustering&&!val?(m_clusterTree=null,m_clustering=!1,s_data(m_allData),m_allData=null):!m_clustering&&val&&(m_clustering=!0,m_this._clusterData()),m_this)},this._clusterData=function(){if(m_clustering){var opts=m_clustering===!0?{radius:.01}:m_clustering,position=m_this.position();m_clusterTree=new geo.util.ClusterGroup(opts,this.layer().width(),this.layer().height()),m_allData.forEach(function(d,i){var pt=geo.util.normalizeCoordinates(position(d,i));pt.index=i,m_clusterTree.addPoint(pt)}),m_lastZoom=null,m_this._handleZoom(m_this.layer().map().zoom())}},this._handleZoom=function(zoom){var z=Math.floor(zoom);if(m_clustering&&z!==m_lastZoom){m_lastZoom=z;var data=m_clusterTree.points(z).map(function(d){return m_allData[d.index]});m_clusterTree.clusters(z).forEach(function(d){d.__cluster=!0,d.__data=[],d.obj.each(function(e){d.__data.push(m_allData[e.index])}),data.push(d)}),m_ignoreData=!0,m_this.data(data),m_this.layer().map().draw()}},this.position=function(val){return void 0===val?m_this.style("position"):(val=geo.util.ensureFunction(val),m_this.style("position",function(d,i){return d.__cluster?d:val(d,i)}),m_this.dataTime().modified(),m_this.modified(),m_this)},this._updateRangeTree=function(){if(!(m_rangeTreeTime.getMTime()>=m_this.dataTime().getMTime())){var pts,position,radius=m_this.style.get("radius"),stroke=m_this.style.get("stroke"),strokeWidth=m_this.style.get("strokeWidth");position=m_this.position(),m_maxRadius=0,pts=m_this.data().map(function(d,i){var pt=position(d);return pt.idx=i,m_maxRadius=Math.max(m_maxRadius,radius(d,i)+(stroke(d,i)?strokeWidth(d,i):0)),pt}),m_rangeTree=new geo.util.RangeTree(pts), -m_rangeTreeTime.modified()}},this.pointSearch=function(p){var min,max,data,box,map,pt,idx=[],found=[],ifound=[],stroke=m_this.style.get("stroke"),strokeWidth=m_this.style.get("strokeWidth"),radius=m_this.style.get("radius");return m_this.selectionAPI()?(data=m_this.data(),data&&data.length?(map=m_this.layer().map(),pt=map.gcsToDisplay(p),min=map.displayToGcs({x:pt.x-m_maxRadius,y:pt.y+m_maxRadius}),max=map.displayToGcs({x:pt.x+m_maxRadius,y:pt.y-m_maxRadius}),box=new geo.util.Box(geo.util.vect(min.x,min.y),geo.util.vect(max.x,max.y)),m_this._updateRangeTree(),m_rangeTree.search(box).forEach(function(q){idx.push(q.idx)}),idx.forEach(function(i){var dx,dy,rad,d=data[i],p=m_this.position()(d,i);rad=radius(data[i],i),rad+=stroke(data[i],i)?strokeWidth(data[i],i):0,p=map.gcsToDisplay(p),dx=p.x-pt.x,dy=p.y-pt.y,Math.sqrt(dx*dx+dy*dy)<=rad&&(found.push(d),ifound.push(i))}),{data:found,index:ifound}):{found:[],index:[]}):[]},this.boxSearch=function(lowerLeft,upperRight){var pos=m_this.position(),idx=[];return m_this.data().forEach(function(d,i){var p=pos(d);p.x>=lowerLeft.x&&p.x<=upperRight.x&&p.y>=lowerLeft.y&&p.y<=upperRight.y&&idx.push(i)}),idx},this.data=function(data){return void 0===data?s_data():(m_clustering&&!m_ignoreData?(m_allData=data,m_this._clusterData()):s_data(data),m_ignoreData=!1,m_this)},this._boundingBox=function(d){var pt,radius;return pt=m_this.position()(d),pt=m_this.layer().map().gcsToDisplay(pt),radius=m_this.style().radius(d),{min:{x:pt.x-radius,y:pt.y-radius},max:{x:pt.x+radius,y:pt.y+radius}}},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{radius:5,stroke:!0,strokeColor:{r:.851,g:.604,b:0},strokeWidth:1.25,strokeOpacity:1,fillColor:{r:1,g:.839,b:.439},fill:!0,fillOpacity:.8,sprites:!1,sprites_image:null,position:function(d){return d}},void 0===arg.style?{}:arg.style);void 0!==arg.position&&(defaultStyle.position=arg.position),m_this.style(defaultStyle),m_this.dataTime().modified(),m_this.geoOn(geo.event.zoom,function(evt){m_this._handleZoom(evt.zoomLevel)})},m_this},geo.event.pointFeature=$.extend({},geo.event.feature),geo.pointFeature.create=function(layer,renderer,spec){"use strict";return spec.type="point",geo.feature.create(layer,spec)},inherit(geo.pointFeature,geo.feature),geo.lineFeature=function(arg){"use strict";if(!(this instanceof geo.lineFeature))return new geo.lineFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_init=this._init;return this.line=function(val){return void 0===val?m_this.style("line"):(m_this.style("line",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this.position=function(val){return void 0===val?m_this.style("position"):(m_this.style("position",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this.pointSearch=function(p){function lineDist2(q,u,v){var t,l2=dist2(u,v);return 1>l2?dist2(q,u):(t=((q.x-u.x)*(v.x-u.x)+(q.y-u.y)*(v.y-u.y))/l2,0>t?dist2(q,u):t>1?dist2(q,v):dist2(q,{x:u.x+t*(v.x-u.x),y:u.y+t*(v.y-u.y)}))}function dist2(u,v){var dx=u.x-v.x,dy=u.y-v.y;return dx*dx+dy*dy}var data,pt,map,line,width,pos,indices=[],found=[];return data=m_this.data(),data&&data.length?(map=m_this.layer().map(),line=m_this.line(),width=m_this.style.get("strokeWidth"),pos=m_this.position(),pt=map.gcsToDisplay(p),data.forEach(function(d,index){var last=null;try{line(d,index).forEach(function(current,j){var p=pos(current,j,d,index),s=map.gcsToDisplay(p),r=Math.ceil(width(p,j,d,index)/2)+2;if(r*=r,last&&lineDist2(pt,s,last)<=r)throw"found";last=s})}catch(err){if("found"!==err)throw err;found.push(d),indices.push(index)}}),{data:found,index:indices}):{found:[],index:[]}},this.boxSearch=function(lowerLeft,upperRight,opts){var pos=m_this.position(),idx=[],line=m_this.line();if(opts=opts||{},opts.partial=opts.partial||!1,opts.partial)throw"Unimplemented query method.";return m_this.data().forEach(function(d,i){var inside=!0;line(d,i).forEach(function(e,j){if(inside){var p=pos(e,j,d,i);p.x>=lowerLeft.x&&p.x<=upperRight.x&&p.y>=lowerLeft.y&&p.y<=upperRight.y||(inside=!1)}}),inside&&idx.push(i)}),idx},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{strokeWidth:1,strokeColor:{r:1,g:.8431372549,b:0},strokeStyle:"solid",strokeOpacity:1,line:function(d){return d},position:function(d){return d}},void 0===arg.style?{}:arg.style);void 0!==arg.line&&(defaultStyle.line=arg.line),void 0!==arg.position&&(defaultStyle.position=arg.position),m_this.style(defaultStyle),m_this.dataTime().modified()},this._init(arg),this},geo.lineFeature.create=function(layer,spec){"use strict";return spec.type="line",geo.feature.create(layer,spec)},inherit(geo.lineFeature,geo.feature),geo.pathFeature=function(arg){"use strict";if(!(this instanceof geo.pathFeature))return new geo.pathFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,m_position=void 0===arg.position?[]:arg.position,s_init=this._init;return this.position=function(val){return void 0===val?m_position:(m_position=val,m_this.dataTime().modified(),m_this.modified(),m_this)},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{strokeWidth:function(){return 1},strokeColor:function(){return{r:1,g:1,b:1}}},void 0===arg.style?{}:arg.style);m_this.style(defaultStyle),m_position&&m_this.dataTime().modified()},this._init(arg),this},inherit(geo.pathFeature,geo.feature),geo.polygonFeature=function(arg){"use strict";function getCoordinates(){var posFunc=m_this.position(),polyFunc=m_this.polygon();m_coordinates=m_this.data().map(function(d,i){var outer,inner,poly=polyFunc(d);return outer=(poly.outer||[]).map(function(d0,j){return posFunc.call(m_this,d0,j,d,i)}),inner=(poly.inner||[]).map(function(hole){return(hole||[]).map(function(d0,k){return posFunc.call(m_this,d0,k,d,i)})}),{outer:outer,inner:inner}})}if(!(this instanceof geo.polygonFeature))return new geo.polygonFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_position,m_polygon,m_this=this,s_init=this._init,s_data=this.data,m_coordinates={outer:[],inner:[]};return m_polygon=void 0===arg.polygon?function(d){return d}:arg.polygon,m_position=void 0===arg.position?function(d){return d}:arg.position,this.data=function(arg){var ret=s_data(arg);return void 0!==arg&&getCoordinates(),ret},this.polygon=function(val){return void 0===val?m_polygon:(m_polygon=val,m_this.dataTime().modified(),m_this.modified(),getCoordinates(),m_this)},this.position=function(val){return void 0===val?m_position:(m_position=val,m_this.dataTime().modified(),m_this.modified(),getCoordinates(),m_this)},this.pointSearch=function(coordinate){var found=[],indices=[],data=m_this.data();return m_coordinates.forEach(function(coord,i){var inside=geo.util.pointInPolygon(coordinate,coord.outer,coord.inner);inside&&(indices.push(i),found.push(data[i]))}),{index:indices,found:found}},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{fillColor:{r:0,g:.5,b:.5},fillOpacity:1},void 0===arg.style?{}:arg.style);m_this.style(defaultStyle),m_position&&m_this.dataTime().modified()},this._init(arg),this},inherit(geo.polygonFeature,geo.feature),geo.planeFeature=function(arg){"use strict";if(!(this instanceof geo.planeFeature))return new geo.planeFeature(arg);arg=arg||{},arg.ul=void 0===arg.ul?[0,1,0]:arg.ul,arg.lr=void 0===arg.lr?[1,0,0]:arg.lr,arg.depth=void 0===arg.depth?0:arg.depth,geo.polygonFeature.call(this,arg);var m_this=this,m_origin=[arg.ul.x,arg.lr.y,arg.depth],m_upperLeft=[arg.ul.x,arg.ul.y,arg.depth],m_lowerRight=[arg.lr.x,arg.lr.y,arg.depth],m_defaultDepth=arg.depth,m_drawOnAsyncResourceLoad=void 0===arg.drawOnAsyncResourceLoad?!0:!1,s_init=this._init;return this.origin=function(val){if(void 0===val)return m_origin;if(val instanceof Array){if(val.length>3||val.length<2)throw"Origin point requires point in 2 or 3 dimension";m_origin=val.slice(0),2===m_origin.length&&(m_origin[2]=m_defaultDepth)}return m_this.dataTime().modified(),m_this.modified(),m_this},this.upperLeft=function(val){if(void 0===val)return m_upperLeft;if(val instanceof Array){if(val.length>3||val.length<2)throw"Upper left point requires point in 2 or 3 dimension";m_upperLeft=val.slice(0),2===m_upperLeft.length&&(m_upperLeft[2]=m_defaultDepth)}return m_this.dataTime().modified(),m_this.modified(),m_this},this.lowerRight=function(val){if(void 0===val)return m_lowerRight;if(val instanceof Array){if(val.length>3||val.length<2)throw"Lower right point requires point in 2 or 3 dimension";m_lowerRight=val.slice(0),2===m_lowerRight.length&&(m_lowerRight[2]=m_defaultDepth),m_this.dataTime().modified()}return m_this.dataTime().modified(),m_this.modified(),m_this},this.drawOnAsyncResourceLoad=function(val){return void 0===val?m_drawOnAsyncResourceLoad:(m_drawOnAsyncResourceLoad=val,m_this)},this._init=function(arg){var style=null;s_init.call(m_this,arg),style=m_this.style(),void 0===style.image&&(style.image=null),m_this.style(style)},this._init(arg),this},inherit(geo.planeFeature,geo.polygonFeature),geo.vectorFeature=function(arg){"use strict";if(!(this instanceof geo.vectorFeature))return new geo.vectorFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_init=this._init,s_style=this.style;this.origin=function(val){return void 0===val?s_style("origin"):(s_style("origin",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this.delta=function(val){return void 0===val?s_style("delta"):(s_style("delta",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{strokeColor:"black",strokeWidth:2,strokeOpacity:1,origin:{x:0,y:0,z:0},delta:function(d){return d},scale:null},void 0===arg.style?{}:arg.style);void 0!==arg.origin&&(defaultStyle.origin=arg.origin),m_this.style(defaultStyle),m_this.dataTime().modified()}},inherit(geo.vectorFeature,geo.feature),geo.geomFeature=function(arg){"use strict";return this instanceof geo.geomFeature?(arg=arg||{},geo.feature.call(this,arg),arg.style=void 0===arg.style?$.extend({},{color:[1,1,1],point_sprites:!1,point_sprites_image:null},arg.style):arg.style,this.style(arg.style),this):new geo.geomFeature(arg)},inherit(geo.geomFeature,geo.feature),geo.graphFeature=function(arg){"use strict";if(!(this instanceof geo.graphFeature))return new geo.graphFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_draw=this.draw,s_style=this.style,m_nodes=null,m_points=null,m_children=function(d){return d.children},m_links=[],s_init=this._init,s_exit=this._exit;return this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend(!0,{},{nodes:{radius:5,fill:!0,fillColor:{r:1,g:0,b:0},strokeColor:{r:0,g:0,b:0}},links:{strokeColor:{r:0,g:0,b:0}},linkType:"path"},void 0===arg.style?{}:arg.style);m_this.style(defaultStyle),m_this.nodes(function(d){return d})},this._build=function(){m_this.children().forEach(function(child){child._build()})},this._update=function(){m_this.children().forEach(function(child){child._update()})},this._exit=function(){return m_this.data([]),m_links.forEach(function(link){link._exit(),m_this.removeChild(link)}),m_links=[],m_points._exit(),m_this.removeChild(m_points),s_exit(),m_this},this.style=function(arg,arg2){var out=s_style.call(m_this,arg,arg2);return out!==m_this?out:(m_points.style(arg.nodes),m_links.forEach(function(l){l.style(arg.links)}),m_this)},this.links=function(arg){return void 0===arg?m_children:(m_children=geo.util.ensureFunction(arg),m_this)},this.nodes=function(val){return void 0===val?m_nodes:(m_nodes=val,m_this.modified(),m_this)},this.nodeFeature=function(){return m_points},this.linkFeatures=function(){return m_links},this.draw=function(){var style,layer=m_this.layer(),data=m_this.data(),nLinks=0;return style=m_this.style(),m_points.data(data),m_points.style(style.nodes),data.forEach(function(source){(source.children||[]).forEach(function(target){var link;nLinks+=1,m_links.lengthdata.length&&(gridH=Math.floor(data.length)/gridW),usePos=null===x0||void 0===x0||null===y0||void 0===y0||!dx||!dy,!usePos&&result.wrapLongitude&&(-180>x0||x0>180||-180>x0+dx*(gridW-1)||x0+dx*(gridW-1)>180)&&dx>-180&&180>dx){for(calcX=[],i=0;gridW>i;i+=1){for(x=x0+i*dx;-180>x;)x+=360;for(;x>180;)x-=360;i&&Math.abs(x-calcX[calcX.length-1])>180&&(x>calcX[calcX.length-1]?(calcX.push(x-360),calcX.push(calcX[calcX.length-2]+360)):(calcX.push(x+360),calcX.push(calcX[calcX.length-2]-360)),skipColumn=i),calcX.push(x)}if(gridW+=2,Math.abs(Math.abs(gridWorig*dx)-360)<.01){for(gridW+=1,x=x0+gridWorig*dx;-180>x;)x+=360;for(;x>180;)x-=360;calcX.push(x)}}for(numPts=gridW*gridH,i=0;numPts>i;i+=1)void 0===skipColumn?val=parseFloat(valueFunc(data[i])):(j=Math.floor(i/gridW),origI=i-j*gridW,origI+=origI>skipColumn?-2:0,origI>=gridWorig&&(origI-=gridWorig),origI+=j*gridWorig,val=parseFloat(valueFunc(data[origI]))),values[i]=isNaN(val)?null:val,null!==values[i]&&(idxMap[i]=usedPts,usedPts+=1,void 0===minval&&(minval=maxval=values[i]),values[i]maxval&&(maxval=values[i]));if(!usedPts)return result;if($.isNumeric(result.minValue)||(result.minValue=minval),$.isNumeric(result.maxValue)||(result.maxValue=maxval),rangeValues&&rangeValues.length===result.colorMap.length+1||(rangeValues=null),rangeValues)for(k=1;krangeValues[k+1]){rangeValues=null;break}for(rangeValues&&(result.minValue=rangeValues[0],result.maxValue=rangeValues[rangeValues.length-1]),range=result.maxValue-result.minValue,range||(result.colorMap=result.colorMap.slice(0,1),range=1,rangeValues=null),result.rangeValues=rangeValues,result.factor=result.colorMap.length/range,j=idx=0;gridH-1>j;j+=1,idx+=1)for(i=0;gridW-1>i;i+=1,idx+=1)null!==values[idx]&&null!==values[idx+1]&&null!==values[idx+gridW]&&null!==values[idx+gridW+1]&&i!==skipColumn&&(result.elements.push(idxMap[idx]),result.elements.push(idxMap[idx+1]),result.elements.push(idxMap[idx+gridW]),result.elements.push(idxMap[idx+gridW+1]),result.elements.push(idxMap[idx+gridW]),result.elements.push(idxMap[idx+1]));for(result.pos=new Array(3*usedPts),result.value=new Array(usedPts),result.opacity=new Array(usedPts),j=i=i3=0;numPts>j;j+=1)if(val=values[j],null!==val){if(item=data[j],usePos?(posVal=posFunc(item),result.pos[i3]=posVal.x,result.pos[i3+1]=posVal.y,result.pos[i3+2]=posVal.z||0):(void 0===skipColumn?result.pos[i3]=x0+dx*(j%gridW):result.pos[i3]=calcX[j%gridW],result.pos[i3+1]=y0+dy*Math.floor(j/gridW),result.pos[i3+2]=0),result.opacity[i]=opacityFunc(item),rangeValues&&val>=result.minValue&&val<=result.maxValue){for(k=1;ki;i+=pointOffset)yCoord=inPos[i+1],yCoord>85.0511&&(yCoord=85.0511),-85.0511>yCoord&&(yCoord=-85.0511),outPos[i+1]=geo.mercator.lat2y(yCoord);return inplace&&(feature.positions(outPos),feature.gcs(destGcs)),outPos}return null}},geo.transform.transformFeature=function(destGcs,feature,inplace){"use strict";if(!feature)throw"Invalid (null) feature";if(!(feature instanceof geo.pointFeature||feature instanceof geo.lineFeature))throw"Supports only point or line feature";if(feature.gcs()===destGcs)return feature.positions();if("EPSG:3857"===destGcs)return geo.transform.osmTransformFeature(destGcs,feature,inplace);var i,noOfComponents=null,pointOffset=0,count=null,inPos=null,outPos=null,projPoint=null,srcGcs=feature.gcs(),projSrcGcs=new proj4.Proj(srcGcs),projDestGcs=new proj4.Proj(destGcs);if(inplace=!!inplace,feature instanceof geo.pointFeature||feature instanceof geo.lineFeature){if(inPos=feature.positions(),count=inPos.length,!(inPos instanceof Array))throw"Supports Array of 2D and 3D points";if(noOfComponents=count%2===0?2:count%3===0?3:null,pointOffset=noOfComponents,2!==noOfComponents&&3!==noOfComponents)throw"Transform points require points in 2D or 3D";for(inplace?outPos=inPos:(outPos=[],outPos.length=inPos.length),i=0;count>i;i+=pointOffset)projPoint=2===noOfComponents?new proj4.Point(inPos[i],inPos[i+1],0):new proj4.Point(inPos[i],inPos[i+1],inPos[i+2]),proj4.transform(projSrcGcs,projDestGcs,projPoint),2===noOfComponents?(outPos[i]=projPoint.x,outPos[i+1]=projPoint.y):(outPos[i]=projPoint.x,outPos[i+1]=projPoint.y,outPos[i+2]=projPoint.z);return inplace&&(feature.positions(outPos),feature.gcs(destGcs)),outPos}return null},geo.transform.transformLayer=function(destGcs,layer,baseLayer){"use strict";var features,count,i;if(!layer)throw"Requires valid layer for tranformation";if(!baseLayer)throw"Requires baseLayer used by the map";if(layer!==baseLayer){if(!(layer instanceof geo.featureLayer))throw"Only feature layer transformation is supported";for(features=layer.features(),count=features.length,i=0,i=0;count>i;i+=1)"EPSG:3857"===destGcs&&baseLayer instanceof geo.osmLayer?geo.transform.osmTransformFeature(destGcs,features[i],!0):geo.transform.transformFeature(destGcs,features[i],!0);layer.gcs(destGcs)}},geo.transform.transformCoordinates=function(srcGcs,destGcs,coordinates,numberOfComponents){"use strict";function handleArrayCoordinates(){if(coordinates[0]instanceof Array)if(2===coordinates[0].length)xAcc=function(index){return coordinates[index][0]},yAcc=function(index){return coordinates[index][1]},writer=function(index,x,y){output[index]=[x,y]};else{if(3!==coordinates[0].length)throw"Invalid coordinates. Requires two or three components per array";xAcc=function(index){return coordinates[index][0]},yAcc=function(index){return coordinates[index][1]},zAcc=function(index){return coordinates[index][2]},writer=function(index,x,y,z){output[index]=[x,y,z]}}else if(2===coordinates.length)offset=2,xAcc=function(index){return coordinates[index*offset]},yAcc=function(index){return coordinates[index*offset+1]},writer=function(index,x,y){output[index]=x,output[index+1]=y};else if(3===coordinates.length)offset=3,xAcc=function(index){return coordinates[index*offset]},yAcc=function(index){return coordinates[index*offset+1]},zAcc=function(index){return coordinates[index*offset+2]},writer=function(index,x,y,z){output[index]=x,output[index+1]=y,output[index+2]=z};else{if(!numberOfComponents)throw"Invalid coordinates";offset=numberOfComponents,xAcc=function(index){return coordinates[index]},yAcc=function(index){return coordinates[index+1]},2===numberOfComponents?writer=function(index,x,y){output[index]=x,output[index+1]=y}:(zAcc=function(index){return coordinates[index+2]},writer=function(index,x,y,z){output[index]=x,output[index+1]=y,output[index+2]=z})}}function handleObjectCoordinates(){if(coordinates[0]&&"x"in coordinates[0]&&"y"in coordinates[0])xAcc=function(index){return coordinates[index].x},yAcc=function(index){return coordinates[index].y},"z"in coordinates[0]?(zAcc=function(index){return coordinates[index].z},writer=function(index,x,y,z){output[i]={x:x,y:y,z:z}}):writer=function(index,x,y){output[index]={x:x,y:y}};else{if(!(coordinates&&"x"in coordinates&&"y"in coordinates))throw"Invalid coordinates";xAcc=function(){return coordinates.x},yAcc=function(){return coordinates.y},"z"in coordinates?(zAcc=function(){return coordinates.z},writer=function(index,x,y,z){output={x:x,y:y,z:z}}):writer=function(index,x,y){output={x:x,y:y}}}}var i,count,offset,xCoord,yCoord,zCoord,xAcc,yAcc,zAcc,writer,output,projPoint,projSrcGcs=new proj4.Proj(srcGcs),projDestGcs=new proj4.Proj(destGcs);if(zAcc=function(){return 0},destGcs===srcGcs)return coordinates;if(!destGcs||!srcGcs)throw"Invalid source or destination GCS";if(coordinates instanceof Array)output=[],output.length=coordinates.length,count=coordinates.length,coordinates[0]instanceof Array||coordinates[0]instanceof Object?(offset=1,coordinates[0]instanceof Array?handleArrayCoordinates():coordinates[0]instanceof Object&&handleObjectCoordinates()):handleArrayCoordinates();else if(coordinates&&coordinates instanceof Object){if(count=1,offset=1,!(coordinates&&"x"in coordinates&&"y"in coordinates))throw"Coordinates are not valid";handleObjectCoordinates()}if("EPSG:3857"===destGcs&&"EPSG:4326"===srcGcs){for(i=0;count>i;i+=offset)xCoord=xAcc(i),yCoord=yAcc(i),zCoord=zAcc(i),yCoord>85.0511&&(yCoord=85.0511),-85.0511>yCoord&&(yCoord=-85.0511),writer(i,xCoord,geo.mercator.lat2y(yCoord),zCoord);return output}for(i=0;count>i;i+=offset)return projPoint=new proj4.Point(xAcc(i),yAcc(i),zAcc(i)),proj4.transform(projSrcGcs,projDestGcs,projPoint),writer(i,projPoint.x,projPoint.y,projPoint.z),output},geo.renderer=function(arg){"use strict";if(!(this instanceof geo.renderer))return new geo.renderer(arg);geo.object.call(this),arg=arg||{};var m_this=this,m_layer=void 0===arg.layer?null:arg.layer,m_canvas=void 0===arg.canvas?null:arg.canvas,m_initialized=!1;return this.layer=function(){return m_layer},this.canvas=function(val){return void 0===val?m_canvas:(m_canvas=val,void m_this.modified())},this.map=function(){return m_layer?m_layer.map():null},this.baseLayer=function(){return m_this.map()?m_this.map().baseLayer():void 0},this.initialized=function(val){return void 0===val?m_initialized:(m_initialized=val,m_this)},this.api=function(){throw"Should be implemented by derivied classes"},this.reset=function(){return!0},this.worldToGcs=function(){throw"Should be implemented by derivied classes"},this.displayToGcs=function(){throw"Should be implemented by derivied classes"},this.gcsToDisplay=function(){throw"Should be implemented by derivied classes"},this.worldToDisplay=function(){throw"Should be implemented by derivied classes"},this.displayToWorld=function(){throw"Should be implemented by derivied classes"},this.relMouseCoords=function(event){var totalOffsetX=0,totalOffsetY=0,canvasX=0,canvasY=0,currentElement=m_this.canvas();do totalOffsetX+=currentElement.offsetLeft-currentElement.scrollLeft,totalOffsetY+=currentElement.offsetTop-currentElement.scrollTop,currentElement=currentElement.offsetParent;while(currentElement);return canvasX=event.pageX-totalOffsetX,canvasY=event.pageY-totalOffsetY,{x:canvasX,y:canvasY}},this._init=function(){},this._resize=function(){},this._render=function(){},this},inherit(geo.renderer,geo.object),geo.osmLayer=function(arg){"use strict";function isTileVisible(tile){return tile.zoom in m_visibleTilesRange&&tile.index_x>=m_visibleTilesRange[tile.zoom].startX&&tile.index_x<=m_visibleTilesRange[tile.zoom].endX&&tile.index_y>=m_visibleTilesRange[tile.zoom].startY&&tile.index_y<=m_visibleTilesRange[tile.zoom].endY?!0:!1}function drawTiles(){m_this._removeTiles(),m_this.draw(),delete m_pendingNewTilesStat[m_updateTimerId]}function updateOSMTiles(request){void 0===request&&(request={}),m_zoom||(m_zoom=m_this.map().zoom()),m_lastVisibleZoom||(m_lastVisibleZoom=m_zoom),m_this._addTiles(request),m_lastVisibleZoom!==m_zoom&&(m_lastVisibleZoom=m_zoom),m_this.updateTime().modified()}if(!(this instanceof geo.osmLayer))return new geo.osmLayer(arg);arg=arg||{},arg&&void 0===arg.attribution&&(arg.attribution='© OpenStreetMap contributors'),geo.featureLayer.call(this,arg);var m_tileUrl,m_tileUrlFromTemplate,m_this=this,s_exit=this._exit,m_tiles={},m_hiddenBinNumber=-1,m_lastVisibleBinNumber=-1,m_visibleBinNumber=1e3,m_pendingNewTiles=[],m_pendingInactiveTiles=[],m_numberOfCachedTiles=0,m_tileCacheSize=100,m_baseUrl="http://tile.openstreetmap.org/",m_mapOpacity=1,m_imageFormat="png",m_updateTimerId=null,m_lastVisibleZoom=null,m_visibleTilesRange={},s_init=this._init,m_pendingNewTilesStat={},s_update=this._update,m_updateDefer=null,m_zoom=null,m_crossOrigin="anonymous";return arg&&void 0!==arg.baseUrl&&(m_baseUrl=arg.baseUrl),"/"!==m_baseUrl.charAt(m_baseUrl.length-1)&&(m_baseUrl+="/"),arg&&void 0!==arg.mapOpacity&&(m_mapOpacity=arg.mapOpacity),arg&&void 0!==arg.imageFormat&&(m_imageFormat=arg.imageFormat),arg&&void 0!==arg.displayLast&&arg.displayLast&&(m_lastVisibleBinNumber=999),arg&&void 0!==arg.useCredentials&&arg.useCredentials&&(m_crossOrigin="use-credentials"),m_tileUrl=function(zoom,x,y){return m_baseUrl+zoom+"/"+x+"/"+y+"."+m_imageFormat},m_tileUrlFromTemplate=function(base){return function(zoom,x,y){return base.replace("",zoom).replace("",x).replace("",y)}},this.tileCacheSize=function(val){return void 0===val?m_tileCacheSize:(m_tileCacheSize=val,void m_this.modified())},this.tileUrl=function(val){return void 0===val?m_tileUrl:(m_tileUrl="string"==typeof val?m_tileUrlFromTemplate(val):val,m_this.modified(),m_this)},this.toLocal=function(input){var i,output,delta;if(input instanceof Array&&input.length>0)if(output=[],output.length=input.length,input[0]instanceof Array)if(delta=input%3===0?3:2,2===delta)for(i=0;i=2&&(output=input.slice(0),output[1]=geo.mercator.lat2y(input[1]));else output={},output.x=input.x,output.y=geo.mercator.lat2y(input.y);return output},this.fromLocal=function(input){var i,output;if(input instanceof Array&&input.length>0)if(output=[],output.length=input.length,input[0]instanceof Object)for(i=0;im_tileCacheSize&&i=m_pendingNewTilesStat[m_updateTimerId].total&&drawTiles(),defer.resolve())}}var feature,lastStartX,lastStartY,lastEndX,lastEndY,currStartX,currStartY,currEndX,currEndY,distWorldDeltaPerTile,ren=m_this.renderer(),llx=0,lly=m_this.height(),urx=m_this.width(),ury=0,temp=null,tile=null,tile1x=null,tile1y=null,tile2x=null,tile2y=null,invJ=null,i=0,j=0,worldPt1=ren.displayToWorld([llx,lly]),worldPt2=ren.displayToWorld([urx,ury]),worldDeltaY=null,displayDeltaY=null,worldDelta=null,displayDelta=null,noOfTilesRequired=null,worldDeltaPerTile=null,minDistWorldDeltaPerTile=null;for(worldPt1[0]=Math.max(worldPt1[0],-180),worldPt1[0]=Math.min(worldPt1[0],180),worldPt1[1]=Math.max(worldPt1[1],-180),worldPt1[1]=Math.min(worldPt1[1],180),worldPt2[0]=Math.max(worldPt2[0],-180),worldPt2[0]=Math.min(worldPt2[0],180),worldPt2[1]=Math.max(worldPt2[1],-180),worldPt2[1]=Math.min(worldPt2[1],180),worldDelta=Math.abs(worldPt2[0]-worldPt1[0]),worldDeltaY=Math.abs(worldPt2[1]-worldPt1[1]),displayDelta=urx-llx,displayDeltaY=lly-ury,displayDeltaY>displayDelta&&(displayDelta=displayDeltaY,worldDelta=worldDeltaY),noOfTilesRequired=Math.round(displayDelta/256),worldDeltaPerTile=worldDelta/noOfTilesRequired,minDistWorldDeltaPerTile=Number.POSITIVE_INFINITY,i=20;i>=2;i-=1)distWorldDeltaPerTile=Math.abs(360/Math.pow(2,i)-worldDeltaPerTile),minDistWorldDeltaPerTile>distWorldDeltaPerTile&&(minDistWorldDeltaPerTile=distWorldDeltaPerTile,m_zoom=i);for(tile1x=geo.mercator.long2tilex(worldPt1[0],m_zoom),tile1y=geo.mercator.lat2tiley(worldPt1[1],m_zoom),tile2x=geo.mercator.long2tilex(worldPt2[0],m_zoom),tile2y=geo.mercator.lat2tiley(worldPt2[1],m_zoom),tile1x=Math.max(tile1x,0),tile1x=Math.min(Math.pow(2,m_zoom)-1,tile1x),tile1y=Math.max(tile1y,0),tile1y=Math.min(Math.pow(2,m_zoom)-1,tile1y),tile2x=Math.max(tile2x,0),tile2x=Math.min(Math.pow(2,m_zoom)-1,tile2x),tile2y=Math.max(tile2y,0),tile2y=Math.min(Math.pow(2,m_zoom)-1,tile2y),tile1x>tile2x&&(temp=tile1x,tile1x=tile2x,tile2x=temp),tile2y>tile1y&&(temp=tile1y,tile1y=tile2y,tile2y=temp),currStartX=tile1x,currEndX=tile2x,currStartY=Math.pow(2,m_zoom)-1-tile1y,currEndY=Math.pow(2,m_zoom)-1-tile2y,currStartY>currEndY&&(temp=currStartY,currStartY=currEndY,currEndY=temp),lastStartX=geo.mercator.long2tilex(worldPt1[0],m_lastVisibleZoom),lastStartY=geo.mercator.lat2tiley(worldPt1[1],m_lastVisibleZoom),lastEndX=geo.mercator.long2tilex(worldPt2[0],m_lastVisibleZoom),lastEndY=geo.mercator.lat2tiley(worldPt2[1],m_lastVisibleZoom),lastStartY=Math.pow(2,m_lastVisibleZoom)-1-lastStartY,lastEndY=Math.pow(2,m_lastVisibleZoom)-1-lastEndY,lastStartY>lastEndY&&(temp=lastStartY,lastStartY=lastEndY,lastEndY=temp),m_visibleTilesRange={},m_visibleTilesRange[m_zoom]={startX:currStartX,endX:currEndX,startY:currStartY,endY:currEndY},m_visibleTilesRange[m_lastVisibleZoom]={startX:lastStartX,endX:lastEndX,startY:lastStartY,endY:lastEndY},m_pendingNewTilesStat[m_updateTimerId]={total:(tile2x-tile1x+1)*(tile1y-tile2y+1),count:0},i=tile1x;tile2x>=i;i+=1)for(j=tile2y;tile1y>=j;j+=1)invJ=Math.pow(2,m_zoom)-1-j,m_this._hasTile(m_zoom,i,invJ)?(tile=m_tiles[m_zoom][i][invJ],tile.feature.bin(m_visibleBinNumber),tile.LOADED&&m_updateTimerId in m_pendingNewTilesStat&&(m_pendingNewTilesStat[m_updateTimerId].count+=1),tile.feature._update()):tile=m_this._addTile(request,m_zoom,i,invJ),tile.lastused=new Date,tile.updateTimerId=m_updateTimerId;for(i=0;i=m_pendingNewTilesStat[m_updateTimerId].total&&drawTiles()},this._updateTiles=function(request){var defer=$.Deferred();return m_this.addDeferred(defer),null!==m_updateTimerId?(clearTimeout(m_updateTimerId),m_updateDefer.resolve(),m_updateDefer=defer,m_updateTimerId in m_pendingNewTilesStat&&delete m_pendingNewTilesStat[m_updateTimerId],m_updateTimerId=setTimeout(function(){updateOSMTiles(request),m_updateDefer.resolve()},100)):(m_updateDefer=defer,m_updateTimerId=setTimeout(function(){updateOSMTiles(request),m_updateDefer.resolve()},0)),m_this},this._init=function(){return s_init.call(m_this),m_this.gcs("EPSG:3857"),m_this.map().zoomRange({min:0,max:18}),m_this},this._update=function(request){m_this._updateTiles(request),s_update.call(m_this,request)},this.updateBaseUrl=function(baseUrl){if(baseUrl&&"/"!==baseUrl.charAt(m_baseUrl.length-1)&&(baseUrl+="/"),baseUrl!==m_baseUrl){void 0!==baseUrl&&(m_baseUrl=baseUrl);var tile,x,y,zoom;for(zoom in m_tiles)for(x in m_tiles[zoom])for(y in m_tiles[zoom][x])tile=m_tiles[zoom][x][y],tile.INVALID=!0,m_this.deleteFeature(tile.feature);m_tiles={},m_pendingNewTiles=[],m_pendingInactiveTiles=[],m_numberOfCachedTiles=0,m_visibleTilesRange={},m_pendingNewTilesStat={},null!==m_updateTimerId&&(clearTimeout(m_updateTimerId),m_updateTimerId=null),this._update()}},this.mapOpacity=function(val){if(void 0===val)return m_mapOpacity;if(val!==m_mapOpacity){m_mapOpacity=val;var zoom,x,y,tile;for(zoom in m_tiles)for(x in m_tiles[zoom])for(y in m_tiles[zoom][x])tile=m_tiles[zoom][x][y],tile.feature.style().opacity=val,tile.feature._update();m_this.modified()}return m_this},this._exit=function(){m_tiles={},m_pendingNewTiles=[],m_pendingInactiveTiles=[],m_numberOfCachedTiles=0,m_visibleTilesRange={},m_pendingNewTilesStat={},s_exit()},arg&&void 0!==arg.tileUrl&&this.tileUrl(arg.tileUrl),this},inherit(geo.osmLayer,geo.featureLayer),geo.registerLayer("osm",geo.osmLayer),geo.gl={},geo.gl.renderer=function(arg){"use strict";return this instanceof geo.gl.renderer?(geo.renderer.call(this,arg),this.contextRenderer=function(){throw"Should be implemented by derived classes"},this):new geo.gl.renderer(arg)},inherit(geo.gl.renderer,geo.renderer),geo.gl.lineFeature=function(arg){"use strict";function createVertexShader(){var vertexShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","attribute vec3 pos;","attribute vec3 prev;","attribute vec3 next;","attribute float offset;","attribute vec3 strokeColor;","attribute float strokeOpacity;","attribute float strokeWidth;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform float pixelWidth;","uniform float aspect;","varying vec3 strokeColorVar;","varying float strokeWidthVar;","varying float strokeOpacityVar;","void main(void)","{"," if (strokeOpacity < 0.0) {"," gl_Position = vec4(2, 2, 0, 1);"," return;"," }"," const float PI = 3.14159265358979323846264;"," vec4 worldPos = projectionMatrix * modelViewMatrix * vec4(pos.xyz, 1);"," if (worldPos.w != 0.0) {"," worldPos = worldPos/worldPos.w;"," }"," vec4 worldNext = projectionMatrix * modelViewMatrix * vec4(next.xyz, 1);"," if (worldNext.w != 0.0) {"," worldNext = worldNext/worldNext.w;"," }"," vec4 worldPrev = projectionMatrix* modelViewMatrix * vec4(prev.xyz, 1);"," if (worldPrev.w != 0.0) {"," worldPrev = worldPrev/worldPrev.w;"," }"," strokeColorVar = strokeColor;"," strokeWidthVar = strokeWidth;"," strokeOpacityVar = strokeOpacity;"," vec2 deltaNext = worldNext.xy - worldPos.xy;"," vec2 deltaPrev = worldPos.xy - worldPrev.xy;"," float angleNext = 0.0, anglePrev = 0.0;"," if (deltaNext.xy != vec2(0.0, 0.0))"," angleNext = atan(deltaNext.y / aspect, deltaNext.x);"," if (deltaPrev.xy == vec2(0.0, 0.0)) anglePrev = angleNext;"," else anglePrev = atan(deltaPrev.y / aspect, deltaPrev.x);"," if (deltaNext.xy == vec2(0.0, 0.0)) angleNext = anglePrev;"," float angle = (anglePrev + angleNext) / 2.0;"," float cosAngle = cos(anglePrev - angle);"," if (cosAngle < 0.1) { cosAngle = sign(cosAngle) * 1.0; angle = 0.0; }"," float distance = (offset * strokeWidth * pixelWidth) /"," cosAngle;"," worldPos.x += distance * sin(angle);"," worldPos.y -= distance * cos(angle) * aspect;"," gl_Position = worldPos;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader}function createFragmentShader(){var fragmentShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","varying vec3 strokeColorVar;","varying float strokeWidthVar;","varying float strokeOpacityVar;","void main () {"," gl_FragColor = vec4 (strokeColorVar, strokeOpacityVar);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader}function createGLLines(){var i,j,k,v,len,lineItem,lineItemData,vertTemp,pos,posIdx3,posBuf,nextBuf,prevBuf,offsetBuf,indicesBuf,strokeWidthBuf,strokeColorBuf,strokeOpacityBuf,dest,dest3,data=m_this.data(),numSegments=0,vert=[{},{}],position=[],posFunc=m_this.position(),strkWidthFunc=m_this.style.get("strokeWidth"),strkColorFunc=m_this.style.get("strokeColor"),strkOpacityFunc=m_this.style.get("strokeOpacity"),order=m_this.featureVertices(),geom=m_mapper.geometryData();for(i=0;i=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()<=m_this.getMTime())&&m_this._build(),m_pixelWidthUnif.set(1/m_this.renderer().width()),m_aspectUniform.set(m_this.renderer().width()/m_this.renderer().height()),m_actor.setVisible(m_this.visible()),m_actor.material().setBinNumber(m_this.bin()),m_this.updateTime().modified()},this._exit=function(){m_this.renderer().contextRenderer().removeActor(m_actor),s_exit()},this._init(arg),this},inherit(geo.gl.lineFeature,geo.lineFeature),geo.registerFeature("vgl","line",geo.gl.lineFeature),geo.gl.pointFeature=function(arg){"use strict";function createVertexShader(){var shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader}function createFragmentShader(){var shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader}function pointPolygon(x,y,w,h){var verts;switch(m_primitiveShape){case"triangle":verts=[x,y-2*h,x-w*Math.sqrt(3),y+h,x+w*Math.sqrt(3),y+h];break;case"sprite":verts=[x,y];break;default: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]}return verts}function createGLPoints(){var i,j,posBuf,posVal,posFunc,unitBuf,indices,radius,radiusVal,radFunc,stroke,strokeVal,strokeFunc,strokeWidth,strokeWidthVal,strokeWidthFunc,strokeOpacity,strokeOpacityVal,strokeOpactityFunc,strokeColor,strokeColorVal,strokeColorFunc,fill,fillVal,fillFunc,fillOpacity,fillOpacityVal,fillOpacityFunc,fillColor,fillColorVal,fillColorFunc,item,ivpf,ivpf3,iunit,i3,numPts=m_this.data().length,unit=pointPolygon(0,0,1,1),position=new Array(3*numPts),vpf=m_this.verticesPerFeature(),data=m_this.data(),geom=m_mapper.geometryData();for(posFunc=m_this.position(),radFunc=m_this.style.get("radius"),strokeFunc=m_this.style.get("stroke"),strokeWidthFunc=m_this.style.get("strokeWidth"),strokeOpactityFunc=m_this.style.get("strokeOpacity"),strokeColorFunc=m_this.style.get("strokeColor"),fillFunc=m_this.style.get("fill"),fillOpacityFunc=m_this.style.get("fillOpacity"),fillColorFunc=m_this.style.get("fillColor"),i=i3=0;numPts>i;i+=1,i3+=3)posVal=posFunc(data[i]),position[i3]=posVal.x,position[i3+1]=posVal.y,position[i3+2]=posVal.z||0;for(position=geo.transform.transformCoordinates(m_this.gcs(),m_this.layer().map().gcs(),position,3),posBuf=getBuffer(geom,"pos",vpf*numPts*3),"sprite"!==m_primitiveShape&&(unitBuf=getBuffer(geom,"unit",vpf*numPts*2)),radius=getBuffer(geom,"rad",vpf*numPts*1),stroke=getBuffer(geom,"stroke",vpf*numPts*1),strokeWidth=getBuffer(geom,"strokeWidth",vpf*numPts*1),strokeOpacity=getBuffer(geom,"strokeOpacity",vpf*numPts*1),strokeColor=getBuffer(geom,"strokeColor",vpf*numPts*3),fill=getBuffer(geom,"fill",vpf*numPts*1),fillOpacity=getBuffer(geom,"fillOpacity",vpf*numPts*1),fillColor=getBuffer(geom,"fillColor",vpf*numPts*3),indices=geom.primitive(0).indices(),indices instanceof Uint16Array&&indices.length===vpf*numPts||(indices=new Uint16Array(vpf*numPts),geom.primitive(0).setIndices(indices)),i=ivpf=ivpf3=iunit=i3=0;numPts>i;i+=1,i3+=3){if(item=data[i],"sprite"!==m_primitiveShape)for(j=0;jj;j+=1,ivpf+=1,ivpf3+=3)posBuf[ivpf3]=position[i3],posBuf[ivpf3+1]=position[i3+1],posBuf[ivpf3+2]=position[i3+2],radius[ivpf]=radiusVal,stroke[ivpf]=strokeVal,strokeWidth[ivpf]=strokeWidthVal,strokeOpacity[ivpf]=strokeOpacityVal,strokeColor[ivpf3]=strokeColorVal.r,strokeColor[ivpf3+1]=strokeColorVal.g,strokeColor[ivpf3+2]=strokeColorVal.b,fill[ivpf]=fillVal,fillOpacity[ivpf]=fillOpacityVal,fillColor[ivpf3]=fillColorVal.r,fillColor[ivpf3+1]=fillColorVal.g,fillColor[ivpf3+2]=fillColorVal.b}geom.boundsDirty(!0),m_mapper.modified(),m_mapper.boundsDirtyTimestamp().modified()}function getBuffer(geom,srcName,len){var data,src=geom.sourceByName(srcName);return data=src.data(),data instanceof Float32Array&&data.length===len?data:(data=new Float32Array(len),src.setData(data),data)}if(!(this instanceof geo.gl.pointFeature))return new geo.gl.pointFeature(arg);arg=arg||{},geo.pointFeature.call(this,arg);var m_this=this,s_exit=this._exit,m_actor=null,m_mapper=null,m_pixelWidthUniform=null,m_aspectUniform=null,m_dynamicDraw=void 0===arg.dynamicDraw?!1:arg.dynamicDraw,m_primitiveShape="sprite",s_init=this._init,s_update=this._update,vertexShaderSource=null,fragmentShaderSource=null;return("triangle"===arg.primitiveShape||"square"===arg.primitiveShape||"sprite"===arg.primitiveShape)&&(m_primitiveShape=arg.primitiveShape),vertexShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","attribute vec3 pos;","attribute float rad;","attribute vec3 fillColor;","attribute vec3 strokeColor;","attribute float fillOpacity;","attribute float strokeWidth;","attribute float strokeOpacity;","attribute float fill;","attribute float stroke;","uniform float pixelWidth;","uniform float aspect;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying vec4 fillColorVar;","varying vec4 strokeColorVar;","varying float radiusVar;","varying float strokeWidthVar;","varying float fillVar;","varying float strokeVar;"],"sprite"!==m_primitiveShape&&(vertexShaderSource=vertexShaderSource.concat(["attribute vec2 unit;","varying vec3 unitVar;"])),vertexShaderSource.push.apply(vertexShaderSource,["void main(void)","{"," strokeWidthVar = strokeWidth;"," // No stroke or fill implies nothing to draw"," if (stroke < 1.0 || strokeWidth <= 0.0 || strokeOpacity <= 0.0) {"," strokeVar = 0.0;"," strokeWidthVar = 0.0;"," }"," else"," strokeVar = 1.0;"," if (fill < 1.0 || rad <= 0.0 || fillOpacity <= 0.0)"," fillVar = 0.0;"," else"," fillVar = 1.0;"," if (fillVar == 0.0 && strokeVar == 0.0) {"," gl_Position = vec4(2, 2, 0, 1);"," return;"," }"," fillColorVar = vec4 (fillColor, fillOpacity);"," strokeColorVar = vec4 (strokeColor, strokeOpacity);"," radiusVar = rad;"]),"sprite"===m_primitiveShape?vertexShaderSource.push.apply(vertexShaderSource,[" gl_Position = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw;"," gl_PointSize = 2.0 * (rad + strokeWidthVar); ","}"]):vertexShaderSource.push.apply(vertexShaderSource,[" unitVar = vec3 (unit, 1.0);"," vec4 p = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw;"," if (p.w != 0.0) {"," p = p / p.w;"," }"," p += (rad + strokeWidthVar) * "," vec4 (unit.x * pixelWidth, unit.y * pixelWidth * aspect, 0.0, 1.0);"," gl_Position = vec4(p.xyz, 1.0);","}"]),vertexShaderSource=vertexShaderSource.join("\n"),fragmentShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","uniform float aspect;","varying vec4 fillColorVar;","varying vec4 strokeColorVar;","varying float radiusVar;","varying float strokeWidthVar;","varying float fillVar;","varying float strokeVar;"],"sprite"!==m_primitiveShape&&fragmentShaderSource.push("varying vec3 unitVar;"),fragmentShaderSource.push.apply(fragmentShaderSource,["void main () {"," vec4 strokeColor, fillColor;"," float endStep;"," // No stroke or fill implies nothing to draw"," if (fillVar == 0.0 && strokeVar == 0.0)"," discard;"]),"sprite"===m_primitiveShape?fragmentShaderSource.push(" float rad = 2.0 * length (gl_PointCoord - vec2(0.5));"):fragmentShaderSource.push(" float rad = length (unitVar.xy);"),fragmentShaderSource.push.apply(fragmentShaderSource,[" if (rad > 1.0)"," discard;"," // If there is no stroke, the fill region should transition to nothing"," if (strokeVar == 0.0) {"," strokeColor = vec4 (fillColorVar.rgb, 0.0);"," endStep = 1.0;"," } else {"," strokeColor = strokeColorVar;"," endStep = radiusVar / (radiusVar + strokeWidthVar);"," }"," // Likewise, if there is no fill, the stroke should transition to nothing"," if (fillVar == 0.0)"," fillColor = vec4 (strokeColor.rgb, 0.0);"," else"," fillColor = fillColorVar;"," // Distance to antialias over"," float antialiasDist = 3.0 / (2.0 * radiusVar);"," if (rad < endStep) {"," float step = smoothstep (endStep - antialiasDist, endStep, rad);"," gl_FragColor = mix (fillColor, strokeColor, step);"," } else {"," float step = smoothstep (1.0 - antialiasDist, 1.0, rad);"," gl_FragColor = mix (strokeColor, vec4 (strokeColor.rgb, 0.0), step);"," }","}"]),fragmentShaderSource=fragmentShaderSource.join("\n"),this.actors=function(){return m_actor?[m_actor]:[]},this.verticesPerFeature=function(){var unit=pointPolygon(0,0,1,1);return unit.length/2},this._init=function(){var prog=vgl.shaderProgram(),vertexShader=createVertexShader(),fragmentShader=createFragmentShader(),posAttr=vgl.vertexAttribute("pos"),unitAttr=vgl.vertexAttribute("unit"),radAttr=vgl.vertexAttribute("rad"),strokeWidthAttr=vgl.vertexAttribute("strokeWidth"),fillColorAttr=vgl.vertexAttribute("fillColor"),fillAttr=vgl.vertexAttribute("fill"),strokeColorAttr=vgl.vertexAttribute("strokeColor"),strokeAttr=vgl.vertexAttribute("stroke"),fillOpacityAttr=vgl.vertexAttribute("fillOpacity"),strokeOpacityAttr=vgl.vertexAttribute("strokeOpacity"),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),mat=vgl.material(),blend=vgl.blend(),geom=vgl.geometryData(),sourcePositions=vgl.sourceDataP3fv({name:"pos"}),sourceUnits=vgl.sourceDataAnyfv(2,vgl.vertexAttributeKeysIndexed.One,{name:"unit"}),sourceRadius=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Two,{name:"rad"}),sourceStrokeWidth=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Three,{name:"strokeWidth"}),sourceFillColor=vgl.sourceDataAnyfv(3,vgl.vertexAttributeKeysIndexed.Four,{name:"fillColor"}),sourceFill=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Five,{name:"fill"}),sourceStrokeColor=vgl.sourceDataAnyfv(3,vgl.vertexAttributeKeysIndexed.Six,{name:"strokeColor"}),sourceStroke=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Seven,{name:"stroke"}),sourceAlpha=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Eight,{name:"fillOpacity"}),sourceStrokeOpacity=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Nine,{name:"strokeOpacity"}),primitive=new vgl.triangles;"sprite"===m_primitiveShape&&(primitive=new vgl.points),m_pixelWidthUniform=new vgl.floatUniform("pixelWidth",2/m_this.renderer().width()),m_aspectUniform=new vgl.floatUniform("aspect",m_this.renderer().width()/m_this.renderer().height()),s_init.call(m_this,arg),m_mapper=vgl.mapper({dynamicDraw:m_dynamicDraw}),prog.addVertexAttribute(posAttr,vgl.vertexAttributeKeys.Position),"sprite"!==m_primitiveShape&&prog.addVertexAttribute(unitAttr,vgl.vertexAttributeKeysIndexed.One),prog.addVertexAttribute(radAttr,vgl.vertexAttributeKeysIndexed.Two),prog.addVertexAttribute(strokeWidthAttr,vgl.vertexAttributeKeysIndexed.Three),prog.addVertexAttribute(fillColorAttr,vgl.vertexAttributeKeysIndexed.Four),prog.addVertexAttribute(fillAttr,vgl.vertexAttributeKeysIndexed.Five),prog.addVertexAttribute(strokeColorAttr,vgl.vertexAttributeKeysIndexed.Six),prog.addVertexAttribute(strokeAttr,vgl.vertexAttributeKeysIndexed.Seven),prog.addVertexAttribute(fillOpacityAttr,vgl.vertexAttributeKeysIndexed.Eight),prog.addVertexAttribute(strokeOpacityAttr,vgl.vertexAttributeKeysIndexed.Nine),prog.addUniform(m_pixelWidthUniform),prog.addUniform(m_aspectUniform),prog.addUniform(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.addSource(sourcePositions),geom.addSource(sourceUnits),geom.addSource(sourceRadius),geom.addSource(sourceStrokeWidth),geom.addSource(sourceFillColor),geom.addSource(sourceFill),geom.addSource(sourceStrokeColor),geom.addSource(sourceStroke),geom.addSource(sourceAlpha),geom.addSource(sourceStrokeOpacity),geom.addPrimitive(primitive),m_mapper.setGeometryData(geom)},this._build=function(){m_actor&&m_this.renderer().contextRenderer().removeActor(m_actor),createGLPoints(),m_this.renderer().contextRenderer().addActor(m_actor),m_this.renderer().contextRenderer().render(),m_this.buildTime().modified()},this._update=function(){s_update.call(m_this),(m_this.dataTime().getMTime()>=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()i;i+=1)buffers.write("pos",position[i],start+i,1),buffers.write("indices",[i],start+i,1),buffers.write("fillColor",fillColorNew[i],start+i,1),buffers.write("fillOpacity",[fillOpacityNew[i]],start+i,1);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)}if(!(this instanceof geo.gl.polygonFeature))return new geo.gl.polygonFeature(arg);arg=arg||{},geo.polygonFeature.call(this,arg);var m_this=this,s_exit=this._exit,m_actor=vgl.actor(),m_mapper=vgl.mapper(),m_material=vgl.material(),s_init=this._init,s_update=this._update;return this._init=function(arg){var blend=vgl.blend(),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),prog.addVertexAttribute(posAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(fillColorAttr,vgl.vertexAttributeKeysIndexed.Two),prog.addVertexAttribute(fillOpacityAttr,vgl.vertexAttributeKeysIndexed.Three),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),m_material.addAttribute(prog),m_material.addAttribute(blend),m_actor.setMapper(m_mapper),m_actor.setMaterial(m_material)},this._build=function(){m_actor&&m_this.renderer().contextRenderer().removeActor(m_actor),createGLPolygons(),m_this.renderer().contextRenderer().addActor(m_actor),m_this.buildTime().modified()},this._update=function(){s_update.call(m_this),(m_this.dataTime().getMTime()>=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()<=m_this.getMTime())&&m_this._build(),m_actor.setVisible(m_this.visible()),m_actor.material().setBinNumber(m_this.bin()),m_this.updateTime().modified()},this._exit=function(){m_this.renderer().contextRenderer().removeActor(m_actor),s_exit()},this._init(arg),this},inherit(geo.gl.polygonFeature,geo.polygonFeature),geo.registerFeature("vgl","polygon",geo.gl.polygonFeature),geo.gl.contourFeature=function(arg){"use strict";function createVertexShader(){var vertexShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","attribute vec3 pos;","attribute float value;","attribute float opacity;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying float valueVar;","varying float opacityVar;","void main(void)","{"," vec4 scrPos = projectionMatrix * modelViewMatrix * vec4(pos.xy, 0, 1);"," if (scrPos.w != 0.0) {"," scrPos = scrPos / scrPos.w;"," }"," valueVar = value;"," opacityVar = opacity;"," gl_Position = scrPos;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader}function createFragmentShader(){var fragmentShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","uniform vec4 minColor;","uniform vec4 maxColor;","uniform float steps;","uniform bool stepped;","uniform sampler2D sampler2d;","varying float valueVar;","varying float opacityVar;","void main () {"," vec4 clr;"," if (valueVar < 0.0) {"," clr = minColor;"," } else if (valueVar > steps) {"," clr = maxColor;"," } else {"," float step;"," if (stepped) {"," step = floor(valueVar) + 0.5;"," if (step > steps) {"," step = steps - 0.5;"," }"," } else {"," step = valueVar;"," }"," clr = texture2D(sampler2d, vec2(step / steps, 0.0));"," }"," gl_FragColor = vec4(clr.rgb, clr.a * opacityVar);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader}function createGLContours(){var i,i3,j,j3,posBuf,opacityBuf,valueBuf,indicesBuf,contour=m_this.createContours(),numPts=contour.elements.length,colorTable=[],geom=m_mapper.geometryData();for(m_minColorUniform.set([contour.minColor.r,contour.minColor.g,contour.minColor.b,contour.minColor.a]),m_maxColorUniform.set([contour.maxColor.r,contour.maxColor.g,contour.maxColor.b,contour.maxColor.a]),m_stepsUniform.set(contour.colorMap.length),m_steppedUniform.set(contour.stepped),i=0;ii;i+=1,i3+=3)j=contour.elements[i],j3=3*j,posBuf[i3]=contour.pos[j3],posBuf[i3+1]=contour.pos[j3+1],posBuf[i3+2]=contour.pos[j3+2],opacityBuf[i]=contour.opacity[j],valueBuf[i]=contour.value[j];indicesBuf=geom.primitive(0).indices(),indicesBuf instanceof Uint16Array&&indicesBuf.length===numPts||(indicesBuf=new Uint16Array(numPts),geom.primitive(0).setIndices(indicesBuf)),geom.boundsDirty(!0),m_mapper.modified(),m_mapper.boundsDirtyTimestamp().modified()}function getBuffer(geom,srcName,len){var data,src=geom.sourceByName(srcName);return data=src.data(),data instanceof Float32Array&&data.length===len?data:(data=new Float32Array(len),src.setData(data),data)}if(!(this instanceof geo.gl.contourFeature))return new geo.gl.contourFeature(arg);arg=arg||{},geo.contourFeature.call(this,arg);var m_this=this,s_exit=this._exit,m_textureUnit=7,m_actor=null,m_mapper=null,m_material=null,m_texture=null,m_minColorUniform=null,m_maxColorUniform=null,m_stepsUniform=null,m_steppedUniform=null,m_dynamicDraw=void 0===arg.dynamicDraw?!1:arg.dynamicDraw,s_init=this._init,s_update=this._update;return this._init=function(arg){var blend=vgl.blend(),prog=vgl.shaderProgram(),mat=vgl.material(),tex=vgl.lookupTable(),geom=vgl.geometryData(),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.INT,"sampler2d"),vertexShader=createVertexShader(),fragmentShader=createFragmentShader(),posAttr=vgl.vertexAttribute("pos"),valueAttr=vgl.vertexAttribute("value"),opacityAttr=vgl.vertexAttribute("opacity"),sourcePositions=vgl.sourceDataP3fv({name:"pos"}),sourceValues=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.One,{name:"value"}),sourceOpacity=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Two,{name:"opacity"}),primitive=new vgl.triangles;s_init.call(m_this,arg),m_mapper=vgl.mapper({dynamicDraw:m_dynamicDraw}),prog.addVertexAttribute(posAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(valueAttr,vgl.vertexAttributeKeysIndexed.One),prog.addVertexAttribute(opacityAttr,vgl.vertexAttributeKeysIndexed.Two),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),m_minColorUniform=new vgl.uniform(vgl.GL.FLOAT_VEC4,"minColor"),prog.addUniform(m_minColorUniform),m_maxColorUniform=new vgl.uniform(vgl.GL.FLOAT_VEC4,"maxColor"),prog.addUniform(m_maxColorUniform),m_stepsUniform=new vgl.uniform(vgl.GL.FLOAT,"steps"),prog.addUniform(m_stepsUniform),m_steppedUniform=new vgl.uniform(vgl.GL.BOOL,"stepped"),prog.addUniform(m_steppedUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),prog.addUniform(samplerUniform),tex.setTextureUnit(m_textureUnit),samplerUniform.set(m_textureUnit),m_material=mat,m_material.addAttribute(prog),m_material.addAttribute(blend),m_texture=tex,m_material.addAttribute(m_texture),m_actor=vgl.actor(),m_actor.setMaterial(m_material),m_actor.setMapper(m_mapper),geom.addSource(sourcePositions),geom.addSource(sourceValues),geom.addSource(sourceOpacity),geom.addPrimitive(primitive),m_mapper.setGeometryData(geom)},this._build=function(){m_actor&&m_this.renderer().contextRenderer().removeActor(m_actor),createGLContours(),m_this.renderer().contextRenderer().addActor(m_actor),m_this.buildTime().modified()},this._update=function(){s_update.call(m_this),(m_this.dataTime().getMTime()>=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()<=m_this.getMTime())&&m_this._build(),m_actor.setVisible(m_this.visible()),m_actor.material().setBinNumber(m_this.bin()),m_this.updateTime().modified()},this._exit=function(){m_this.renderer().contextRenderer().removeActor(m_actor),s_exit()},this._init(arg),this},inherit(geo.gl.contourFeature,geo.contourFeature),geo.registerFeature("vgl","contour",geo.gl.contourFeature),geo.gl.vglRenderer=function(arg){"use strict";if(!(this instanceof geo.gl.vglRenderer))return new geo.gl.vglRenderer(arg);arg=arg||{},geo.gl.renderer.call(this,arg);var m_this=this,m_contextRenderer=null,m_viewer=null,m_width=0,m_height=0,m_initialized=!1,s_init=this._init;return this.width=function(){return m_width},this.height=function(){return m_height},this.displayToWorld=function(input){var i,delta,output,temp,point,ren=m_this.contextRenderer(),cam=ren.camera(),fdp=ren.focusDisplayPoint();if(input instanceof Array&&input.length>0)if(output=[],input[0]instanceof Object)for(delta=1,i=0;i0)if(output=[],input[0]instanceof Object)for(delta=1,i=0;i0&&m_contextRenderer.setLayer(m_viewer.renderWindow().renderers().length),m_this},this._resize=function(x,y,w,h){var baseContextRenderer,baseCamera,focalPoint,position,zoom,newZ,mapCenter,vglRenderer=m_this.contextRenderer(),map=m_this.layer().map(),camera=vglRenderer.camera(),renderWindow=m_viewer.renderWindow(),layer=m_this.layer(),baseLayer=layer.map().baseLayer();return m_width=w,m_height=h,m_this.canvas().attr("width",w),m_this.canvas().attr("height",h),renderWindow.positionAndResize(x,y,w,h),m_this._render(),baseLayer&&!m_initialized?(m_initialized=!0,vglRenderer&&vglRenderer.camera()||console.log("Zoom event triggered on unconnected vgl renderer."),position=camera.position(),zoom=map.zoom(),newZ=camera.zoomToHeight(zoom,w,h),layer!==baseLayer?(baseContextRenderer=baseLayer.renderer().contextRenderer(),baseCamera=baseContextRenderer.camera(),position=baseCamera.position(),focalPoint=baseCamera.focalPoint(),camera.setPosition(position[0],position[1],position[2]),camera.setFocalPoint(focalPoint[0],focalPoint[1],focalPoint[2])):(mapCenter=layer.toLocal(layer.map().center()),focalPoint=camera.focalPoint(),camera.setPosition(mapCenter.x,mapCenter.y,newZ),camera.setFocalPoint(mapCenter.x,mapCenter.y,focalPoint[2])),camera.setParallelExtents({zoom:zoom}),m_this._updateRendererCamera(),m_this):void 0},this._render=function(){return m_viewer.render(),m_this},this._updateRendererCamera=function(){var pos,fp,cr,pe,vglRenderer=m_this.contextRenderer(),renderWindow=m_viewer.renderWindow(),camera=vglRenderer.camera();vglRenderer.resetCameraClippingRange(),pos=camera.position(),fp=camera.focalPoint(),cr=camera.clippingRange(),pe=camera.parallelExtents(),renderWindow.renderers().forEach(function(renderer){var cam=renderer.camera();cam!==camera&&(cam.setPosition(pos[0],pos[1],pos[2]),cam.setFocalPoint(fp[0],fp[1],fp[2]),cam.setClippingRange(cr[0],cr[1]),cam.setParallelExtents(pe),renderer.render())})},m_this.layer().geoOn(geo.event.pan,function(evt){var camera,focusPoint,centerDisplay,centerGeo,newCenterDisplay,newCenterGeo,renderWindow,vglRenderer=m_this.contextRenderer(),layer=m_this.layer();evt.geo&&evt.geo._triggeredBy!==layer&&(vglRenderer&&vglRenderer.camera()||console.log("Pan event triggered on unconnected VGL renderer."),renderWindow=m_viewer.renderWindow(),camera=vglRenderer.camera(),focusPoint=renderWindow.focusDisplayPoint(),centerDisplay=[m_width/2,m_height/2,0],centerGeo=renderWindow.displayToWorld(centerDisplay[0],centerDisplay[1],focusPoint,vglRenderer),newCenterDisplay=[centerDisplay[0]+evt.screenDelta.x,centerDisplay[1]+evt.screenDelta.y],newCenterGeo=renderWindow.displayToWorld(newCenterDisplay[0],newCenterDisplay[1],focusPoint,vglRenderer),camera.pan(centerGeo[0]-newCenterGeo[0],centerGeo[1]-newCenterGeo[1],centerGeo[2]-newCenterGeo[2]),evt.center={x:newCenterGeo[0],y:newCenterGeo[1],z:newCenterGeo[2]},m_this._updateRendererCamera())}),m_this.layer().geoOn(geo.event.zoom,function(evt){var camera,renderWindow,center,dir,focusPoint,position,newZ,vglRenderer=m_this.contextRenderer(),layer=m_this.layer();if(evt.geo&&evt.geo._triggeredBy!==layer){vglRenderer&&vglRenderer.camera()||console.log("Zoom event triggered on unconnected vgl renderer."),renderWindow=m_viewer.renderWindow(),camera=vglRenderer.camera(),focusPoint=camera.focalPoint(),position=camera.position();var windowSize=renderWindow.windowSize();newZ=camera.zoomToHeight(evt.zoomLevel,windowSize[0],windowSize[1]),evt.pan=null,evt.screenPosition&&(center=renderWindow.displayToWorld(evt.screenPosition.x,evt.screenPosition.y,focusPoint,vglRenderer),dir=[center[0]-position[0],center[1]-position[1],center[2]-position[2]],evt.center=layer.fromLocal({x:position[0]+dir[0]*(1-newZ/position[2]),y:position[1]+dir[1]*(1-newZ/position[2])})),camera.setPosition(position[0],position[1],newZ),camera.setParallelExtents({zoom:evt.zoomLevel}),m_this._updateRendererCamera()}}),m_this.layer().geoOn(geo.event.parallelprojection,function(evt){var camera,vglRenderer=m_this.contextRenderer(),layer=m_this.layer();evt.geo&&evt.geo._triggeredBy!==layer&&(vglRenderer&&vglRenderer.camera()||console.log("Parallel projection event triggered on unconnected VGL renderer."),camera=vglRenderer.camera(),camera.setEnableParallelProjection(evt.parallelProjection),m_this._updateRendererCamera())}),this},inherit(geo.gl.vglRenderer,geo.gl.renderer),geo.registerRenderer("vgl",geo.gl.vglRenderer),geo.d3={},function(){"use strict";var chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",strLength=8;geo.d3.uniqueID=function(){var i,strArray=[];for(strArray.length=strLength,i=0;strLength>i;i+=1)strArray[i]=chars.charAt(Math.floor(Math.random()*chars.length));return strArray.join("")},geo.event.d3Rescale="geo_d3_rescale"}(),geo.d3.object=function(arg){"use strict";if(!(this instanceof geo.object))return new geo.d3.object(arg);geo.sceneObject.call(this);var m_id="d3-"+geo.d3.uniqueID(),s_exit=this._exit,m_this=this,s_draw=this.draw;return this._d3id=function(){return m_id},this.select=function(){return m_this.renderer().select(m_this._d3id())},this.draw=function(){return m_this._update(),s_draw(),m_this},this._exit=function(){m_this.renderer()._removeFeature(m_this._d3id()),s_exit()},this},inherit(geo.d3.object,geo.sceneObject),geo.d3.pointFeature=function(arg){"use strict";if(!(this instanceof geo.d3.pointFeature))return new geo.d3.pointFeature(arg);arg=arg||{},geo.pointFeature.call(this,arg),geo.d3.object.call(this);var m_sticky,m_this=this,s_init=this._init,s_update=this._update,m_buildTime=geo.timestamp(),m_style={};return this._init=function(arg){return s_init.call(m_this,arg),m_sticky=m_this.layer().sticky(),m_this},this._build=function(){var data=m_this.data(),s_style=m_this.style.get(),m_renderer=m_this.renderer(),pos_func=m_this.position();return s_update.call(m_this),data||(data=[]),m_style.id=m_this._d3id(),m_style.data=data,m_style.append="circle",m_style.attributes={r:m_renderer._convertScale(s_style.radius),cx:function(d){return m_renderer.worldToDisplay(pos_func(d)).x},cy:function(d){return m_renderer.worldToDisplay(pos_func(d)).y}},m_style.style=s_style,m_style.classes=["d3PointFeature"],m_this.renderer()._drawFeatures(m_style),m_buildTime.modified(),m_this.updateTime().modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.getMTime()>=m_buildTime.getMTime()&&m_this._build(),m_this},this._init(arg),this},inherit(geo.d3.pointFeature,geo.pointFeature),geo.registerFeature("d3","point",geo.d3.pointFeature),geo.d3.lineFeature=function(arg){"use strict";if(!(this instanceof geo.d3.lineFeature))return new geo.d3.lineFeature(arg);arg=arg||{},geo.lineFeature.call(this,arg),geo.d3.object.call(this);var m_this=this,s_init=this._init,m_buildTime=geo.timestamp(),s_update=this._update;return this._init=function(arg){return s_init.call(m_this,arg),m_this},this._build=function(){var data=m_this.data()||[],s_style=m_this.style(),m_renderer=m_this.renderer(),pos_func=m_this.position(),line=d3.svg.line().x(function(d){return m_renderer.worldToDisplay(d).x}).y(function(d){return m_renderer.worldToDisplay(d).y});return s_update.call(m_this),s_style.fill=function(){return!1},data.forEach(function(item,idx){function wrapStyle(func){return geo.util.isFunction(func)?function(){return func(ln[0],0,item,idx)}:func}var m_style,key,ln=m_this.line()(item,idx),style={};for(key in s_style)s_style.hasOwnProperty(key)&&(style[key]=wrapStyle(s_style[key]));m_style={data:[ln.map(function(d,i){return pos_func(d,i,item,idx)})],append:"path",attributes:{d:line},id:m_this._d3id()+idx,classes:["d3LineFeature","d3SubLine-"+idx],style:style},m_renderer._drawFeatures(m_style)}),m_buildTime.modified(),m_this.updateTime().modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.getMTime()>=m_buildTime.getMTime()&&m_this._build(),m_this},this._init(arg),this},inherit(geo.d3.lineFeature,geo.lineFeature),geo.registerFeature("d3","line",geo.d3.lineFeature),geo.d3.pathFeature=function(arg){"use strict";if(!(this instanceof geo.d3.pathFeature))return new geo.d3.pathFeature(arg);arg=arg||{},geo.pathFeature.call(this,arg),geo.d3.object.call(this);var m_this=this,s_init=this._init,m_buildTime=geo.timestamp(),s_update=this._update,m_style={};return m_style.style={},this._init=function(arg){return s_init.call(m_this,arg),m_this},this._build=function(){var tmp,diag,data=m_this.data()||[],s_style=m_this.style(),m_renderer=m_this.renderer();return s_update.call(m_this),diag=function(d){var p={source:d.source,target:d.target};return d3.svg.diagonal()(p)},tmp=[],data.forEach(function(d,i){var src,trg;i=m_buildTime.getMTime()&&m_this._build(),m_this},this._init(arg),this},inherit(geo.d3.pathFeature,geo.pathFeature),geo.registerFeature("d3","path",geo.d3.pathFeature),geo.d3.graphFeature=function(arg){"use strict";var m_this=this;return this instanceof geo.d3.graphFeature?(geo.graphFeature.call(this,arg),this.select=function(){var renderer=m_this.renderer(),selection={},node=m_this.nodeFeature(),links=m_this.linkFeatures();return selection.nodes=renderer.select(node._d3id()),selection.links=links.map(function(link){return renderer.select(link._d3id())}),selection},this):new geo.d3.graphFeature(arg)},inherit(geo.d3.graphFeature,geo.graphFeature),geo.registerFeature("d3","graph",geo.d3.graphFeature),geo.d3.planeFeature=function(arg){"use strict";function normalize(pt){return Array.isArray(pt)?{x:pt[0],y:pt[1]}:pt}if(!(this instanceof geo.d3.planeFeature))return new geo.d3.planeFeature(arg);geo.planeFeature.call(this,arg),geo.d3.object.call(this);var m_this=this,m_style={},s_update=this._update,s_init=this._init,m_buildTime=geo.timestamp();return this._build=function(){var origin=normalize(m_this.origin()),ul=normalize(m_this.upperLeft()),lr=normalize(m_this.lowerRight()),renderer=m_this.renderer(),s=m_this.style();return delete s.fill_color,delete s.color,delete s.opacity,s.screenCoordinates||(origin=renderer.worldToDisplay(origin),ul=renderer.worldToDisplay(ul),lr=renderer.worldToDisplay(lr)),m_style.id=m_this._d3id(),m_style.style=s,m_style.attributes={x:ul.x,y:ul.y,width:lr.x-origin.x,height:origin.y-ul.y},m_style.append="rect",m_style.data=[0],m_style.classes=["d3PlaneFeature"],renderer._drawFeatures(m_style),m_buildTime.modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.dataTime().getMTime()>=m_buildTime.getMTime()&&m_this._build(),m_this},this._init=function(arg){return s_init.call(m_this,arg||{}),m_this.style({stroke:function(){return!1},fill:function(){return!0},fillColor:function(){return{r:.3,g:.3,b:.3}},fillOpacity:function(){return.5}}),m_this},this._init(),this},inherit(geo.d3.planeFeature,geo.planeFeature),geo.registerFeature("d3","plane",geo.d3.planeFeature),geo.d3.vectorFeature=function(arg){"use strict";function markerID(d,i){return m_this._d3id()+"_marker_"+i}function updateMarkers(data,stroke,opacity){var renderer=m_this.renderer(),sel=m_this.renderer()._definitions().selectAll("marker.geo-vector").data(data);sel.enter().append("marker").attr("id",markerID).attr("class","geo-vector").attr("viewBox","0 0 10 10").attr("refX","1").attr("refY","5").attr("markerWidth","5").attr("markerHeight","5").attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z"),sel.exit().remove(),sel.style("stroke",renderer._convertColor(stroke)).style("fill",renderer._convertColor(stroke)).style("opacity",opacity)}if(!(this instanceof geo.d3.vectorFeature))return new geo.d3.vectorFeature(arg);arg=arg||{},geo.vectorFeature.call(this,arg),geo.d3.object.call(this);var m_sticky,m_this=this,s_init=this._init,s_exit=this._exit,s_update=this._update,m_buildTime=geo.timestamp(),m_style={};return this._init=function(arg){return s_init.call(m_this,arg),m_sticky=m_this.layer().sticky(),m_this},this._build=function(){function getScale(){return scale/m_renderer.scaleFactor()}var data=m_this.data(),s_style=m_this.style.get(),m_renderer=m_this.renderer(),orig_func=m_this.origin(),size_func=m_this.delta(),cache=[],scale=m_this.style("scale"),max=Number.NEGATIVE_INFINITY;return s_update.call(m_this),data||(data=[]),cache=data.map(function(d,i){var origin=m_renderer.worldToDisplay(orig_func(d,i)),delta=size_func(d,i);return max=Math.max(max,delta.x*delta.x+delta.y*delta.y),{x1:origin.x,y1:origin.y,dx:delta.x,dy:-delta.y}}),max=Math.sqrt(max),scale||(scale=75/max),m_style.id=m_this._d3id(),m_style.data=data,m_style.append="line",m_style.attributes={x1:function(d,i){return cache[i].x1},y1:function(d,i){return cache[i].y1},x2:function(d,i){return cache[i].x1+getScale()*cache[i].dx},y2:function(d,i){return cache[i].y1+getScale()*cache[i].dy},"marker-end":function(d,i){return"url(#"+markerID(d,i)+")"}},m_style.style={stroke:function(){return!0},strokeColor:s_style.strokeColor,strokeWidth:s_style.strokeWidth,strokeOpacity:s_style.strokeOpacity},m_style.classes=["d3VectorFeature"],updateMarkers(data,s_style.strokeColor,s_style.strokeOpacity),m_this.renderer()._drawFeatures(m_style),m_buildTime.modified(),m_this.updateTime().modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.getMTime()>=m_buildTime.getMTime()?m_this._build():updateMarkers(m_style.data,m_style.style.strokeColor,m_style.style.strokeOpacity),m_this},this._exit=function(){s_exit.call(m_this),m_style={},updateMarkers([],null,null)},this._init(arg),this},inherit(geo.d3.vectorFeature,geo.vectorFeature),geo.registerFeature("d3","vector",geo.d3.vectorFeature),geo.d3.d3Renderer=function(arg){"use strict";function setAttrs(select,attrs){var key;for(key in attrs)attrs.hasOwnProperty(key)&&select.attr(key,attrs[key])}function setStyles(select,styles){function fillFunc(){return styles.fill.apply(this,arguments)?null:"none"}function strokeFunc(){return styles.stroke.apply(this,arguments)?null:"none"}var key,k,f;for(key in styles)styles.hasOwnProperty(key)&&(f=null,k=null,"strokeColor"===key?(k="stroke",f=m_this._convertColor(styles[key],styles.stroke)):"stroke"===key&&styles[key]?(k="stroke",f=strokeFunc):"strokeWidth"===key?(k="stroke-width",f=m_this._convertScale(styles[key])):"strokeOpacity"===key?(k="stroke-opacity",f=styles[key]):"fillColor"===key?(k="fill",f=m_this._convertColor(styles[key],styles.fill)):"fill"!==key||styles.hasOwnProperty("fillColor")?"fillOpacity"===key&&(k="fill-opacity",f=styles[key]):(k="fill",f=fillFunc),k&&select.style(k,f))}function getMap(){var layer=m_this.layer();return layer?layer.map():null}function getGroup(){return m_svg.select(".group-"+m_this._d3id()); -}function initCorners(){var layer=m_this.layer(),map=layer.map(),width=m_this.layer().width(),height=m_this.layer().height();if(m_width=width,m_height=height,!m_width||!m_height)throw"Map layer has size 0";m_corners={upperLeft:map.displayToGcs({x:0,y:0}),lowerRight:map.displayToGcs({x:width,y:height})}}function setTransform(){if(m_corners||initCorners(),m_sticky){var dx,dy,scale,layer=m_this.layer(),map=layer.map(),upperLeft=map.gcsToDisplay(m_corners.upperLeft),lowerRight=map.gcsToDisplay(m_corners.lowerRight),group=getGroup();dx=upperLeft.x,dy=upperLeft.y,scale=(lowerRight.y-upperLeft.y)/m_height,group.attr("transform","matrix("+[scale,0,0,scale,dx,dy].join()+")"),m_scale=scale,m_dx=dx,m_dy=dy}}function baseToLocal(pt){return{x:(pt.x-m_dx)/m_scale,y:(pt.y-m_dy)/m_scale}}function localToBase(pt){return{x:pt.x*m_scale+m_dx,y:pt.y*m_scale+m_dy}}if(!(this instanceof geo.d3.d3Renderer))return new geo.d3.d3Renderer(arg);geo.renderer.call(this,arg);var s_exit=this._exit;geo.d3.object.call(this,arg),arg=arg||{};var m_this=this,m_sticky=null,m_features={},m_corners=null,m_width=null,m_height=null,m_scale=1,m_dx=0,m_dy=0,m_svg=null,m_defs=null;return this._convertColor=function(f,g){return f=geo.util.ensureFunction(f),g=g||function(){return!0},function(){var c="none";return g.apply(this,arguments)&&(c=f.apply(this,arguments),c.hasOwnProperty("r")&&c.hasOwnProperty("g")&&c.hasOwnProperty("b")&&(c=d3.rgb(255*c.r,255*c.g,255*c.b))),c}},this._convertPosition=function(f){return f=geo.util.ensureFunction(f),function(){return m_this.worldToDisplay(f.apply(this,arguments))}},this._convertScale=function(f){return f=geo.util.ensureFunction(f),function(){return f.apply(this,arguments)/m_scale}},this._init=function(){if(!m_this.canvas()){var canvas;m_svg=d3.select(m_this.layer().node().get(0)).append("svg"),m_defs=m_svg.append("defs");var shadow=m_defs.append("filter").attr("id","geo-highlight").attr("x","-100%").attr("y","-100%").attr("width","300%").attr("height","300%");shadow.append("feMorphology").attr("operator","dilate").attr("radius",2).attr("in","SourceAlpha").attr("result","dilateOut"),shadow.append("feGaussianBlur").attr("stdDeviation",5).attr("in","dilateOut").attr("result","blurOut"),shadow.append("feColorMatrix").attr("type","matrix").attr("values","-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0").attr("in","blurOut").attr("result","invertOut"),shadow.append("feBlend").attr("in","SourceGraphic").attr("in2","invertOut").attr("mode","normal"),canvas=m_svg.append("g"),shadow=m_defs.append("filter").attr("id","geo-blur").attr("x","-100%").attr("y","-100%").attr("width","300%").attr("height","300%"),shadow.append("feGaussianBlur").attr("stdDeviation",20).attr("in","SourceGraphic"),m_sticky=m_this.layer().sticky(),m_svg.attr("class",m_this._d3id()),m_svg.attr("width",m_this.layer().node().width()),m_svg.attr("height",m_this.layer().node().height()),canvas.attr("class","group-"+m_this._d3id()),m_this.canvas(canvas)}},this.displayToWorld=function(pt){var map=getMap();if(!map)throw"Cannot project until this layer is connected to a map.";return pt=Array.isArray(pt)?pt.map(function(x){return map.displayToGcs(localToBase(x))}):map.displayToGcs(localToBase(pt))},this.worldToDisplay=function(pt){var map=getMap();if(!map)throw"Cannot project until this layer is connected to a map.";var v;return v=Array.isArray(pt)?pt.map(function(x){return baseToLocal(map.gcsToDisplay(x))}):baseToLocal(map.gcsToDisplay(pt))},this.api=function(){return"d3"},this.scaleFactor=function(){return m_scale},this._resize=function(x,y,w,h){m_corners||initCorners(),m_svg.attr("width",w),m_svg.attr("height",h),setTransform(),m_this.layer().geoTrigger(geo.event.d3Rescale,{scale:m_scale},!0)},this._update=function(){},this._exit=function(){m_features={},m_this.canvas().remove(),s_exit()},this._definitions=function(){return m_defs},this._drawFeatures=function(arg){return m_features[arg.id]={data:arg.data,index:arg.dataIndex,style:arg.style,attributes:arg.attributes,classes:arg.classes,append:arg.append},m_this.__render(arg.id)},this.__render=function(id){var key;if(void 0===id){for(key in m_features)m_features.hasOwnProperty(key)&&m_this.__render(key);return m_this}var data=m_features[id].data,index=m_features[id].index,style=m_features[id].style,attributes=m_features[id].attributes,classes=m_features[id].classes,append=m_features[id].append,selection=m_this.select(id).data(data,index);return selection.enter().append(append),selection.exit().remove(),setAttrs(selection,attributes),selection.attr("class",classes.concat([id]).join(" ")),setStyles(selection,style),m_this},this.select=function(id){return getGroup().selectAll("."+id)},this._removeFeature=function(id){return m_this.select(id).remove(),delete m_features[id],m_this},this.draw=function(){},this.layer().geoOn(geo.event.pan,setTransform),this.layer().geoOn(geo.event.zoom,function(){setTransform(),m_this.__render(),m_this.layer().geoTrigger(geo.event.d3Rescale,{scale:m_scale},!0)}),this.layer().geoOn(geo.event.resize,function(event){m_this._resize(event.x,event.y,event.width,event.height)}),this._init(arg),this},inherit(geo.d3.d3Renderer,geo.renderer),geo.registerRenderer("d3",geo.d3.d3Renderer),geo.gui={},geo.gui.uiLayer=function(arg){"use strict";if(arg.renderer="d3",arg.sticky=!1,!(this instanceof geo.gui.uiLayer))return new geo.gui.uiLayer(arg);geo.layer.call(this,arg);var m_this=this,s_exit=this._exit;this.createWidget=function(widgetName,arg){var newWidget=geo.createWidget(widgetName,m_this,m_this.renderer(),arg);return m_this.addChild(newWidget),newWidget._init(),m_this.modified(),newWidget},this.deleteWidget=function(widget){return widget._exit(),m_this.removeChild(widget),m_this.modified(),m_this},this._exit=function(){m_this.children().forEach(function(child){m_this.deleteWidget(child)}),s_exit()}},inherit(geo.gui.uiLayer,geo.layer),geo.registerLayer("ui",geo.gui.uiLayer),geo.gui.widget=function(arg){"use strict";if(!(this instanceof geo.gui.widget))return new geo.gui.widget(arg);geo.sceneObject.call(this,arg);var m_this=this,s_exit=this._exit,m_layer=arg.layer;this._init=function(){m_this.modified()},this._exit=function(){m_this.children().forEach(function(child){m_this._deleteFeature(child)}),s_exit()},this._createFeature=function(featureName,arg){var newFeature=geo.createFeature(featureName,m_this,m_this.renderer(),arg);return m_this.addChild(newFeature),m_this.modified(),newFeature},this._deleteFeature=function(feature){return m_this.removeChild(feature),feature._exit(),m_this},this.layer=function(){return m_layer}},inherit(geo.gui.widget,geo.sceneObject),geo.gui.sliderWidget=function(arg){"use strict";function put_icon(icon,base,cx,cy,size){var g=base.append("g"),s=size/1024;return g.append("g").append("g").attr("transform","translate("+cx+","+cy+") scale("+s+") translate(-512,-512)").append("path").attr("d",icon).attr("class","geo-glyphicon"),g}if(!(this instanceof geo.gui.sliderWidget))return new geo.gui.sliderWidget(arg);geo.gui.widget.call(this,arg);var m_xscale,m_yscale,m_plus,m_minus,m_track,m_nub,m_plusIcon,m_minusIcon,m_group,m_lowContrast,m_this=this,s_exit=this._exit,m_width=20,m_height=100,m_nubSize=10,m_highlightDur=100;m_plusIcon="M512 81.92c-237.568 0-430.080 192.614-430.080 430.080 0 237.568 192.563 430.080 430.080 430.080s430.080-192.563 430.080-430.080c0-237.517-192.563-430.080-430.080-430.080zM564.326 564.326v206.182h-104.653v-206.182h-206.234v-104.653h206.182v-206.234h104.704v206.182h206.182v104.704h-206.182z",m_minusIcon="M512 81.92c-237.568 0-430.080 192.614-430.080 430.080 0 237.568 192.563 430.080 430.080 430.080s430.080-192.563 430.080-430.080c0-237.517-192.563-430.080-430.080-430.080zM770.56 459.674v104.704h-517.12v-104.704h517.12z",m_lowContrast={white:"#f4f4f4",black:"#505050"},this._init=function(){function respond(evt,trans){var z=m_yscale.invert(d3.mouse(m_this.layer().node()[0])[1]),zrange=map.zoomRange();z=(1-z)*(zrange.max-zrange.min)+zrange.min,trans?map.transition({zoom:z,ease:d3.ease("cubic-in-out"),duration:500,done:m_this._update()}):(map.zoom(z),m_this._update()),evt.stopPropagation()}var svg=m_this.layer().renderer().canvas(),x0=40,y0=40+m_width,map=m_this.layer().map();m_xscale=d3.scale.linear().domain([-4,4]).range([x0,x0+m_width]),m_yscale=d3.scale.linear().domain([0,1]).range([y0,y0+m_height]),svg=svg.append("g").classed("geo-ui-slider",!0),m_group=svg,m_plus=svg.append("g"),m_plus.append("circle").datum({fill:"white",stroke:null}).classed("geo-zoom-in",!0).attr("cx",m_xscale(0)).attr("cy",m_yscale(0)-m_width+2).attr("r",m_width/2).style({cursor:"pointer"}).on("click",function(){var z=map.zoom();map.transition({zoom:z+1,ease:d3.ease("cubic-in-out"),duration:500})}).on("mousedown",function(){d3.event.stopPropagation()}),put_icon(m_plusIcon,m_plus,m_xscale(0),m_yscale(0)-m_width+2,m_width+5).style("cursor","pointer").style("pointer-events","none").select("path").datum({fill:"black",stroke:null}),m_minus=svg.append("g"),m_minus.append("circle").datum({fill:"white",stroke:null}).classed("geo-zoom-out",!0).attr("cx",m_xscale(0)).attr("cy",m_yscale(1)+m_width-2).attr("r",m_width/2).style({cursor:"pointer"}).on("click",function(){var z=map.zoom();map.transition({zoom:z-1,ease:d3.ease("cubic-in-out"),duration:500})}).on("mousedown",function(){d3.event.stopPropagation()}),put_icon(m_minusIcon,m_minus,m_xscale(0),m_yscale(1)+m_width-2,m_width+5).style("cursor","pointer").style("pointer-events","none").select("path").datum({fill:"black",stroke:null}),m_track=svg.append("rect").datum({fill:"white",stroke:"black"}).classed("geo-zoom-track",!0).attr("x",m_xscale(0)-m_width/6).attr("y",m_yscale(0)).attr("rx",m_width/10).attr("ry",m_width/10).attr("width",m_width/3).attr("height",m_height).style({cursor:"pointer"}).on("click",function(){respond(d3.event,!0)}),m_nub=svg.append("rect").datum({fill:"black",stroke:null}).classed("geo-zoom-nub",!0).attr("x",m_xscale(-4)).attr("y",m_yscale(.5)-m_nubSize/2).attr("rx",3).attr("ry",3).attr("width",m_width).attr("height",m_nubSize).style({cursor:"pointer"}).on("mousedown",function(){d3.select(document).on("mousemove.geo.slider",function(){respond(d3.event)}),d3.select(document).on("mouseup.geo.slider",function(){respond(d3.event),d3.select(document).on(".geo.slider",null)}),d3.event.stopPropagation()});var mouseOver=function(){d3.select(this).attr("filter","url(#geo-highlight)"),m_group.selectAll("rect,path,circle").transition().duration(m_highlightDur).style("fill",function(d){return d.fill||null}).style("stroke",function(d){return d.stroke||null})},mouseOut=function(){d3.select(this).attr("filter",null),m_group.selectAll("circle,rect,path").transition().duration(m_highlightDur).style("fill",function(d){return m_lowContrast[d.fill]||null}).style("stroke",function(d){return m_lowContrast[d.stroke]||null})};m_group.selectAll("*").on("mouseover",mouseOver).on("mouseout",mouseOut),m_this.layer().geoOn(geo.event.zoom,function(){m_this._update()}),mouseOut(),m_this._update()},this._exit=function(){m_group.remove(),m_this.layer().geoOff(geo.event.zoom),s_exit()},this._update=function(obj){var map=m_this.layer().map(),zoomRange=map.zoomRange(),zoom=map.zoom(),zoomScale=d3.scale.linear();obj=obj||{},zoom=obj.value||zoom,zoomScale.domain([zoomRange.min,zoomRange.max]).range([1,0]).clamp(!0),m_nub.attr("y",m_yscale(zoomScale(zoom))-m_nubSize/2)}},inherit(geo.gui.sliderWidget,geo.gui.widget),geo.registerWidget("d3","slider",geo.gui.sliderWidget),geo.gui.legendWidget=function(arg){"use strict";if(!(this instanceof geo.gui.legendWidget))return new geo.gui.legendWidget(arg);geo.gui.widget.call(this,arg);var m_this=this,m_categories=[],m_top=null,m_group=null,m_border=null,m_spacing=20,m_padding=12;this.categories=function(arg){return void 0===arg?m_categories.slice():(m_categories=arg.slice().map(function(d){return"line"===d.type&&(d.style.fill=!1,d.style.stroke=!0),d}),m_this.draw(),m_this)},this.size=function(){var height,width=1,test=m_this.layer().renderer().canvas().append("text").style("opacity",1e-6);return m_categories.forEach(function(d){test.text(d.name),width=Math.max(width,test.node().getBBox().width)}),test.remove(),height=m_spacing*(m_categories.length+1),{width:width+50,height:height}},this.draw=function(){function applyColor(selection){selection.style("fill",function(d){return d.style.fill||void 0===d.style.fill?d.style.fillColor:"none"}).style("fill-opacity",function(d){return void 0===d.style.fillOpacity?1:d.style.fillOpacity}).style("stroke",function(d){return d.style.stroke||void 0===d.style.stroke?d.style.strokeColor:"none"}).style("stroke-opacity",function(d){return void 0===d.style.strokeOpacity?1:d.style.strokeOpacity}).style("stroke-width",function(d){return void 0===d.style.strokeWidth?1.5:d.style.strokeWidth})}m_this._init(),m_border.attr("height",m_this.size().height+2*m_padding).style("display",null);var scale=m_this._scale(),labels=m_group.selectAll("g.geo-label").data(m_categories,function(d){return d.name}),g=labels.enter().append("g").attr("class","geo-label").attr("transform",function(d,i){return"translate(0,"+scale.y(i)+")"});return applyColor(g.filter(function(d){return"point"!==d.type&&"line"!==d.type}).append("rect").attr("x",0).attr("y",-6).attr("rx",5).attr("ry",5).attr("width",40).attr("height",12)),applyColor(g.filter(function(d){return"point"===d.type}).append("circle").attr("cx",20).attr("cy",0).attr("r",6)),applyColor(g.filter(function(d){return"line"===d.type}).append("line").attr("x1",0).attr("y1",0).attr("x2",40).attr("y2",0)),g.append("text").attr("x","50px").attr("y",0).attr("dy","0.3em").text(function(d){return d.name}),m_this},this._scale=function(){return{x:d3.scale.linear().domain([0,1]).range([0,m_this.size().width]),y:d3.scale.linear().domain([0,m_categories.length-1]).range([m_padding/2,m_this.size().height-m_padding/2])}},this._init=function(){var w=m_this.size().width+2*m_padding,h=m_this.size().height+2*m_padding,nw=m_this.layer().map().node().width(),margin=20;m_top&&m_top.remove(),m_top=m_this.layer().renderer().canvas().append("g").attr("transform","translate("+(nw-w-margin)+","+margin+")"),m_group=m_top.append("g").attr("transform","translate("+[m_padding-1.5,m_padding]+")"),m_border=m_group.append("rect").attr("x",-m_padding).attr("y",-m_padding).attr("width",w).attr("height",h).attr("rx",3).attr("ry",3).style({stroke:"black","stroke-width":"1.5px",fill:"white","fill-opacity":.75,display:"none"}),m_group.on("mousedown",function(){d3.event.stopPropagation()}),m_group.on("mouseover",function(){m_border.transition().duration(250).style("fill-opacity",1)}),m_group.on("mouseout",function(){m_border.transition().duration(250).style("fill-opacity",.75)})},this.geoOn(geo.event.resize,function(){this.draw()})},inherit(geo.gui.legendWidget,geo.gui.widget),geo.registerWidget("d3","legend",geo.gui.legendWidget),function($,geo,d3){"use strict";var load=function(){function isColorKey(key){return"color"===key.slice(key.length-5,key.length).toLowerCase()}function makeColorScale(data,acc){function wrap(func){return geo.util.isFunction(func)?function(){return func(acc.apply(this,arguments))}:func(acc)}if(!d3)return console.warn("d3 is unavailable, cannot apply color scales."),acc;var domain,cannotHandle=!1,doNotHandle=!0,categorical=!1,min=Number.POSITIVE_INFINITY,max=Number.NEGATIVE_INFINITY;return domain=geo.util.isFunction(acc)?d3.set(data.map(acc)).values():[acc],domain.forEach(function(v){("string"!=typeof v||"object"!=typeof geo.util.convertColor(v))&&(doNotHandle=!1),"string"==typeof v?categorical=!0:isFinite(v)?+v>max?max=+v:min>+v&&(min=+v):cannotHandle=!0}),cannotHandle?acc:doNotHandle?acc:wrap(categorical?domain.length<=10?d3.scale.category10().domain(domain):domain.length<=20?d3.scale.category20().domain(domain):d3.scale.category20().domain(domain):d3.scale.linear().range(["rgb(252,141,89)","rgb(255,255,191)","rgb(145,191,219)"]).domain([min,(min+max)/2,max]))}return $.widget?void $.widget("geojs.geojsMap",{options:{center:{latitude:0,longitude:0},zoom:0,width:null,height:null,layers:[],data:[],tileUrl:"http://tile.openstreetmap.org///.png",attribution:void 0,baseLayer:"osm",baseRenderer:"vgl"},_create:function(){!this._map&&this.element.length&&(this._map=geo.map({width:this.options.width,height:this.options.height,zoom:this.options.zoom,center:this.options.center,node:this.element.get(0)}),this._baseLayer=this._map.createLayer(this.options.baseLayer,{renderer:this.options.baseRenderer,tileUrl:this.options.tileUrl,attribution:this.options.attribution}),this._resize({width:800,height:600}),this._layers=[],this.update())},update:function(layers){var m_this=this;return this.options.layers=layers||this.options.layers||[],this._layers.forEach(function(layer){layer.clear(),m_this._map.deleteLayer(layer)}),this._layers=this.options.layers.map(function(layer){return layer.data=layer.data||m_this.options.data,(layer.features||[]).forEach(function(feature){var scl,data=feature.data||layer.data||[];"point"===feature.type&&(feature.size?feature._size=geo.util.ensureFunction(feature.size):null===feature.size&&delete feature._size,data.length&&feature._size&&(scl=d3.scale.linear().domain(d3.extent(data,feature._size)).range([5,20]),feature.radius=function(){return scl(feature._size.apply(this,arguments))}),delete feature.size);var key;for(key in feature)feature.hasOwnProperty(key)&&isColorKey(key)&&(feature[key]=makeColorScale(data,feature[key]))}),geo.layer.create(m_this._map,layer)}),this.redraw(),this},map:function(){return this._map},tileUrl:function(url){return this._baseLayer.tileUrl(url),this._baseLayer.updateBaseUrl(),this},_resize:function(size){var width=this.options.width,height=this.options.height;size&&(width=size.width,height=size.height),width||(width=this.element.width()),height||(height=this.element.height()),this._map.resize(0,0,width,height)},redraw:function(){return this._resize(),this}}):void($.fn.geojsMap=function(){throw new Error("The geojs jquery plugin requires jquery ui to be available.")})};$(load)}($||window.$,geo||window.geo,d3||window.d3); \ No newline at end of file +function inherit(C,P){"use strict";var F=function(){};F.prototype=P.prototype,C.prototype=new F,C.uber=P.prototype,C.prototype.constructor=C}var geo={};if(window.geo=geo,geo.renderers={},geo.features={},geo.fileReaders={},geo.rendererLayerAdjustments={},geo.inherit=function(C,P){"use strict";var F=inherit.func();F.prototype=P.prototype,C.prototype=new F,C.prototype.constructor=C},geo.inherit.func=function(){"use strict";return function(){}},window.inherit=geo.inherit,geo.extend=function(props){"use strict";var child=Object.create(this.prototype);return $.extend(child.prototype,props||{}),child},geo.registerFileReader=function(name,func){"use strict";void 0===geo.fileReaders&&(geo.fileReaders={}),geo.fileReaders[name]=func},geo.createFileReader=function(name,opts){"use strict";return geo.fileReaders.hasOwnProperty(name)?geo.fileReaders[name](opts):null},geo.registerRenderer=function(name,func){"use strict";void 0===geo.renderers&&(geo.renderers={}),geo.renderers[name]=func},geo.createRenderer=function(name,layer,canvas,options){"use strict";if(geo.renderers.hasOwnProperty(name)){var ren=geo.renderers[name]({layer:layer,canvas:canvas,options:options});return ren._init(),ren}return null},geo.registerFeature=function(category,name,func){"use strict";void 0===geo.features&&(geo.features={}),category in geo.features||(geo.features[category]={}),geo.features[category][name]=func},geo.createFeature=function(name,layer,renderer,arg){"use strict";var category=renderer.api(),options={layer:layer,renderer:renderer};if(category in geo.features&&name in geo.features[category]){void 0!==arg&&$.extend(!0,options,arg);var feature=geo.features[category][name](options);return layer.gcs=function(){return layer.map().gcs()},feature}return null},geo.registerLayerAdjustment=function(category,name,func){"use strict";void 0===geo.rendererLayerAdjustments&&(geo.rendererLayerAdjustments={}),category in geo.rendererLayerAdjustments||(geo.rendererLayerAdjustments[category]={}),geo.rendererLayerAdjustments[category][name]=func},geo.adjustLayerForRenderer=function(name,layer){"use strict";var rendererName=layer.rendererName();rendererName&&geo.rendererLayerAdjustments&&geo.rendererLayerAdjustments[rendererName]&&geo.rendererLayerAdjustments[rendererName][name]&&geo.rendererLayerAdjustments[rendererName][name].apply(layer)},geo.registerLayer=function(name,func){"use strict";void 0===geo.layers&&(geo.layers={}),geo.layers[name]=func},geo.createLayer=function(name,map,arg){"use strict";var options={map:map,renderer:"vgl"},layer=null;return name in geo.layers?(void 0!==arg&&$.extend(!0,options,arg),layer=geo.layers[name](options),layer._init(),layer):null},geo.registerWidget=function(category,name,func){"use strict";void 0===geo.widgets&&(geo.widgets={}),category in geo.widgets||(geo.widgets[category]={}),geo.widgets[category][name]=func},geo.createWidget=function(name,layer,arg){"use strict";var options={layer:layer};if(name in geo.widgets.dom)return void 0!==arg&&$.extend(!0,options,arg),geo.widgets.dom[name](options);throw new Error("Cannot create unknown widget "+name)},window.requestAnimationFrame||(window.requestAnimationFrame=function(func){"use strict";window.setTimeout(func,15)}),Math.log2||(Math.log2=function(){"use strict";return Math.log.apply(Math,arguments)/Math.LN2}),Math.sinh=Math.sinh||function(x){"use strict";var y=Math.exp(x);return(y-1/y)/2},geo.version="0.6.0-rc.1","undefined"==typeof ogs)var ogs={};ogs.namespace=function(ns_string){"use strict";var i,parts=ns_string.split("."),parent=ogs;for("ogs"===parts[0]&&(parts=parts.slice(1)),i=0;ithis.computeBoundsTimestamp().getMTime()&&this.m_parent.boundsDirtyTimestamp.modified(),this.computeBounds(),visitor.mode()===visitor.TraverseAllChildren)for(i=0;ithis.boundsDirtyTimestamp().getMTime()))for(i=0;ii;i+=1)istep=2*i,jstep=2*i+1,childBounds[istep]bounds[jstep]&&(bounds[jstep]=childBounds[jstep]);this.setBounds(bounds[0],bounds[1],bounds[2],bounds[3],bounds[4],bounds[5])}},this},inherit(vgl.groupNode,vgl.node),vgl.actor=function(){"use strict";if(!(this instanceof vgl.actor))return new vgl.actor;vgl.node.call(this);var m_this=this,m_transformMatrix=mat4.create(),m_referenceFrame=vgl.boundingObject.ReferenceFrame.Relative,m_mapper=null;return this.matrix=function(){return m_transformMatrix},this.setMatrix=function(tmatrix){tmatrix!==m_transformMatrix&&(m_transformMatrix=tmatrix,m_this.modified())},this.referenceFrame=function(){return m_referenceFrame},this.setReferenceFrame=function(referenceFrame){return referenceFrame!==m_referenceFrame?(m_referenceFrame=referenceFrame,m_this.modified(),!0):!1},this.mapper=function(){return m_mapper},this.setMapper=function(mapper){mapper!==m_mapper&&(m_mapper=mapper,m_this.boundsModified())},this.accept=function(visitor){visitor=visitor},this.ascend=function(visitor){visitor=visitor},this.computeLocalToWorldMatrix=function(matrix,visitor){matrix=matrix,visitor=visitor},this.computeWorldToLocalMatrix=function(matrix,visitor){matrix=matrix,visitor=visitor},this.computeBounds=function(){if(null===m_mapper||void 0===m_mapper)return void m_this.resetBounds();var mapperBounds,minPt,maxPt,newBounds,computeBoundsTimestamp=m_this.computeBoundsTimestamp();(m_this.boundsDirtyTimestamp().getMTime()>computeBoundsTimestamp.getMTime()||m_mapper.boundsDirtyTimestamp().getMTime()>computeBoundsTimestamp.getMTime())&&(m_mapper.computeBounds(),mapperBounds=m_mapper.bounds(),minPt=[mapperBounds[0],mapperBounds[2],mapperBounds[4]],maxPt=[mapperBounds[1],mapperBounds[3],mapperBounds[5]],vec3.transformMat4(minPt,minPt,m_transformMatrix),vec3.transformMat4(maxPt,maxPt,m_transformMatrix),newBounds=[minPt[0]>maxPt[0]?maxPt[0]:minPt[0],minPt[0]>maxPt[0]?minPt[0]:maxPt[0],minPt[1]>maxPt[1]?maxPt[1]:minPt[1],minPt[1]>maxPt[1]?minPt[1]:maxPt[1],minPt[2]>maxPt[2]?maxPt[2]:minPt[2],minPt[2]>maxPt[2]?minPt[2]:maxPt[2]],m_this.setBounds(newBounds[0],newBounds[1],newBounds[2],newBounds[3],newBounds[4],newBounds[5]),computeBoundsTimestamp.modified())},m_this},inherit(vgl.actor,vgl.node),vgl.freezeObject=function(obj){"use strict";var freezedObject=Object.freeze(obj);return"undefined"==typeof freezedObject&&(freezedObject=function(o){return o}),freezedObject},vgl.defaultValue=function(a,b){"use strict";return"undefined"!=typeof a?a:b},vgl.defaultValue.EMPTY_OBJECT=vgl.freezeObject({}),vgl.graphicsObject=function(type){"use strict";if(type=type,!(this instanceof vgl.graphicsObject))return new vgl.graphicsObject;vgl.object.call(this);var m_this=this;return this._setup=function(renderState){return renderState=renderState,!1},this._cleanup=function(renderState){return renderState=renderState,!1},this.bind=function(renderState){return renderState=renderState,!1},this.undoBind=function(renderState){return renderState=renderState,!1},this.render=function(renderState){return renderState=renderState,!1},this.remove=function(renderState){m_this._cleanup(renderState)},m_this},inherit(vgl.graphicsObject,vgl.object),vgl.geojsonReader=function(){"use strict";return this instanceof vgl.geojsonReader?(this.readScalars=function(coordinates,geom,size_estimate,idx){var array=null,s=null,r=null,g=null,b=null;"values"===this.m_scalarFormat&&4===coordinates.length?(s=coordinates[3],array=geom.sourceData(vgl.vertexAttributeKeys.Scalar),array||(array=new vgl.sourceDataSf,this.m_scalarRange&&array.setScalarRange(this.m_scalarRange[0],this.m_scalarRange[1]),void 0!==size_estimate&&(array.data().length=size_estimate),geom.addSource(array)),void 0===size_estimate?array.pushBack(s):array.insertAt(idx,s)):"rgb"===this.m_scalarFormat&&6===coordinates.length&&(array=geom.sourceData(vgl.vertexAttributeKeys.Color),array||(array=new vgl.sourceDataC3fv,void 0!==size_estimate&&(array.length=3*size_estimate),geom.addSource(array)),r=coordinates[3],g=coordinates[4],b=coordinates[5],void 0===size_estimate?array.pushBack([r,g,b]):array.insertAt(idx,[r,g,b]))},this.readPoint=function(coordinates){var geom=new vgl.geometryData,vglpoints=new vgl.points,vglcoords=new vgl.sourceDataP3fv,indices=new Uint16Array(1),x=null,y=null,z=null,i=null;for(geom.addSource(vglcoords),i=0;1>i;i+=1)indices[i]=i,x=coordinates[0],y=coordinates[1],z=0,coordinates.length>2&&(z=coordinates[2]),vglcoords.pushBack([x,y,z]),this.readScalars(coordinates,geom);return vglpoints.setIndices(indices),geom.addPrimitive(vglpoints),geom.setName("aPoint"),geom},this.readMultiPoint=function(coordinates){var i,geom=new vgl.geometryData,vglpoints=new vgl.points,vglcoords=new vgl.sourceDataP3fv,indices=new Uint16Array(coordinates.length),pntcnt=0,estpntcnt=coordinates.length,x=null,y=null,z=null;for(vglcoords.data().length=3*estpntcnt,i=0;i2&&(z=coordinates[i][2]),vglcoords.insertAt(pntcnt,[x,y,z]),this.readScalars(coordinates[i],geom,estpntcnt,pntcnt),pntcnt+=1;return vglpoints.setIndices(indices),geom.addPrimitive(vglpoints),geom.addSource(vglcoords),geom.setName("manyPoints"),geom},this.readLineString=function(coordinates){var geom=new vgl.geometryData,vglline=new vgl.lineStrip,vglcoords=new vgl.sourceDataP3fv,indices=[],i=null,x=null,y=null,z=null;for(vglline.setIndicesPerPrimitive(coordinates.length),i=0;i2&&(z=coordinates[i][2]),vglcoords.pushBack([x,y,z]),this.readScalars(coordinates[i],geom);return vglline.setIndices(indices),geom.addPrimitive(vglline),geom.addSource(vglcoords),geom.setName("aLineString"),geom},this.readMultiLineString=function(coordinates){var geom=new vgl.geometryData,vglcoords=new vgl.sourceDataP3fv,pntcnt=0,estpntcnt=2*coordinates.length,i=null,j=null,x=null,y=null,z=null,indices=null,vglline=null,thisLineLength=null;for(vglcoords.data().length=3*estpntcnt,j=0;ji;i+=1)indices.push(pntcnt),x=coordinates[j][i][0],y=coordinates[j][i][1],z=0,coordinates[j][i].length>2&&(z=coordinates[j][i][2]),vglcoords.insertAt(pntcnt,[x,y,z]),this.readScalars(coordinates[j][i],geom,2*estpntcnt,pntcnt),pntcnt+=1;vglline.setIndices(indices),geom.addPrimitive(vglline)}return geom.setName("aMultiLineString"),geom.addSource(vglcoords),geom},this.readPolygon=function(coordinates){var geom=new vgl.geometryData,vglcoords=new vgl.sourceDataP3fv,x=null,y=null,z=null,thisPolyLength=coordinates[0].length,vl=1,i=null,indices=null,vgltriangle=null;for(i=0;thisPolyLength>i;i+=1)x=coordinates[0][i][0],y=coordinates[0][i][1],z=0,coordinates[0][i].length>2&&(z=coordinates[0][i][2]),vglcoords.pushBack([x,y,z]),this.readScalars(coordinates[0][i],geom),i>1&&(indices=new Uint16Array([0,vl,i]),vgltriangle=new vgl.triangles,vgltriangle.setIndices(indices),geom.addPrimitive(vgltriangle),vl=i);return geom.setName("POLY"),geom.addSource(vglcoords),geom},this.readMultiPolygon=function(coordinates){var geom=new vgl.geometryData,vglcoords=new vgl.sourceDataP3fv,ccount=0,numPolys=coordinates.length,pntcnt=0,estpntcnt=3*numPolys,vgltriangle=new vgl.triangles,indexes=[],i=null,j=null,x=null,y=null,z=null,thisPolyLength=null,vf=null,vl=null,flip=null,flipped=!1,tcount=0;for(vglcoords.data().length=3*numPolys,j=0;numPolys>j;j+=1)for(thisPolyLength=coordinates[j][0].length,vf=ccount,vl=ccount+1,flip=[!1,!1,!1],i=0;thisPolyLength>i;i+=1)x=coordinates[j][0][i][0],y=coordinates[j][0][i][1],z=0,coordinates[j][0][i].length>2&&(z=coordinates[j][0][i][2]),flipped=!1,x>180&&(flipped=!0,x-=360),0===i?flip[0]=flipped:flip[1+(i-1)%2]=flipped,vglcoords.insertAt(pntcnt,[x,y,z]),this.readScalars(coordinates[j][0][i],geom,estpntcnt,pntcnt),pntcnt+=1,i>1&&(flip[0]===flip[1]&&flip[1]===flip[2]&&(indexes[3*tcount+0]=vf,indexes[3*tcount+1]=vl,indexes[3*tcount+2]=ccount,tcount+=1),vl=ccount),ccount+=1;return vgltriangle.setIndices(indexes),geom.addPrimitive(vgltriangle),geom.setName("aMultiPoly"),geom.addSource(vglcoords),geom},this.readGJObjectInt=function(object){if(!object.hasOwnProperty("type"))return null;object.properties&&object.properties.ScalarFormat&&"values"===object.properties.ScalarFormat&&(this.m_scalarFormat="values",object.properties.ScalarRange&&(this.m_scalarRange=object.properties.ScalarRange)),object.properties&&object.properties.ScalarFormat&&"rgb"===object.properties.ScalarFormat&&(this.m_scalarFormat="rgb");var ret,type=object.type,next=null,nextset=null,i=null;switch(type){case"Point":ret=this.readPoint(object.coordinates);break;case"MultiPoint":ret=this.readMultiPoint(object.coordinates);break;case"LineString":ret=this.readLineString(object.coordinates);break;case"MultiLineString":ret=this.readMultiLineString(object.coordinates);break;case"Polygon":ret=this.readPolygon(object.coordinates);break;case"MultiPolygon":ret=this.readMultiPolygon(object.coordinates);break;case"GeometryCollection":for(nextset=[],i=0;im_max)&&(m_max=value),(null===m_min||m_min>value)&&(m_min=value),this.data()[this.data().length]=value},this.insertAt=function(index,value){(null===m_max||value>m_max)&&(m_max=value),(null===m_min||m_min>value)&&(m_min=value),this.data()[index]=value},this.scalarRange=function(){return null===m_fixedmin||null===m_fixedmax?[m_min,m_max]:[m_fixedmin,m_fixedmax]},this.setScalarRange=function(min,max){m_fixedmin=min,m_fixedmax=max},this},inherit(vgl.sourceDataSf,vgl.sourceData),vgl.sourceDataDf=function(arg){"use strict";return this instanceof vgl.sourceDataDf?(vgl.sourceData.call(this,arg),this.addAttribute(vgl.vertexAttributeKeys.Scalar,vgl.GL.FLOAT,4,0,4,1,!1),this.pushBack=function(value){this.data()[this.data().length]=value},this.insertAt=function(index,value){this.data()[index]=value},this):new vgl.sourceDataDf(arg)},inherit(vgl.sourceDataDf,vgl.sourceData),vgl.geometryData=function(){"use strict";if(!(this instanceof vgl.geometryData))return new vgl.geometryData;vgl.data.call(this);var m_name="",m_primitives=[],m_sources=[],m_bounds=[0,0,0,0,0,0],m_computeBoundsTimestamp=vgl.timestamp(),m_boundsDirtyTimestamp=vgl.timestamp();return this.type=function(){return vgl.data.geometry},this.name=function(){return m_name},this.setName=function(name){m_name=name},this.addSource=function(source,sourceName){return void 0!==sourceName&&source.setName(sourceName),-1===m_sources.indexOf(source)?(m_sources.push(source),source.hasKey(vgl.vertexAttributeKeys.Position)&&m_boundsDirtyTimestamp.modified(),!0):!1},this.source=function(index){return indexm_computeBoundsTimestamp.getMTime()&&this.computeBounds(),m_bounds},this.boundsDirty=function(dirty){return dirty&&m_boundsDirtyTimestamp.modified(),m_boundsDirtyTimestamp.getMTime()>m_computeBoundsTimestamp.getMTime()},this.resetBounds=function(){m_bounds[0]=0,m_bounds[1]=0,m_bounds[2]=0,m_bounds[3]=0,m_bounds[4]=0,m_bounds[5]=0},this.setBounds=function(minX,maxX,minY,maxY,minZ,maxZ){return m_bounds[0]=minX,m_bounds[1]=maxX,m_bounds[2]=minY,m_bounds[3]=maxY,m_bounds[4]=minZ,m_bounds[5]=maxZ,m_computeBoundsTimestamp.modified(),!0},this.computeBounds=function(){if(m_boundsDirtyTimestamp.getMTime()>m_computeBoundsTimestamp.getMTime()){var j,ib,jb,maxv,minv,vertexIndex,attr=vgl.vertexAttributeKeys.Position,sourceData=this.sourceData(attr),data=sourceData.data(),numberOfComponents=sourceData.attributeNumberOfComponents(attr),stride=sourceData.attributeStride(attr),offset=sourceData.attributeOffset(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),count=data.length,value=null;for(stride/=sizeOfDataType,offset/=sizeOfDataType,this.resetBounds(),j=0;numberOfComponents>j;j+=1){for(ib=2*j,jb=2*j+1,maxv=minv=count?m_bounds[jb]=data[offset+j]:0,vertexIndex=offset+stride+j;count>vertexIndex;vertexIndex+=stride)value=data[vertexIndex],value>maxv&&(maxv=value),minv>value&&(minv=value);m_bounds[ib]=minv,m_bounds[jb]=maxv}m_computeBoundsTimestamp.modified()}},this.findClosestVertex=function(point){var vi,vPos,dx,dy,dz,dist,i,attr=vgl.vertexAttributeKeys.Position,sourceData=this.sourceData(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),numberOfComponents=sourceData.attributeNumberOfComponents(attr),data=sourceData.data(),stride=sourceData.attributeStride(attr)/sizeOfDataType,offset=sourceData.attributeOffset(attr)/sizeOfDataType,minDist=Number.MAX_VALUE,minIndex=null;for(3!==numberOfComponents&&console.log("[warning] Find closest vertex assumes threecomponent vertex "),point.z||(point={x:point.x,y:point.y,z:0}),vi=offset,i=0;vidist&&(minDist=dist,minIndex=i);return minIndex},this.getPosition=function(index){var attr=vgl.vertexAttributeKeys.Position,sourceData=this.sourceData(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),numberOfComponents=sourceData.attributeNumberOfComponents(attr),data=sourceData.data(),stride=sourceData.attributeStride(attr)/sizeOfDataType,offset=sourceData.attributeOffset(attr)/sizeOfDataType;return 3!==numberOfComponents&&console.log("[warning] getPosition assumes three component data"),[data[offset+index*stride],data[offset+index*stride+1],data[offset+index*stride+2]]},this.getScalar=function(index){var numberOfComponents,sizeOfDataType,data,stride,offset,attr=vgl.vertexAttributeKeys.Scalar,sourceData=this.sourceData(attr);return sourceData?(numberOfComponents=sourceData.attributeNumberOfComponents(attr),sizeOfDataType=sourceData.sizeOfAttributeDataType(attr),data=sourceData.data(),stride=sourceData.attributeStride(attr)/sizeOfDataType,offset=sourceData.attributeOffset(attr)/sizeOfDataType,index*stride+offset>=data.length&&console.log("access out of bounds in getScalar"),data[index*stride+offset]):null},this},inherit(vgl.geometryData,vgl.data),vgl.mapper=function(arg){"use strict";function deleteVertexBufferObjects(renderState){var i;for(i=0;ii;i+=1){for(bufferId=m_context.createBuffer(),m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,bufferId),data=m_geomData.source(i).data(),data instanceof Float32Array||(data=new Float32Array(data)),m_context.bufferData(vgl.GL.ARRAY_BUFFER,data,m_dynamicDraw?vgl.GL.DYNAMIC_DRAW:vgl.GL.STATIC_DRAW),keys=m_geomData.source(i).keys(),ks=[],j=0;jk;k+=1)bufferId=m_context.createBuffer(),m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,bufferId),m_context.bufferData(vgl.GL.ARRAY_BUFFER,m_geomData.primitive(k).indices(),vgl.GL.STATIC_DRAW),m_buffers[i]=bufferId,i+=1;m_glCompileTimestamp.modified()}}function cleanUpDrawObjects(renderState){renderState=renderState,m_bufferVertexAttributeMap={},m_buffers=[]}function setupDrawObjects(renderState){deleteVertexBufferObjects(renderState),cleanUpDrawObjects(renderState),createVertexBufferObjects(renderState),m_dirty=!1}if(!(this instanceof vgl.mapper))return new vgl.mapper(arg);vgl.boundingObject.call(this),arg=arg||{};var m_dirty=!0,m_color=[0,1,1],m_geomData=null,m_buffers=[],m_bufferVertexAttributeMap={},m_dynamicDraw=void 0===arg.dynamicDraw?!1:arg.dynamicDraw,m_glCompileTimestamp=vgl.timestamp(),m_context=null;return this.computeBounds=function(){if(null===m_geomData||"undefined"==typeof m_geomData)return void this.resetBounds();var computeBoundsTimestamp=this.computeBoundsTimestamp(),boundsDirtyTimestamp=this.boundsDirtyTimestamp(),geomBounds=null;boundsDirtyTimestamp.getMTime()>computeBoundsTimestamp.getMTime()&&(geomBounds=m_geomData.bounds(),this.setBounds(geomBounds[0],geomBounds[1],geomBounds[2],geomBounds[3],geomBounds[4],geomBounds[5]),computeBoundsTimestamp.modified())},this.color=function(){return m_color},this.setColor=function(r,g,b){m_color[0]=r,m_color[1]=g,m_color[2]=b,this.modified()},this.geometryData=function(){return m_geomData},this.setGeometryData=function(geom){m_geomData!==geom&&(m_geomData=geom,this.modified(),this.boundsDirtyTimestamp().modified())},this.updateSourceBuffer=function(sourceName,values,renderState){if(renderState&&(m_context=renderState.m_context),!m_context)return!1;for(var bufferIndex=-1,i=0;ibufferIndex||bufferIndex>=m_buffers.length?!1:(values||(values=m_geomData.source(i).dataToFloat32Array()),m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,m_buffers[bufferIndex]),values instanceof Float32Array?m_context.bufferSubData(vgl.GL.ARRAY_BUFFER,0,values):m_context.bufferSubData(vgl.GL.ARRAY_BUFFER,0,new Float32Array(values)),!0)},this.getSourceBuffer=function(sourceName){var source=m_geomData.sourceByName(sourceName);return source?source.dataToFloat32Array():new Float32Array},this.render=function(renderState){(this.getMTime()>m_glCompileTimestamp.getMTime()||renderState.m_contextChanged)&&setupDrawObjects(renderState),m_context=renderState.m_context,m_context.vertexAttrib3fv(vgl.vertexAttributeKeys.Color,this.color());var i,bufferIndex=0,j=0,noOfPrimitives=null,primitive=null;for(i in m_bufferVertexAttributeMap)if(m_bufferVertexAttributeMap.hasOwnProperty(i)){for(m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,m_buffers[bufferIndex]),j=0;jj;j+=1){switch(m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,m_buffers[bufferIndex]),bufferIndex+=1,primitive=m_geomData.primitive(j),primitive.primitiveType()){case vgl.GL.POINTS:m_context.drawArrays(vgl.GL.POINTS,0,primitive.numberOfIndices());break;case vgl.GL.LINES:m_context.drawArrays(vgl.GL.LINES,0,primitive.numberOfIndices());break;case vgl.GL.LINE_STRIP:m_context.drawArrays(vgl.GL.LINE_STRIP,0,primitive.numberOfIndices());break;case vgl.GL.TRIANGLES:m_context.drawArrays(vgl.GL.TRIANGLES,0,primitive.numberOfIndices());break;case vgl.GL.TRIANGLE_STRIP:m_context.drawArrays(vgl.GL.TRIANGLE_STRIP,0,primitive.numberOfIndices())}m_context.bindBuffer(vgl.GL.ARRAY_BUFFER,null)}},this},inherit(vgl.mapper,vgl.boundingObject),vgl.groupMapper=function(){"use strict";if(!(this instanceof vgl.groupMapper))return new vgl.groupMapper;vgl.mapper.call(this);var m_createMappersTimestamp=vgl.timestamp(),m_mappers=[],m_geomDataArray=[];return this.geometryData=function(index){return void 0!==index&&index0?m_geomDataArray[0]:null},this.setGeometryData=function(geom){(1!==m_geomDataArray.length||m_geomDataArray[0]!==geom)&&(m_geomDataArray=[],m_geomDataArray.push(geom),this.modified())},this.geometryDataArray=function(){return m_geomDataArray},this.setGeometryDataArray=function(geoms){if(geoms instanceof Array){if(m_geomDataArray!==geoms)return m_geomDataArray=[],m_geomDataArray=geoms,this.modified(),!0}else console.log("[error] Requies array of geometry data");return!1},this.computeBounds=function(){if(null===m_geomDataArray||void 0===m_geomDataArray)return void this.resetBounds();var computeBoundsTimestamp=this.computeBoundsTimestamp(),boundsDirtyTimestamp=this.boundsDirtyTimestamp(),m_bounds=this.bounds(),geomBounds=null,i=null;if(boundsDirtyTimestamp.getMTime()>computeBoundsTimestamp.getMTime()){for(i=0;igeomBounds[0]&&(m_bounds[0]=geomBounds[0]),m_bounds[1]geomBounds[2]&&(m_bounds[2]=geomBounds[2]),m_bounds[3]geomBounds[4]&&(m_bounds[4]=geomBounds[4]),m_bounds[5]m_createMappersTimestamp.getMTime()){for(i=0;i0&&m_this.m_resetScene&&(m_this.resetCamera(),m_this.m_resetScene=!1),i=0;i=0&&sortedActors.push([actor.material().binNumber(),actor]);for(sortedActors.sort(function(a,b){return a[0]-b[0]}),i=0;idiagonals[1]?diagonals[0]>diagonals[2]?diagonals[0]/2:diagonals[2]/2:diagonals[1]>diagonals[2]?diagonals[1]/2:diagonals[2]/2,angle=aspect>=1?2*Math.atan(Math.tan(.5*angle)/aspect):2*Math.atan(Math.tan(.5*angle)*aspect),distance=radius/Math.sin(.5*angle),vup=m_this.m_camera.viewUpDirection(),Math.abs(vec3.dot(vup,vn))>.999&&m_this.m_camera.setViewUpDirection(-vup[2],vup[0],vup[1]),m_this.m_camera.setFocalPoint(center[0],center[1],center[2]),m_this.m_camera.setPosition(center[0]+distance*-vn[0],center[1]+distance*-vn[1],center[2]+distance*-vn[2]),m_this.resetCameraClippingRange(visibleBounds)},this.hasValidBounds=function(bounds){return bounds[0]===Number.MAX_VALUE||bounds[1]===-Number.MAX_VALUE||bounds[2]===Number.MAX_VALUE||bounds[3]===-Number.MAX_VALUE||bounds[4]===Number.MAX_VALUE||bounds[5]===-Number.MAX_VALUE?!1:!0},this.resetCameraClippingRange=function(bounds){if("undefined"==typeof bounds&&(m_this.m_camera.computeBounds(),bounds=m_this.m_camera.bounds()),m_this.hasValidBounds(bounds)){var vn=m_this.m_camera.viewPlaneNormal(),position=m_this.m_camera.position(),a=-vn[0],b=-vn[1],c=-vn[2],d=-(a*position[0]+b*position[1]+c*position[2]),range=vec2.create(),dist=null,i=null,j=null,k=null;if(m_this.m_resetClippingRange){for(range[0]=a*bounds[0]+b*bounds[2]+c*bounds[4]+d,range[1]=1e-18,k=0;2>k;k+=1)for(j=0;2>j;j+=1)for(i=0;2>i;i+=1)dist=a*bounds[i]+b*bounds[2+j]+c*bounds[4+k]+d,range[0]=distrange[1]?dist:range[1];range[0]<0&&(range[0]=0),range[0]=.99*range[0]-.5*(range[1]-range[0]),range[1]=1.01*range[1]+.5*(range[1]-range[0]),range[0]=range[0]>=range[1]?.01*range[1]:range[0],m_this.m_nearClippingPlaneTolerance||(m_this.m_nearClippingPlaneTolerance=.01,m_this.m_depthBits&&m_this.m_depthBits>16&&(m_this.m_nearClippingPlaneTolerance=.001)),range[0]x||0>y||0>=width||0>=height)return void console.log("[error] Invalid position and resize values",x,y,width,height);if(m_this.m_resizable&&(m_this.m_width=width,m_this.m_height=height,m_this.m_camera.setViewAspect(width/height),m_this.m_camera.setParallelExtents({width:width,height:height}),m_this.modified()),m_this.m_renderPasses)for(i=0;im_width||0===m_renderers[i].width()||m_renderers[i].height()>m_height||0===m_renderers[i].height())&&m_renderers[i].resize(m_x,m_y,m_width,m_height);return!0}catch(e){}return m_context||console("[ERROR] Unable to initialize WebGL. Your browser may not support it."),!1},this.context=function(){return m_context},this._cleanup=function(renderState){var i;for(i=0;iwidth?(m_currPos.x=0,m_outsideCanvas=!0):m_currPos.x=coords.x,coords.y<0||coords.y>height?(m_currPos.y=0,m_outsideCanvas=!0):m_currPos.y=coords.y,m_outsideCanvas!==!0?(fp=cam.focalPoint(),fwp=vec4.fromValues(fp[0],fp[1],fp[2],1),fdp=ren.worldToDisplay(fwp,cam.viewMatrix(),cam.projectionMatrix(),width,height),dp1=vec4.fromValues(m_currPos.x,m_currPos.y,fdp[2],1),dp2=vec4.fromValues(m_lastPos.x,m_lastPos.y,fdp[2],1),wp1=ren.displayToWorld(dp1,cam.viewMatrix(),cam.projectionMatrix(),width,height),wp2=ren.displayToWorld(dp2,cam.viewMatrix(),cam.projectionMatrix(),width,height),dx=wp1[0]-wp2[0],dy=wp1[1]-wp2[1],dz=wp1[2]-wp2[2],m_midMouseBtnDown&&(cam.pan(-dx,-dy,-dz),m_that.viewer().render()),m_leftMouseBtnDown&&(cam.rotate(m_lastPos.x-m_currPos.x,m_lastPos.y-m_currPos.y),ren.resetCameraClippingRange(),m_that.viewer().render()),m_rightMouseBtnDown&&(m_zTrans=2*(m_currPos.y-m_lastPos.y)/height,m_zTrans>0?cam.zoom(1-Math.abs(m_zTrans)):cam.zoom(1+Math.abs(m_zTrans)),ren.resetCameraClippingRange(),m_that.viewer().render()),m_lastPos.x=m_currPos.x,m_lastPos.y=m_currPos.y,!1):void 0},this.handleMouseDown=function(event){var coords;return 0===event.button&&(m_leftMouseBtnDown=!0),1===event.button&&(m_midMouseBtnDown=!0),2===event.button&&(m_rightMouseBtnDown=!0),coords=m_that.viewer().relMouseCoords(event),coords.x<0?m_lastPos.x=0:m_lastPos.x=coords.x,coords.y<0?m_lastPos.y=0:m_lastPos.y=coords.y,!1},this.handleMouseUp=function(event){return 0===event.button&&(m_leftMouseBtnDown=!1),1===event.button&&(m_midMouseBtnDown=!1),2===event.button&&(m_rightMouseBtnDown=!1),!1},this.handleMouseWheel=function(event){var ren=m_that.viewer().renderWindow().activeRenderer(),cam=ren.camera();return event.originalEvent.wheelDelta<0?cam.zoom(.9):cam.zoom(1.1),ren.resetCameraClippingRange(),m_that.viewer().render(),!0},this},inherit(vgl.trackballInteractorStyle,vgl.interactorStyle),vgl.pvwInteractorStyle=function(){"use strict";function render(){m_renderer.resetCameraClippingRange(),m_that.viewer().render()}if(!(this instanceof vgl.pvwInteractorStyle))return new vgl.pvwInteractorStyle;vgl.trackballInteractorStyle.call(this);var m_width,m_height,m_renderer,m_camera,m_outsideCanvas,m_coords,m_currentMousePos,m_focalPoint,m_focusWorldPt,m_focusDisplayPt,m_displayPt1,m_displayPt2,m_worldPt1,m_worldPt2,m_dx,m_dy,m_dz,m_zTrans,m_that=this,m_leftMouseButtonDown=!1,m_rightMouseButtonDown=!1,m_middleMouseButtonDown=!1,m_mouseLastPos={x:0,y:0};return this.handleMouseMove=function(event){var rens=[],i=null,secCameras=[],deltaxy=null;for(m_width=m_that.viewer().renderWindow().windowSize()[0],m_height=m_that.viewer().renderWindow().windowSize()[1],m_renderer=m_that.viewer().renderWindow().activeRenderer(),m_camera=m_renderer.camera(),m_outsideCanvas=!1,m_coords=m_that.viewer().relMouseCoords(event),m_currentMousePos={x:0,y:0},rens=m_that.viewer().renderWindow().renderers(),i=0;im_width?(m_currentMousePos.x=0,m_outsideCanvas=!0):m_currentMousePos.x=m_coords.x,m_coords.y<0||m_coords.y>m_height?(m_currentMousePos.y=0,m_outsideCanvas=!0):m_currentMousePos.y=m_coords.y,m_outsideCanvas!==!0){if(m_focalPoint=m_camera.focalPoint(),m_focusWorldPt=vec4.fromValues(m_focalPoint[0],m_focalPoint[1],m_focalPoint[2],1),m_focusDisplayPt=m_renderer.worldToDisplay(m_focusWorldPt,m_camera.viewMatrix(),m_camera.projectionMatrix(),m_width,m_height),m_displayPt1=vec4.fromValues(m_currentMousePos.x,m_currentMousePos.y,m_focusDisplayPt[2],1),m_displayPt2=vec4.fromValues(m_mouseLastPos.x,m_mouseLastPos.y,m_focusDisplayPt[2],1),m_worldPt1=m_renderer.displayToWorld(m_displayPt1,m_camera.viewMatrix(),m_camera.projectionMatrix(),m_width,m_height),m_worldPt2=m_renderer.displayToWorld(m_displayPt2,m_camera.viewMatrix(),m_camera.projectionMatrix(),m_width,m_height),m_dx=m_worldPt1[0]-m_worldPt2[0],m_dy=m_worldPt1[1]-m_worldPt2[1],m_dz=m_worldPt1[2]-m_worldPt2[2],m_middleMouseButtonDown&&(m_camera.pan(-m_dx,-m_dy,-m_dz),render()),m_leftMouseButtonDown){for(deltaxy=[m_mouseLastPos.x-m_currentMousePos.x,m_mouseLastPos.y-m_currentMousePos.y],m_camera.rotate(deltaxy[0],deltaxy[1]),i=0;i0?m_camera.zoom(1-Math.abs(m_zTrans)):m_camera.zoom(1+Math.abs(m_zTrans)),render()),m_mouseLastPos.x=m_currentMousePos.x,m_mouseLastPos.y=m_currentMousePos.y,!1}},this.handleMouseDown=function(event){return 0===event.button&&(m_leftMouseButtonDown=!0),1===event.button&&(m_middleMouseButtonDown=!0),2===event.button&&(m_rightMouseButtonDown=!0),m_coords=m_that.viewer().relMouseCoords(event),m_coords.x<0?m_mouseLastPos.x=0:m_mouseLastPos.x=m_coords.x,m_coords.y<0?m_mouseLastPos.y=0:m_mouseLastPos.y=m_coords.y,!1},this.handleMouseUp=function(event){return 0===event.button&&(m_leftMouseButtonDown=!1),1===event.button&&(m_middleMouseButtonDown=!1),2===event.button&&(m_rightMouseButtonDown=!1),!1},this},inherit(vgl.pvwInteractorStyle,vgl.trackballInteractorStyle),vgl.viewer=function(canvas,options){"use strict";if(!(this instanceof vgl.viewer))return new vgl.viewer(canvas,options);vgl.object.call(this);var m_that=this,m_canvas=canvas,m_ready=!0,m_interactorStyle=null,m_renderer=vgl.renderer(options),m_renderWindow=vgl.renderWindow(m_canvas);return this.canvas=function(){return m_canvas},this.renderWindow=function(){return m_renderWindow},this.init=function(){null!==m_renderWindow?m_renderWindow._setup():console.log("[ERROR] No render window attached")},this.exit=function(renderState){null!==m_renderWindow?m_renderWindow._cleanup(renderState):console.log("[ERROR] No render window attached")},this.interactorStyle=function(){return m_interactorStyle},this.setInteractorStyle=function(style){style!==m_interactorStyle&&(m_interactorStyle=style,m_interactorStyle.setViewer(this),this.modified())},this.handleMouseDown=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);2===event.button&&fixedEvent.preventDefault(),fixedEvent.state="down",fixedEvent.type=vgl.event.mousePress,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseUp=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.state="up",fixedEvent.type=vgl.event.mouseRelease,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseMove=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.mouseMove,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseWheel=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.mouseWheel,$(m_that).trigger(fixedEvent)}return!0},this.handleMouseOut=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.mouseOut,$(m_that).trigger(fixedEvent)}return!0},this.handleKeyPress=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.keyPress,$(m_that).trigger(fixedEvent)}return!0},this.handleContextMenu=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.contextMenu,$(m_that).trigger(fixedEvent)}return!1},this.handleClick=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.click,$(m_that).trigger(fixedEvent)}return!1},this.handleDoubleClick=function(event){if(m_ready===!0){var fixedEvent=$.event.fix(event||window.event);fixedEvent.preventDefault(),fixedEvent.type=vgl.event.dblClick,$(m_that).trigger(fixedEvent)}return!1},this.relMouseCoords=function(event){if(void 0===event.pageX||void 0===event.pageY)throw"Missing attributes pageX and pageY on the event";var totalOffsetX=0,totalOffsetY=0,canvasX=0,canvasY=0,currentElement=m_canvas;do totalOffsetX+=currentElement.offsetLeft-currentElement.scrollLeft,totalOffsetY+=currentElement.offsetTop-currentElement.scrollTop,currentElement=currentElement.offsetParent;while(currentElement);return canvasX=event.pageX-totalOffsetX,canvasY=event.pageY-totalOffsetY,{x:canvasX,y:canvasY}},this.render=function(){m_renderWindow.render()},this.bindEventHandlers=function(){$(m_canvas).on("mousedown",this.handleMouseDown),$(m_canvas).on("mouseup",this.handleMouseUp),$(m_canvas).on("mousemove",this.handleMouseMove),$(m_canvas).on("mousewheel",this.handleMouseWheel),$(m_canvas).on("contextmenu",this.handleContextMenu)},this.unbindEventHandlers=function(){$(m_canvas).off("mousedown",this.handleMouseDown),$(m_canvas).off("mouseup",this.handleMouseUp),$(m_canvas).off("mousemove",this.handleMouseMove),$(m_canvas).off("mousewheel",this.handleMouseWheel),$(m_canvas).off("contextmenu",this.handleContextMenu)},this._init=function(){this.bindEventHandlers(),m_renderWindow.addRenderer(m_renderer)},this._init(),this},inherit(vgl.viewer,vgl.object),vgl.shader=function(type){"use strict";if(!(this instanceof vgl.shader))return new vgl.shader(type);vgl.object.call(this);var m_shaderHandle=null,m_compileTimestamp=vgl.timestamp(),m_shaderType=type,m_shaderSource="";this.shaderHandle=function(){return m_shaderHandle},this.shaderType=function(){return m_shaderType},this.shaderSource=function(){return m_shaderSource},this.setShaderSource=function(source){m_shaderSource=source,this.modified()},this.compile=function(renderState){return this.getMTime()-1)return!1;var i;for(i=0;i-1?!1:(m_uniforms.push(uniform),void m_this.modified())},this.addVertexAttribute=function(attr,key){m_vertexAttributes[key]=attr,m_this.modified()},this.uniformLocation=function(name){return m_uniformNameToLocation[name]},this.attributeLocation=function(name){return m_vertexAttributeNameToLocation[name]},this.uniform=function(name){var i;for(i=0;i=this.getMTime())){for(m_this._setup(renderState),i=0;im_setupTimestamp.getMTime()&&this.setup(renderState),activateTextureUnit(renderState),renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,this.m_textureHandle)},this.undoBind=function(renderState){renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,null)},this.image=function(){return this.m_image},this.setImage=function(image){return null!==image?(this.m_image=image,this.updateDimensions(),this.modified(),!0):!1},this.nearestPixel=function(){return this.m_nearestPixel},this.setNearestPixel=function(nearest){return nearest=nearest?!0:!1,nearest!==this.m_nearestPixel?(this.m_nearestPixel=nearest,this.modified(),!0):!1},this.textureUnit=function(){return this.m_textureUnit},this.setTextureUnit=function(unit){return this.m_textureUnit===unit?!1:(this.m_textureUnit=unit,this.modified(),!0)},this.width=function(){return this.m_width},this.setWidth=function(width){return m_that.m_width!==width?(m_that.m_width=width,m_that.modified(),!0):!1},this.height=function(){return m_that.m_height},this.setHeight=function(height){return m_that.m_height!==height?(m_that.m_height=height,m_that.modified(),!0):!1},this.depth=function(){return this.m_depth},this.setDepth=function(depth){return null===this.m_image?!1:(this.m_depth=depth,this.modified(),!0)},this.textureHandle=function(){return this.m_textureHandle},this.internalFormat=function(){return this.m_internalFormat},this.setInternalFormat=function(internalFormat){return this.m_internalFormat!==internalFormat?(this.m_internalFormat=internalFormat,this.modified(),!0):!1},this.pixelFormat=function(){return this.m_pixelFormat},this.setPixelFormat=function(pixelFormat){return null===this.m_image?!1:(this.m_pixelFormat=pixelFormat,this.modified(),!0)},this.pixelDataType=function(){return this.m_pixelDataType}, +this.setPixelDataType=function(pixelDataType){return null===this.m_image?!1:(this.m_pixelDataType=pixelDataType,this.modified(),!0)},this.computeInternalFormatUsingImage=function(){this.m_internalFormat=vgl.GL.RGBA,this.m_pixelFormat=vgl.GL.RGBA,this.m_pixelDataType=vgl.GL.UNSIGNED_BYTE},this.updateDimensions=function(){null!==this.m_image&&(this.m_width=this.m_image.width,this.m_height=this.m_image.height,this.m_depth=0)},this},inherit(vgl.texture,vgl.materialAttribute),vgl.lookupTable=function(){"use strict";if(!(this instanceof vgl.lookupTable))return new vgl.lookupTable;vgl.texture.call(this);var m_setupTimestamp=vgl.timestamp(),m_range=[0,0];return this.m_colorTable=[.07514311,.468049805,1,1,.247872569,.498782363,1,1,.339526309,.528909511,1,1,.409505078,.558608486,1,1,.468487184,.588057293,1,1,.520796675,.617435078,1,1,.568724526,.646924167,1,1,.613686735,.676713218,1,1,.656658579,.707001303,1,1,.698372844,.738002964,1,1,.739424025,.769954435,1,1,.780330104,.803121429,1,1,.821573924,.837809045,1,1,.863634967,.874374691,1,1,.907017747,.913245283,1,1,.936129275,.938743558,.983038586,1,.943467973,.943498599,.943398095,1,.990146732,.928791426,.917447482,1,1,.88332677,.861943246,1,1,.833985467,.803839606,1,1,.788626485,.750707739,1,1,.746206642,.701389973,1,1,.70590052,.654994046,1,1,.667019783,.610806959,1,1,.6289553,.568237474,1,1,.591130233,.526775617,1,1,.552955184,.485962266,1,1,.513776083,.445364274,1,1,.472800903,.404551679,1,1,.428977855,.363073592,1,1,.380759558,.320428137,1,.961891484,.313155629,.265499262,1,.916482116,.236630659,.209939162,1].map(function(x){return 255*x}),this.setup=function(renderState){0===this.textureUnit()?renderState.m_context.activeTexture(vgl.GL.TEXTURE0):1===this.textureUnit()&&renderState.m_context.activeTexture(vgl.GL.TEXTURE1),renderState.m_context.deleteTexture(this.m_textureHandle),this.m_textureHandle=renderState.m_context.createTexture(),renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,this.m_textureHandle),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_MIN_FILTER,vgl.GL.LINEAR),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_MAG_FILTER,vgl.GL.LINEAR),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_WRAP_S,vgl.GL.CLAMP_TO_EDGE),renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D,vgl.GL.TEXTURE_WRAP_T,vgl.GL.CLAMP_TO_EDGE),renderState.m_context.pixelStorei(vgl.GL.UNPACK_ALIGNMENT,1),this.m_width=this.m_colorTable.length/4,this.m_height=1,this.m_depth=0,renderState.m_context.texImage2D(vgl.GL.TEXTURE_2D,0,vgl.GL.RGBA,this.m_width,this.m_height,this.m_depth,vgl.GL.RGBA,vgl.GL.UNSIGNED_BYTE,new Uint8Array(this.m_colorTable)),renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D,null),m_setupTimestamp.modified()},this.colorTable=function(){return this.m_colorTable},this.setColorTable=function(colors){return this.m_colorTable===colors?!1:(this.m_colorTable=colors,this.modified(),!0)},this.range=function(){return m_range},this.setRange=function(range){return m_range===range?!1:(m_range=range,this.modified(),!0)},this.updateRange=function(range){range instanceof Array||console.log("[error] Invalid data type for range. Requires array [min,max]"),range[0]m_range[1]&&(m_range[1]=range[1],this.modified())},this},inherit(vgl.lookupTable,vgl.texture),vgl.uniform=function(type,name){"use strict";if(!(this instanceof vgl.uniform))return new vgl.uniform;this.getTypeNumberOfComponents=function(type){switch(type){case vgl.GL.FLOAT:case vgl.GL.INT:case vgl.GL.BOOL:return 1;case vgl.GL.FLOAT_VEC2:case vgl.GL.INT_VEC2:case vgl.GL.BOOL_VEC2:return 2;case vgl.GL.FLOAT_VEC3:case vgl.GL.INT_VEC3:case vgl.GL.BOOL_VEC3:return 3;case vgl.GL.FLOAT_VEC4:case vgl.GL.INT_VEC4:case vgl.GL.BOOL_VEC4:return 4;case vgl.GL.FLOAT_MAT3:return 9;case vgl.GL.FLOAT_MAT4:return 16;default:return 0}};var m_type=type,m_name=name,m_dataArray=[];return m_dataArray.length=this.getTypeNumberOfComponents(m_type),this.name=function(){return m_name},this.type=function(){return m_type},this.get=function(){return m_dataArray},this.set=function(value){var i=0;if(16===m_dataArray.length)for(i=0;16>i;i+=1)m_dataArray[i]=value[i];else if(9===m_dataArray.length)for(i=0;9>i;i+=1)m_dataArray[i]=value[i];else if(4===m_dataArray.length)for(i=0;4>i;i+=1)m_dataArray[i]=value[i];else if(3===m_dataArray.length)for(i=0;3>i;i+=1)m_dataArray[i]=value[i];else if(2===m_dataArray.length)for(i=0;2>i;i+=1)m_dataArray[i]=value[i];else m_dataArray[0]=value},this.callGL=function(renderState,location){if(!(this.m_numberElements<1))switch(m_type){case vgl.GL.BOOL:case vgl.GL.INT:renderState.m_context.uniform1iv(location,m_dataArray);break;case vgl.GL.FLOAT:renderState.m_context.uniform1fv(location,m_dataArray);break;case vgl.GL.FLOAT_VEC2:renderState.m_context.uniform2fv(location,m_dataArray);break;case vgl.GL.FLOAT_VEC3:renderState.m_context.uniform3fv(location,m_dataArray);break;case vgl.GL.FLOAT_VEC4:renderState.m_context.uniform4fv(location,m_dataArray);break;case vgl.GL.FLOAT_MAT3:renderState.m_context.uniformMatrix3fv(location,vgl.GL.FALSE,m_dataArray);break;case vgl.GL.FLOAT_MAT4:renderState.m_context.uniformMatrix4fv(location,vgl.GL.FALSE,m_dataArray)}},this.update=function(renderState,program){renderState=renderState,program=program},this},vgl.modelViewUniform=function(name){"use strict";return this instanceof vgl.modelViewUniform?(0===name.length&&(name="modelViewMatrix"),vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program,this.set(renderState.m_modelViewMatrix)},this):new vgl.modelViewUniform(name)},inherit(vgl.modelViewUniform,vgl.uniform),vgl.modelViewOriginUniform=function(name,origin){"use strict";if(!(this instanceof vgl.modelViewOriginUniform))return new vgl.modelViewOriginUniform(name,origin);0===name.length&&(name="modelViewMatrix");var m_origin=vec3.fromValues(origin[0],origin[1],origin[2]);return vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program;var view=mat4.create();if(mat4.translate(view,renderState.m_modelViewMatrix,m_origin),renderState.m_modelViewAlignment){var align=renderState.m_modelViewAlignment;view[12]=Math.round(view[12]/align.roundx)*align.roundx+align.dx,view[13]=Math.round(view[13]/align.roundy)*align.roundy+align.dy}this.set(view)},this},inherit(vgl.modelViewOriginUniform,vgl.uniform),vgl.projectionUniform=function(name){"use strict";return this instanceof vgl.projectionUniform?(0===name.length&&(name="projectionMatrix"),vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program,this.set(renderState.m_projectionMatrix)},this):new vgl.projectionUniform(name)},inherit(vgl.projectionUniform,vgl.uniform),vgl.floatUniform=function(name,value){"use strict";return this instanceof vgl.floatUniform?(0===name.length&&(name="floatUniform"),value=void 0===value?1:value,vgl.uniform.call(this,vgl.GL.FLOAT,name),void this.set(value)):new vgl.floatUniform(name,value)},inherit(vgl.floatUniform,vgl.uniform),vgl.normalMatrixUniform=function(name){"use strict";return this instanceof vgl.normalMatrixUniform?(0===name.length&&(name="normalMatrix"),vgl.uniform.call(this,vgl.GL.FLOAT_MAT4,name),this.set(mat4.create()),this.update=function(renderState,program){program=program,this.set(renderState.m_normalMatrix)},this):new vgl.normalMatrixUniform(name)},inherit(vgl.normalMatrixUniform,vgl.uniform),vgl.vertexAttributeKeys={Position:0,Normal:1,TextureCoordinate:2,Color:3,Scalar:4,CountAttributeIndex:5},vgl.vertexAttributeKeysIndexed={Zero:0,One:1,Two:2,Three:3,Four:4,Five:5,Six:6,Seven:7,Eight:8,Nine:9},vgl.vertexAttribute=function(name){"use strict";if(!(this instanceof vgl.vertexAttribute))return new vgl.vertexAttribute(name);var m_name=name;this.name=function(){return m_name},this.bindVertexData=function(renderState,key){var geometryData=renderState.m_mapper.geometryData(),sourceData=geometryData.sourceData(key),program=renderState.m_material.shaderProgram();renderState.m_context.vertexAttribPointer(program.attributeLocation(m_name),sourceData.attributeNumberOfComponents(key),sourceData.attributeDataType(key),sourceData.normalized(key),sourceData.attributeStride(key),sourceData.attributeOffset(key)),renderState.m_context.enableVertexAttribArray(program.attributeLocation(m_name))},this.undoBindVertexData=function(renderState,key){key=key;var program=renderState.m_material.shaderProgram();renderState.m_context.disableVertexAttribArray(program.attributeLocation(m_name))}},vgl.source=function(){"use strict";return this instanceof vgl.source?(vgl.object.call(this),this.create=function(){},this):new vgl.source},inherit(vgl.source,vgl.object),vgl.planeSource=function(){"use strict";if(!(this instanceof vgl.planeSource))return new vgl.planeSource;vgl.source.call(this);var m_origin=[0,0,0],m_point1=[1,0,0],m_point2=[0,1,0],m_normal=[0,0,1],m_xresolution=1,m_yresolution=1,m_geom=null;this.setOrigin=function(x,y,z){m_origin[0]=x,m_origin[1]=y,m_origin[2]=z},this.setPoint1=function(x,y,z){m_point1[0]=x,m_point1[1]=y,m_point1[2]=z},this.setPoint2=function(x,y,z){m_point2[0]=x,m_point2[1]=y,m_point2[2]=z},this.create=function(){m_geom=new vgl.geometryData;var i,j,k,ii,numPts,numPolys,sourceTexCoords,x=[],tc=[],v1=[],v2=[],pts=[],posIndex=0,normIndex=0,colorIndex=0,texCoordIndex=0,positions=[],normals=[],colors=[],texCoords=[],indices=[],tristrip=null,sourcePositions=null,sourceColors=null;for(x.length=3,tc.length=2,v1.length=3,v2.length=3,pts.length=3,i=0;3>i;i+=1)v1[i]=m_point1[i]-m_origin[i],v2[i]=m_point2[i]-m_origin[i];for(numPts=(m_xresolution+1)*(m_yresolution+1),numPolys=m_xresolution*m_yresolution*2,positions.length=3*numPts,normals.length=3*numPts,texCoords.length=2*numPts,indices.length=numPts,k=0,i=0;m_yresolution+1>i;i+=1)for(tc[1]=i/m_yresolution,j=0;m_xresolution+1>j;j+=1){for(tc[0]=j/m_xresolution,ii=0;3>ii;ii+=1)x[ii]=m_origin[ii]+tc[0]*v1[ii]+tc[1]*v2[ii];positions[posIndex++]=x[0],positions[posIndex++]=x[1],positions[posIndex++]=x[2],colors[colorIndex++]=1,colors[colorIndex++]=1,colors[colorIndex++]=1,normals[normIndex++]=m_normal[0],normals[normIndex++]=m_normal[1],normals[normIndex++]=m_normal[2],texCoords[texCoordIndex++]=tc[0],texCoords[texCoordIndex++]=tc[1]}for(i=0;m_yresolution>i;i+=1)for(j=0;m_xresolution>j;j+=1)pts[0]=j+i*(m_xresolution+1),pts[1]=pts[0]+1,pts[2]=pts[0]+m_xresolution+2,pts[3]=pts[0]+m_xresolution+1;for(i=0;numPts>i;i+=1)indices[i]=i;return tristrip=new vgl.triangleStrip,tristrip.setIndices(indices),sourcePositions=vgl.sourceDataP3fv(),sourcePositions.pushBack(positions),sourceColors=vgl.sourceDataC3fv(),sourceColors.pushBack(colors),sourceTexCoords=vgl.sourceDataT2fv(),sourceTexCoords.pushBack(texCoords),m_geom.addSource(sourcePositions),m_geom.addSource(sourceColors),m_geom.addSource(sourceTexCoords),m_geom.addPrimitive(tristrip),m_geom}},inherit(vgl.planeSource,vgl.source),vgl.pointSource=function(){"use strict";if(!(this instanceof vgl.pointSource))return new vgl.pointSource;vgl.source.call(this);var m_this=this,m_positions=[],m_colors=[],m_textureCoords=[],m_size=[],m_geom=null;this.getPositions=function(){return m_positions},this.setPositions=function(positions){positions instanceof Array?m_positions=positions:console.log("[ERROR] Invalid data type for positions. Array is required."),m_this.modified()},this.getColors=function(){return m_colors},this.setColors=function(colors){colors instanceof Array?m_colors=colors:console.log("[ERROR] Invalid data type for colors. Array is required."),m_this.modified()},this.getSize=function(){return m_size},this.setSize=function(size){m_size=size,this.modified()},this.setTextureCoordinates=function(texcoords){texcoords instanceof Array?m_textureCoords=texcoords:console.log("[ERROR] Invalid data type for texture coordinates. Array is required."),m_this.modified()},this.create=function(){if(m_geom=new vgl.geometryData,m_positions.length%3!==0)return void console.log("[ERROR] Invalid length of the points array");var pointsPrimitive,sourcePositions,sourceColors,sourceTexCoords,sourceSize,numPts=m_positions.length/3,i=0,indices=[];for(indices.length=numPts,i=0;numPts>i;i+=1)indices[i]=i;if(sourceSize=vgl.sourceDataDf(),numPts!==m_size.length)for(i=0;numPts>i;i+=1)sourceSize.pushBack(m_size);else sourceSize.setData(m_size);return m_geom.addSource(sourceSize),pointsPrimitive=new vgl.points,pointsPrimitive.setIndices(indices),sourcePositions=vgl.sourceDataP3fv(),sourcePositions.pushBack(m_positions),m_geom.addSource(sourcePositions),m_colors.length>0&&m_colors.length===m_positions.length?(sourceColors=vgl.sourceDataC3fv(),sourceColors.pushBack(m_colors),m_geom.addSource(sourceColors)):m_colors.length>0&&m_colors.length!==m_positions.length&&console.log("[ERROR] Number of colors are different than number of points"),m_textureCoords.length>0&&m_textureCoords.length===m_positions.length?(sourceTexCoords=vgl.sourceDataT2fv(),sourceTexCoords.pushBack(m_textureCoords),m_geom.addSource(sourceTexCoords)):m_textureCoords.length>0&&m_textureCoords.length/2!==m_positions.length/3&&console.log("[ERROR] Number of texture coordinates are different than number of points"),m_geom.addPrimitive(pointsPrimitive),m_geom}},inherit(vgl.pointSource,vgl.source),vgl.lineSource=function(positions,colors){"use strict";if(!(this instanceof vgl.lineSource))return new vgl.lineSource;vgl.source.call(this);var m_positions=positions,m_colors=colors;this.setPositions=function(positions){return positions instanceof Array?(m_positions=positions,this.modified(),!0):(console.log("[ERROR] Invalid data type for positions. Array is required."),!1)},this.setColors=function(colors){return colors instanceof Array?(m_colors=colors,this.modified(),!0):(console.log("[ERROR] Invalid data type for colors. Array is required."),!1)},this.create=function(){if(!m_positions)return void console.log("[error] Invalid positions");if(m_positions.length%3!==0)return void console.log("[error] Line source requires 3d points");if(m_positions.length%3!==0)return void console.log("[ERROR] Invalid length of the points array");var i,linesPrimitive,sourcePositions,sourceColors,m_geom=new vgl.geometryData,numPts=m_positions.length/3,indices=[];for(indices.length=numPts,i=0;numPts>i;i+=1)indices[i]=i;return linesPrimitive=new vgl.lines,linesPrimitive.setIndices(indices),sourcePositions=vgl.sourceDataP3fv(),sourcePositions.pushBack(m_positions),m_geom.addSource(sourcePositions),m_colors&&m_colors.length>0&&m_colors.length===m_positions.length?(sourceColors=vgl.sourceDataC3fv(),sourceColors.pushBack(m_colors),m_geom.addSource(sourceColors)):m_colors&&m_colors.length>0&&m_colors.length!==m_positions.length&&console.log("[error] Number of colors are different than number of points"),m_geom.addPrimitive(linesPrimitive),m_geom}},inherit(vgl.lineSource,vgl.source),vgl.utils=function(){"use strict";return this instanceof vgl.utils?(vgl.object.call(this),this):new vgl.utils},inherit(vgl.utils,vgl.object),vgl.utils.computePowerOfTwo=function(value,pow){"use strict";for(pow=pow||1;value>pow;)pow*=2;return pow},vgl.utils.createTextureVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 textureCoord;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying highp vec3 iTextureCoord;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);"," iTextureCoord = textureCoord;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createTextureFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying highp vec3 iTextureCoord;","uniform sampler2D sampler2d;","uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4(texture2D(sampler2d, vec2(iTextureCoord.s, iTextureCoord.t)).xyz, opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createRgbaTextureFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying highp vec3 iTextureCoord;","uniform sampler2D sampler2d;","uniform mediump float opacity;","void main(void) {"," mediump vec4 color = vec4(texture2D(sampler2d, vec2(iTextureCoord.s, iTextureCoord.t)).xyzw);"," color.w *= opacity;"," gl_FragColor = color;","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 vertexColor;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying mediump vec3 iVertexColor;","varying highp vec3 iTextureCoord;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);"," iVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createPointVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 vertexColor;","attribute float vertexSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying mediump vec3 iVertexColor;","varying highp vec3 iTextureCoord;","void main(void)","{","gl_PointSize = vertexSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);"," iVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createVertexShaderSolidColor=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createVertexShaderColorMap=function(context,min,max){"use strict";context=context,min=min,max=max;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute float vertexScalar;","uniform mediump float pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform float lutMin;","uniform float lutMax;","varying mediump float iVertexScalar;","void main(void)","{","gl_PointSize = pointSize;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);","iVertexScalar = (vertexScalar-lutMin)/(lutMax-lutMin);","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying mediump vec3 iVertexColor;","uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4(iVertexColor, opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createPhongVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute highp vec3 vertexPosition;","attribute mediump vec3 vertexNormal;","attribute mediump vec3 vertexColor;","uniform highp mat4 projectionMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 normalMatrix;","varying highp vec4 varPosition;","varying mediump vec3 varNormal;","varying mediump vec3 varVertexColor;","void main(void)","{","varPosition = modelViewMatrix * vec4(vertexPosition, 1.0);","gl_Position = projectionMatrix * varPosition;","varNormal = vec3(normalMatrix * vec4(vertexNormal, 0.0));","varVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createPhongFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["uniform mediump float opacity;","precision mediump float;","varying vec3 varNormal;","varying vec4 varPosition;","varying mediump vec3 varVertexColor;","const vec3 lightPos = vec3(0.0, 0.0,10000.0);","const vec3 ambientColor = vec3(0.01, 0.01, 0.01);","const vec3 specColor = vec3(0.0, 0.0, 0.0);","void main() {","vec3 normal = normalize(varNormal);","vec3 lightDir = normalize(lightPos);","vec3 reflectDir = -reflect(lightDir, normal);","vec3 viewDir = normalize(-varPosition.xyz);","float lambertian = max(dot(lightDir, normal), 0.0);","vec3 color = vec3(0.0);","if(lambertian > 0.0) {"," color = lambertian * varVertexColor;","}","gl_FragColor = vec4(color * opacity, 1.0 - opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createFragmentShaderSolidColor=function(context,color){"use strict";var fragmentShaderSource=["uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4("+color[0]+","+color[1]+","+color[2]+", opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createFragmentShaderColorMap=function(context){"use strict";context=context;var fragmentShaderSource=["varying mediump float iVertexScalar;","uniform sampler2D sampler2d;","uniform mediump float opacity;","void main(void) {","gl_FragColor = vec4(texture2D(sampler2d, vec2(iVertexScalar, 0.0)).xyz, opacity);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createPointSpritesVertexShader=function(context){"use strict";context=context;var vertexShaderSource=["attribute vec3 vertexPosition;","attribute vec3 vertexColor;","uniform mediump vec2 pointSize;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform float height;","varying mediump vec3 iVertexColor;","varying highp float iVertexScalar;","void main(void)","{","mediump float realPointSize = pointSize.y;","if (pointSize.x > pointSize.y) {"," realPointSize = pointSize.x;}","gl_PointSize = realPointSize ;","iVertexScalar = vertexPosition.z;","gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition.xy, height, 1.0);"," iVertexColor = vertexColor;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader},vgl.utils.createPointSpritesFragmentShader=function(context){"use strict";context=context;var fragmentShaderSource=["varying mediump vec3 iVertexColor;","varying highp float iVertexScalar;","uniform sampler2D opacityLookup;","uniform highp float lutMin;","uniform highp float lutMax;","uniform sampler2D scalarsToColors;","uniform int useScalarsToColors;","uniform int useVertexColors;","uniform mediump vec2 pointSize;","uniform mediump float vertexColorWeight;","void main(void) {","mediump vec2 realTexCoord;","if (pointSize.x > pointSize.y) {"," realTexCoord = vec2(1.0, pointSize.y/pointSize.x) * gl_PointCoord;","} else {"," realTexCoord = vec2(pointSize.x/pointSize.y, 1.0) * gl_PointCoord;","}","highp float texOpacity = texture2D(opacityLookup, realTexCoord).w;","if (useScalarsToColors == 1) {"," gl_FragColor = vec4(texture2D(scalarsToColors, vec2((iVertexScalar - lutMin)/(lutMax - lutMin), 0.0)).xyz, texOpacity);","} else if (useVertexColors == 1) {"," gl_FragColor = vec4(iVertexColor, texOpacity);","} else {"," gl_FragColor = vec4(texture2D(opacityLookup, realTexCoord).xyz, texOpacity);","}}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader},vgl.utils.createTextureMaterial=function(isRgba,origin){"use strict";var modelViewUniform,mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createTextureVertexShader(vgl.GL),fragmentShader=null,posVertAttr=new vgl.vertexAttribute("vertexPosition"),texCoordVertAttr=new vgl.vertexAttribute("textureCoord"),pointsizeUniform=new vgl.floatUniform("pointSize",5),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.INT,"sampler2d"),opacityUniform=null;return modelViewUniform=void 0!==origin?new vgl.modelViewOriginUniform("modelViewMatrix",origin):new vgl.modelViewUniform("modelViewMatrix"),samplerUniform.set(0),prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(texCoordVertAttr,vgl.vertexAttributeKeys.TextureCoordinate),prog.addUniform(pointsizeUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),fragmentShader=isRgba?vgl.utils.createRgbaTextureFragmentShader(vgl.GL):vgl.utils.createTextureFragmentShader(vgl.GL),opacityUniform=new vgl.floatUniform("opacity",1),prog.addUniform(opacityUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createGeometryMaterial=function(){"use strict";var mat=new vgl.material,prog=new vgl.shaderProgram,pointSize=5,opacity=1,vertexShader=vgl.utils.createVertexShader(vgl.GL),fragmentShader=vgl.utils.createFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),pointsizeUniform=new vgl.floatUniform("pointSize",pointSize),opacityUniform=new vgl.floatUniform("opacity",opacity),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat},vgl.utils.createPointGeometryMaterial=function(opacity){"use strict";opacity=void 0===opacity?1:opacity;var mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createPointVertexShader(vgl.GL),fragmentShader=vgl.utils.createFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),sizeVertAttr=new vgl.vertexAttribute("vertexSize"),opacityUniform=new vgl.floatUniform("opacity",opacity),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addVertexAttribute(sizeVertAttr,vgl.vertexAttributeKeys.Scalar),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createPhongMaterial=function(){"use strict";var mat=new vgl.material,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createPhongVertexShader(vgl.GL),fragmentShader=vgl.utils.createPhongFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),normalVertAttr=new vgl.vertexAttribute("vertexNormal"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),opacityUniform=new vgl.floatUniform("opacity",1),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),normalUniform=new vgl.normalMatrixUniform("normalMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(normalVertAttr,vgl.vertexAttributeKeys.Normal),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addUniform(normalUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat},vgl.utils.createColorMaterial=function(){"use strict";var mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createVertexShader(vgl.GL),fragmentShader=vgl.utils.createFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),texCoordVertAttr=new vgl.vertexAttribute("textureCoord"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),pointsizeUniform=new vgl.floatUniform("pointSize",5),opacityUniform=new vgl.floatUniform("opacity",1),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addVertexAttribute(texCoordVertAttr,vgl.vertexAttributeKeys.TextureCoordinate),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createColorMappedMaterial=function(lut){"use strict";lut||(lut=new vgl.lookupTable);var scalarRange=lut.range(),mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createVertexShaderColorMap(vgl.GL,scalarRange[0],scalarRange[1]),fragmentShader=vgl.utils.createFragmentShaderColorMap(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),scalarVertAttr=new vgl.vertexAttribute("vertexScalar"),pointsizeUniform=new vgl.floatUniform("pointSize",5),opacityUniform=new vgl.floatUniform("opacity",1),lutMinUniform=new vgl.floatUniform("lutMin",scalarRange[0]),lutMaxUniform=new vgl.floatUniform("lutMax",scalarRange[1]),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.FLOAT,"sampler2d"),lookupTable=lut;return samplerUniform.set(0),prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(scalarVertAttr,vgl.vertexAttributeKeys.Scalar),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(lutMinUniform),prog.addUniform(lutMaxUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat.addAttribute(lookupTable),mat},vgl.utils.updateColorMappedMaterial=function(mat,lut){"use strict";if(!mat)return void console.log("[warning] Invalid material. Nothing to update.");if(!lut)return void console.log("[warning] Invalid lookup table. Nothing to update.");var lutMin=mat.shaderProgram().uniform("lutMin"),lutMax=mat.shaderProgram().uniform("lutMax");lutMin.set(lut.range()[0]),lutMax.set(lut.range()[1]),mat.setAttribute(lut)},vgl.utils.createSolidColorMaterial=function(color){"use strict";color||(color=[1,1,1]);var mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createVertexShaderSolidColor(vgl.GL),fragmentShader=vgl.utils.createFragmentShaderSolidColor(vgl.GL,color),posVertAttr=new vgl.vertexAttribute("vertexPosition"),pointsizeUniform=new vgl.floatUniform("pointSize",5),opacityUniform=new vgl.floatUniform("opacity",1),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix");return prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addUniform(pointsizeUniform),prog.addUniform(opacityUniform),prog.addUniform(modelViewUniform), +prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),mat},vgl.utils.createPointSpritesMaterial=function(image,lut){"use strict";var scalarRange=void 0===lut?[0,1]:lut.range(),mat=new vgl.material,blend=new vgl.blend,prog=new vgl.shaderProgram,vertexShader=vgl.utils.createPointSpritesVertexShader(vgl.GL),fragmentShader=vgl.utils.createPointSpritesFragmentShader(vgl.GL),posVertAttr=new vgl.vertexAttribute("vertexPosition"),colorVertAttr=new vgl.vertexAttribute("vertexColor"),heightUniform=new vgl.floatUniform("height",0),vertexColorWeightUniform=new vgl.floatUniform("vertexColorWeight",0),lutMinUniform=new vgl.floatUniform("lutMin",scalarRange[0]),lutMaxUniform=new vgl.floatUniform("lutMax",scalarRange[1]),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.INT,"opacityLookup"),scalarsToColors=new vgl.uniform(vgl.GL.INT,"scalarsToColors"),useScalarsToColors=new vgl.uniform(vgl.GL.INT,"useScalarsToColors"),useVertexColors=new vgl.uniform(vgl.GL.INT,"useVertexColors"),pointSize=new vgl.uniform(vgl.GL.FLOAT_VEC2,"pointSize"),texture=new vgl.texture;return samplerUniform.set(0),scalarsToColors.set(1),useScalarsToColors.set(0),useVertexColors.set(0),pointSize.set([1,1]),prog.addVertexAttribute(posVertAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(colorVertAttr,vgl.vertexAttributeKeys.Color),prog.addUniform(heightUniform),prog.addUniform(vertexColorWeightUniform),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addUniform(samplerUniform),prog.addUniform(useVertexColors),prog.addUniform(useScalarsToColors),prog.addUniform(pointSize),prog.addShader(fragmentShader),prog.addShader(vertexShader),mat.addAttribute(prog),mat.addAttribute(blend),lut&&(prog.addUniform(scalarsToColors),useScalarsToColors.set(1),prog.addUniform(lutMinUniform),prog.addUniform(lutMaxUniform),lut.setTextureUnit(1),mat.addAttribute(lut)),texture.setImage(image),texture.setTextureUnit(0),mat.addAttribute(texture),mat},vgl.utils.createPlane=function(originX,originY,originZ,point1X,point1Y,point1Z,point2X,point2Y,point2Z){"use strict";var mapper=new vgl.mapper,planeSource=new vgl.planeSource,mat=vgl.utils.createGeometryMaterial(),actor=new vgl.actor;return planeSource.setOrigin(originX,originY,originZ),planeSource.setPoint1(point1X,point1Y,point1Z),planeSource.setPoint2(point2X,point2Y,point2Z),mapper.setGeometryData(planeSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createTexturePlane=function(originX,originY,originZ,point1X,point1Y,point1Z,point2X,point2Y,point2Z,isRgba){"use strict";var mapper=new vgl.mapper,planeSource=new vgl.planeSource,mat=vgl.utils.createTextureMaterial(isRgba,[originX,originY,originZ]),actor=new vgl.actor;return planeSource.setPoint1(point1X-originX,point1Y-originY,point1Z-originZ),planeSource.setPoint2(point2X-originX,point2Y-originY,point2Z-originZ),mapper.setGeometryData(planeSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createPoints=function(positions,size,colors,texcoords,opacity){"use strict";if(!positions)return console.log("[ERROR] Cannot create points without positions"),null;opacity=void 0===opacity?1:opacity;var mapper=new vgl.mapper,pointSource=new vgl.pointSource,mat=vgl.utils.createPointGeometryMaterial(opacity),actor=new vgl.actor;return pointSource.setPositions(positions),colors&&pointSource.setColors(colors),texcoords&&pointSource.setTextureCoordinates(texcoords),size?pointSource.setSize(size):pointSource.setSize(1),mapper.setGeometryData(pointSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createPointSprites=function(image,positions,colors,texcoords){"use strict";if(!image)return console.log("[ERROR] Point sprites requires an image"),null;if(!positions)return console.log("[ERROR] Cannot create points without positions"),null;var mapper=new vgl.mapper,pointSource=new vgl.pointSource,mat=vgl.utils.createPointSpritesMaterial(image),actor=new vgl.actor;return pointSource.setPositions(positions),colors&&pointSource.setColors(colors),texcoords&&pointSource.setTextureCoordinates(texcoords),mapper.setGeometryData(pointSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createLines=function(positions,colors){"use strict";if(!positions)return console.log("[ERROR] Cannot create points without positions"),null;var mapper=new vgl.mapper,lineSource=new vgl.lineSource,mat=vgl.utils.createGeometryMaterial(),actor=new vgl.actor;return lineSource.setPositions(positions),colors&&lineSource.setColors(colors),mapper.setGeometryData(lineSource.create()),actor.setMapper(mapper),actor.setMaterial(mat),actor},vgl.utils.createColorLegend=function(varname,lookupTable,origin,width,height,countMajor,countMinor){"use strict";function createLabels(varname,positions,range){if(!positions)return void console.log("[error] Create labels requires positions (x,y,z) array");if(positions.length%3!==0)return void console.log("[error] Create labels require positions array contain 3d points");if(!range)return void console.log("[error] Create labels requires Valid range");var i,actor=null,size=vgl.utils.computePowerOfTwo(48),index=0,actors=[],origin=[],pt1=[],pt2=[],delta=positions[6]-positions[0],axisLabelOffset=4;for(origin.length=3,pt1.length=3,pt2.length=3,i=0;2>i;i+=1)index=i*(positions.length-3),origin[0]=positions[index]-delta,origin[1]=positions[index+1]-2*delta,origin[2]=positions[index+2],pt1[0]=positions[index]+delta,pt1[1]=origin[1],pt1[2]=origin[2],pt2[0]=origin[0],pt2[1]=positions[1],pt2[2]=origin[2],actor=vgl.utils.createTexturePlane(origin[0],origin[1],origin[2],pt1[0],pt1[1],pt1[2],pt2[0],pt2[1],pt2[2],!0),actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute),actor.material().setBinNumber(vgl.material.RenderBin.Overlay),actor.material().addAttribute(vgl.utils.create2DTexture(range[i].toFixed(2).toString(),12,null)),actors.push(actor);return origin[0]=.5*(positions[0]+positions[positions.length-3]-size),origin[1]=positions[1]+axisLabelOffset,origin[2]=positions[2],pt1[0]=origin[0]+size,pt1[1]=origin[1],pt1[2]=origin[2],pt2[0]=origin[0],pt2[1]=origin[1]+size,pt2[2]=origin[2],actor=vgl.utils.createTexturePlane(origin[0],origin[1],origin[2],pt1[0],pt1[1],pt1[2],pt2[0],pt2[1],pt2[2],!0),actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute),actor.material().setBinNumber(vgl.material.RenderBin.Overlay),actor.material().addAttribute(vgl.utils.create2DTexture(varname,24,null)),actors.push(actor),actors}function createTicksAndLabels(varname,lut,originX,originY,originZ,pt1X,pt1Y,pt1Z,pt2X,pt2Y,pt2Z,countMajor,countMinor,heightMajor,heightMinor){heightMinor=heightMinor;var width=pt2X-pt1X,index=null,delta=width/countMajor,positions=[],actors=[];for(index=0;countMajor>=index;index+=1)positions.push(pt1X+delta*index),positions.push(pt1Y),positions.push(pt1Z),positions.push(pt1X+delta*index),positions.push(pt1Y+heightMajor),positions.push(pt1Z);return actors=actors.concat(createLabels(varname,positions,lut.range()))}if(!lookupTable)return console.log("[error] Invalid lookup table"),[];var pt1X=origin[0]+width,pt1Y=origin[1],pt1Z=0,pt2X=origin[0],pt2Y=origin[1]+height,pt2Z=0,actors=[],actor=null,mat=null,group=vgl.groupNode();return actor=vgl.utils.createTexturePlane(origin[0],origin[1],origin[2],pt1X,pt1Y,pt1Z,pt2X,pt2Y,pt2Z,!0),mat=actor.material(),mat.addAttribute(lookupTable),actor.setMaterial(mat),group.addChild(actor),actor.material().setBinNumber(vgl.material.RenderBin.Overlay),actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute),actors.push(actor),actors=actors.concat(createTicksAndLabels(varname,lookupTable,origin[0],origin[1],origin[1],pt2X,pt1Y,pt1Z,pt1X,pt1Y,pt1Z,countMajor,countMinor,5,3))},vgl.utils.create2DTexture=function(textToWrite,textSize,color,font,alignment,baseline,bold){"use strict";var canvas=document.getElementById("textRendering"),ctx=null,texture=vgl.texture();return font=font||"sans-serif",alignment=alignment||"center",baseline=baseline||"bottom","undefined"==typeof bold&&(bold=!0),canvas||(canvas=document.createElement("canvas")),ctx=canvas.getContext("2d"),canvas.setAttribute("id","textRendering"),canvas.style.display="none",canvas.height=vgl.utils.computePowerOfTwo(8*textSize),canvas.width=canvas.height,ctx.fillStyle="rgba(0, 0, 0, 0)",ctx.fillRect(0,0,ctx.canvas.width,ctx.canvas.height),ctx.fillStyle="rgba(200, 85, 10, 1.0)",ctx.textAlign=alignment,ctx.textBaseline=baseline,ctx.font=4*textSize+"px "+font,bold&&(ctx.font="bold "+ctx.font),ctx.fillText(textToWrite,canvas.width/2,canvas.height/2,canvas.width),texture.setImage(canvas),texture.updateDimensions(),texture},vgl.picker=function(){"use strict";if(!(this instanceof vgl.picker))return new vgl.picker;vgl.object.call(this);var m_actors=[];return this.getActors=function(){return m_actors},this.pick=function(selectionX,selectionY,renderer){if(void 0===selectionX)return 0;if(void 0===selectionY)return 0;if(void 0===renderer)return 0;m_actors=[];var actors,count,i,bb,tmin,tmax,tymin,tymax,tzmin,tzmax,actor,camera=renderer.camera(),width=renderer.width(),height=renderer.height(),fpoint=camera.focalPoint(),focusWorldPt=vec4.fromValues(fpoint[0],fpoint[1],fpoint[2],1),focusDisplayPt=renderer.worldToDisplay(focusWorldPt,camera.viewMatrix(),camera.projectionMatrix(),width,height),displayPt=vec4.fromValues(selectionX,selectionY,focusDisplayPt[2],1),worldPt=renderer.displayToWorld(displayPt,camera.viewMatrix(),camera.projectionMatrix(),width,height),cameraPos=camera.position(),ray=[];for(i=0;3>i;i+=1)ray[i]=worldPt[i]-cameraPos[i];for(actors=renderer.sceneRoot().children(),count=0,i=0;i=0?(tmin=(bb[0]-cameraPos[0])/ray[0],tmax=(bb[1]-cameraPos[0])/ray[0]):(tmin=(bb[1]-cameraPos[0])/ray[0],tmax=(bb[0]-cameraPos[0])/ray[0]),ray[1]>=0?(tymin=(bb[2]-cameraPos[1])/ray[1],tymax=(bb[3]-cameraPos[1])/ray[1]):(tymin=(bb[3]-cameraPos[1])/ray[1],tymax=(bb[2]-cameraPos[1])/ray[1]),tmin>tymax||tymin>tmax)continue;if(tymin>tmin&&(tmin=tymin),tmax>tymax&&(tmax=tymax),ray[2]>=0?(tzmin=(bb[4]-cameraPos[2])/ray[2],tzmax=(bb[5]-cameraPos[2])/ray[2]):(tzmin=(bb[5]-cameraPos[2])/ray[2],tzmax=(bb[4]-cameraPos[2])/ray[2]),tmin>tzmax||tzmin>tmax)continue;tzmin>tmin&&(tmin=tzmin),tmax>tzmax&&(tmax=tzmax),m_actors[count]=actor,count+=1}return count},this},inherit(vgl.picker,vgl.object),vgl.shapefileReader=function(){"use strict";if(!(this instanceof vgl.shapefileReader))return new vgl.shapefileReader;var m_that=this,SHP_NULL=0,SHP_POINT=1,SHP_POLYGON=5,SHP_POLYLINE=3;return this.int8=function(data,offset){return data.charCodeAt(offset)},this.bint32=function(data,offset){return((255&data.charCodeAt(offset))<<24)+((255&data.charCodeAt(offset+1))<<16)+((255&data.charCodeAt(offset+2))<<8)+(255&data.charCodeAt(offset+3))},this.lint32=function(data,offset){return((255&data.charCodeAt(offset+3))<<24)+((255&data.charCodeAt(offset+2))<<16)+((255&data.charCodeAt(offset+1))<<8)+(255&data.charCodeAt(offset))},this.bint16=function(data,offset){return((255&data.charCodeAt(offset))<<8)+(255&data.charCodeAt(offset+1))},this.lint16=function(data,offset){return((255&data.charCodeAt(offset+1))<<8)+(255&data.charCodeAt(offset))},this.ldbl64=function(data,offset){var b0=255&data.charCodeAt(offset),b1=255&data.charCodeAt(offset+1),b2=255&data.charCodeAt(offset+2),b3=255&data.charCodeAt(offset+3),b4=255&data.charCodeAt(offset+4),b5=255&data.charCodeAt(offset+5),b6=255&data.charCodeAt(offset+6),b7=255&data.charCodeAt(offset+7),sign=1-2*(b7>>7),exp=((127&b7)<<4)+((240&b6)>>4)-1023,frac=(15&b6)*Math.pow(2,48)+b5*Math.pow(2,40)+b4*Math.pow(2,32)+b3*Math.pow(2,24)+b2*Math.pow(2,16)+b1*Math.pow(2,8)+b0;return sign*(1+frac*Math.pow(2,-52))*Math.pow(2,exp)},this.lfloat32=function(data,offset){var b0=255&data.charCodeAt(offset),b1=255&data.charCodeAt(offset+1),b2=255&data.charCodeAt(offset+2),b3=255&data.charCodeAt(offset+3),sign=1-2*(b3>>7),exp=((127&b3)<<1)+((254&b2)>>7)-127,frac=(127&b2)*Math.pow(2,16)+b1*Math.pow(2,8)+b0;return sign*(1+frac*Math.pow(2,-23))*Math.pow(2,exp)},this.str=function(data,offset,length){for(var chars=[],index=offset;offset+length>index;){var c=data[index];if(0===c.charCodeAt(0))break;chars.push(c),index+=1}return chars.join("")},this.readHeader=function(data){var code=this.bint32(data,0),length=this.bint32(data,24),version=this.lint32(data,28),shapetype=this.lint32(data,32);return{code:code,length:length,version:version,shapetype:shapetype}},this.loadShx=function(data){for(var indices=[],appendIndex=function(offset){return indices.push(2*m_that.bint32(data,offset)),offset+8},offset=100;offsetheader_offset;)headers.push(readHeader(header_offset)),header_offset+=HEADER_LENGTH;for(var records=[],record_offset=header_size;header_size+num_entries*record_size>record_offset;){var declare=m_that.str(data,record_offset,1);if("*"===declare)record_offset+=record_size;else{record_offset+=1;for(var record={},i=0;i=start;i-=1){var x=m_that.ldbl64(data,offset+16*i),y=m_that.ldbl64(data,offset+16*i+8);ring.push([x,y])}return ring},readRecord=function(offset){var num_parts,num_points,parts_start,points_start,i,start,end,ring,rings,record_offset=offset+8,geom_type=m_that.lint32(data,record_offset);if(geom_type===SHP_NULL)console.log("NULL Shape");else if(geom_type===SHP_POINT){var x=m_that.ldbl64(data,record_offset+4),y=m_that.ldbl64(data,record_offset+12);features.push({type:"Point",attr:{},geom:[[x,y]]})}else if(geom_type===SHP_POLYGON){for(num_parts=m_that.lint32(data,record_offset+36),num_points=m_that.lint32(data,record_offset+40),parts_start=offset+52,points_start=offset+52+4*num_parts,rings=[],i=0;num_parts>i;i+=1)start=m_that.lint32(data,parts_start+4*i),end=num_parts>i+1?m_that.lint32(data,parts_start+4*(i+1)):num_points,ring=readRing(points_start,start,end),rings.push(ring);features.push({type:"Polygon",attr:{},geom:[rings]})}else{if(geom_type!==SHP_POLYLINE)throw"Not Implemented: "+geom_type;for(num_parts=m_that.lint32(data,record_offset+36),num_points=m_that.lint32(data,record_offset+40),parts_start=offset+52,points_start=offset+52+4*num_parts,rings=[],i=0;num_parts>i;i+=1)start=m_that.lint32(data,parts_start+4*i),end=num_parts>i+1?m_that.lint32(data,parts_start+4*(i+1)):num_points,ring=readRing(points_start,start,end),rings.push(ring);features.push({type:"Polyline",attr:{},geom:[rings]})}},attr=this.loadDBF(dbf_data);for(i=0;i=m_base64Str.length)return END_OF_INPUT;if(nextCharacter=m_base64Str.charAt(m_base64Count),m_base64Count+=1,m_reverseBase64Chars[nextCharacter])return m_reverseBase64Chars[nextCharacter];if("A"===nextCharacter)return 0}return END_OF_INPUT},this.decode64=function(str){var result="",inBuffer=new Array(4),done=!1;for(m_base64Str=str,m_base64Count=0;!done&&(inBuffer[0]=this.readReverseBase64())!==END_OF_INPUT&&(inBuffer[1]=this.readReverseBase64())!==END_OF_INPUT;)inBuffer[2]=this.readReverseBase64(),inBuffer[3]=this.readReverseBase64(),result+=this.ntos(inBuffer[0]<<2&255|inBuffer[1]>>4),inBuffer[2]!==END_OF_INPUT?(result+=this.ntos(inBuffer[1]<<4&255|inBuffer[2]>>2),inBuffer[3]!==END_OF_INPUT?result+=this.ntos(inBuffer[2]<<6&255|inBuffer[3]):done=!0):done=!0;return result},this.readNumber=function(ss){var v=ss[m_pos++]+(ss[m_pos++]<<8)+(ss[m_pos++]<<16)+(ss[m_pos++]<<24);return v},this.readF3Array=function(numberOfPoints,ss){var i,size=4*numberOfPoints*3,test=new Int8Array(size),points=null;for(i=0;size>i;i+=1)test[i]=ss[m_pos],m_pos+=1;return points=new Float32Array(test.buffer)},this.readColorArray=function(numberOfPoints,ss,vglcolors){var i,idx=0,tmp=new Array(3*numberOfPoints);for(i=0;numberOfPoints>i;i+=1)tmp[idx++]=ss[m_pos++]/255,tmp[idx++]=ss[m_pos++]/255,tmp[idx++]=ss[m_pos++]/255,m_pos++;vglcolors.insert(tmp)},this.parseObject=function(vtkObject){var size,actor,colorMapData,shaderProg,opacityUniform,lookupTable,colorTable,windowSize,width,height,position,geom=new vgl.geometryData,mapper=vgl.mapper(),ss=[],type=null,data=null,matrix=null,material=null;for(data=atob(vtkObject.data),i=0;ii;i+=1)p[idx++]=points[3*i],p[idx++]=points[3*i+1],p[idx++]=points[3*i+2];for(vglpoints.insert(p),geom.addSource(vglpoints),vglcolors=new vgl.sourceDataC3fv,this.readColorArray(numberOfPoints,ss,vglcolors),geom.addSource(vglcolors),vgllines=new vgl.lines,geom.addPrimitive(vgllines),numberOfIndex=this.readNumber(ss),temp=new Int8Array(2*numberOfIndex),i=0;2*numberOfIndex>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;for(index=new Uint16Array(temp.buffer),vgllines.setIndices(index),vgllines.setPrimitiveType(vgl.GL.LINES),size=64,temp=new Int8Array(size),i=0;size>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;return m=new Float32Array(temp.buffer),mat4.copy(matrix,m),matrix},this.parseMeshData=function(geom,ss){var numberOfIndex,numberOfPoints,points,temp,index,size,m,i,tcoord,vglpoints=null,vglcolors=null,normals=null,matrix=mat4.create(),vgltriangles=null,pn=null,idx=0;for(numberOfPoints=this.readNumber(ss),pn=new Array(6*numberOfPoints),vglpoints=new vgl.sourceDataP3N3f,points=this.readF3Array(numberOfPoints,ss),normals=this.readF3Array(numberOfPoints,ss),i=0;numberOfPoints>i;i+=1)pn[idx++]=points[3*i],pn[idx++]=points[3*i+1],pn[idx++]=points[3*i+2],pn[idx++]=normals[3*i],pn[idx++]=normals[3*i+1],pn[idx++]=normals[3*i+2];for(vglpoints.insert(pn),geom.addSource(vglpoints),vglcolors=new vgl.sourceDataC3fv,this.readColorArray(numberOfPoints,ss,vglcolors),geom.addSource(vglcolors),temp=[],vgltriangles=new vgl.triangles,numberOfIndex=this.readNumber(ss),temp=new Int8Array(2*numberOfIndex),i=0;2*numberOfIndex>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;for(index=new Uint16Array(temp.buffer),vgltriangles.setIndices(index),geom.addPrimitive(vgltriangles),size=64,temp=new Int8Array(size),i=0;size>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;return m=new Float32Array(temp.buffer),mat4.copy(matrix,m),tcoord=null,matrix},this.parsePointData=function(geom,ss){var numberOfPoints,points,indices,temp,size,m,matrix=mat4.create(),vglpoints=null,vglcolors=null,vglVertexes=null,p=null,idx=0;for(numberOfPoints=this.readNumber(ss),p=new Array(3*numberOfPoints),vglpoints=new vgl.sourceDataP3fv,points=this.readF3Array(numberOfPoints,ss),indices=new Uint16Array(numberOfPoints),i=0;numberOfPoints>i;i+=1)indices[i]=i,p[idx++]=points[3*i],p[idx++]=points[3*i+1],p[idx++]=points[3*i+2];for(vglpoints.insert(p),geom.addSource(vglpoints),vglcolors=new vgl.sourceDataC3fv,this.readColorArray(numberOfPoints,ss,vglcolors),geom.addSource(vglcolors),vglVertexes=new vgl.points,vglVertexes.setIndices(indices),geom.addPrimitive(vglVertexes),size=64,temp=new Int8Array(size),i=0;size>i;i+=1)temp[i]=ss[m_pos],m_pos+=1;return m=new Float32Array(temp.buffer),mat4.copy(matrix,m),matrix},this.parseColorMapData=function(geom,ss,numColors){var tmpArray,size,xrgb,i,c,obj={};for(obj.numOfColors=numColors,size=8,tmpArray=new Int8Array(size),i=0;size>i;i+=1)tmpArray[i]=ss[m_pos],m_pos+=1;for(obj.position=new Float32Array(tmpArray.buffer),size=8,tmpArray=new Int8Array(size),i=0;size>i;i+=1)tmpArray[i]=ss[m_pos],m_pos+=1;for(obj.size=new Float32Array(tmpArray.buffer),obj.colors=[],c=0;ci;i+=1)tmpArray[i]=ss[m_pos],m_pos+=1;xrgb=[new Float32Array(tmpArray.buffer)[0],ss[m_pos++],ss[m_pos++],ss[m_pos++]],obj.colors[c]=xrgb}for(obj.orientation=ss[m_pos++],obj.numOfLabels=ss[m_pos++],obj.title="";m_pos=0;layer-=1)renderer=this.getRenderer(layer),this.parseSceneMetadata(renderer,layer);return m_viewer},this.createViewer=function(node){var interactorStyle;return null===m_viewer&&(m_node=node,m_viewer=vgl.viewer(node),m_viewer.init(),m_viewer.renderWindow().removeRenderer(m_viewer.renderWindow().activeRenderer()),m_viewer.renderWindow().addRenderer(new vgl.depthPeelRenderer),m_vtkRenderedList[0]=m_viewer.renderWindow().activeRenderer(),m_viewer.renderWindow().resize(node.width,node.height),interactorStyle=vgl.pvwInteractorStyle(),m_viewer.setInteractorStyle(interactorStyle)),m_viewer},this.deleteViewer=function(){m_vtkRenderedList={},m_viewer=null},this.updateCanvas=function(node){return m_node=node,m_viewer.renderWindow().resize(node.width,node.height),m_viewer},this.numObjects=function(){return m_vtkObjectCount},this.getRenderer=function(layer){var renderer;return renderer=m_vtkRenderedList[layer],(null===renderer||"undefined"==typeof renderer)&&(renderer=new vgl.renderer,renderer.setResetScene(!1),renderer.setResetClippingRange(!1),m_viewer.renderWindow().addRenderer(renderer),0!==layer&&renderer.camera().setClearMask(vgl.GL.DepthBufferBit),m_vtkRenderedList[layer]=renderer),renderer},this.setVtkScene=function(scene){m_vtkScene=scene},this},vgl.DataBuffers=function(initialSize){"use strict";if(!(this instanceof vgl.DataBuffers))return new vgl.DataBuffers(initialSize);var size,data={};size=initialSize||0===initialSize?initialSize:256;var current=0,copyArray=function(dst,src,start,count){dst||console.log("ack"),start||(start=0),count||(count=src.length);for(var i=0;count>i;i+=1)dst[start+i]=src[i]},resize=function(min_expand){var new_size=size;for(min_expand>2*new_size&&(new_size=min_expand);min_expand>new_size;)new_size*=2;size=new_size;for(var name in data)if(data.hasOwnProperty(name)){var newArray=new Float32Array(new_size*data[name].len),oldArray=data[name].array;copyArray(newArray,oldArray),data[name].array=newArray,data[name].dirty=!0}};this.create=function(name,len){if(!len)throw"Length of buffer must be a positive integer";var array=new Float32Array(size*len);return data[name]={array:array,len:len,dirty:!1},data[name].array},this.alloc=function(num){current+num>=size&&resize(current+num);var start=current;return current+=num,start},this.get=function(name){return data[name].array},this.write=function(name,array,start,count){copyArray(data[name].array,array,start*data[name].len,count*data[name].len),data[name].dirty=!0},this.repeat=function(name,elem,start,count){for(var i=0;count>i;i+=1)copyArray(data[name].array,elem,(start+i)*data[name].len,data[name].len);data[name].dirty=!0},this.count=function(){return current},this.data=function(name){return data[name].array}},function(){"use strict";function setNumeric(){var i;for(i=0;in?!1:(outer.forEach(function(vert,i){var j=(n+i-1)%n,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;intersect&&(inside=!inside)}),(inner||[]).forEach(function(hole){inside=inside&&!geo.util.pointInPolygon(point,hole)}),inside)},isFunction:function(f){return"function"==typeof f},ensureFunction:function(f){return geo.util.isFunction(f)?f:function(){return f}},randomString:function(n){var s,i,r;for(n=n||8,s="",i=0;n>i;i+=1)r=Math.floor(Math.random()*chars.length),s+=chars.substring(r,r+1);return s},convertColor:function(color){return void 0!==color.r&&void 0!==color.g&&void 0!==color.b?color:("string"==typeof color&&(geo.util.cssColors.hasOwnProperty(color)?color=geo.util.cssColors[color]:"#"===color.charAt(0)&&(color=parseInt(color.slice(1),16))),isFinite(color)&&(color={r:((16711680&color)>>16)/255,g:((65280&color)>>8)/255,b:(255&color)/255}),color)},normalizeCoordinates:function(p){return p=p||{},Array.isArray(p)?{x:p[0],y:p[1],z:p[2]||0}:{x:setNumeric(p.x,p.longitude,p.lng,p.lon,0),y:setNumeric(p.y,p.latitude,p.lat,0),z:setNumeric(p.z,p.elevation,p.elev,p.height,0)}},radiusEarth:6378137,lincomb:function(ca,a,cb,b){return a.x=ca*(a.x||0)+cb*(b.x||0),a.y=ca*(a.y||0)+cb*(b.y||0),a.z=ca*(a.x||0)+cb*(b.x||0),a},scale:function(a,b,pow){return a.x=(a.x||0)*Math.pow(b.x||1,pow),a.y=(a.y||0)*Math.pow(b.y||1,pow),a.z=(a.z||0)*Math.pow(b.z||1,pow),a}},geo.util.cssColors={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850, +mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074}}(),function(){"use strict";function vect(x,y){return new Vector2D(x,y)}var RangeNode=function(elem,start,end,current){this.data=elem[current],this.left=null,this.right=null,start!==current&&(this.left=new RangeNode(elem,start,current-1,parseInt((start+(current-1))/2,10))),end!==current&&(this.right=new RangeNode(elem,current+1,end,parseInt((end+(current+1))/2,10))),this.elem=elem,this.start=start,this.end=end,this.subtree=null,this.search=rangeNodeSearch},rangeNodeSearch=function(result,box){var m_this=this,xrange=function(b){return b.x_in(m_this.elem[m_this.start])&&b.x_in(m_this.elem[m_this.end])},yrange=function(b,start,end){return b.y_in(m_this.subtree[start])&&b.y_in(m_this.subtree[end])},subquery=function(result,box,start,end,current){if(yrange(box,start,end))for(var i=start;end>=i;i++)result.push(m_this.subtree[i]);else box.y_in(m_this.subtree[current])&&result.push(m_this.subtree[current]),box.y_left(m_this.subtree[current])?current!==end&&subquery(result,box,current+1,end,parseInt((end+(current+1))/2,10)):box.x_right(m_this.subtree[current])?current!==start&&subquery(result,box,start,current-1,parseInt((start+(current-1))/2,10)):(current!==end&&subquery(result,box,current+1,end,parseInt((end+(current+1))/2,10)),current!==start&&subquery(result,box,start,current-1,parseInt((start+(current-1))/2,10)))};return xrange(box)?(this.subtree||(this.subtree=this.elem.slice(this.start,this.end+1),this.subtree.sort(function(a,b){return a.y-b.y})),void subquery(result,box,0,this.subtree.length-1,parseInt((this.subtree.length-1)/2,10))):(box.contains(this.data)&&result.push(this.data),void(box.x_left(this.data)?this.right&&this.right.search(result,box):box.x_right(this.data)?this.left&&this.left.search(result,box):(this.left&&this.left.search(result,box),this.right&&this.right.search(result,box))))},RangeTree=function(elem){elem.sort(function(a,b){return a.x-b.x}),elem.length>0?this.root=new RangeNode(elem,0,elem.length-1,parseInt((elem.length-1)/2,10)):this.root=null,this.search=function(_box){if(!this.root)return[];var box=_box.clone(),result=[];return this.root.search(result,box),result}},Box=function(v1,v2){this.min=v1.clone(),this.max=v2.clone(),this.contains=function(p){return v1.x<=p.x&&v2.x>=p.x&&v1.y<=p.y&&v2.y>=p.y},this.x_in=function(p){return v1.x<=p.x&&v2.x>=p.x},this.x_left=function(p){return v1.x>=p.x},this.x_right=function(p){return v2.x<=p.x},this.y_in=function(p){return v1.y<=p.y&&v2.y>=p.y},this.y_left=function(p){return v1.y>=p.y},this.y_right=function(p){return v2.y<=p.y},this.area=function(){return(this.max.x-this.min.x)*(this.max.y-this.min.y)},this.height=function(){return this.max.y-this.min.y},this.width=function(){return this.max.x-this.min.x},this.vertex=function(index){switch(index){case 0:return this.min.clone();case 1:return new vect(this.max.x,this.min.y);case 2:return this.max.clone();case 3:return new vect(this.min.x,this.max.y);default:throw"Index out of bounds: "+index}},this.intersects=function(box){for(var i=0;4>i;i++)for(var j=0;4>j;j++)if(vect.intersects(this.vertex(i),this.vertex((i+1)%4),box.vertex(j),box.vertex((j+1)%4)))return!0;return this.contains(box.min)&&this.contains(box.max)&&this.contains(new vect(box.min.x,box.max.y))&&this.contains(new vect(box.max.x,box.min.y))?!0:box.contains(this.min)&&box.contains(this.max)&&box.contains(new vect(this.min.x,this.max.y))&&box.contains(new vect(this.max.x,this.min.y))?!0:!1},this.union=function(b){this.min.x=Math.min(this.min.x,b.min.x),this.min.y=Math.min(this.min.y,b.min.y),this.max.x=Math.max(this.max.x,b.max.x),this.max.y=Math.max(this.max.y,b.max.y)},this.centroid=function(){return new vect((this.max.x+this.min.x)/2,(this.max.y+this.min.y)/2)},this.clone=function(){return new Box(v1,v2)}},Vector2D=function(x,y){this.x=x,this.y=y,this.add=function(v){return this.x+=v.x,this.y+=v.y,this},this.sub=function(v){return this.x-=v.x,this.y-=v.y,this},this.scale=function(s){return this.x*=s,this.y*=s,this},this.length=function(){return Math.sqrt(this.x*this.x+this.y*this.y)},this.normalize=function(){var scale=this.length();return 0===scale?this:(this.x/=scale,this.y/=scale,this)},this.div=function(v){return this.x/=v.x,this.y/=v.y,this},this.floor=function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},this.zero=function(tol){return tol=tol||0,this.length()<=tol},this.dot=function(v){return this.x*v.x+this.y*v.y},this.cross=function(v){return this.x*v.y-this.y*v.x},this.rotate=function(omega){var cos=Math.cos(omega),sin=Math.sin(omega);return xp=cos*this.x-sin*this.y,yp=sin*this.x+cos*this.y,this.x=xp,this.y=yp,this},this.clone=function(){return new Vector2D(this.x,this.y)},this.array=function(){return[this.x,this.y]}};vect.scale=function(v,s){return v.clone().scale(s)},vect.add=function(v1,v2){return v1.clone().add(v2)},vect.sub=function(v1,v2){return v1.clone().sub(v2)},vect.dist=function(v1,v2){return v1.clone().sub(v2).length()},vect.dir=function(v1,v2){return v1.clone().sub(v2).normalize()},vect.dot=function(v1,v2){return v1.x*v2.x+v1.y*v2.y},vect.cross=function(v1,v2){return v1.x*v2.y-v1.y*v2.x},vect.left=function(a,b,c,tol){tol||(tol=0);var v1=vect.sub(b,a),v2=vect.sub(c,a);return vect.cross(v1,v2)>=-tol},vect.intersects=function(a,b,c,d,tol){return tol||(tol=0),vect.left(a,b,c,tol)!=vect.left(a,b,d,tol)&&vect.left(c,d,b,tol)!=vect.left(c,d,a,tol)},vect.intersect2dt=function(a,b,c,d){var denom=a.x*(d.y-c.y)+b.x*(c.y-d.y)+d.x*(b.y-a.y)+c.x*(a.y-b.y);if(0===denom)return 1/0;var num_t=(a.x*(d.y-c.y)+c.x*(a.y-d.y)+d.x*(c.y-a.y),-(a.x*(c.y-b.y)+b.x*(a.y-c.y)+c.x*(b.y-a.y))),t=num_t/denom;return t},vect.intersect2dpos=function(a,b,c,d){var denom=a.x*(d.y-c.y)+b.x*(c.y-d.y)+d.x*(b.y-a.y)+c.x*(a.y-b.y);if(0===denom)return 1/0;var num_s=a.x*(d.y-c.y)+c.x*(a.y-d.y)+d.x*(c.y-a.y),s=num_s/denom,dir=vect.sub(b,a);return dir.scale(s),vect.add(a,dir)},vect.rotate=function(v,omega){var cos=Math.cos(omega),sin=Math.sin(omega);xp=cos*v.x-sin*v.y,yp=sin*v.x+cos*v.y;var c=new vect(xp,yp);return c},vect.normalize=function(v){return v.clone().normalize()},geo.util.RangeTree=RangeTree,geo.util.Box=Box,geo.util.vect=vect}(),function(){"use strict";var L={};L.Util={stamp:function(obj){return obj._leaflet_id=obj._leaflet_id||++L.Util.lastId,obj._leaflet_id},lastId:0},geo.util.DistanceGrid=function(cellSize){this._cellSize=cellSize,this._sqCellSize=cellSize*cellSize,this._grid={},this._objectPoint={}},geo.util.DistanceGrid.prototype={addObject:function(obj,point){var x=this._getCoord(point.x),y=this._getCoord(point.y),grid=this._grid,row=grid[y]=grid[y]||{},cell=row[x]=row[x]||[],stamp=L.Util.stamp(obj);point.obj=obj,this._objectPoint[stamp]=point,cell.push(obj)},updateObject:function(obj,point){this.removeObject(obj),this.addObject(obj,point)},removeObject:function(obj,point){var i,len,x=this._getCoord(point.x),y=this._getCoord(point.y),grid=this._grid,row=grid[y]=grid[y]||{},cell=row[x]=row[x]||[];for(delete this._objectPoint[L.Util.stamp(obj)],i=0,len=cell.length;len>i;i++)if(cell[i]===obj)return cell.splice(i,1),1===len&&delete row[x],!0},eachObject:function(fn,context){var i,j,k,len,row,cell,removed,grid=this._grid;for(i in grid){row=grid[i];for(j in row)for(cell=row[j],k=0,len=cell.length;len>k;k++)removed=fn.call(context,cell[k]),removed&&(k--,len--)}},getNearObject:function(point){var i,j,k,row,cell,len,obj,dist,x=this._getCoord(point.x),y=this._getCoord(point.y),objectPoint=this._objectPoint,closestDistSq=this._sqCellSize,closest=null;for(i=y-1;y+1>=i;i++)if(row=this._grid[i])for(j=x-1;x+1>=j;j++)if(cell=row[j])for(k=0,len=cell.length;len>k;k++)obj=cell[k],dist=this._sqDist(objectPoint[L.Util.stamp(obj)],point),closestDistSq>dist&&(closestDistSq=dist,closest=obj);return closest},contents:function(){return $.map(this._objectPoint,function(val){return val})},_getCoord:function(x){return Math.floor(x/this._cellSize)},_sqDist:function(p,p2){var dx=p2.x-p.x,dy=p2.y-p.y;return dx*dx+dy*dy}}}(),function(){"use strict";function ClusterTree(group,zoom,children){this._group=group,this._zoom=zoom,this._points=[],this._clusters=[],this._count=0,this._parent=null,this._coord=null;var that=this;(children||[]).forEach(function(c){that._add(c)})}function C(opts,width,height){this._opts=$.extend({maxZoom:18,radius:.05},opts),this._opts.width=this._opts.width||width||256,this._opts.height=this._opts.height||height||256,this._clusters={},this._points={};var zoom,scl;for(zoom=this._opts.maxZoom;zoom>=0;zoom-=1)scl=this._scaleAtLevel(zoom,this._opts.width,this._opts.height),this._clusters[zoom]=new geo.util.DistanceGrid(scl),this._points[zoom]=new geo.util.DistanceGrid(scl);this._topClusterLevel=new ClusterTree(this,-1)}ClusterTree.prototype._add=function(pt){var inc=1;pt instanceof ClusterTree?(this._clusters.push(pt),inc=pt._count):this._points.push(pt),pt._parent=this,this._increment(inc)},ClusterTree.prototype._increment=function(inc){this._coord=null,this._count+=inc,this._parent&&this._parent._increment(inc)},ClusterTree.prototype.count=function(){return this._count},ClusterTree.prototype.each=function(func){var i;for(i=0;i=0;zoom-=1){if(closest=this._clusters[zoom].getNearObject(point))return void closest._add(point);if(closest=this._points[zoom].getNearObject(point)){if(parent=closest._parent)for(z=parent._points.length-1;z>=0;z-=1)if(parent._points[z]===closest){parent._points.splice(z,1),parent._increment(-1);break}for(parent||$.noop(),newCluster=new ClusterTree(this,zoom,[closest,point]),this._clusters[zoom].addObject(newCluster,newCluster.coords()),lastParent=newCluster,z=zoom-1;z>parent._zoom;z-=1)lastParent=new ClusterTree(this,z,[lastParent]),this._clusters[z].addObject(lastParent,lastParent.coords());for(parent._add(lastParent),z=zoom;z>=0&&this._points[z].removeObject(closest,closest);z-=1);return}this._points[zoom].addObject(point,point)}this._topClusterLevel._add(point)},C.prototype.points=function(zoom){return zoom=Math.min(Math.max(Math.floor(zoom),0),this._opts.maxZoom-1),this._points[Math.floor(zoom)].contents()},C.prototype.clusters=function(zoom){return zoom=Math.min(Math.max(Math.floor(zoom),0),this._opts.maxZoom-1),this._clusters[Math.floor(zoom)].contents()},geo.util.ClusterGroup=C}(),function(){"use strict";geo.util.scale={d3:d3.scale}}(),geo.object=function(){"use strict";if(!(this instanceof geo.object))return new geo.object;var m_this=this,m_eventHandlers={},m_idleHandlers=[],m_promiseCount=0;return this.onIdle=function(handler){return m_promiseCount?m_idleHandlers.push(handler):handler(),m_this},this.addPromise=function(promise){function onDone(){m_promiseCount-=1,m_promiseCount||m_idleHandlers.splice(0,m_idleHandlers.length).forEach(function(handler){handler()})}return m_promiseCount+=1,promise.then(onDone,onDone),m_this},this.geoOn=function(event,handler){return Array.isArray(event)?(event.forEach(function(e){m_this.geoOn(e,handler)}),m_this):(m_eventHandlers.hasOwnProperty(event)||(m_eventHandlers[event]=[]),m_eventHandlers[event].push(handler),m_this)},this.geoTrigger=function(event,args){return Array.isArray(event)?(event.forEach(function(e){m_this.geoTrigger(e,args)}),m_this):(args=args||{},args.event=event,m_eventHandlers.hasOwnProperty(event)&&m_eventHandlers[event].forEach(function(handler){handler.call(m_this,args)}),m_this)},this.geoOff=function(event,arg){if(void 0===event&&(m_eventHandlers={},m_idleHandlers=[],m_promiseCount=0),Array.isArray(event))return event.forEach(function(e){m_this.geoOff(e,arg)}),m_this;if(arg){if(Array.isArray(arg))return arg.forEach(function(handler){m_this.geoOff(event,handler)}),m_this}else m_eventHandlers[event]=[];return m_eventHandlers.hasOwnProperty(event)&&(m_eventHandlers[event]=m_eventHandlers[event].filter(function(f){return f!==arg})),m_this},this._exit=function(){m_this.geoOff()},vgl.object.call(this),this},inherit(geo.object,vgl.object),geo.sceneObject=function(arg){"use strict";if(!(this instanceof geo.sceneObject))return new geo.sceneObject;geo.object.call(this,arg);var m_this=this,m_parent=null,m_children=[],s_exit=this._exit,s_trigger=this.geoTrigger,s_addPromise=this.addPromise,s_onIdle=this.onIdle;return this.addPromise=function(promise){m_parent?m_parent.addPromise(promise):s_addPromise(promise)},this.onIdle=function(handler){m_parent?m_parent.onIdle(handler):s_onIdle(handler)},this.parent=function(arg){return void 0===arg?m_parent:(m_parent=arg,m_this)},this.addChild=function(child){return Array.isArray(child)?(child.forEach(m_this.addChild),m_this):(child.parent(m_this),m_children.push(child),m_this)},this.removeChild=function(child){return Array.isArray(child)?(child.forEach(m_this.removeChild),m_this):(m_children=m_children.filter(function(c){return c!==child}),m_this)},this.children=function(){return m_children.slice()},this.draw=function(arg){return m_this.children().forEach(function(child){child.draw(arg)}),m_this},this.geoTrigger=function(event,args,childrenOnly){var geoArgs;return args=args||{},geoArgs=args.geo||{},args.geo=geoArgs,geoArgs.stopPropagation?m_this:!childrenOnly&&m_parent&&geoArgs._triggeredBy!==m_parent?(geoArgs._triggeredBy=m_this,m_parent.geoTrigger(event,args),m_this):(s_trigger.call(m_this,event,args),geoArgs.stopPropagation?m_this:(m_children.forEach(function(child){geoArgs._triggeredBy=m_this,child.geoTrigger(event,args)}),m_this))},this._exit=function(){m_this.children=[],delete m_this.parent,s_exit()},this},inherit(geo.sceneObject,geo.object),geo.timestamp=function(){"use strict";return this instanceof geo.timestamp?void vgl.timestamp.call(this):new geo.timestamp},inherit(geo.timestamp,vgl.timestamp),geo.transform=function(options){"use strict";function generate_proj4(){m_proj=new proj4(m_this.source(),m_this.target())}if(!(this instanceof geo.transform))return new geo.transform(options);var m_proj,m_source,m_target,m_this=this;return this.source=function(arg){return void 0===arg?m_source||"EPSG:4326":(m_source=arg,generate_proj4(),m_this)},this.target=function(arg){return void 0===arg?m_target||"EPSG:3857":(m_target=arg,generate_proj4(),m_this)},this._forward=function(point){var pt=m_proj.forward(point);return pt.z=point.z||0,pt},this._inverse=function(point){var pt=m_proj.inverse(point);return pt.z=point.z||0,pt},this.forward=function(point){return Array.isArray(point)?point.map(m_this._forward):m_this._forward(point)},this.inverse=function(point){return Array.isArray(point)?point.map(m_this._inverse):m_this._inverse(point)},options=options||{},this.source(options.source),this.target(options.target),geo.object.call(this),this},geo.transform.transformCoordinates=function(srcPrj,tgtPrj,coordinates,numberOfComponents){"use strict";function handleArrayCoordinates(){if(coordinates[0]instanceof Array)if(2===coordinates[0].length)xAcc=function(index){return coordinates[index][0]},yAcc=function(index){return coordinates[index][1]},writer=function(index,x,y){output[index]=[x,y]};else{if(3!==coordinates[0].length)throw"Invalid coordinates. Requires two or three components per array";xAcc=function(index){return coordinates[index][0]},yAcc=function(index){return coordinates[index][1]},zAcc=function(index){return coordinates[index][2]},writer=function(index,x,y,z){output[index]=[x,y,z]}}else if(2===coordinates.length)offset=2,xAcc=function(index){return coordinates[index*offset]},yAcc=function(index){return coordinates[index*offset+1]},writer=function(index,x,y){output[index]=x,output[index+1]=y};else if(3===coordinates.length)offset=3,xAcc=function(index){return coordinates[index*offset]},yAcc=function(index){return coordinates[index*offset+1]},zAcc=function(index){return coordinates[index*offset+2]},writer=function(index,x,y,z){output[index]=x,output[index+1]=y,output[index+2]=z};else{if(!numberOfComponents)throw"Invalid coordinates";if(2!==numberOfComponents&&3!==numberOfComponents)throw"Number of components should be two or three";offset=numberOfComponents,xAcc=function(index){return coordinates[index]},yAcc=function(index){return coordinates[index+1]},2===numberOfComponents?writer=function(index,x,y){output[index]=x,output[index+1]=y}:(zAcc=function(index){return coordinates[index+2]},writer=function(index,x,y,z){output[index]=x,output[index+1]=y,output[index+2]=z})}}function handleObjectCoordinates(){if(coordinates[0]&&"x"in coordinates[0]&&"y"in coordinates[0])xAcc=function(index){return coordinates[index].x},yAcc=function(index){return coordinates[index].y},"z"in coordinates[0]?(zAcc=function(index){return coordinates[index].z},writer=function(index,x,y,z){output[i]={x:x,y:y,z:z}}):writer=function(index,x,y){output[index]={x:x,y:y}};else{if(!(coordinates&&"x"in coordinates&&"y"in coordinates))throw"Invalid coordinates";xAcc=function(){return coordinates.x},yAcc=function(){return coordinates.y},"z"in coordinates?(zAcc=function(){return coordinates.z},writer=function(index,x,y,z){output={x:x,y:y,z:z}}):writer=function(index,x,y){output={x:x,y:y}}}}if(srcPrj===tgtPrj)return coordinates;var i,count,offset,xAcc,yAcc,zAcc,writer,output,projPoint,trans=geo.transform({source:srcPrj,target:tgtPrj});if(zAcc=function(){return 0},coordinates instanceof Array)output=[],output.length=coordinates.length,count=coordinates.length,coordinates[0]instanceof Array||coordinates[0]instanceof Object?(offset=1,coordinates[0]instanceof Array?handleArrayCoordinates():coordinates[0]instanceof Object&&handleObjectCoordinates()):handleArrayCoordinates();else if(coordinates&&coordinates instanceof Object){if(count=1,offset=1,!(coordinates&&"x"in coordinates&&"y"in coordinates))throw"Coordinates are not valid";handleObjectCoordinates()}for(i=0;count>i;i+=offset)projPoint=trans.forward({x:xAcc(i),y:yAcc(i),z:zAcc(i)}),writer(i,projPoint.x,projPoint.y,projPoint.z);return output},geo.transform.affineForward=function(def,coords){"use strict";var i,origin=def.origin,scale=def.scale||{x:1,y:1,z:1};for(i=0;i0&&viewport.height>0))throw new Error("Invalid viewport dimensions");(viewport.width!==this._viewport.width||viewport.height!==this._viewport.height)&&(this._viewport.width&&this._viewport.height&&(this._scale(vec3.fromValues(this._viewport.width/viewport.width,this._viewport.height/viewport.height,1)),this._translate(vec3.fromValues((viewport.width-this._viewport.width)/2,(viewport.height-this._viewport.height)/2,0))),this._viewport={width:viewport.width,height:viewport.height},this._update(),this.geoTrigger(geo.event.camera.viewport,{camera:this,viewport:this.viewport}))}}),this._resetView=function(){return mat4.identity(this._view),this},this._translate=function(offset){mat4.translate(this._view,this._view,offset)},this._scale=function(scale){mat4.scale(this._view,this._view,scale)},this._worldToClip4=function(point){return geo.camera.applyTransform(this._transform,point)},this._clipToWorld4=function(point){return geo.camera.applyTransform(this._inverse,point)},this.applyProjection=function(pt){var w;return"perspective"===this._projection?(w=1/(pt[3]||1),pt[0]=w*pt[0],pt[1]=w*pt[1],pt[2]=w*pt[2],pt[3]=w):pt[3]=1,pt},this.unapplyProjection=function(pt){var w;return"perspective"===this._projection?(w=pt[3]||1,pt[0]=w*pt[0],pt[1]=w*pt[1],pt[2]=w*pt[2],pt[3]=w):pt[3]=1,pt},this.worldToDisplay4=function(point){return point[2]-=2,this._worldToClip4(point),point=this.applyProjection(point),point[0]=this._viewport.width*(1+point[0])/2,point[1]=this._viewport.height*(1-point[1])/2,point[2]=(1+point[2])/2,point},this.displayToWorld4=function(point){return point[0]=2*point[0]/this._viewport.width-1,point[1]=-2*point[1]/this._viewport.height+1,point[2]=2*point[2]-1,point=this.unapplyProjection(point),this._clipToWorld4(point),point[2]+=2,point},this.worldToDisplay=function(point){var z=0,w=1;return point=this.worldToDisplay4([point.x,point.y,z,w]),{x:point[0],y:point[1],z:point[2]}},this.displayToWorld=function(point){var z=1,w=2;return point=this.displayToWorld4([point.x,point.y,z,w]),{x:point[0],y:point[1]}},this._getBounds=function(){var pt,bds={};return pt=this.displayToWorld({x:0,y:this._viewport.height}),bds.left=pt.x,bds.bottom=pt.y,pt=this.displayToWorld({x:this._viewport.width,y:0}),bds.right=pt.x,bds.top=pt.y,bds},this._setBounds=function(bounds){var c_ar,v_ar,w,h,translate=vec3.create(),scale=vec3.create();return bounds.near=bounds.near||0,bounds.far=bounds.far||1,this._resetView(),w=Math.abs(bounds.right-bounds.left),h=Math.abs(bounds.top-bounds.bottom),c_ar=w/h,v_ar=this._viewport.width/this._viewport.height,c_ar>=v_ar?(h=w/v_ar,scale[0]=2/w,scale[1]=2/h):(w=h*v_ar,scale[0]=2/w,scale[1]=2/h),scale[2]=1,this._scale(scale),translate[0]=-(bounds.left+bounds.right)/2,translate[1]=-(bounds.bottom+bounds.top)/2,translate[2]=0,this._translate(translate),this},this.pan=function(offset){this._translate(vec3.fromValues(offset.x,offset.y,offset.z||0)),this._update()},this.zoom=function(zoom){mat4.scale(this._view,this._view,vec3.fromValues(zoom,zoom,zoom)),this._update()},this.css=function(transform){var m;switch((transform||"").toLowerCase()){case"display":case"":m=this.display;break;case"world":m=this.world;break;default:throw new Error("Unknown transform "+transform)}return geo.camera.css(m)},this.ppMatrix=function(mat,prec){function f(i){var d=t[i],s=d.toExponential(prec);return d>=0&&(s=" "+s),s}var t=mat;return prec=prec||2,[[f(0),f(4),f(8),f(12)].join(" "),[f(1),f(5),f(9),f(13)].join(" "),[f(2),f(6),f(10),f(14)].join(" "),[f(3),f(7),f(11),f(15)].join(" ")].join("\n")},this.toString=function(){return this.ppMatrix(this._transform)},this.debug=function(){return["bounds",JSON.stringify(this.bounds),"view:",this.ppMatrix(this._view),"projection:",this.ppMatrix(this._proj),"transform:",this.ppMatrix(this._transform)].join("\n")},this.valueOf=function(){return this._transform},this._resetView(),this.projection=spec.projection||"parallel",spec.viewport&&(this.viewport=spec.viewport),this._update(),this):new geo.camera(spec)},geo.camera.projection={perspective:!0,parallel:!0},geo.camera.bounds={left:-1,right:1,top:1,bottom:-1,far:-2,near:-1},geo.camera.css=function(t){return"matrix3d("+[t[0].toFixed(20),t[1].toFixed(20),t[2].toFixed(20),t[3].toFixed(20),t[4].toFixed(20),t[5].toFixed(20),t[6].toFixed(20),t[7].toFixed(20),t[8].toFixed(20),t[9].toFixed(20),t[10].toFixed(20),t[11].toFixed(20),t[12].toFixed(20),t[13].toFixed(20),t[14].toFixed(20),t[15].toFixed(20)].join(",")+")"},geo.camera.affine=function(pre,scale,post){var mat=mat4.create();return post&&mat4.translate(mat,mat,[post.x||0,post.y||0,post.z||0]),scale&&mat4.scale(mat,mat,[scale.x||1,scale.y||1,scale.z||1]),pre&&mat4.translate(mat,mat,[pre.x||0,pre.y||0,pre.z||0]),mat},geo.camera.applyTransform=function(t,pt){return vec4.transformMat4(pt,pt,t)},geo.camera.combine=function(A,B){return mat4.mul(mat4.create(),A,B)},inherit(geo.camera,geo.object)}(),geo.layer=function(arg){"use strict";if(!(this instanceof geo.layer))return new geo.layer(arg);arg=arg||{},geo.sceneObject.call(this,arg);var m_zIndex,m_this=this,s_exit=this._exit,m_id=void 0===arg.id?geo.layer.newLayerId():arg.id,m_name="",m_map=void 0===arg.map?null:arg.map,m_node=null,m_canvas=null,m_renderer=null,m_initialized=!1,m_rendererName=void 0===arg.renderer?"vgl":arg.renderer,m_dataTime=geo.timestamp(),m_updateTime=geo.timestamp(),m_sticky=void 0===arg.sticky?!0:arg.sticky,m_active=void 0===arg.active?!0:arg.active,m_opacity=void 0===arg.opacity?1:arg.opacity,m_attribution=arg.attribution||null;if(!m_map)throw new Error("Layers must be initialized on a map.");return this.rendererName=function(){return m_rendererName},this.zIndex=function(zIndex){return void 0===zIndex?m_zIndex:(m_zIndex=zIndex,m_node.css("z-index",m_zIndex),m_this)},this.moveUp=function(n){var order,i,tmp,sign,me=null;for(void 0===n&&(n=1),sign=1,0>n&&(sign=-1,n=-n),order=m_this.map().layers().sort(function(a,b){return sign*(a.zIndex()-b.zIndex())}),i=0;i=i-me))break;tmp=m_this.zIndex(),m_this.zIndex(order[i].zIndex()),order[i].zIndex(tmp)}return m_this},this.moveDown=function(n){return void 0===n&&(n=1),m_this.moveUp(-n)},this.moveToTop=function(){return m_this.moveUp(m_this.map().children().length-1)},this.moveToBottom=function(){return m_this.moveDown(m_this.map().children().length-1)},this.sticky=function(){return m_sticky},this.active=function(){return m_active},this.node=function(){return m_node},this.id=function(val){return void 0===val?m_id:(m_id=geo.newLayerId(),m_this.modified(),m_this)},this.name=function(val){return void 0===val?m_name:(m_name=val,m_this.modified(),m_this)},this.map=function(){return m_map},this.renderer=function(){return m_renderer},this.canvas=function(){return m_canvas},this.dataTime=function(){return m_dataTime},this.updateTime=function(){return m_updateTime},this.initialized=function(val){return void 0!==val?(m_initialized=val,m_this):m_initialized},this.toLocal=function(input){return m_this._toLocalMatrix&&geo.camera.applyTransform(m_this._toLocalMatrix,input),input},this.fromLocal=function(input){return m_this._fromLocalMatrix&&geo.camera.applyTransform(m_this._fromLocalMatrix,input),input},this.attribution=function(arg){return void 0!==arg?(m_attribution=arg,m_this.map().updateAttribution(),m_this):m_attribution},this._init=function(){if(m_initialized)return m_this;m_map.node().append(m_node);var options=$.extend({},arg);return delete options.map,null===m_rendererName?(m_renderer=null,m_canvas=m_node):m_canvas?m_renderer=geo.createRenderer(m_rendererName,m_this,m_canvas,options):(m_renderer=geo.createRenderer(m_rendererName,m_this,void 0,options),m_canvas=m_renderer.canvas()),m_this.active()||m_node.css("pointerEvents","none"),m_initialized=!0,m_this.geoOn(geo.event.resize,function(event){m_this._update({event:event})}),m_this.geoOn(geo.event.pan,function(event){m_this._update({event:event})}),m_this.geoOn(geo.event.zoom,function(event){m_this._update({event:event})}),m_this},this._exit=function(){m_this.geoOff(),m_renderer&&m_renderer._exit(),m_node.off(),m_node.remove(),arg={},m_canvas=null,m_renderer=null,s_exit()},this._update=function(){},this.width=function(){return m_this.map().size().width},this.height=function(){return m_this.map().size().height},this.opacity=function(opac){return void 0!==opac?(m_opacity=opac,m_node.css("opacity",m_opacity),m_this):m_opacity},void 0===arg.zIndex&&(arg.zIndex=m_map.children().length),m_zIndex=arg.zIndex,m_node=$(document.createElement("div")),m_node.attr("id",m_name),m_node.css("position","absolute"),m_node.css("width","100%"),m_node.css("height","100%"),m_this.opacity(m_opacity),m_this.zIndex(m_zIndex),m_this},geo.layer.newLayerId=function(){"use strict";var currentId=1;return function(){var id=currentId;return currentId+=1,id}}(),geo.layer.create=function(map,spec){"use strict";if(spec=spec||{},spec.type="feature","feature"!==spec.type)return console.warn("Unsupported layer type"),null;if(spec.renderer=spec.renderer||"vgl","d3"!==spec.renderer&&"vgl"!==spec.renderer)return console.warn("Invalid renderer"),null;var layer=map.createLayer(spec.type,spec);return layer?(spec.features.forEach(function(f){f.data=f.data||spec.data,f.feature=geo.feature.create(layer,f)}),layer):(console.warn("Unable to create a layer"),null)},inherit(geo.layer,geo.sceneObject),geo.featureLayer=function(arg){"use strict";if(!(this instanceof geo.featureLayer))return new geo.featureLayer(arg); +geo.layer.call(this,arg);var m_this=this,m_features=[],s_init=this._init,s_exit=this._exit,s_update=this._update,s_draw=this.draw;return this.createFeature=function(featureName,arg){var newFeature=geo.createFeature(featureName,m_this,m_this.renderer(),arg);return m_this.addChild(newFeature),m_features.push(newFeature),m_this.features(m_features),m_this.modified(),newFeature},this.deleteFeature=function(feature){var i;for(i=0;im_this.updateTime().getMTime())for(i=0;i=deltaT?30:deltaT;var sf=springForce(),speed=calcSpeed(v),vx=v.x/speed,vy=v.y/speed;return speed*=Math.exp(-m_options.momentum.drag*deltaT),calcSpeed(sf)*deltaT+speed0?(vx*=speed,vy*=speed):(vx=0,vy=0),{x:vx-sf.x*deltaT,y:vy-sf.y*deltaT})}function springForce(){var xplus,xminus,yplus,yminus;if(!m_options.spring.enabled)return{x:0,y:0};var ul=m_this.map().gcsToDisplay({x:-180,y:82}),lr=m_this.map().gcsToDisplay({x:180,y:-82}),c=m_options.spring.springConstant,width=m_this.map().node().width(),height=m_this.map().node().height();return xplus=c*Math.max(0,ul.x),xminus=c*Math.max(0,width-lr.x),yplus=c*Math.max(0,ul.y)/2,yminus=c*Math.max(0,height-lr.y)/2,{x:xplus-xminus,y:yplus-yminus}}if(!(this instanceof geo.mapInteractor))return new geo.mapInteractor(args);geo.object.call(this);var m_mouse,m_keyboard,m_state,$node,m_options=args||{},m_this=this,m_wheelQueue={x:0,y:0},m_throttleTime=10,m_wait=!1,m_disableThrottle=!0,m_selectionLayer=null,m_selectionPlane=null,m_paused=!1,m_clickMaybe=!1;return m_options=$.extend(!0,{panMoveButton:"left",panMoveModifiers:{},zoomMoveButton:"right",zoomMoveModifiers:{},panWheelEnabled:!1,panWheelModifiers:{},zoomWheelEnabled:!0,zoomWheelModifiers:{},wheelScaleX:1,wheelScaleY:1,zoomScale:1,selectionButton:"left",selectionModifiers:{shift:!0},momentum:{enabled:!0,maxSpeed:2.5,minSpeed:.01,drag:.01},spring:{enabled:!1,springConstant:5e-5},click:{enabled:!0,buttons:{left:!0,right:!0,middle:!0},duration:0,cancelOnMove:!0}},m_options),m_mouse={page:{x:0,y:0},map:{x:0,y:0},buttons:{left:!1,right:!1,middle:!1},modifiers:{alt:!1,ctrl:!1,shift:!1,meta:!1},time:new Date,deltaTime:1,velocity:{x:0,y:0}},m_keyboard={},m_state={},this._connectEvents=function(){return m_options.map?(m_this._disconnectEvents(),$node=$(m_options.map.node()),$node.on("mousemove.geojs",m_this._handleMouseMove),$node.on("mousedown.geojs",m_this._handleMouseDown),$node.on("mouseup.geojs",m_this._handleMouseUp),$node.on("wheel.geojs",m_this._handleMouseWheel),$node.on("dragstart",function(){return!1}),("right"===m_options.panMoveButton||"right"===m_options.zoomMoveButton)&&$node.on("contextmenu.geojs",function(){return!1}),m_this):m_this},this._disconnectEvents=function(){return $node&&($node.off(".geojs"),$node=null),m_this},this.map=function(val){return void 0!==val?(m_options.map=val,m_this._connectEvents(),m_this):m_options.map},this.options=function(opts){return void 0===opts?$.extend({},m_options):($.extend(m_options,opts),m_this)},this._getMousePosition=function(evt){var dt,t,offset=$node.offset();t=(new Date).valueOf(),dt=t-m_mouse.time,m_mouse.time=t,m_mouse.deltaTime=dt,m_mouse.velocity={x:(evt.pageX-m_mouse.page.x)/dt,y:(evt.pageY-m_mouse.page.y)/dt},m_mouse.page={x:evt.pageX,y:evt.pageY},m_mouse.map={x:evt.pageX-offset.left,y:evt.pageY-offset.top};try{m_mouse.geo=m_this.map().displayToGcs(m_mouse.map)}catch(e){m_mouse.geo=null}},this._getMouseButton=function(evt){1===evt.which?m_mouse.buttons.left="mouseup"!==evt.type:3===evt.which?m_mouse.buttons.right="mouseup"!==evt.type:2===evt.which&&(m_mouse.buttons.middle="mouseup"!==evt.type)},this._getMouseModifiers=function(evt){m_mouse.modifiers.alt=evt.altKey,m_mouse.modifiers.ctrl=evt.ctrlKey,m_mouse.modifiers.meta=evt.metaKey,m_mouse.modifiers.shift=evt.shiftKey},this._getSelection=function(){var origin=m_state.origin,mouse=m_this.mouse(),map=m_this.map(),display={},gcs={};return display.upperLeft={x:Math.min(origin.map.x,mouse.map.x),y:Math.min(origin.map.y,mouse.map.y)},display.lowerRight={x:Math.max(origin.map.x,mouse.map.x),y:Math.max(origin.map.y,mouse.map.y)},display.upperRight={x:display.lowerRight.x,y:display.upperLeft.y},display.lowerLeft={x:display.upperLeft.x,y:display.lowerRight.y},gcs.upperLeft=map.displayToGcs(display.upperLeft),gcs.lowerRight=map.displayToGcs(display.lowerRight),gcs.upperRight=map.displayToGcs(display.upperRight),gcs.lowerLeft=map.displayToGcs(display.lowerLeft),m_selectionPlane.origin([display.lowerLeft.x,display.lowerLeft.y,0]),m_selectionPlane.upperLeft([display.upperLeft.x,display.upperLeft.y,0]),m_selectionPlane.lowerRight([display.lowerRight.x,display.lowerRight.y,0]),m_selectionPlane.draw(),{display:display,gcs:gcs,mouse:mouse,origin:$.extend({},m_state.origin)}},this.cancel=function(action){var out;return out=action?m_state.action===action:!!m_state.action,out&&(m_state={}),out},this._handleMouseDown=function(evt){var action=null;m_paused||("momentum"===m_state.action&&(m_state={}),m_this._getMousePosition(evt),m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),!m_options.click.enabled||m_mouse.buttons.left&&!m_options.click.buttons.left||m_mouse.buttons.right&&!m_options.click.buttons.right||m_mouse.buttons.middle&&!m_options.click.buttons.middle||(m_clickMaybe=!0,m_options.click.duration>0&&window.setTimeout(function(){m_clickMaybe=!1},m_options.click.duration)),eventMatch(m_options.panMoveButton,m_options.panMoveModifiers)?action="pan":eventMatch(m_options.zoomMoveButton,m_options.zoomMoveModifiers)?action="zoom":eventMatch(m_options.selectionButton,m_options.selectionModifiers)&&(action="select"),m_mouse.velocity={x:0,y:0},action&&(m_state={action:action,origin:$.extend(!0,{},m_mouse),delta:{x:0,y:0}},"select"===action&&(m_selectionLayer&&(m_selectionLayer.clear(),m_this.map().deleteLayer(m_selectionLayer),m_selectionLayer=null),m_selectionLayer=m_this.map().createLayer("feature",{renderer:"d3"}),m_selectionPlane=m_selectionLayer.createFeature("plane"),m_selectionPlane.style({screenCoordinates:!0,fillOpacity:function(){return.25}}),m_this.map().geoTrigger(geo.event.brushstart,m_this._getSelection())),$(document).on("mousemove.geojs",m_this._handleMouseMoveDocument),$(document).on("mouseup.geojs",m_this._handleMouseUpDocument)))},this._handleMouseMove=function(evt){m_paused||m_state.action||(m_options.click.cancelOnMove&&(m_clickMaybe=!1),m_clickMaybe||(m_this._getMousePosition(evt),m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),m_this.map().geoTrigger(geo.event.mousemove,m_this.mouse())))},this._handleMouseMoveDocument=function(evt){var dx,dy,selectionObj;if(!m_paused&&(m_this._getMousePosition(evt),m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),m_options.click.cancelOnMove&&(m_clickMaybe=!1),!m_clickMaybe))return m_state.action?void(doRespond()&&(dx=m_mouse.map.x-m_state.origin.map.x-m_state.delta.x,dy=m_mouse.map.y-m_state.origin.map.y-m_state.delta.y,m_state.delta.x+=dx,m_state.delta.y+=dy,"pan"===m_state.action?m_this.map().pan({x:dx,y:dy}):"zoom"===m_state.action?m_this.map().zoom(m_this.map().zoom()-dy*m_options.zoomScale/120,m_state):"select"===m_state.action&&(selectionObj=m_this._getSelection(),m_this.map().geoTrigger(geo.event.brush,selectionObj)),evt.preventDefault())):void console.log("WARNING: Invalid state in mapInteractor.")},this._handleMouseUpDocument=function(evt){var selectionObj,oldAction;m_paused||(m_clickMaybe=!1,m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),$(document).off(".geojs"),m_mouse.buttons.right&&evt.preventDefault(),"select"===m_state.action&&(selectionObj=m_this._getSelection(),m_selectionLayer.clear(),m_this.map().deleteLayer(m_selectionLayer),m_selectionLayer=null,m_selectionPlane=null,m_this.map().geoTrigger(geo.event.brushend,selectionObj)),oldAction=m_state.action,m_state={},m_options.momentum.enabled&&"pan"===oldAction&&m_this.springBack(!0))},this._handleMouseUp=function(evt){m_paused||m_clickMaybe&&m_this._handleMouseClick(evt)},this._handleMouseClick=function(evt){m_this._getMouseButton(evt),m_this._getMouseModifiers(evt),m_this.cancel("pan"),$(document).off(".geojs"),m_clickMaybe=!1,m_this.map().geoTrigger(geo.event.mouseclick,m_this.mouse())},this._handleMouseWheel=function(evt){var zoomFactor;if(!m_paused){if(evt.deltaFactor=1,1===evt.originalEvent.deltaMode?evt.deltaFactor=12:2===evt.originalEvent.deltaMode&&(evt.deltaFactor=$(window).height()),evt.deltaX=evt.originalEvent.deltaX||0,evt.deltaY=evt.originalEvent.deltaY||0,m_this._getMouseModifiers(evt),evt.deltaX=evt.deltaX*m_options.wheelScaleX*evt.deltaFactor/120,evt.deltaY=evt.deltaY*m_options.wheelScaleY*evt.deltaFactor/120,evt.preventDefault(),!doRespond())return m_wheelQueue.x+=evt.deltaX,void(m_wheelQueue.y+=evt.deltaY);evt.deltaX+=m_wheelQueue.x,evt.deltaY+=m_wheelQueue.y,m_wheelQueue={x:0,y:0},m_options.panWheelEnabled&&eventMatch("wheel",m_options.panWheelModifiers)?m_this.map().pan({x:evt.deltaX,y:-evt.deltaY}):m_options.zoomWheelEnabled&&eventMatch("wheel",m_options.zoomWheelModifiers)&&(zoomFactor=-evt.deltaY,m_this.map().zoom(m_this.map().zoom()+zoomFactor,m_mouse))}},this.springBack=function(initialVelocity){"momentum"!==m_state.action&&(initialVelocity||(m_mouse.velocity={x:0,y:0}),m_state.action="momentum",m_state.origin=m_this.mouse(),m_state.start=new Date,m_state.handler=function(){var v,s,last,dt;if(dt=Math.min(m_mouse.deltaTime,30),"momentum"===m_state.action&&m_this.map()&&!m_this.map().transition()){if(last=m_state.start.valueOf(),m_state.start=new Date,v=modifyVelocity(m_mouse.velocity,m_state.start-last),!v)return void(m_state={});s=calcSpeed(v),s>m_options.momentum.maxSpeed&&(s=m_options.momentum.maxSpeed/s,v.x=v.x*s,v.y=v.y*s),isFinite(v.x)&&isFinite(v.y)||(v.x=0,v.y=0),m_mouse.velocity.x=v.x,m_mouse.velocity.y=v.y,m_this.map().pan({x:m_mouse.velocity.x*dt,y:m_mouse.velocity.y*dt}),m_state.handler&&window.requestAnimationFrame(m_state.handler)}},m_state.handler&&window.requestAnimationFrame(m_state.handler))},this._handleDoubleClick=function(){},this.destroy=function(){m_this._disconnectEvents(),m_this.map(null)},this.mouse=function(){return $.extend(!0,{},m_mouse)},this.keyboard=function(){return $.extend(!0,{},m_keyboard)},this.state=function(){return $.extend(!0,{},m_state)},this.pause=function(value){return void 0===value?m_paused:(m_paused=!!value,m_this)},this.simulateEvent=function(type,options){var evt,page,offset,which;return m_this.map()?(page=options.page||{},options.map&&(offset=$node.offset(),page.x=options.map.x+offset.left,page.y=options.map.y+offset.top),"left"===options.button?which=1:"right"===options.button?which=3:"middle"===options.button&&(which=2),options.modifiers=options.modifiers||[],options.wheelDelta=options.wheelDelta||{},evt=$.Event(type,{pageX:page.x,pageY:page.y,which:which,altKey:options.modifiers.indexOf("alt")>=0,ctrlKey:options.modifiers.indexOf("ctrl")>=0,metaKey:options.modifiers.indexOf("meta")>=0,shiftKey:options.modifiers.indexOf("shift")>=0,originalEvent:{deltaX:options.wheelDelta.x,deltaY:options.wheelDelta.y,deltaMode:options.wheelMode}}),void $node.trigger(evt)):m_this},this._connectEvents(),this},inherit(geo.mapInteractor,geo.object),geo.clock=function(opts){"use strict";if(!(this instanceof geo.clock))return new geo.clock(opts);opts=opts||{},geo.object.call(this,opts);var m_this=this,m_now=new Date(0),m_start=null,m_end=null,m_step=null,m_rate=null,m_loop=Number.POSITIVE_INFINITY,m_currentLoop=0,m_state="stop",m_currentAnimation=null,m_object=null;this.object=function(arg){return void 0===arg?m_object:(m_object=arg,m_this)},this._attached=function(){return m_object instanceof geo.object},this.now=function(arg){var previous=m_now;return void 0===arg?m_now:(m_now=arg,m_now!==previous&&m_this._attached()&&m_this.object().geoTrigger(geo.event.clock.change,{previous:previous,current:m_now,clock:m_this}),m_this)},this.start=function(arg){return void 0===arg?m_start:(m_start=arg,m_this)},this.end=function(arg){return void 0===arg?m_end:(m_end=arg,m_this)},this.step=function(arg){return void 0===arg?m_step:(m_step=arg,m_this)},this.loop=function(arg){return void 0===arg?m_loop:(m_loop=arg,m_this)},this.state=function(arg,step){return void 0===arg?m_state:["stop","play","pause"].indexOf(arg)<0?(console.log("WARNING: Ignored invalid state: "+arg),m_this):("play"===arg&&"stop"===m_state&&(m_currentLoop=0,m_this.now(m_this.start())),"play"===arg&&"play"!==m_state&&(m_state=arg,m_this._animate(step||1)),m_state=arg,m_this)},this.framerate=function(arg){return void 0===arg?m_rate:(m_rate=arg,m_this)},this.stepForward=function(){return m_this.state("pause"),m_this._setNextFrame(1),m_this},this.stepBackward=function(){return m_this.state("pause"),m_this._setNextFrame(-1),m_this},this._setNextFrame=function(step){var next=new Date(m_this.now().valueOf()+step*m_this.step());return next>=m_this.end()||next<=m_this.start()?m_this.loop()<=m_currentLoop?void m_this.state("stop"):(m_currentLoop+=1,void(step>=0?m_this.now(m_this.start()):m_this.now(m_this.end()))):void m_this.now(next)},this._animate=function(step){function frame(){myAnimation===m_currentAnimation&&(m_this._setNextFrame(step),"play"===m_this.state()?m_this.framerate()?window.setTimeout(frame,1e3/m_this.framerate()):window.requestAnimationFrame(frame):m_this._attached()&&m_this.object().geoTrigger(geo.event.clock[m_this.state()],{current:m_this.now(),clock:m_this}))}var myAnimation={};m_currentAnimation=myAnimation,m_this._attached()&&m_this.object().geoTrigger(geo.event.clock.play,{current:m_this.now(),clock:m_this}),m_this.framerate()?window.setTimeout(frame,1e3/m_this.framerate()):window.requestAnimationFrame(frame)}},inherit(geo.clock,geo.object),function(){"use strict";geo.tile=function(spec){return this instanceof geo.tile?(this._index=spec.index,this._size=spec.size,this._overlap=spec.overlap||{x:0,y:0},this._wrap=spec.wrap||{x:1,y:1},this._url=spec.url,this._fetched=!1,Object.defineProperty(this,"index",{get:function(){return this._index}}),Object.defineProperty(this,"size",{get:function(){return this._size}}),Object.defineProperty(this,"overlap",{get:function(){return this._overlap}}),this.fetch=function(){return this._fetched||$.get(this._url).promise(this),this},this.then=function(onSuccess,onFailure){return this.fetch(),this.then(onSuccess,onFailure),this},this["catch"]=function(method){return this.then(void 0,method),this},this.toString=function(){return[this._index.level||0,this._index.y,this._index.x].join("_")},this.bounds=function(index,shift){var left,right,bottom,top;return left=this.size.x*(this.index.x-index.x)-this.overlap.x-shift.x,right=left+this.size.x+2*this.overlap.x,top=this.size.y*(this.index.y-index.y)-this.overlap.y-shift.y,bottom=top+this.size.y+2*this.overlap.y,{left:left,right:right,bottom:bottom,top:top}},Object.defineProperty(this,"bottom",{get:function(){return this.size.y*(this.index.y+1)+this.overlap.y}}),Object.defineProperty(this,"top",{get:function(){return this.size.y*this.index.y-this.overlap.y}}),Object.defineProperty(this,"left",{get:function(){return this.size.x*this.index.x-this.overlap.x}}),Object.defineProperty(this,"right",{get:function(){return this.size.x*(this.index.x+1)+this.overlap.x}}),Object.defineProperty(this,"levelSize",{value:{width:Math.pow(2,this.index.level||0)*this.size.x,height:Math.pow(2,this.index.level||0)*this.size.y}}),void(this.fadeIn=function(duration){return $.noop(duration),this})):new geo.tile(spec)}}(),function(){"use strict";geo.imageTile=function(spec){return this instanceof geo.imageTile?(spec.size=spec.size||{x:256,y:256},this._image=null,this._cors=spec.crossDomain||"anonymous",geo.tile.call(this,spec),Object.defineProperty(this,"image",{get:function(){return this._image}}),this.fetch=function(){var defer;return this._image||(this._image=new Image(this.size.x,this.size.y),this._url.indexOf(":")>=0&&this._url.indexOf("/")>=0&&this._url.indexOf(":")n;)this.remove(this._atime[this._atime.length-1]);this._size=n}}),Object.defineProperty(this,"length",{get:function(){return this._atime.length}}),this._access=function(hash){return this._atime.indexOf(hash)},this.remove=function(tile){var hash="string"==typeof tile?tile:tile.toString();return hash in this._cache?(this._atime.splice(this._access(hash),1),delete this._cache[hash],!0):!1},this.clear=function(){return this._cache={},this._atime=[],this},this.get=function(hash){return hash="string"==typeof hash?hash:hash.toString(),hash in this._cache?(this._atime.splice(this._access(hash),1),this._atime.unshift(hash),this._cache[hash]):null},this.add=function(tile){this.remove(tile);var hash=tile.toString();for(this._cache[hash]=tile,this._atime.unshift(hash);this._atime.length>this.size;)hash=this._atime.pop(),delete this._cache[hash]},this.clear(),this):new geo.tileCache(options)}}(),function(){"use strict";function modulo(a,b){return(a%b+b)%b}function m_getTileSubdomain(x,y,subdomains){return subdomains[modulo(x+y,subdomains.length)]}function m_tileUrlFromTemplate(base){return function(x,y,z,subdomains){return base.replace("{s}",m_getTileSubdomain(x,y,subdomains)).replace("{z}",z).replace("{x}",x).replace("{y}",y)}}geo.tileLayer=function(options){if(!(this instanceof geo.tileLayer))return new geo.tileLayer(options);if(geo.featureLayer.call(this,options),options=$.extend(!0,{},this.constructor.defaults,options||{}),options.cacheSize||(options.cacheSize=options.keepLower?400:200),"string"===$.type(options.subdomains)&&(options.subdomains=options.subdomains.split("")),options.baseUrl){var url=options.baseUrl;url&&"/"!==url.charAt(url.length-1)&&(url+="/"),options.url=url+"{z}/{x}/{y}."+(options.imageFormat||"png")}options.originalUrl=options.url,"string"===$.type(options.url)&&(options.url=m_tileUrlFromTemplate(options.url));var lastZoom=null,lastX=null,lastY=null,s_init=this._init,_deferredPurge=null;return this._options=$.extend(!0,{},options),this.attribution(options.attribution),this._activeTiles={},this._tileTree={},this._cache=geo.tileCache({size:options.cacheSize}),Object.defineProperty(this,"options",{get:function(){return $.extend({},this._options)}}),Object.defineProperty(this,"cache",{get:function(){return this._cache}}),Object.defineProperty(this,"activeTiles",{get:function(){return $.extend({},this._activeTiles)}}),this.tilesAtZoom=function(level){var s=Math.pow(2,level);return{x:s,y:s}},this.isValid=function(index){return this._options.minLevel<=index.level&&index.level<=this._options.maxLevel&&(this._options.wrapX||0<=index.x&&index.x<=this.tilesAtZoom(index.level).x-1)&&(this._options.wrapY||0<=index.y&&index.y<=this.tilesAtZoom(index.level).y-1)?!0:!1},this._origin=function(level){var index,offset,origin=this.toLevel(this.toLocal(this.map().origin()),level),o=this._options;return index={x:Math.floor(origin.x/o.tileWidth),y:Math.floor(origin.y/o.tileHeight)},offset={x:origin.x-o.tileWidth*index.x,y:origin.y-o.tileHeight*index.y},{index:index,offset:offset}},this._tileBounds=function(tile){var origin=this._origin(tile.index.level);return tile.bounds(origin.index,origin.offset)},this.tileAtPoint=function(point,level){var o=this._origin(level),map=this.map();point=this.displayToLevel(map.gcsToDisplay(point,null),level);var to=this._options.tileOffset(level);to&&(point.x+=to.x,point.y+=to.y);var tile={x:Math.floor(o.index.x+(o.offset.x+point.x)/this._options.tileWidth),y:Math.floor(o.index.y+(o.offset.y+point.y)/this._options.tileHeight)};return tile},this.gcsTileBounds=function(indexOrTile,gcs){var tile=indexOrTile.index?indexOrTile:geo.tile({index:indexOrTile,size:{x:this._options.tileWidth,y:this._options.tileHeight},url:""}),to=this._options.tileOffset(tile.index.level),bounds=tile.bounds({x:0,y:0},to),map=this.map(),unit=map.unitsPerPixel(tile.index.level),coord=[{x:bounds.left*unit,y:this._topDown()*bounds.top*unit},{x:bounds.right*unit,y:this._topDown()*bounds.bottom*unit}];return gcs=null===gcs?map.gcs():void 0===gcs?map.ingcs():gcs,gcs!==map.gcs()&&(coord=geo.transform.transformCoordinates(gcs,map.gcs(),coord)),{left:coord[0].x,top:coord[0].y,right:coord[1].x,bottom:coord[1].y}},this._getTile=function(index,source){var urlParams=source||index;return geo.tile({index:index,size:{x:this._options.tileWidth,y:this._options.tileHeight},url:this._options.url(urlParams.x,urlParams.y,urlParams.level||0,this._options.subdomains)})},this._getTileCached=function(index,source){var tile=this.cache.get(this._tileHash(index));return null===tile&&(tile=this._getTile(index,source),this.cache.add(tile)),tile},this._tileHash=function(index){return[index.level||0,index.y,index.x].join("_")},this._getTileRange=function(level,bounds){return{start:this.tileAtPoint({x:bounds.left,y:bounds.top},level),end:this.tileAtPoint({x:bounds.right,y:bounds.bottom},level)}},this._getTiles=function(maxLevel,bounds,sorted){var i,j,index,nTilesLevel,start,end,indexRange,source,center,level,tiles=[],minLevel=this._options.keepLower?0:maxLevel;for(level=minLevel;maxLevel>=level;level+=1)for(indexRange=this._getTileRange(level,bounds),start=indexRange.start,end=indexRange.end,nTilesLevel=this.tilesAtZoom(level),index={level:level},index.nx=nTilesLevel.x,index.ny=nTilesLevel.y,i=start.x;i<=end.x;i+=1)for(index.x=i,j=start.y;j<=end.y;j+=1)index.y=j,source=$.extend({},index),this._options.wrapX&&(source.x=modulo(index.x,index.nx)),this._options.wrapY&&(source.y=modulo(index.y,index.ny)),this.isValid(source)&&tiles.push(this._getTileCached($.extend({},index),source));return sorted&&(center={x:(start.x+end.x)/2,y:(start.y+end.y)/2},tiles.sort(this._loadMetric(center))),tiles},this.prefetch=function(level,bounds){var tiles;return tiles=this._getTiles(level,bounds,!0),$.when.apply($,tiles.map(function(tile){return tile.fetch()}))},this._loadMetric=function(center){return function(a,b){var a0,b0,dx,dy,cx,cy,scale;return a.level!==b.level?b.level-a.level:(scale=Math.pow(2,a.level-center.level),cx=center.x*scale,cy=center.y*scale,dx=a.x-cx,dy=a.y-cy,a0=dx*dx+dy*dy,dx=b.x-cx,dy=b.y-cy,b0=dx*dx+dy*dy,a0-b0)}},this.fromLevel=function(coord,level){var s=Math.pow(2,-level);return{x:coord.x*s,y:coord.y*s}},this.toLevel=function(coord,level){var s=Math.pow(2,level);return{x:coord.x*s,y:coord.y*s}},this.drawTile=function(tile){var hash=tile.toString();this._activeTiles.hasOwnProperty(hash)?this._moveToTop(tile):this._drawTile(tile),this._activeTiles[hash]=tile},this._drawTile=function(tile){if(null!==this.renderer())throw new Error("This draw method is not valid on renderer managed layers.");var div=$(this._getSubLayer(tile.index.level)),bounds=this._tileBounds(tile),duration=this._options.animationDuration,container=$('
').attr("tile-reference",tile.toString());container.append(tile.image),container.css({position:"absolute",left:bounds.left+"px",top:bounds.top+"px"}),duration>0&&tile.fadeIn(duration),div.append(container),tile["catch"](function(){console.warn("Could not load tile at "+tile.index),this._remove(tile)}.bind(this))},this.remove=function(tile){var hash=tile.toString(),value=this._activeTiles[hash];return value instanceof geo.tile&&this._remove(value),delete this._activeTiles[hash],value},this._remove=function(tile){tile.image&&(tile.image.parentElement?$(tile.image.parentElement).remove():console.log("No parent element to remove "+tile.toString(),tile),$(tile.image).remove())},this._moveToTop=function(tile){$.noop(tile)},this._getViewBounds=function(){var map=this.map(),mapZoom=map.zoom(),zoom=this._options.tileRounding(mapZoom),scale=Math.pow(2,mapZoom-zoom),size=map.size(),ul=this.displayToLevel({x:0,y:0}),lr=this.displayToLevel({x:size.width,y:size.height});return{level:zoom,scale:scale,left:ul.x,right:lr.x,bottom:lr.y,top:ul.y}},this._purge=function(zoom,doneLoading){var tile,hash,bounds={};if(!this._updating){bounds=this._getViewBounds();for(hash in this._activeTiles)tile=this._activeTiles[hash],this._canPurge(tile,bounds,zoom,doneLoading)&&this.remove(tile);return this}},this.clear=function(){var tile,tiles=[];for(tile in this._activeTiles)tiles.push(this.remove(tile));return this._tileTree={},tiles},this.reset=function(){this.clear(),this._cache.clear()},this.toLocal=function(pt,zoom){var map=this.map(),unit=map.unitsPerPixel(void 0===zoom?map.zoom():zoom);return{x:pt.x/unit,y:this._topDown()*pt.y/unit}},this.fromLocal=function(pt,zoom){var map=this.map(),unit=map.unitsPerPixel(void 0===zoom?map.zoom():zoom);return{x:pt.x*unit,y:this._topDown()*pt.y*unit}},this._topDown=function(){return this._options.topDown?1:-1},this._getSubLayer=function(level){var node=this.canvas().find("div[data-tile-layer="+level.toFixed()+"]").get(0);return node||(node=$('
').css("transform-origin","0px").get(0),this.canvas().append(node)),node},this._updateSubLayers=function(level){this.canvas().find(".geo-tile-layer").each(function(idx,el){var $el=$(el),layer=parseInt($el.data("tileLayer"));$el.css("transform","scale("+Math.pow(2,level-layer)+")")}.bind(this))},this._update=function(){var tiles,map=this.map(),mapZoom=map.zoom(),zoom=this._options.tileRounding(mapZoom),center=this.displayToLevel(void 0,zoom),bounds=map.bounds(void 0,null),view=this._getViewBounds(),myPurge={};_deferredPurge=myPurge,tiles=this._getTiles(zoom,bounds,!0),this._updateSubLayers(zoom);var to=this._options.tileOffset(zoom);null===this.renderer()&&(this.canvas().css("transform-origin","center center"),this.canvas().css("transform","scale("+Math.pow(2,mapZoom-zoom)+")translate("+-to.x+"px,"+-to.y+"px)translate("+map.size().width/2+"px,"+map.size().height/2+"px)translate("+-(view.left+view.right)/2+"px,"+-(view.bottom+view.top)/2+"px)")),this.canvas().attr({scale:Math.pow(2,mapZoom-zoom),dx:-to.x+-(view.left+view.right)/2,dy:-to.y+-(view.bottom+view.top)/2}),lastZoom=mapZoom,lastX=center.x,lastY=center.y,this._tileTree={},tiles.forEach(function(tile){tile.then(function(){if(tile===this.cache.get(tile.toString())){var mapZoom=map.zoom(),zoom=this._options.tileRounding(mapZoom),bounds=this._getViewBounds();if(this._canPurge(tile,bounds,zoom))return void this.remove(tile);this.drawTile(tile),this._setTileTree(tile)}}.bind(this)),this.addPromise(tile)}.bind(this)),$.when.apply($,tiles).done(function(){_deferredPurge===myPurge&&this._purge(zoom,!0)}.bind(this))},this._setTileTree=function(tile){var index=tile.index;this._tileTree[index.level]=this._tileTree[index.level]||{},this._tileTree[index.level][index.x]=this._tileTree[index.level][index.x]||{},this._tileTree[index.level][index.x][index.y]=tile},this._getTileTree=function(index){return((this._tileTree[index.level]||{})[index.x]||{})[index.y]||null},this._isCovered=function(tile){var level=tile.index.level,x=tile.index.x,y=tile.index.y,tiles=[];return(tiles=this._getTileTree({level:level-1,x:Math.floor(x/2),y:Math.floor(y/2)}))?[tiles]:(tiles=[this._getTileTree({level:level+1,x:2*x,y:2*y}),this._getTileTree({level:level+1,x:2*x+1,y:2*y}),this._getTileTree({level:level+1,x:2*x,y:2*y+1}),this._getTileTree({level:level+1,x:2*x+1,y:2*y+1})],tiles.every(function(t){return null!==t})?tiles:null)},this._outOfBounds=function(tile,bounds){var to=this._options.tileOffset(tile.index.level),scale=1;return tile.index.level!==bounds.level&&(scale=Math.pow(2,(bounds.level||0)-(tile.index.level||0))),(tile.bottom-to.y)*scalebounds.right||(tile.top-to.y)*scale>bounds.bottom||(tile.right-to.x)*scalear_vp?(sclx=1,scly=ar_bds/ar_vp):(scly=1,sclx=ar_vp/ar_bds),{x:sclx,y:scly}}function calculate_zoom(bounds){var z,scl=camera_scaling(bounds);return z=scl.y>scl.x?-Math.log2(Math.abs(bounds.right-bounds.left)*scl.x/(m_width*m_unitsPerPixel)):-Math.log2(Math.abs(bounds.top-bounds.bottom)*scl.y/(m_height*m_unitsPerPixel))}function reset_minimum_zoom(){m_clampZoom?m_validZoomRange.min=Math.max(m_validZoomRange.origMin,calculate_zoom(m_maxBounds)):m_validZoomRange.min=m_validZoomRange.origMin}function fix_zoom(zoom,ignoreDiscreteZoom){return zoom=Math.max(Math.min(m_validZoomRange.max,zoom),m_validZoomRange.min),m_discreteZoom&&!ignoreDiscreteZoom&&(zoom=Math.round(zoom),zoomm_maxBounds.right-m_maxBounds.left?dx=m_maxBounds.left-(bounds.right-bounds.left-(m_maxBounds.right-m_maxBounds.left))/2-bounds.left:bounds.leftm_maxBounds.right&&(dx=m_maxBounds.right-bounds.right),dx&&(bounds={left:bounds.left+=dx,right:bounds.right+=dx,top:bounds.top,bottom:bounds.bottom})),m_clampBoundsY&&(bounds.top-bounds.bottom>m_maxBounds.top-m_maxBounds.bottom?dy=m_maxBounds.bottom-(bounds.top-bounds.bottom-(m_maxBounds.top-m_maxBounds.bottom))/2-bounds.bottom:bounds.top>m_maxBounds.top?dy=m_maxBounds.top-bounds.top:bounds.bottom=m_transition.end.time||next)return next||(m_this.center(m_transition.end.center,null),m_this.zoom(m_transition.end.zoom)),m_transition=null,m_this.geoTrigger(geo.event.transitionend,defaultOpts),done&&done(),void(next&&(m_queuedTransition=null,m_this.transition(next)));var z=m_transition.ease((time-m_transition.start.time)/defaultOpts.duration),p=m_transition.interp(z);m_transition.zCoord&&(p[2]=z2zoom(p[2])),m_this.center({x:p[0],y:p[1]},null),m_this.zoom(p[2],void 0,!0),window.requestAnimationFrame(anim)}if(void 0===opts)return m_transition;if(m_transition)return m_queuedTransition=opts,m_this;var units=m_this.unitsPerPixel(0),defaultOpts={center:m_this.center(void 0,null),zoom:m_this.zoom(),duration:1e3,ease:function(t){return t},interp:defaultInterp,done:null,zCoord:!0};return opts.center&&(gcs=null===gcs?m_gcs:void 0===gcs?m_ingcs:gcs,opts.center=geo.util.normalizeCoordinates(opts.center),gcs!==m_gcs&&(opts.center=geo.transform.transformCoordinates(gcs,m_gcs,[opts.center])[0])),$.extend(defaultOpts,opts),m_transition={start:{center:m_this.center(void 0,null),zoom:m_this.zoom()},end:{center:defaultOpts.center,zoom:fix_zoom(defaultOpts.zoom)},ease:defaultOpts.ease,zCoord:defaultOpts.zCoord,done:defaultOpts.done,duration:defaultOpts.duration},defaultOpts.zCoord?m_transition.interp=defaultOpts.interp([m_transition.start.center.x,m_transition.start.center.y,zoom2z(m_transition.start.zoom)],[m_transition.end.center.x,m_transition.end.center.y,zoom2z(m_transition.end.zoom)]):m_transition.interp=defaultOpts.interp([m_transition.start.center.x,m_transition.start.center.y,m_transition.start.zoom],[m_transition.end.center.x,m_transition.end.center.y,m_transition.end.zoom]),m_this.geoTrigger(geo.event.transitionstart,defaultOpts),defaultOpts.cancelNavigation?(m_this.geoTrigger(geo.event.transitionend,defaultOpts),m_this):(defaultOpts.cancelAnimation?(defaultOpts.duration=0,anim(0)):window.requestAnimationFrame(anim),m_this)},this.bounds=function(bds,gcs){var nav;if(gcs=null===gcs?m_gcs:void 0===gcs?m_ingcs:gcs,void 0!==bds){if(gcs!==m_gcs){var trans=geo.transform.transformCoordinates(gcs,m_gcs,[{x:bds.left,y:bds.top},{x:bds.right,y:bds.bottom}]);bds={left:trans[0].x,top:trans[0].y,right:trans[1].x,bottom:trans[1].y}}bds=fix_bounds(bds),nav=m_this.zoomAndCenterFromBounds(bds,null),m_this.zoom(nav.zoom),m_this.center(nav.center,null)}return m_this.boundsFromZoomAndCenter(m_zoom,m_center,gcs)},this.zoomAndCenterFromBounds=function(bounds,gcs){var center,zoom;if(gcs=null===gcs?m_gcs:void 0===gcs?m_ingcs:gcs,gcs!==m_gcs){var trans=geo.transform.transformCoordinates(gcs,m_gcs,[{x:bounds.left,y:bounds.top},{x:bounds.right,y:bounds.bottom}]);bounds={left:trans[0].x,top:trans[0].y,right:trans[1].x,bottom:trans[1].y}}if(bounds.left>=bounds.right||bounds.bottom>=bounds.top)throw new Error("Invalid bounds provided");return zoom=fix_zoom(calculate_zoom(bounds)),bounds=fix_bounds(bounds),center={x:(bounds.left+bounds.right)/2-m_origin.x,y:(bounds.top+bounds.bottom)/2-m_origin.y},{zoom:zoom,center:center}},this.boundsFromZoomAndCenter=function(zoom,center,gcs){var width,height,bounds,units;return gcs=null===gcs?m_gcs:void 0===gcs?m_ingcs:gcs,zoom=fix_zoom(zoom),units=m_this.unitsPerPixel(zoom),center=m_this.gcsToWorld(center,gcs),width=m_width*units/2,height=m_height*units/2,bounds={left:center.x-width+m_origin.x,right:center.x+width+m_origin.x,bottom:center.y-height+m_origin.y,top:center.y+height+m_origin.y},fix_bounds(bounds)},this.discreteZoom=function(discreteZoom){return void 0===discreteZoom?m_discreteZoom:(discreteZoom=discreteZoom?!0:!1,m_discreteZoom!==discreteZoom&&(m_discreteZoom=discreteZoom,m_discreteZoom&&m_this.zoom(Math.round(m_this.zoom()))),m_this)},this.layers=this.children,this.updateAttribution=function(){m_this.node().find(".geo-attribution").remove();var $a=$("
").addClass("geo-attribution").css({position:"absolute",right:"0px",bottom:"0px","padding-right":"5px",cursor:"auto",font:'11px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif',"z-index":"1001",background:"rgba(255,255,255,0.7)",clear:"both",display:"block","pointer-events":"auto"}).on("mousedown",function(evt){evt.stopPropagation()});return m_this.children().forEach(function(layer){var content=layer.attribution();content&&$("").addClass("geo-attribution-layer").css({"padding-left":"5px"}).html(content).appendTo($a)}),$a.appendTo(m_this.node()),m_this},m_origin={x:0,y:0},this.zoomRange(arg),m_zoom=fix_zoom(m_zoom),this.center($.extend({},arg.center||m_center),void 0),this.interactor(arg.interactor||geo.mapInteractor()),this.clock(arg.clock||geo.clock()),arg.autoResize&&$(window).resize(resizeSelf),m_this.geoOn([geo.event.layerAdd,geo.event.layerRemove],m_this.updateAttribution),this},geo.map.create=function(spec){"use strict";var map=geo.map(spec);return map?(spec.data=spec.data||[],spec.layers=spec.layers||[],spec.layers.forEach(function(l){l.data=l.data||spec.data,l.layer=geo.layer.create(map,l)}),map):(console.warn("Could not create map."),null)},inherit(geo.map,geo.sceneObject),geo.feature=function(arg){"use strict";if(!(this instanceof geo.feature))return new geo.feature(arg);geo.sceneObject.call(this),arg=arg||{};var m_this=this,s_exit=this._exit,m_selectionAPI=void 0===arg.selectionAPI?!1:arg.selectionAPI,m_style={},m_layer=void 0===arg.layer?null:arg.layer,m_gcs=arg.gcs,m_visible=void 0===arg.visible?!0:arg.visible,m_bin=void 0===arg.bin?0:arg.bin,m_renderer=void 0===arg.renderer?null:arg.renderer,m_dataTime=geo.timestamp(),m_buildTime=geo.timestamp(),m_updateTime=geo.timestamp(),m_selectedFeatures=[];return this._bindMouseHandlers=function(){m_selectionAPI&&(m_this._unbindMouseHandlers(),m_this.geoOn(geo.event.mousemove,m_this._handleMousemove),m_this.geoOn(geo.event.mouseclick,m_this._handleMouseclick),m_this.geoOn(geo.event.brushend,m_this._handleBrushend),m_this.geoOn(geo.event.brush,m_this._handleBrush))},this._unbindMouseHandlers=function(){m_this.geoOff(geo.event.mousemove,m_this._handleMousemove),m_this.geoOff(geo.event.mouseclick,m_this._handleMouseclick),m_this.geoOff(geo.event.brushend,m_this._handleBrushend),m_this.geoOff(geo.event.brush,m_this._handleBrush)},this.pointSearch=function(){return{index:[],found:[]}},this._handleMousemove=function(){var mouse=m_this.layer().map().interactor().mouse(),data=m_this.data(),over=m_this.pointSearch(mouse.geo),newFeatures=[],oldFeatures=[],lastTop=-1,top=-1;m_selectedFeatures.length&&(lastTop=m_selectedFeatures[m_selectedFeatures.length-1]),newFeatures=over.index.filter(function(i){return m_selectedFeatures.indexOf(i)<0}),oldFeatures=m_selectedFeatures.filter(function(i){return over.index.indexOf(i)<0}),geo.feature.eventID+=1,newFeatures.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mouseover,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===newFeatures.length-1},!0)}),geo.feature.eventID+=1,oldFeatures.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mouseout,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===oldFeatures.length-1},!0)}),geo.feature.eventID+=1,over.index.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mousemove,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===over.index.length-1},!0)}),m_selectedFeatures=over.index,m_selectedFeatures.length&&(top=m_selectedFeatures[m_selectedFeatures.length-1]),lastTop!==top&&(-1!==lastTop&&m_this.geoTrigger(geo.event.feature.mouseoff,{data:data[lastTop],index:lastTop,mouse:mouse},!0),-1!==top&&m_this.geoTrigger(geo.event.feature.mouseon,{data:data[top],index:top,mouse:mouse},!0))},this._handleMouseclick=function(){var mouse=m_this.layer().map().interactor().mouse(),data=m_this.data(),over=m_this.pointSearch(mouse.geo);geo.feature.eventID+=1,over.index.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.mouseclick,{data:data[i],index:i,mouse:mouse,eventID:geo.feature.eventID,top:idx===over.index.length-1},!0)})},this._handleBrush=function(brush){var idx=m_this.boxSearch(brush.gcs.lowerLeft,brush.gcs.upperRight),data=m_this.data();geo.feature.eventID+=1,idx.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.brush,{data:data[i],index:i,mouse:brush.mouse,brush:brush,eventID:geo.feature.eventID,top:idx===idx.length-1},!0)})},this._handleBrushend=function(brush){var idx=m_this.boxSearch(brush.gcs.lowerLeft,brush.gcs.upperRight),data=m_this.data();geo.feature.eventID+=1,idx.forEach(function(i,idx){m_this.geoTrigger(geo.event.feature.brushend,{data:data[i],index:i,mouse:brush.mouse,brush:brush,eventID:geo.feature.eventID,top:idx===idx.length-1},!0)})},this.style=function(arg1,arg2){return void 0===arg1?m_style:"string"==typeof arg1&&void 0===arg2?m_style[arg1]:void 0===arg2?(m_style=$.extend({},m_style,arg1),m_this.modified(),m_this):(m_style[arg1]=arg2,m_this.modified(),m_this)},this.style.get=function(key){var tmp,out;if(void 0===key){var k,all={};for(k in m_style)m_style.hasOwnProperty(k)&&(all[k]=m_this.style.get(k));return all}return key.toLowerCase().match(/color$/)?geo.util.isFunction(m_style[key])?(tmp=geo.util.ensureFunction(m_style[key]),out=function(){return geo.util.convertColor(tmp.apply(this,arguments))}):out=geo.util.ensureFunction(geo.util.convertColor(m_style[key])):out=geo.util.ensureFunction(m_style[key]),out},this.layer=function(){return m_layer},this.renderer=function(){return m_renderer},this.gcs=function(val){return void 0===val?void 0===m_gcs&&m_renderer?m_renderer.layer().map().ingcs():m_gcs:(m_gcs=val,m_this.modified(),m_this)},this.featureGcsToDisplay=function(c){var map=m_renderer.layer().map();return c=map.gcsToWorld(c,map.ingcs()),c=map.worldToDisplay(c),m_renderer.baseToLocal&&(c=m_renderer.baseToLocal(c)),c},this.visible=function(val){return void 0===val?m_visible:(m_visible=val,m_this.modified(),m_visible?m_this._bindMouseHandlers():m_this._unbindMouseHandlers(),m_this)},this.bin=function(val){return void 0===val?m_bin:(m_bin=val,m_this.modified(),m_this)},this.dataTime=function(val){return void 0===val?m_dataTime:(m_dataTime=val,m_this.modified(),m_this)},this.buildTime=function(val){return void 0===val?m_buildTime:(m_buildTime=val,m_this.modified(),m_this)},this.updateTime=function(val){return void 0===val?m_updateTime:(m_updateTime=val,m_this.modified(),m_this)},this.data=function(data){return void 0===data?m_this.style("data")||[]:(m_this.style("data",data),m_this.dataTime().modified(),m_this.modified(),m_this)},this.selectionAPI=function(){return m_selectionAPI},this._init=function(arg){if(!m_layer)throw"Feature requires a valid layer";m_style=$.extend({},{opacity:1},void 0===arg.style?{}:arg.style),m_this._bindMouseHandlers()},this._build=function(){},this._update=function(){},this._exit=function(){m_this._unbindMouseHandlers(),m_selectedFeatures=[],m_style={},arg={},s_exit()},this._init(arg),this},geo.feature.eventID=0,geo.feature.create=function(layer,spec){"use strict";var type=spec.type;if(!(layer instanceof geo.layer))return console.warn("Invalid layer"),null;if("object"!=typeof spec)return console.warn("Invalid spec"),null;var feature=layer.createFeature(type);return feature?(spec=spec||{},spec.data=spec.data||[],feature.style(spec)):(console.warn("Could not create feature type '"+type+"'"),null)},inherit(geo.feature,geo.sceneObject),geo.pointFeature=function(arg){"use strict";if(!(this instanceof geo.pointFeature))return new geo.pointFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_init=this._init,m_rangeTree=null,m_rangeTreeTime=geo.timestamp(),s_data=this.data,m_maxRadius=0,m_clustering=arg.clustering,m_clusterTree=null,m_allData=[],m_lastZoom=null,m_ignoreData=!1;return this.clustering=function(val){return void 0===val?m_clustering:(m_clustering&&!val?(m_clusterTree=null,m_clustering=!1,s_data(m_allData),m_allData=null):!m_clustering&&val&&(m_clustering=!0,m_this._clusterData()),m_this)},this._clusterData=function(){if(m_clustering){var opts=m_clustering===!0?{radius:.01}:m_clustering,position=m_this.position();m_clusterTree=new geo.util.ClusterGroup(opts,m_this.layer().width(),m_this.layer().height()),m_allData.forEach(function(d,i){var pt=geo.util.normalizeCoordinates(position(d,i));pt.index=i,m_clusterTree.addPoint(pt)}),m_lastZoom=null,m_this._handleZoom(m_this.layer().map().zoom())}},this._handleZoom=function(zoom){var z=Math.floor(zoom);if(m_clustering&&z!==m_lastZoom){m_lastZoom=z;var data=m_clusterTree.points(z).map(function(d){return m_allData[d.index]});m_clusterTree.clusters(z).forEach(function(d){d.__cluster=!0,d.__data=[],d.obj.each(function(e){d.__data.push(m_allData[e.index])}),data.push(d)}),m_ignoreData=!0,m_this.data(data),m_this.layer().map().draw()}},this.position=function(val){return void 0===val?m_this.style("position"):(val=geo.util.ensureFunction(val),m_this.style("position",function(d,i){return d.__cluster?d:val(d,i)}),m_this.dataTime().modified(),m_this.modified(),m_this)},this._updateRangeTree=function(){if(!(m_rangeTreeTime.getMTime()>=m_this.dataTime().getMTime())){var pts,position,radius=m_this.style.get("radius"),stroke=m_this.style.get("stroke"),strokeWidth=m_this.style.get("strokeWidth");position=m_this.position(),m_maxRadius=0,pts=m_this.data().map(function(d,i){var pt=position(d);return pt.idx=i,m_maxRadius=Math.max(m_maxRadius,radius(d,i)+(stroke(d,i)?strokeWidth(d,i):0)),pt}),m_rangeTree=new geo.util.RangeTree(pts),m_rangeTreeTime.modified()}},this.pointSearch=function(p){var min,max,data,box,map,pt,idx=[],found=[],ifound=[],stroke=m_this.style.get("stroke"),strokeWidth=m_this.style.get("strokeWidth"),radius=m_this.style.get("radius");return m_this.selectionAPI()?(data=m_this.data(),data&&data.length?(map=m_this.layer().map(),pt=map.gcsToDisplay(p),min=map.displayToGcs({x:pt.x-m_maxRadius,y:pt.y+m_maxRadius}),max=map.displayToGcs({x:pt.x+m_maxRadius,y:pt.y-m_maxRadius}),box=new geo.util.Box(geo.util.vect(min.x,min.y),geo.util.vect(max.x,max.y)),m_this._updateRangeTree(),m_rangeTree.search(box).forEach(function(q){idx.push(q.idx)}),idx.forEach(function(i){var dx,dy,rad,d=data[i],p=m_this.position()(d,i);rad=radius(data[i],i),rad+=stroke(data[i],i)?strokeWidth(data[i],i):0,p=map.gcsToDisplay(p),dx=p.x-pt.x,dy=p.y-pt.y,Math.sqrt(dx*dx+dy*dy)<=rad&&(found.push(d),ifound.push(i))}),{data:found,index:ifound}):{found:[],index:[]}):[]},this.boxSearch=function(lowerLeft,upperRight){var pos=m_this.position(),idx=[];return m_this.data().forEach(function(d,i){var p=pos(d);p.x>=lowerLeft.x&&p.x<=upperRight.x&&p.y>=lowerLeft.y&&p.y<=upperRight.y&&idx.push(i)}),idx},this.data=function(data){return void 0===data?s_data():(m_clustering&&!m_ignoreData?(m_allData=data,m_this._clusterData()):s_data(data),m_ignoreData=!1,m_this)},this._boundingBox=function(d){var pt,radius;return pt=m_this.position()(d),pt=m_this.layer().map().gcsToDisplay(pt),radius=m_this.style().radius(d),{min:{x:pt.x-radius,y:pt.y-radius},max:{x:pt.x+radius,y:pt.y+radius}}},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{radius:5,stroke:!0,strokeColor:{r:.851,g:.604,b:0},strokeWidth:1.25,strokeOpacity:1,fillColor:{r:1,g:.839,b:.439},fill:!0,fillOpacity:.8,sprites:!1,sprites_image:null,position:function(d){return d}},void 0===arg.style?{}:arg.style);void 0!==arg.position&&(defaultStyle.position=arg.position),m_this.style(defaultStyle),m_this.dataTime().modified(),m_this.geoOn(geo.event.zoom,function(evt){m_this._handleZoom(evt.zoomLevel)})},m_this},geo.event.pointFeature=$.extend({},geo.event.feature),geo.pointFeature.create=function(layer,renderer,spec){"use strict";return spec.type="point",geo.feature.create(layer,spec)},inherit(geo.pointFeature,geo.feature),geo.lineFeature=function(arg){"use strict";if(!(this instanceof geo.lineFeature))return new geo.lineFeature(arg);arg=arg||{},geo.feature.call(this,arg); +var m_this=this,s_init=this._init;return this.line=function(val){return void 0===val?m_this.style("line"):(m_this.style("line",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this.position=function(val){return void 0===val?m_this.style("position"):(m_this.style("position",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this.pointSearch=function(p){function lineDist2(q,u,v){var t,l2=dist2(u,v);return 1>l2?dist2(q,u):(t=((q.x-u.x)*(v.x-u.x)+(q.y-u.y)*(v.y-u.y))/l2,0>t?dist2(q,u):t>1?dist2(q,v):dist2(q,{x:u.x+t*(v.x-u.x),y:u.y+t*(v.y-u.y)}))}function dist2(u,v){var dx=u.x-v.x,dy=u.y-v.y;return dx*dx+dy*dy}var data,pt,map,line,width,pos,indices=[],found=[];return data=m_this.data(),data&&data.length?(map=m_this.layer().map(),line=m_this.line(),width=m_this.style.get("strokeWidth"),pos=m_this.position(),pt=m_this.featureGcsToDisplay(p),data.forEach(function(d,index){var last=null;try{line(d,index).forEach(function(current,j){var p=pos(current,j,d,index),s=m_this.featureGcsToDisplay(p),r=Math.ceil(width(p,j,d,index)/2)+2;if(r*=r,last&&lineDist2(pt,s,last)<=r)throw"found";last=s})}catch(err){if("found"!==err)throw err;found.push(d),indices.push(index)}}),{data:found,index:indices}):{found:[],index:[]}},this.boxSearch=function(lowerLeft,upperRight,opts){var pos=m_this.position(),idx=[],line=m_this.line();if(opts=opts||{},opts.partial=opts.partial||!1,opts.partial)throw"Unimplemented query method.";return m_this.data().forEach(function(d,i){var inside=!0;line(d,i).forEach(function(e,j){if(inside){var p=pos(e,j,d,i);p.x>=lowerLeft.x&&p.x<=upperRight.x&&p.y>=lowerLeft.y&&p.y<=upperRight.y||(inside=!1)}}),inside&&idx.push(i)}),idx},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{strokeWidth:1,strokeColor:{r:1,g:.8431372549,b:0},strokeStyle:"solid",strokeOpacity:1,line:function(d){return d},position:function(d){return d}},void 0===arg.style?{}:arg.style);void 0!==arg.line&&(defaultStyle.line=arg.line),void 0!==arg.position&&(defaultStyle.position=arg.position),m_this.style(defaultStyle),m_this.dataTime().modified()},this._init(arg),this},geo.lineFeature.create=function(layer,spec){"use strict";return spec.type="line",geo.feature.create(layer,spec)},inherit(geo.lineFeature,geo.feature),geo.pathFeature=function(arg){"use strict";if(!(this instanceof geo.pathFeature))return new geo.pathFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,m_position=void 0===arg.position?[]:arg.position,s_init=this._init;return this.position=function(val){return void 0===val?m_position:(m_position=val,m_this.dataTime().modified(),m_this.modified(),m_this)},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{strokeWidth:function(){return 1},strokeColor:function(){return{r:1,g:1,b:1}}},void 0===arg.style?{}:arg.style);m_this.style(defaultStyle),m_position&&m_this.dataTime().modified()},this._init(arg),this},inherit(geo.pathFeature,geo.feature),geo.polygonFeature=function(arg){"use strict";function getCoordinates(){var posFunc=m_this.position(),polyFunc=m_this.polygon();m_coordinates=m_this.data().map(function(d,i){var outer,inner,poly=polyFunc(d);return outer=(poly.outer||[]).map(function(d0,j){return posFunc.call(m_this,d0,j,d,i)}),inner=(poly.inner||[]).map(function(hole){return(hole||[]).map(function(d0,k){return posFunc.call(m_this,d0,k,d,i)})}),{outer:outer,inner:inner}})}if(!(this instanceof geo.polygonFeature))return new geo.polygonFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_position,m_polygon,m_this=this,s_init=this._init,s_data=this.data,m_coordinates={outer:[],inner:[]};return m_polygon=void 0===arg.polygon?function(d){return d}:arg.polygon,m_position=void 0===arg.position?function(d){return d}:arg.position,this.data=function(arg){var ret=s_data(arg);return void 0!==arg&&getCoordinates(),ret},this.polygon=function(val){return void 0===val?m_polygon:(m_polygon=val,m_this.dataTime().modified(),m_this.modified(),getCoordinates(),m_this)},this.position=function(val){return void 0===val?m_position:(m_position=val,m_this.dataTime().modified(),m_this.modified(),getCoordinates(),m_this)},this.pointSearch=function(coordinate){var found=[],indices=[],data=m_this.data();return m_coordinates.forEach(function(coord,i){var inside=geo.util.pointInPolygon(coordinate,coord.outer,coord.inner);inside&&(indices.push(i),found.push(data[i]))}),{index:indices,found:found}},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{fillColor:{r:0,g:.5,b:.5},fillOpacity:1},void 0===arg.style?{}:arg.style);m_this.style(defaultStyle),m_position&&m_this.dataTime().modified()},this._init(arg),this},inherit(geo.polygonFeature,geo.feature),geo.planeFeature=function(arg){"use strict";if(!(this instanceof geo.planeFeature))return new geo.planeFeature(arg);arg=arg||{},arg.ul=void 0===arg.ul?[0,1,0]:arg.ul,arg.lr=void 0===arg.lr?[1,0,0]:arg.lr,arg.depth=void 0===arg.depth?0:arg.depth,geo.polygonFeature.call(this,arg);var m_this=this,m_origin=[arg.ul.x,arg.lr.y,arg.depth],m_upperLeft=[arg.ul.x,arg.ul.y,arg.depth],m_lowerRight=[arg.lr.x,arg.lr.y,arg.depth],m_defaultDepth=arg.depth,m_drawOnAsyncResourceLoad=void 0===arg.drawOnAsyncResourceLoad?!0:!1,s_init=this._init;return this.origin=function(val){if(void 0===val)return m_origin;if(val instanceof Array){if(val.length>3||val.length<2)throw"Origin point requires point in 2 or 3 dimension";m_origin=val.slice(0),2===m_origin.length&&(m_origin[2]=m_defaultDepth)}return m_this.dataTime().modified(),m_this.modified(),m_this},this.upperLeft=function(val){if(void 0===val)return m_upperLeft;if(val instanceof Array){if(val.length>3||val.length<2)throw"Upper left point requires point in 2 or 3 dimension";m_upperLeft=val.slice(0),2===m_upperLeft.length&&(m_upperLeft[2]=m_defaultDepth)}return m_this.dataTime().modified(),m_this.modified(),m_this},this.lowerRight=function(val){if(void 0===val)return m_lowerRight;if(val instanceof Array){if(val.length>3||val.length<2)throw"Lower right point requires point in 2 or 3 dimension";m_lowerRight=val.slice(0),2===m_lowerRight.length&&(m_lowerRight[2]=m_defaultDepth),m_this.dataTime().modified()}return m_this.dataTime().modified(),m_this.modified(),m_this},this.drawOnAsyncResourceLoad=function(val){return void 0===val?m_drawOnAsyncResourceLoad:(m_drawOnAsyncResourceLoad=val,m_this)},this._init=function(arg){var style=null;s_init.call(m_this,arg),style=m_this.style(),void 0===style.image&&(style.image=null),m_this.style(style)},this._init(arg),this},inherit(geo.planeFeature,geo.polygonFeature),geo.vectorFeature=function(arg){"use strict";if(!(this instanceof geo.vectorFeature))return new geo.vectorFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_init=this._init,s_style=this.style;this.origin=function(val){return void 0===val?s_style("origin"):(s_style("origin",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this.delta=function(val){return void 0===val?s_style("delta"):(s_style("delta",val),m_this.dataTime().modified(),m_this.modified(),m_this)},this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend({},{strokeColor:"black",strokeWidth:2,strokeOpacity:1,origin:{x:0,y:0,z:0},delta:function(d){return d},scale:null},void 0===arg.style?{}:arg.style);void 0!==arg.origin&&(defaultStyle.origin=arg.origin),m_this.style(defaultStyle),m_this.dataTime().modified()}},inherit(geo.vectorFeature,geo.feature),geo.geomFeature=function(arg){"use strict";return this instanceof geo.geomFeature?(arg=arg||{},geo.feature.call(this,arg),arg.style=void 0===arg.style?$.extend({},{color:[1,1,1],point_sprites:!1,point_sprites_image:null},arg.style):arg.style,this.style(arg.style),this):new geo.geomFeature(arg)},inherit(geo.geomFeature,geo.feature),geo.graphFeature=function(arg){"use strict";if(!(this instanceof geo.graphFeature))return new geo.graphFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_draw=this.draw,s_style=this.style,m_nodes=null,m_points=null,m_children=function(d){return d.children},m_links=[],s_init=this._init,s_exit=this._exit;return this._init=function(arg){s_init.call(m_this,arg);var defaultStyle=$.extend(!0,{},{nodes:{radius:5,fill:!0,fillColor:{r:1,g:0,b:0},strokeColor:{r:0,g:0,b:0}},links:{strokeColor:{r:0,g:0,b:0}},linkType:"path"},void 0===arg.style?{}:arg.style);m_this.style(defaultStyle),m_this.nodes(function(d){return d})},this._build=function(){m_this.children().forEach(function(child){child._build()})},this._update=function(){m_this.children().forEach(function(child){child._update()})},this._exit=function(){return m_this.data([]),m_links.forEach(function(link){link._exit(),m_this.removeChild(link)}),m_links=[],m_points._exit(),m_this.removeChild(m_points),s_exit(),m_this},this.style=function(arg,arg2){var out=s_style.call(m_this,arg,arg2);return out!==m_this?out:(m_points.style(arg.nodes),m_links.forEach(function(l){l.style(arg.links)}),m_this)},this.links=function(arg){return void 0===arg?m_children:(m_children=geo.util.ensureFunction(arg),m_this)},this.nodes=function(val){return void 0===val?m_nodes:(m_nodes=val,m_this.modified(),m_this)},this.nodeFeature=function(){return m_points},this.linkFeatures=function(){return m_links},this.draw=function(){var style,layer=m_this.layer(),data=m_this.data(),nLinks=0;return style=m_this.style(),m_points.data(data),m_points.style(style.nodes),data.forEach(function(source){(source.children||[]).forEach(function(target){var link;nLinks+=1,m_links.lengthdata.length&&(gridH=Math.floor(data.length)/gridW),usePos=null===x0||void 0===x0||null===y0||void 0===y0||!dx||!dy,!usePos&&result.wrapLongitude&&(-180>x0||x0>180||-180>x0+dx*(gridW-1)||x0+dx*(gridW-1)>180)&&dx>-180&&180>dx){for(calcX=[],i=0;gridW>i;i+=1){for(x=x0+i*dx;-180>x;)x+=360;for(;x>180;)x-=360;i&&Math.abs(x-calcX[calcX.length-1])>180&&(x>calcX[calcX.length-1]?(calcX.push(x-360),calcX.push(calcX[calcX.length-2]+360)):(calcX.push(x+360),calcX.push(calcX[calcX.length-2]-360)),skipColumn=i),calcX.push(x)}if(gridW+=2,Math.abs(Math.abs(gridWorig*dx)-360)<.01){for(gridW+=1,x=x0+gridWorig*dx;-180>x;)x+=360;for(;x>180;)x-=360;calcX.push(x)}}for(numPts=gridW*gridH,i=0;numPts>i;i+=1)void 0===skipColumn?val=parseFloat(valueFunc(data[i])):(j=Math.floor(i/gridW),origI=i-j*gridW,origI+=origI>skipColumn?-2:0,origI>=gridWorig&&(origI-=gridWorig),origI+=j*gridWorig,val=parseFloat(valueFunc(data[origI]))),values[i]=isNaN(val)?null:val,null!==values[i]&&(idxMap[i]=usedPts,usedPts+=1,void 0===minval&&(minval=maxval=values[i]),values[i]maxval&&(maxval=values[i]));if(!usedPts)return result;if($.isNumeric(result.minValue)||(result.minValue=minval),$.isNumeric(result.maxValue)||(result.maxValue=maxval),rangeValues&&rangeValues.length===result.colorMap.length+1||(rangeValues=null),rangeValues)for(k=1;krangeValues[k+1]){rangeValues=null;break}for(rangeValues&&(result.minValue=rangeValues[0],result.maxValue=rangeValues[rangeValues.length-1]),range=result.maxValue-result.minValue,range||(result.colorMap=result.colorMap.slice(0,1),range=1,rangeValues=null),result.rangeValues=rangeValues,result.factor=result.colorMap.length/range,j=idx=0;gridH-1>j;j+=1,idx+=1)for(i=0;gridW-1>i;i+=1,idx+=1)null!==values[idx]&&null!==values[idx+1]&&null!==values[idx+gridW]&&null!==values[idx+gridW+1]&&i!==skipColumn&&(result.elements.push(idxMap[idx]),result.elements.push(idxMap[idx+1]),result.elements.push(idxMap[idx+gridW]),result.elements.push(idxMap[idx+gridW+1]),result.elements.push(idxMap[idx+gridW]),result.elements.push(idxMap[idx+1]));for(result.pos=new Array(3*usedPts),result.value=new Array(usedPts),result.opacity=new Array(usedPts),j=i=i3=0;numPts>j;j+=1)if(val=values[j],null!==val){if(item=data[j],usePos?(posVal=posFunc(item),result.pos[i3]=posVal.x,result.pos[i3+1]=posVal.y,result.pos[i3+2]=posVal.z||0):(void 0===skipColumn?result.pos[i3]=x0+dx*(j%gridW):result.pos[i3]=calcX[j%gridW],result.pos[i3+1]=y0+dy*Math.floor(j/gridW),result.pos[i3+2]=0),result.opacity[i]=opacityFunc(item),rangeValues&&val>=result.minValue&&val<=result.maxValue){for(k=1;kOpenStreetMap contributors'}),inherit(geo.osmLayer,geo.tileLayer),geo.registerLayer("osm",geo.osmLayer)}(),geo.domRenderer=function(arg){"use strict";if(!(this instanceof geo.domRenderer))return new geo.domRenderer(arg);geo.renderer.call(this,arg),arg=arg||{};var m_this=this;return this.api=function(){return"dom"},this._init=function(){var layer=m_this.layer().node();!m_this.canvas()&&layer&&layer.length&&m_this.canvas(layer[0])},this._init(arg),this},inherit(geo.domRenderer,geo.renderer),geo.registerRenderer("dom",geo.domRenderer),geo.choroplethFeature=function(arg){"use strict";if(!(this instanceof geo.choroplethFeature))return new geo.choroplethFeature(arg);arg=arg||{},geo.feature.call(this,arg);var m_this=this,s_init=this._init,m_choropleth=$.extend({},{colorRange:[{r:.07514311,g:.468049805,b:1},{r:.468487184,g:.588057293,b:1},{r:.656658579,g:.707001303,b:1},{r:.821573924,g:.837809045,b:1},{r:.943467973,g:.943498599,b:.943398095},{r:1,g:.788626485,b:.750707739},{r:1,g:.6289553,b:.568237474},{r:1,g:.472800903,b:.404551679},{r:.916482116,g:.236630659,b:.209939162}],scale:d3.scale.quantize(),accessors:{geoId:function(geoFeature){return geoFeature.properties.GEO_ID},scalarId:function(scalarElement){return scalarElement.id},scalarValue:function(scalarElement){return scalarElement.value}}},arg.choropleth);return this.scalar=function(data,aggregator){var scalarId,scalarValue;return void 0===data?m_this.choropleth.get("scalar")():(scalarId=m_this.choropleth.get("accessors")().scalarId,scalarValue=m_this.choropleth.get("accessors")().scalarValue,m_choropleth.scalar=data,m_choropleth.scalarAggregator=aggregator||d3.mean,m_choropleth.scalar._dictionary=data.reduce(function(accumeDictionary,scalarElement){var id,value;return id=scalarId(scalarElement),value=scalarValue(scalarElement),accumeDictionary[id]=accumeDictionary[id]?accumeDictionary[id].push(value):[value],accumeDictionary},{}),m_this.dataTime().modified(),m_this)},this.choropleth=function(arg1,arg2){var choropleth;return void 0===arg1?m_choropleth:"string"==typeof arg1&&void 0===arg2?m_choropleth[arg1]:(void 0===arg2?(choropleth=$.extend({},m_choropleth,arg1),m_choropleth=choropleth):m_choropleth[arg1]=arg2,m_this.modified(),m_this)},this.choropleth.get=function(key){var k,all={};if(void 0===key){for(k in m_choropleth)m_choropleth.hasOwnProperty(k)&&(all[k]=m_this.choropleth.get(k));return all}return geo.util.ensureFunction(m_choropleth[key])},this._addPolygonFeature=function(feature,fillColor){var newFeature=m_this.layer().createFeature("polygon",{});return"Polygon"===feature.geometry.type?newFeature.data([{type:"Polygon",coordinates:feature.geometry.coordinates}]):"MultiPolygon"===feature.geometry.type&&newFeature.data(feature.geometry.coordinates.map(function(coordinateMap){return{type:"Polygon",coordinates:coordinateMap}})),newFeature.polygon(function(d){return{outer:d.coordinates[0],inner:d.coordinates[1]}}).position(function(d){return{x:d[0],y:d[1]}}).style({fillColor:fillColor}),newFeature},this._featureToPolygons=function(feature,fillValue){return m_this._addPolygonFeature(feature,fillValue)},this._generateScale=function(valueAccessor){var extent=d3.extent(m_this.scalar(),valueAccessor||void 0);return m_this.choropleth().scale.domain(extent).range(m_this.choropleth().colorRange),m_this},this.createChoropleth=function(){var choropleth=m_this.choropleth,data=m_this.data(),scalars=m_this.scalar(),valueFunc=choropleth.get("accessors")().scalarValue,getFeatureId=choropleth.get("accessors")().geoId;return m_this._generateScale(valueFunc),data.map(function(feature){var id=getFeatureId(feature),valueArray=scalars._dictionary[id],accumulatedScalarValue=choropleth().scalarAggregator(valueArray),fillColor=m_this.choropleth().scale(accumulatedScalarValue);return m_this._featureToPolygons(feature,fillColor)})},this._init=function(arg){s_init.call(m_this,arg),m_choropleth&&m_this.dataTime().modified()},this._init(arg),this},inherit(geo.choroplethFeature,geo.feature),geo.gl={},geo.gl.lineFeature=function(arg){"use strict";function createVertexShader(){var vertexShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","attribute vec3 pos;","attribute vec3 prev;","attribute vec3 next;","attribute float offset;","attribute vec3 strokeColor;","attribute float strokeOpacity;","attribute float strokeWidth;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform float pixelWidth;","uniform float aspect;","varying vec3 strokeColorVar;","varying float strokeWidthVar;","varying float strokeOpacityVar;","void main(void)","{"," if (strokeOpacity < 0.0) {"," gl_Position = vec4(2, 2, 0, 1);"," return;"," }"," const float PI = 3.14159265358979323846264;"," vec4 worldPos = projectionMatrix * modelViewMatrix * vec4(pos.xyz, 1);"," if (worldPos.w != 0.0) {"," worldPos = worldPos/worldPos.w;"," }"," vec4 worldNext = projectionMatrix * modelViewMatrix * vec4(next.xyz, 1);"," if (worldNext.w != 0.0) {"," worldNext = worldNext/worldNext.w;"," }"," vec4 worldPrev = projectionMatrix* modelViewMatrix * vec4(prev.xyz, 1);"," if (worldPrev.w != 0.0) {"," worldPrev = worldPrev/worldPrev.w;"," }"," strokeColorVar = strokeColor;"," strokeWidthVar = strokeWidth;"," strokeOpacityVar = strokeOpacity;"," vec2 deltaNext = worldNext.xy - worldPos.xy;"," vec2 deltaPrev = worldPos.xy - worldPrev.xy;"," float angleNext = 0.0, anglePrev = 0.0;"," if (deltaNext.xy != vec2(0.0, 0.0))"," angleNext = atan(deltaNext.y / aspect, deltaNext.x);"," if (deltaPrev.xy == vec2(0.0, 0.0)) anglePrev = angleNext;"," else anglePrev = atan(deltaPrev.y / aspect, deltaPrev.x);"," if (deltaNext.xy == vec2(0.0, 0.0)) angleNext = anglePrev;"," float angle = (anglePrev + angleNext) / 2.0;"," float cosAngle = cos(anglePrev - angle);"," if (cosAngle < 0.1) { cosAngle = sign(cosAngle) * 1.0; angle = 0.0; }"," float distance = (offset * strokeWidth * pixelWidth) /"," cosAngle;"," worldPos.x += distance * sin(angle);"," worldPos.y -= distance * cos(angle) * aspect;"," gl_Position = worldPos;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader}function createFragmentShader(){var fragmentShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","varying vec3 strokeColorVar;","varying float strokeWidthVar;","varying float strokeOpacityVar;","void main () {"," gl_FragColor = vec4 (strokeColorVar, strokeOpacityVar);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader}function createGLLines(){var i,j,k,v,len,lineItem,lineItemData,vertTemp,pos,posIdx3,posBuf,nextBuf,prevBuf,offsetBuf,indicesBuf,strokeWidthBuf,strokeColorBuf,strokeOpacityBuf,dest,dest3,data=m_this.data(),numSegments=0,vert=[{},{}],position=[],posFunc=m_this.position(),strkWidthFunc=m_this.style.get("strokeWidth"),strkColorFunc=m_this.style.get("strokeColor"),strkOpacityFunc=m_this.style.get("strokeOpacity"),order=m_this.featureVertices(),geom=m_mapper.geometryData();for(i=0;i=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()<=m_this.getMTime())&&m_this._build(),m_pixelWidthUnif.set(1/m_this.renderer().width()),m_aspectUniform.set(m_this.renderer().width()/m_this.renderer().height()),m_actor.setVisible(m_this.visible()),m_actor.material().setBinNumber(m_this.bin()),m_this.updateTime().modified()},this._exit=function(){m_this.renderer().contextRenderer().removeActor(m_actor),s_exit()},this._init(arg),this},inherit(geo.gl.lineFeature,geo.lineFeature),geo.registerFeature("vgl","line",geo.gl.lineFeature),geo.gl.pointFeature=function(arg){"use strict";function createVertexShader(){var shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader}function createFragmentShader(){var shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader}function pointPolygon(x,y,w,h){var verts;switch(m_primitiveShape){case"triangle":verts=[x,y-2*h,x-w*Math.sqrt(3),y+h,x+w*Math.sqrt(3),y+h];break;case"sprite":verts=[x,y];break;default: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]}return verts}function createGLPoints(){var i,j,posBuf,posVal,posFunc,unitBuf,indices,radius,radiusVal,radFunc,stroke,strokeVal,strokeFunc,strokeWidth,strokeWidthVal,strokeWidthFunc,strokeOpacity,strokeOpacityVal,strokeOpactityFunc,strokeColor,strokeColorVal,strokeColorFunc,fill,fillVal,fillFunc,fillOpacity,fillOpacityVal,fillOpacityFunc,fillColor,fillColorVal,fillColorFunc,item,ivpf,ivpf3,iunit,i3,numPts=m_this.data().length,unit=pointPolygon(0,0,1,1),position=new Array(3*numPts),vpf=m_this.verticesPerFeature(),data=m_this.data(),geom=m_mapper.geometryData(); +for(posFunc=m_this.position(),radFunc=m_this.style.get("radius"),strokeFunc=m_this.style.get("stroke"),strokeWidthFunc=m_this.style.get("strokeWidth"),strokeOpactityFunc=m_this.style.get("strokeOpacity"),strokeColorFunc=m_this.style.get("strokeColor"),fillFunc=m_this.style.get("fill"),fillOpacityFunc=m_this.style.get("fillOpacity"),fillColorFunc=m_this.style.get("fillColor"),i=i3=0;numPts>i;i+=1,i3+=3)posVal=posFunc(data[i]),position[i3]=posVal.x,position[i3+1]=posVal.y,position[i3+2]=posVal.z||0;for(position=geo.transform.transformCoordinates(m_this.gcs(),m_this.layer().map().gcs(),position,3),posBuf=getBuffer(geom,"pos",vpf*numPts*3),"sprite"!==m_primitiveShape&&(unitBuf=getBuffer(geom,"unit",vpf*numPts*2)),radius=getBuffer(geom,"rad",vpf*numPts*1),stroke=getBuffer(geom,"stroke",vpf*numPts*1),strokeWidth=getBuffer(geom,"strokeWidth",vpf*numPts*1),strokeOpacity=getBuffer(geom,"strokeOpacity",vpf*numPts*1),strokeColor=getBuffer(geom,"strokeColor",vpf*numPts*3),fill=getBuffer(geom,"fill",vpf*numPts*1),fillOpacity=getBuffer(geom,"fillOpacity",vpf*numPts*1),fillColor=getBuffer(geom,"fillColor",vpf*numPts*3),indices=geom.primitive(0).indices(),indices instanceof Uint16Array&&indices.length===vpf*numPts||(indices=new Uint16Array(vpf*numPts),geom.primitive(0).setIndices(indices)),i=ivpf=ivpf3=iunit=i3=0;numPts>i;i+=1,i3+=3){if(item=data[i],"sprite"!==m_primitiveShape)for(j=0;jj;j+=1,ivpf+=1,ivpf3+=3)posBuf[ivpf3]=position[i3],posBuf[ivpf3+1]=position[i3+1],posBuf[ivpf3+2]=position[i3+2],radius[ivpf]=radiusVal,stroke[ivpf]=strokeVal,strokeWidth[ivpf]=strokeWidthVal,strokeOpacity[ivpf]=strokeOpacityVal,strokeColor[ivpf3]=strokeColorVal.r,strokeColor[ivpf3+1]=strokeColorVal.g,strokeColor[ivpf3+2]=strokeColorVal.b,fill[ivpf]=fillVal,fillOpacity[ivpf]=fillOpacityVal,fillColor[ivpf3]=fillColorVal.r,fillColor[ivpf3+1]=fillColorVal.g,fillColor[ivpf3+2]=fillColorVal.b}geom.boundsDirty(!0),m_mapper.modified(),m_mapper.boundsDirtyTimestamp().modified()}function getBuffer(geom,srcName,len){var data,src=geom.sourceByName(srcName);return data=src.data(),data instanceof Float32Array&&data.length===len?data:(data=new Float32Array(len),src.setData(data),data)}if(!(this instanceof geo.gl.pointFeature))return new geo.gl.pointFeature(arg);arg=arg||{},geo.pointFeature.call(this,arg);var m_this=this,s_exit=this._exit,m_actor=null,m_mapper=null,m_pixelWidthUniform=null,m_aspectUniform=null,m_dynamicDraw=void 0===arg.dynamicDraw?!1:arg.dynamicDraw,m_primitiveShape="sprite",s_init=this._init,s_update=this._update,vertexShaderSource=null,fragmentShaderSource=null;return("triangle"===arg.primitiveShape||"square"===arg.primitiveShape||"sprite"===arg.primitiveShape)&&(m_primitiveShape=arg.primitiveShape),vertexShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","attribute vec3 pos;","attribute float rad;","attribute vec3 fillColor;","attribute vec3 strokeColor;","attribute float fillOpacity;","attribute float strokeWidth;","attribute float strokeOpacity;","attribute float fill;","attribute float stroke;","uniform float pixelWidth;","uniform float aspect;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying vec4 fillColorVar;","varying vec4 strokeColorVar;","varying float radiusVar;","varying float strokeWidthVar;","varying float fillVar;","varying float strokeVar;"],"sprite"!==m_primitiveShape&&(vertexShaderSource=vertexShaderSource.concat(["attribute vec2 unit;","varying vec3 unitVar;"])),vertexShaderSource.push.apply(vertexShaderSource,["void main(void)","{"," strokeWidthVar = strokeWidth;"," // No stroke or fill implies nothing to draw"," if (stroke < 1.0 || strokeWidth <= 0.0 || strokeOpacity <= 0.0) {"," strokeVar = 0.0;"," strokeWidthVar = 0.0;"," }"," else"," strokeVar = 1.0;"," if (fill < 1.0 || rad <= 0.0 || fillOpacity <= 0.0)"," fillVar = 0.0;"," else"," fillVar = 1.0;"," if (fillVar == 0.0 && strokeVar == 0.0) {"," gl_Position = vec4(2, 2, 0, 1);"," return;"," }"," fillColorVar = vec4 (fillColor, fillOpacity);"," strokeColorVar = vec4 (strokeColor, strokeOpacity);"," radiusVar = rad;"]),"sprite"===m_primitiveShape?vertexShaderSource.push.apply(vertexShaderSource,[" gl_Position = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw;"," gl_PointSize = 2.0 * (rad + strokeWidthVar); ","}"]):vertexShaderSource.push.apply(vertexShaderSource,[" unitVar = vec3 (unit, 1.0);"," vec4 p = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw;"," if (p.w != 0.0) {"," p = p / p.w;"," }"," p += (rad + strokeWidthVar) * "," vec4 (unit.x * pixelWidth, unit.y * pixelWidth * aspect, 0.0, 1.0);"," gl_Position = vec4(p.xyz, 1.0);","}"]),vertexShaderSource=vertexShaderSource.join("\n"),fragmentShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","uniform float aspect;","varying vec4 fillColorVar;","varying vec4 strokeColorVar;","varying float radiusVar;","varying float strokeWidthVar;","varying float fillVar;","varying float strokeVar;"],"sprite"!==m_primitiveShape&&fragmentShaderSource.push("varying vec3 unitVar;"),fragmentShaderSource.push.apply(fragmentShaderSource,["void main () {"," vec4 strokeColor, fillColor;"," float endStep;"," // No stroke or fill implies nothing to draw"," if (fillVar == 0.0 && strokeVar == 0.0)"," discard;"]),"sprite"===m_primitiveShape?fragmentShaderSource.push(" float rad = 2.0 * length (gl_PointCoord - vec2(0.5));"):fragmentShaderSource.push(" float rad = length (unitVar.xy);"),fragmentShaderSource.push.apply(fragmentShaderSource,[" if (rad > 1.0)"," discard;"," // If there is no stroke, the fill region should transition to nothing"," if (strokeVar == 0.0) {"," strokeColor = vec4 (fillColorVar.rgb, 0.0);"," endStep = 1.0;"," } else {"," strokeColor = strokeColorVar;"," endStep = radiusVar / (radiusVar + strokeWidthVar);"," }"," // Likewise, if there is no fill, the stroke should transition to nothing"," if (fillVar == 0.0)"," fillColor = vec4 (strokeColor.rgb, 0.0);"," else"," fillColor = fillColorVar;"," // Distance to antialias over"," float antialiasDist = 3.0 / (2.0 * radiusVar);"," if (rad < endStep) {"," float step = smoothstep (endStep - antialiasDist, endStep, rad);"," gl_FragColor = mix (fillColor, strokeColor, step);"," } else {"," float step = smoothstep (1.0 - antialiasDist, 1.0, rad);"," gl_FragColor = mix (strokeColor, vec4 (strokeColor.rgb, 0.0), step);"," }","}"]),fragmentShaderSource=fragmentShaderSource.join("\n"),this.actors=function(){return m_actor?[m_actor]:[]},this.verticesPerFeature=function(){var unit=pointPolygon(0,0,1,1);return unit.length/2},this._init=function(){var prog=vgl.shaderProgram(),vertexShader=createVertexShader(),fragmentShader=createFragmentShader(),posAttr=vgl.vertexAttribute("pos"),unitAttr=vgl.vertexAttribute("unit"),radAttr=vgl.vertexAttribute("rad"),strokeWidthAttr=vgl.vertexAttribute("strokeWidth"),fillColorAttr=vgl.vertexAttribute("fillColor"),fillAttr=vgl.vertexAttribute("fill"),strokeColorAttr=vgl.vertexAttribute("strokeColor"),strokeAttr=vgl.vertexAttribute("stroke"),fillOpacityAttr=vgl.vertexAttribute("fillOpacity"),strokeOpacityAttr=vgl.vertexAttribute("strokeOpacity"),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),mat=vgl.material(),blend=vgl.blend(),geom=vgl.geometryData(),sourcePositions=vgl.sourceDataP3fv({name:"pos"}),sourceUnits=vgl.sourceDataAnyfv(2,vgl.vertexAttributeKeysIndexed.One,{name:"unit"}),sourceRadius=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Two,{name:"rad"}),sourceStrokeWidth=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Three,{name:"strokeWidth"}),sourceFillColor=vgl.sourceDataAnyfv(3,vgl.vertexAttributeKeysIndexed.Four,{name:"fillColor"}),sourceFill=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Five,{name:"fill"}),sourceStrokeColor=vgl.sourceDataAnyfv(3,vgl.vertexAttributeKeysIndexed.Six,{name:"strokeColor"}),sourceStroke=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Seven,{name:"stroke"}),sourceAlpha=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Eight,{name:"fillOpacity"}),sourceStrokeOpacity=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Nine,{name:"strokeOpacity"}),primitive=new vgl.triangles;"sprite"===m_primitiveShape&&(primitive=new vgl.points),m_pixelWidthUniform=new vgl.floatUniform("pixelWidth",2/m_this.renderer().width()),m_aspectUniform=new vgl.floatUniform("aspect",m_this.renderer().width()/m_this.renderer().height()),s_init.call(m_this,arg),m_mapper=vgl.mapper({dynamicDraw:m_dynamicDraw}),prog.addVertexAttribute(posAttr,vgl.vertexAttributeKeys.Position),"sprite"!==m_primitiveShape&&prog.addVertexAttribute(unitAttr,vgl.vertexAttributeKeysIndexed.One),prog.addVertexAttribute(radAttr,vgl.vertexAttributeKeysIndexed.Two),prog.addVertexAttribute(strokeWidthAttr,vgl.vertexAttributeKeysIndexed.Three),prog.addVertexAttribute(fillColorAttr,vgl.vertexAttributeKeysIndexed.Four),prog.addVertexAttribute(fillAttr,vgl.vertexAttributeKeysIndexed.Five),prog.addVertexAttribute(strokeColorAttr,vgl.vertexAttributeKeysIndexed.Six),prog.addVertexAttribute(strokeAttr,vgl.vertexAttributeKeysIndexed.Seven),prog.addVertexAttribute(fillOpacityAttr,vgl.vertexAttributeKeysIndexed.Eight),prog.addVertexAttribute(strokeOpacityAttr,vgl.vertexAttributeKeysIndexed.Nine),prog.addUniform(m_pixelWidthUniform),prog.addUniform(m_aspectUniform),prog.addUniform(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.addSource(sourcePositions),geom.addSource(sourceUnits),geom.addSource(sourceRadius),geom.addSource(sourceStrokeWidth),geom.addSource(sourceFillColor),geom.addSource(sourceFill),geom.addSource(sourceStrokeColor),geom.addSource(sourceStroke),geom.addSource(sourceAlpha),geom.addSource(sourceStrokeOpacity),geom.addPrimitive(primitive),m_mapper.setGeometryData(geom)},this._build=function(){m_actor&&m_this.renderer().contextRenderer().removeActor(m_actor),createGLPoints(),m_this.renderer().contextRenderer().addActor(m_actor),m_this.renderer().contextRenderer().render(),m_this.buildTime().modified()},this._update=function(){s_update.call(m_this),(m_this.dataTime().getMTime()>=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()i;i+=1)buffers.write("pos",position[i],start+i,1),buffers.write("indices",[i],start+i,1),buffers.write("fillColor",fillColorNew[i],start+i,1),buffers.write("fillOpacity",[fillOpacityNew[i]],start+i,1);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)}if(!(this instanceof geo.gl.polygonFeature))return new geo.gl.polygonFeature(arg);arg=arg||{},geo.polygonFeature.call(this,arg);var m_this=this,s_exit=this._exit,m_actor=vgl.actor(),m_mapper=vgl.mapper(),m_material=vgl.material(),s_init=this._init,s_update=this._update;return this._init=function(arg){var blend=vgl.blend(),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),prog.addVertexAttribute(posAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(fillColorAttr,vgl.vertexAttributeKeysIndexed.Two),prog.addVertexAttribute(fillOpacityAttr,vgl.vertexAttributeKeysIndexed.Three),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),m_material.addAttribute(prog),m_material.addAttribute(blend),m_actor.setMapper(m_mapper),m_actor.setMaterial(m_material)},this._build=function(){m_actor&&m_this.renderer().contextRenderer().removeActor(m_actor),createGLPolygons(),m_this.renderer().contextRenderer().addActor(m_actor),m_this.buildTime().modified()},this._update=function(){s_update.call(m_this),(m_this.dataTime().getMTime()>=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()<=m_this.getMTime())&&m_this._build(),m_actor.setVisible(m_this.visible()),m_actor.material().setBinNumber(m_this.bin()),m_this.updateTime().modified()},this._exit=function(){m_this.renderer().contextRenderer().removeActor(m_actor),s_exit()},this._init(arg),this},inherit(geo.gl.polygonFeature,geo.polygonFeature),geo.registerFeature("vgl","polygon",geo.gl.polygonFeature),geo.gl.contourFeature=function(arg){"use strict";function createVertexShader(){var vertexShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","attribute vec3 pos;","attribute float value;","attribute float opacity;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","varying float valueVar;","varying float opacityVar;","void main(void)","{"," vec4 scrPos = projectionMatrix * modelViewMatrix * vec4(pos.xy, 0, 1);"," if (scrPos.w != 0.0) {"," scrPos = scrPos / scrPos.w;"," }"," valueVar = value;"," opacityVar = opacity;"," gl_Position = scrPos;","}"].join("\n"),shader=new vgl.shader(vgl.GL.VERTEX_SHADER);return shader.setShaderSource(vertexShaderSource),shader}function createFragmentShader(){var fragmentShaderSource=["#ifdef GL_ES"," precision highp float;","#endif","uniform vec4 minColor;","uniform vec4 maxColor;","uniform float steps;","uniform bool stepped;","uniform sampler2D sampler2d;","varying float valueVar;","varying float opacityVar;","void main () {"," vec4 clr;"," if (valueVar < 0.0) {"," clr = minColor;"," } else if (valueVar > steps) {"," clr = maxColor;"," } else {"," float step;"," if (stepped) {"," step = floor(valueVar) + 0.5;"," if (step > steps) {"," step = steps - 0.5;"," }"," } else {"," step = valueVar;"," }"," clr = texture2D(sampler2d, vec2(step / steps, 0.0));"," }"," gl_FragColor = vec4(clr.rgb, clr.a * opacityVar);","}"].join("\n"),shader=new vgl.shader(vgl.GL.FRAGMENT_SHADER);return shader.setShaderSource(fragmentShaderSource),shader}function createGLContours(){var i,i3,j,j3,posBuf,opacityBuf,valueBuf,indicesBuf,contour=m_this.createContours(),numPts=contour.elements.length,colorTable=[],geom=m_mapper.geometryData();for(m_minColorUniform.set([contour.minColor.r,contour.minColor.g,contour.minColor.b,contour.minColor.a]),m_maxColorUniform.set([contour.maxColor.r,contour.maxColor.g,contour.maxColor.b,contour.maxColor.a]),m_stepsUniform.set(contour.colorMap.length),m_steppedUniform.set(contour.stepped),i=0;ii;i+=1,i3+=3)j=contour.elements[i],j3=3*j,posBuf[i3]=contour.pos[j3],posBuf[i3+1]=contour.pos[j3+1],posBuf[i3+2]=contour.pos[j3+2],opacityBuf[i]=contour.opacity[j],valueBuf[i]=contour.value[j];indicesBuf=geom.primitive(0).indices(),indicesBuf instanceof Uint16Array&&indicesBuf.length===numPts||(indicesBuf=new Uint16Array(numPts),geom.primitive(0).setIndices(indicesBuf)),geom.boundsDirty(!0),m_mapper.modified(),m_mapper.boundsDirtyTimestamp().modified()}function getBuffer(geom,srcName,len){var data,src=geom.sourceByName(srcName);return data=src.data(),data instanceof Float32Array&&data.length===len?data:(data=new Float32Array(len),src.setData(data),data)}if(!(this instanceof geo.gl.contourFeature))return new geo.gl.contourFeature(arg);arg=arg||{},geo.contourFeature.call(this,arg);var m_this=this,s_exit=this._exit,m_textureUnit=7,m_actor=null,m_mapper=null,m_material=null,m_texture=null,m_minColorUniform=null,m_maxColorUniform=null,m_stepsUniform=null,m_steppedUniform=null,m_dynamicDraw=void 0===arg.dynamicDraw?!1:arg.dynamicDraw,s_init=this._init,s_update=this._update;return this._init=function(arg){var blend=vgl.blend(),prog=vgl.shaderProgram(),mat=vgl.material(),tex=vgl.lookupTable(),geom=vgl.geometryData(),modelViewUniform=new vgl.modelViewUniform("modelViewMatrix"),projectionUniform=new vgl.projectionUniform("projectionMatrix"),samplerUniform=new vgl.uniform(vgl.GL.INT,"sampler2d"),vertexShader=createVertexShader(),fragmentShader=createFragmentShader(),posAttr=vgl.vertexAttribute("pos"),valueAttr=vgl.vertexAttribute("value"),opacityAttr=vgl.vertexAttribute("opacity"),sourcePositions=vgl.sourceDataP3fv({name:"pos"}),sourceValues=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.One,{name:"value"}),sourceOpacity=vgl.sourceDataAnyfv(1,vgl.vertexAttributeKeysIndexed.Two,{name:"opacity"}),primitive=new vgl.triangles;s_init.call(m_this,arg),m_mapper=vgl.mapper({dynamicDraw:m_dynamicDraw}),prog.addVertexAttribute(posAttr,vgl.vertexAttributeKeys.Position),prog.addVertexAttribute(valueAttr,vgl.vertexAttributeKeysIndexed.One),prog.addVertexAttribute(opacityAttr,vgl.vertexAttributeKeysIndexed.Two),prog.addUniform(modelViewUniform),prog.addUniform(projectionUniform),m_minColorUniform=new vgl.uniform(vgl.GL.FLOAT_VEC4,"minColor"),prog.addUniform(m_minColorUniform),m_maxColorUniform=new vgl.uniform(vgl.GL.FLOAT_VEC4,"maxColor"),prog.addUniform(m_maxColorUniform),m_stepsUniform=new vgl.uniform(vgl.GL.FLOAT,"steps"),prog.addUniform(m_stepsUniform),m_steppedUniform=new vgl.uniform(vgl.GL.BOOL,"stepped"),prog.addUniform(m_steppedUniform),prog.addShader(fragmentShader),prog.addShader(vertexShader),prog.addUniform(samplerUniform),tex.setTextureUnit(m_textureUnit),samplerUniform.set(m_textureUnit),m_material=mat,m_material.addAttribute(prog),m_material.addAttribute(blend),m_texture=tex,m_material.addAttribute(m_texture),m_actor=vgl.actor(),m_actor.setMaterial(m_material),m_actor.setMapper(m_mapper),geom.addSource(sourcePositions),geom.addSource(sourceValues),geom.addSource(sourceOpacity),geom.addPrimitive(primitive),m_mapper.setGeometryData(geom)},this._build=function(){m_actor&&m_this.renderer().contextRenderer().removeActor(m_actor),createGLContours(),m_this.renderer().contextRenderer().addActor(m_actor),m_this.buildTime().modified()},this._update=function(){s_update.call(m_this),(m_this.dataTime().getMTime()>=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()<=m_this.getMTime())&&m_this._build(),m_actor.setVisible(m_this.visible()),m_actor.material().setBinNumber(m_this.bin()),m_this.updateTime().modified()},this._exit=function(){m_this.renderer().contextRenderer().removeActor(m_actor),s_exit()},this._init(arg),this},inherit(geo.gl.contourFeature,geo.contourFeature),geo.registerFeature("vgl","contour",geo.gl.contourFeature),geo.gl.vglRenderer=function(arg){"use strict";if(!(this instanceof geo.gl.vglRenderer))return new geo.gl.vglRenderer(arg);arg=arg||{},geo.renderer.call(this,arg);var m_this=this,m_contextRenderer=null,m_viewer=null,m_width=0,m_height=0,m_renderAnimFrameRef=null,s_init=this._init;return this.width=function(){return m_width},this.height=function(){return m_height},this.contextRenderer=function(){return m_contextRenderer},this.api=function(){return"vgl"},this._init=function(){if(m_this.initialized())return m_this;s_init.call(m_this);var canvas=$(document.createElement("canvas"));canvas.attr("class","webgl-canvas"),$(m_this.layer().node().get(0)).append(canvas),m_viewer=vgl.viewer(canvas.get(0),arg.options),m_viewer.init(),m_contextRenderer=m_viewer.renderWindow().activeRenderer(),m_contextRenderer.setResetScene(!1),m_viewer.renderWindow().renderers().length>0&&m_contextRenderer.setLayer(m_viewer.renderWindow().renderers().length),m_this.canvas(canvas);var map=m_this.layer().map(),mapSize=map.size();return m_this._resize(0,0,mapSize.width,mapSize.height),m_this},this._resize=function(x,y,w,h){var renderWindow=m_viewer.renderWindow();return m_width=w,m_height=h,m_this.canvas().attr("width",w),m_this.canvas().attr("height",h),renderWindow.positionAndResize(x,y,w,h),m_this._updateRendererCamera(),m_this._render(),m_this},this._render=function(){return null===m_renderAnimFrameRef&&(m_renderAnimFrameRef=window.requestAnimationFrame(function(){m_viewer.render(),m_renderAnimFrameRef=null})),m_this},this._updateRendererCamera=function(){var renderWindow=m_viewer.renderWindow(),map=m_this.layer().map(),camera=map.camera(),view=camera.view,proj=camera.projectionMatrix;proj[15]&&(proj=mat4.scale(mat4.create(),proj,[1,1,-1])),proj=mat4.translate(mat4.create(),proj,[0,0,camera.constructor.bounds.far]),renderWindow.renderers().forEach(function(renderer){var cam=renderer.camera();cam.setViewMatrix(view),cam.setProjectionMatrix(proj),proj[1]||proj[2]||proj[3]||proj[4]||proj[6]||proj[7]||proj[8]||proj[9]||proj[11]||1!==proj[15]||parseFloat(map.zoom().toFixed(6))!==parseFloat(map.zoom().toFixed(0))?cam.viewAlignment=function(){return null}:cam.viewAlignment=function(){var align={roundx:2/camera.viewport.width,roundy:2/camera.viewport.height};return align.dx=camera.viewport.width%2?.5*align.roundx:0,align.dy=camera.viewport.height%2?.5*align.roundy:0,align}})},m_this.layer().geoOn(geo.event.pan,function(evt){m_this._updateRendererCamera()}),m_this.layer().geoOn(geo.event.zoom,function(evt){m_this._updateRendererCamera()}),m_this.layer().geoOn(geo.event.parallelprojection,function(evt){var camera,vglRenderer=m_this.contextRenderer(),layer=m_this.layer();evt.geo&&evt.geo._triggeredBy!==layer&&(vglRenderer&&vglRenderer.camera()||console.log("Parallel projection event triggered on unconnected VGL renderer."),camera=vglRenderer.camera(),camera.setEnableParallelProjection(evt.parallelProjection),m_this._updateRendererCamera())}),this},inherit(geo.gl.vglRenderer,geo.renderer),geo.registerRenderer("vgl",geo.gl.vglRenderer),geo.gl.tileLayer=function(){"use strict";var m_this=this;this._drawTile=function(tile){var bounds=this._tileBounds(tile),level=tile.index.level||0,to=this._options.tileOffset(level),ul=this.fromLocal(this.fromLevel({x:bounds.left-to.x,y:bounds.top-to.y},level),0),lr=this.fromLocal(this.fromLevel({x:bounds.right-to.x,y:bounds.bottom-to.y},level),0);tile.feature=m_this.createFeature("plane",{drawOnAsyncResourceLoad:!0}).origin([ul.x,lr.y,1e-7*level]).upperLeft([ul.x,ul.y,1e-7*level]).lowerRight([lr.x,lr.y,1e-7*level]).style({image:tile._image}),tile.feature._update(),m_this.draw()},this._remove=function(tile){tile.feature&&(m_this.deleteFeature(tile.feature),tile.feature=null,m_this.draw())},this._getSubLayer=function(){},this._updateSubLayer=function(){}},geo.registerLayerAdjustment("vgl","tile",geo.gl.tileLayer),geo.gl.choroplethFeature=function(arg){"use strict";function createGLChoropleth(){return m_this.createChoropleth()}if(!(this instanceof geo.gl.choroplethFeature))return new geo.gl.choroplethFeature(arg);arg=arg||{},geo.choroplethFeature.call(this,arg);var m_this=this,m_gl_polygons=null,s_exit=this._exit,s_init=this._init,s_update=this._update; +return this._init=function(arg){s_init.call(m_this,arg)},this._build=function(){return m_this.buildTime().modified(),m_gl_polygons=createGLChoropleth()},this._update=function(){s_update.call(m_this),(m_this.dataTime().getMTime()>=m_this.buildTime().getMTime()||m_this.updateTime().getMTime()<=m_this.getMTime())&&(m_this._wipePolygons(),m_this._build()),m_this.updateTime().modified()},this._wipePolygons=function(){m_gl_polygons&&m_gl_polygons.map(function(polygon){return polygon._exit()}),m_gl_polygons=null},this._exit=function(){m_this._wipePolygons(),s_exit()},this._init(arg),this},inherit(geo.gl.choroplethFeature,geo.choroplethFeature),geo.registerFeature("vgl","choropleth",geo.gl.choroplethFeature),geo.d3={},function(){"use strict";var chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz",strLength=8;geo.d3.uniqueID=function(){var i,strArray=[];for(strArray.length=strLength,i=0;strLength>i;i+=1)strArray[i]=chars.charAt(Math.floor(Math.random()*chars.length));return strArray.join("")},geo.event.d3Rescale="geo_d3_rescale"}(),geo.d3.object=function(arg){"use strict";if(!(this instanceof geo.object))return new geo.d3.object(arg);geo.sceneObject.call(this);var m_id="d3-"+geo.d3.uniqueID(),s_exit=this._exit,m_this=this,s_draw=this.draw;return this._d3id=function(){return m_id},this.select=function(){return m_this.renderer().select(m_this._d3id())},this.draw=function(){return m_this._update(),s_draw(),m_this},this._exit=function(){m_this.renderer()._removeFeature(m_this._d3id()),s_exit()},this},inherit(geo.d3.object,geo.sceneObject),geo.d3.d3Renderer=function(arg){"use strict";function setAttrs(select,attrs){var key;for(key in attrs)attrs.hasOwnProperty(key)&&select.attr(key,attrs[key])}function setStyles(select,styles){function fillFunc(){return styles.fill.apply(m_this,arguments)?null:"none"}function strokeFunc(){return styles.stroke.apply(m_this,arguments)?null:"none"}var key,k,f;for(key in styles)styles.hasOwnProperty(key)&&(f=null,k=null,"strokeColor"===key?(k="stroke",f=m_this._convertColor(styles[key],styles.stroke)):"stroke"===key&&styles[key]?(k="stroke",f=strokeFunc):"strokeWidth"===key?(k="stroke-width",f=m_this._convertScale(styles[key])):"strokeOpacity"===key?(k="stroke-opacity",f=styles[key]):"fillColor"===key?(k="fill",f=m_this._convertColor(styles[key],styles.fill)):"fill"!==key||styles.hasOwnProperty("fillColor")?"fillOpacity"===key&&(k="fill-opacity",f=styles[key]):(k="fill",f=fillFunc),k&&select.style(k,f))}function getGroup(parentId){return parentId?m_svg.select(".group-"+parentId):m_svg.select(".group-"+m_this._d3id())}function initCorners(){var layer=m_this.layer(),map=layer.map(),width=m_this.layer().map().size().width,height=m_this.layer().map().size().height;if(m_width=width,m_height=height,!m_width||!m_height)throw"Map layer has size 0";m_corners={upperLeft:map.displayToGcs({x:0,y:0},null),lowerRight:map.displayToGcs({x:width,y:height},null)}}if(!(this instanceof geo.d3.d3Renderer))return new geo.d3.d3Renderer(arg);geo.renderer.call(this,arg);var s_exit=this._exit;geo.d3.object.call(this,arg),arg=arg||{};var m_this=this,m_sticky=null,m_features={},m_corners=null,m_width=null,m_height=null,m_scale=1,m_dx=0,m_dy=0,m_svg=null,m_defs=null;return this._convertColor=function(f,g){return f=geo.util.ensureFunction(f),g=g||function(){return!0},function(){var c="none";return g.apply(m_this,arguments)&&(c=f.apply(m_this,arguments),c.hasOwnProperty("r")&&c.hasOwnProperty("g")&&c.hasOwnProperty("b")&&(c=d3.rgb(255*c.r,255*c.g,255*c.b))),c}},this._convertPosition=function(f){return f=geo.util.ensureFunction(f),function(){return m_this.layer().map().worldToDisplay(f.apply(m_this,arguments))}},this._convertScale=function(f){return f=geo.util.ensureFunction(f),function(){return f.apply(m_this,arguments)/m_scale}},this._setTransform=function(){if(m_corners||initCorners(),m_sticky){var dx,dy,scale,layer=m_this.layer(),map=layer.map(),upperLeft=map.gcsToDisplay(m_corners.upperLeft,null),lowerRight=map.gcsToDisplay(m_corners.lowerRight,null),group=getGroup(),canvas=m_this.canvas();null!==canvas.attr("scale")?(scale=canvas.attr("scale")||1,dx=(canvas.attr("dx")||0)*scale,dy=(canvas.attr("dy")||0)*scale,dx+=map.size().width/2,dy+=map.size().height/2):(dx=upperLeft.x,dy=upperLeft.y,scale=(lowerRight.y-upperLeft.y)/m_height),group.attr("transform","matrix("+[scale,0,0,scale,dx,dy].join()+")"),m_scale=scale,m_dx=dx,m_dy=dy}},this.baseToLocal=function(pt){return{x:(pt.x-m_dx)/m_scale,y:(pt.y-m_dy)/m_scale}},this.localToBase=function(pt){return{x:pt.x*m_scale+m_dx,y:pt.y*m_scale+m_dy}},this._init=function(arg){if(!m_this.canvas()){var canvas;arg.widget=arg.widget||!1,m_svg="d3Parent"in arg?d3.select(arg.d3Parent).append("svg"):d3.select(m_this.layer().node().get(0)).append("svg"),m_defs=m_svg.append("defs");var shadow=m_defs.append("filter").attr("id","geo-highlight").attr("x","-100%").attr("y","-100%").attr("width","300%").attr("height","300%");shadow.append("feMorphology").attr("operator","dilate").attr("radius",2).attr("in","SourceAlpha").attr("result","dilateOut"),shadow.append("feGaussianBlur").attr("stdDeviation",5).attr("in","dilateOut").attr("result","blurOut"),shadow.append("feColorMatrix").attr("type","matrix").attr("values","-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0").attr("in","blurOut").attr("result","invertOut"),shadow.append("feBlend").attr("in","SourceGraphic").attr("in2","invertOut").attr("mode","normal"),arg.widget||(canvas=m_svg.append("g")),shadow=m_defs.append("filter").attr("id","geo-blur").attr("x","-100%").attr("y","-100%").attr("width","300%").attr("height","300%"),shadow.append("feGaussianBlur").attr("stdDeviation",20).attr("in","SourceGraphic"),m_sticky=m_this.layer().sticky(),m_svg.attr("class",m_this._d3id()),m_svg.attr("width",m_this.layer().node().width()),m_svg.attr("height",m_this.layer().node().height()),arg.widget?m_this.canvas(m_svg):(canvas.attr("class","group-"+m_this._d3id()),m_this.canvas(canvas))}m_this._setTransform()},this.api=function(){return"d3"},this.scaleFactor=function(){return m_scale},this._resize=function(x,y,w,h){m_corners||initCorners(),m_svg.attr("width",w),m_svg.attr("height",h),m_this._setTransform(),m_this.layer().geoTrigger(geo.event.d3Rescale,{scale:m_scale},!0)},this._update=function(){},this._exit=function(){m_features={},m_this.canvas().remove(),s_exit()},this._definitions=function(){return m_defs},this._drawFeatures=function(arg){return m_features[arg.id]={data:arg.data,index:arg.dataIndex,style:arg.style,attributes:arg.attributes,classes:arg.classes,append:arg.append,parentId:arg.parentId},m_this.__render(arg.id,arg.parentId)},this.__render=function(id,parentId){var key;if(void 0===id){for(key in m_features)m_features.hasOwnProperty(key)&&m_this.__render(key);return m_this}var data=m_features[id].data,index=m_features[id].index,style=m_features[id].style,attributes=m_features[id].attributes,classes=m_features[id].classes,append=m_features[id].append,selection=m_this.select(id,parentId).data(data,index);return selection.enter().append(append),selection.exit().remove(),setAttrs(selection,attributes),selection.attr("class",classes.concat([id]).join(" ")),setStyles(selection,style),m_this},this.select=function(id,parentId){return getGroup(parentId).selectAll("."+id)},this._removeFeature=function(id){return m_this.select(id).remove(),delete m_features[id],m_this},this.draw=function(){},this.layer().geoOn(geo.event.pan,m_this._setTransform),this.layer().geoOn(geo.event.zoom,function(){m_this._setTransform(),m_this.__render(),m_this.layer().geoTrigger(geo.event.d3Rescale,{scale:m_scale},!0)}),this.layer().geoOn(geo.event.resize,function(event){m_this._resize(event.x,event.y,event.width,event.height)}),this._init(arg),this},inherit(geo.d3.d3Renderer,geo.renderer),geo.registerRenderer("d3",geo.d3.d3Renderer),geo.d3.tileLayer=function(){"use strict";var m_this=this,s_update=this._update,s_init=this._init;this._drawTile=function(tile){var bounds=m_this._tileBounds(tile),parentNode=m_this._getSubLayer(tile.index.level);tile.feature=m_this.createFeature("plane",{drawOnAsyncResourceLoad:!0}).origin([bounds.left,bounds.top]).upperLeft([bounds.left,bounds.top]).lowerRight([bounds.right,bounds.bottom]).style({image:tile._url,opacity:1,reference:tile.toString(),parentId:parentNode.attr("data-tile-layer-id")}),tile.feature._update(),m_this.draw()},this._getSubLayer=function(level){var node=m_this.canvas().select('g[data-tile-layer="'+level.toFixed()+'"]');if(node.empty()){node=m_this.canvas().append("g");var id=geo.d3.uniqueID();node.classed("group-"+id,!0),node.classed("geo-tile-layer",!0),node.attr("data-tile-layer",level.toFixed()),node.attr("data-tile-layer-id",id)}return node},this._updateSubLayers=function(level){$.each(m_this.canvas().selectAll(".geo-tile-layer")[0],function(idx,el){var layer=parseInt($(el).attr("data-tile-layer"));el=m_this._getSubLayer(layer);var scale=Math.pow(2,level-layer);el.attr("transform","matrix("+[scale,0,0,scale,0,0].join()+")")})},this._init=function(){var sublayer;for(s_init.apply(m_this,arguments),sublayer=0;sublayer<=m_this._options.maxLevel;sublayer+=1)m_this._getSubLayer(sublayer)},this._update=function(){s_update.apply(m_this,arguments),m_this.renderer()._setTransform()},this._remove=function(tile){tile.feature&&(m_this.deleteFeature(tile.feature),tile.feature=null),tile.image&&$(tile.image).remove()}},geo.registerLayerAdjustment("d3","tile",geo.d3.tileLayer),geo.d3.pointFeature=function(arg){"use strict";if(!(this instanceof geo.d3.pointFeature))return new geo.d3.pointFeature(arg);arg=arg||{},geo.pointFeature.call(this,arg),geo.d3.object.call(this);var m_sticky,m_this=this,s_init=this._init,s_update=this._update,m_buildTime=geo.timestamp(),m_style={};return this._init=function(arg){return s_init.call(m_this,arg),m_sticky=m_this.layer().sticky(),m_this},this._build=function(){var data=m_this.data(),s_style=m_this.style.get(),m_renderer=m_this.renderer(),pos_func=m_this.position();return s_update.call(m_this),data||(data=[]),m_style.id=m_this._d3id(),m_style.data=data,m_style.append="circle",m_style.attributes={r:m_renderer._convertScale(s_style.radius),cx:function(d){return m_this.featureGcsToDisplay(pos_func(d)).x},cy:function(d){return m_this.featureGcsToDisplay(pos_func(d)).y}},m_style.style=s_style,m_style.classes=["d3PointFeature"],m_this.renderer()._drawFeatures(m_style),m_buildTime.modified(),m_this.updateTime().modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.getMTime()>=m_buildTime.getMTime()&&m_this._build(),m_this},this._init(arg),this},inherit(geo.d3.pointFeature,geo.pointFeature),geo.registerFeature("d3","point",geo.d3.pointFeature),geo.d3.lineFeature=function(arg){"use strict";if(!(this instanceof geo.d3.lineFeature))return new geo.d3.lineFeature(arg);arg=arg||{},geo.lineFeature.call(this,arg),geo.d3.object.call(this);var m_this=this,s_init=this._init,m_buildTime=geo.timestamp(),s_update=this._update;return this._init=function(arg){return s_init.call(m_this,arg),m_this},this._build=function(){var data=m_this.data()||[],s_style=m_this.style(),m_renderer=m_this.renderer(),pos_func=m_this.position(),line=d3.svg.line().x(function(d){return m_this.featureGcsToDisplay(d).x}).y(function(d){return m_this.featureGcsToDisplay(d).y});return s_update.call(m_this),s_style.fill=function(){return!1},data.forEach(function(item,idx){function wrapStyle(func){return geo.util.isFunction(func)?function(){return func(ln[0],0,item,idx)}:func}var m_style,key,ln=m_this.line()(item,idx),style={};for(key in s_style)s_style.hasOwnProperty(key)&&(style[key]=wrapStyle(s_style[key]));m_style={data:[ln.map(function(d,i){return pos_func(d,i,item,idx)})],append:"path",attributes:{d:line},id:m_this._d3id()+idx,classes:["d3LineFeature","d3SubLine-"+idx],style:style},m_renderer._drawFeatures(m_style)}),m_buildTime.modified(),m_this.updateTime().modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.getMTime()>=m_buildTime.getMTime()&&m_this._build(),m_this},this._init(arg),this},inherit(geo.d3.lineFeature,geo.lineFeature),geo.registerFeature("d3","line",geo.d3.lineFeature),geo.d3.pathFeature=function(arg){"use strict";if(!(this instanceof geo.d3.pathFeature))return new geo.d3.pathFeature(arg);arg=arg||{},geo.pathFeature.call(this,arg),geo.d3.object.call(this);var m_this=this,s_init=this._init,m_buildTime=geo.timestamp(),s_update=this._update,m_style={};return m_style.style={},this._init=function(arg){return s_init.call(m_this,arg),m_this},this._build=function(){var tmp,diag,data=m_this.data()||[],s_style=m_this.style();return s_update.call(m_this),diag=function(d){var p={source:d.source,target:d.target};return d3.svg.diagonal()(p)},tmp=[],data.forEach(function(d,i){var src,trg;i=m_buildTime.getMTime()&&m_this._build(),m_this},this._init(arg),this},inherit(geo.d3.pathFeature,geo.pathFeature),geo.registerFeature("d3","path",geo.d3.pathFeature),geo.d3.graphFeature=function(arg){"use strict";var m_this=this;return this instanceof geo.d3.graphFeature?(geo.graphFeature.call(this,arg),this.select=function(){var renderer=m_this.renderer(),selection={},node=m_this.nodeFeature(),links=m_this.linkFeatures();return selection.nodes=renderer.select(node._d3id()),selection.links=links.map(function(link){return renderer.select(link._d3id())}),selection},this):new geo.d3.graphFeature(arg)},inherit(geo.d3.graphFeature,geo.graphFeature),geo.registerFeature("d3","graph",geo.d3.graphFeature),geo.d3.planeFeature=function(arg){"use strict";function normalize(pt){return Array.isArray(pt)?{x:pt[0],y:pt[1]}:pt}if(!(this instanceof geo.d3.planeFeature))return new geo.d3.planeFeature(arg);geo.planeFeature.call(this,arg),geo.d3.object.call(this);var m_this=this,m_style={},s_update=this._update,s_init=this._init,m_buildTime=geo.timestamp();return this._build=function(){var ul=normalize(m_this.upperLeft()),lr=normalize(m_this.lowerRight()),renderer=m_this.renderer(),s=m_this.style();return delete s.fill_color,delete s.color,delete s.opacity,m_style.id=m_this._d3id(),m_style.style=s,m_style.attributes={x:ul.x,y:ul.y,width:Math.abs(lr.x-ul.x),height:Math.abs(lr.y-ul.y),reference:s.reference},s.image?(m_style.append="image",m_style.attributes["xlink:href"]=s.image):m_style.append="rect",m_style.data=[0],m_style.classes=["d3PlaneFeature"],s.parentId&&(m_style.parentId=s.parentId),renderer._drawFeatures(m_style),m_buildTime.modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.dataTime().getMTime()>=m_buildTime.getMTime()&&m_this._build(),m_this},this._init=function(arg){return s_init.call(m_this,arg||{}),m_this.style({stroke:function(){return!1},fill:function(){return!0},fillColor:function(){return{r:.3,g:.3,b:.3}},fillOpacity:function(){return.5}}),m_this},this._init(),this},inherit(geo.d3.planeFeature,geo.planeFeature),geo.registerFeature("d3","plane",geo.d3.planeFeature),geo.d3.vectorFeature=function(arg){"use strict";function markerID(d,i){return m_this._d3id()+"_marker_"+i}function updateMarkers(data,stroke,opacity){var renderer=m_this.renderer(),sel=m_this.renderer()._definitions().selectAll("marker.geo-vector").data(data);sel.enter().append("marker").attr("id",markerID).attr("class","geo-vector").attr("viewBox","0 0 10 10").attr("refX","1").attr("refY","5").attr("markerWidth","5").attr("markerHeight","5").attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z"),sel.exit().remove(),sel.style("stroke",renderer._convertColor(stroke)).style("fill",renderer._convertColor(stroke)).style("opacity",opacity)}if(!(this instanceof geo.d3.vectorFeature))return new geo.d3.vectorFeature(arg);arg=arg||{},geo.vectorFeature.call(this,arg),geo.d3.object.call(this);var m_sticky,m_this=this,s_init=this._init,s_exit=this._exit,s_update=this._update,m_buildTime=geo.timestamp(),m_style={};return this._init=function(arg){return s_init.call(m_this,arg),m_sticky=m_this.layer().sticky(),m_this},this._build=function(){function getScale(){return scale/m_renderer.scaleFactor()}var data=m_this.data(),s_style=m_this.style.get(),m_renderer=m_this.renderer(),orig_func=m_this.origin(),size_func=m_this.delta(),cache=[],scale=m_this.style("scale"),max=Number.NEGATIVE_INFINITY;return s_update.call(m_this),data||(data=[]),cache=data.map(function(d,i){var origin=m_this.featureGcsToDisplay(orig_func(d,i)),delta=size_func(d,i);return max=Math.max(max,delta.x*delta.x+delta.y*delta.y),{x1:origin.x,y1:origin.y,dx:delta.x,dy:-delta.y}}),max=Math.sqrt(max),scale||(scale=75/max),m_style.id=m_this._d3id(),m_style.data=data,m_style.append="line",m_style.attributes={x1:function(d,i){return cache[i].x1},y1:function(d,i){return cache[i].y1},x2:function(d,i){return cache[i].x1+getScale()*cache[i].dx},y2:function(d,i){return cache[i].y1+getScale()*cache[i].dy},"marker-end":function(d,i){return"url(#"+markerID(d,i)+")"}},m_style.style={stroke:function(){return!0},strokeColor:s_style.strokeColor,strokeWidth:s_style.strokeWidth,strokeOpacity:s_style.strokeOpacity},m_style.classes=["d3VectorFeature"],updateMarkers(data,s_style.strokeColor,s_style.strokeOpacity),m_this.renderer()._drawFeatures(m_style),m_buildTime.modified(),m_this.updateTime().modified(),m_this},this._update=function(){return s_update.call(m_this),m_this.getMTime()>=m_buildTime.getMTime()?m_this._build():updateMarkers(m_style.data,m_style.style.strokeColor,m_style.style.strokeOpacity),m_this},this._exit=function(){s_exit.call(m_this),m_style={},updateMarkers([],null,null)},this._init(arg),this},inherit(geo.d3.vectorFeature,geo.vectorFeature),geo.registerFeature("d3","vector",geo.d3.vectorFeature),geo.gui={},geo.gui.uiLayer=function(arg){"use strict";if(arg.renderer="dom",arg.sticky=!1,!(this instanceof geo.gui.uiLayer))return new geo.gui.uiLayer(arg);geo.layer.call(this,arg);var m_this=this,s_exit=this._exit;this.createWidget=function(widgetName,arg){var newWidget=geo.createWidget(widgetName,m_this,arg);return arg&&"parent"in arg||m_this.addChild(newWidget),newWidget._init(arg),m_this.modified(),newWidget},this.deleteWidget=function(widget){return widget._exit(),m_this.removeChild(widget),m_this.modified(),m_this},this._exit=function(){m_this.children().forEach(function(child){m_this.deleteWidget(child)}),s_exit()}},inherit(geo.gui.uiLayer,geo.layer),geo.registerLayer("ui",geo.gui.uiLayer),geo.gui.widget=function(arg){"use strict";if(!(this instanceof geo.gui.widget))return new geo.gui.widget(arg);geo.sceneObject.call(this,arg);var m_this=this,s_exit=this._exit,m_layer=arg.layer,m_canvas=null;if(arg.position=void 0===arg.position?{left:0,top:0}:arg.position,void 0!==arg.parent&&!(arg.parent instanceof geo.gui.widget))throw"Parent must be of type geo.gui.widget";this._init=function(){m_this.modified()},this._exit=function(){m_this.children().forEach(function(child){m_this._deleteFeature(child)}),m_this.layer().geoOff(geo.event.pan,m_this.repositionEvent),m_this.parentCanvas().removeChild(m_this.canvas()),s_exit()},this._createFeature=function(featureName,arg){var newFeature=geo.createFeature(featureName,m_this,m_this.renderer(),arg);return m_this.addChild(newFeature),m_this.modified(),newFeature},this._deleteFeature=function(feature){return m_this.removeChild(feature),feature._exit(),m_this},this.layer=function(){return m_layer},this._createCanvas=function(){throw"Must be defined in derived classes"},this.canvas=function(val){return void 0===val?m_canvas:void(m_canvas=val)},this._appendChild=function(){m_this.parentCanvas().appendChild(m_this.canvas())},this.parentCanvas=function(){return void 0===m_this.parent?m_this.layer().canvas():m_this.parent().canvas()},this.position=function(){var position;return arg&&arg.hasOwnProperty("position")&&arg.position.hasOwnProperty("x")&&arg.position.hasOwnProperty("y")?(position=m_this.layer().map().gcsToDisplay(arg.position),{left:position.x,top:position.y}):arg.position},this.reposition=function(position){position=position||m_this.position(),m_this.canvas().style.position="absolute";for(var cssAttr in position)position.hasOwnProperty(cssAttr)&&(m_this.canvas().style[cssAttr]=position[cssAttr]+"px")},this.repositionEvent=function(){return m_this.reposition()},this.isInViewport=function(){var position=m_this.position(),layer=m_this.layer();return position.left>=0&&position.top>=0&&position.left<=layer.width()&&position.top<=layer.height()},arg&&arg.hasOwnProperty("position")&&arg.position.hasOwnProperty("x")&&arg.position.hasOwnProperty("y")&&this.layer().geoOn(geo.event.pan,m_this.repositionEvent)},inherit(geo.gui.widget,geo.sceneObject),geo.gui.domWidget=function(arg){"use strict";if(!(this instanceof geo.gui.domWidget))return new geo.gui.domWidget(arg);geo.gui.widget.call(this,arg);var m_this=this,m_default_canvas="div";return this._init=function(){arg.hasOwnProperty("parent")&&arg.parent.addChild(m_this),m_this._createCanvas(),m_this._appendChild(),m_this.canvas().addEventListener("mousedown",function(e){e.stopPropagation()}),m_this.reposition()},this._createCanvas=function(){m_this.canvas(document.createElement(arg.el||m_default_canvas))},this},inherit(geo.gui.domWidget,geo.gui.widget),geo.registerWidget("dom","dom",geo.gui.domWidget),geo.gui.svgWidget=function(arg){"use strict";if(!(this instanceof geo.gui.svgWidget))return new geo.gui.svgWidget(arg);geo.gui.domWidget.call(this,arg);var m_this=this,m_renderer=null;return this._init=function(arg){var d3Parent;arg.hasOwnProperty("parent")&&(arg.parent.addChild(m_this),d3Parent=arg.parent.canvas()),m_this._createCanvas(d3Parent),m_this.canvas().addEventListener("mousedown",function(e){e.stopPropagation()}),m_this.reposition()},this._createCanvas=function(d3Parent){var rendererOpts={layer:m_this.layer(),widget:!0};d3Parent&&(rendererOpts.d3Parent=d3Parent),m_renderer=geo.d3.d3Renderer(rendererOpts),m_this.canvas(m_renderer.canvas()[0][0])},this},inherit(geo.gui.svgWidget,geo.gui.domWidget),geo.registerWidget("dom","svg",geo.gui.svgWidget),geo.gui.sliderWidget=function(arg){"use strict";function put_icon(icon,base,cx,cy,size){var g=base.append("g"),s=size/1024;return g.append("g").append("g").attr("transform","translate("+cx+","+cy+") scale("+s+") translate(-512,-512)").append("path").attr("d",icon).attr("class","geo-glyphicon"),g}if(!(this instanceof geo.gui.sliderWidget))return new geo.gui.sliderWidget(arg);geo.gui.svgWidget.call(this,arg);var m_xscale,m_yscale,m_plus,m_minus,m_track,m_nub,m_plusIcon,m_minusIcon,m_group,m_lowContrast,m_this=this,s_exit=this._exit,s_createCanvas=this._createCanvas,s_appendChild=this._appendChild,m_width=20,m_height=100,m_nubSize=10,m_highlightDur=100;m_plusIcon="M512 81.92c-237.568 0-430.080 192.614-430.080 430.080 0 237.568 192.563 430.080 430.080 430.080s430.080-192.563 430.080-430.080c0-237.517-192.563-430.080-430.080-430.080zM564.326 564.326v206.182h-104.653v-206.182h-206.234v-104.653h206.182v-206.234h104.704v206.182h206.182v104.704h-206.182z",m_minusIcon="M512 81.92c-237.568 0-430.080 192.614-430.080 430.080 0 237.568 192.563 430.080 430.080 430.080s430.080-192.563 430.080-430.080c0-237.517-192.563-430.080-430.080-430.080zM770.56 459.674v104.704h-517.12v-104.704h517.12z",m_lowContrast={white:"#f4f4f4",black:"#505050"},this._init=function(){function respond(evt,trans){var z=m_yscale.invert(d3.mouse(m_this.layer().node()[0])[1]),zrange=map.zoomRange();z=(1-z)*(zrange.max-zrange.min)+zrange.min,trans?map.transition({zoom:z,ease:d3.ease("cubic-in-out"),duration:500,done:m_this._update()}):(map.zoom(z),m_this._update()),evt.stopPropagation()}s_createCanvas(),s_appendChild(),m_this.reposition();var svg=d3.select(m_this.canvas()),x0=40,y0=40+m_width,map=m_this.layer().map();m_xscale=d3.scale.linear().domain([-4,4]).range([x0,x0+m_width]),m_yscale=d3.scale.linear().domain([0,1]).range([y0,y0+m_height]),svg=svg.append("g").classed("geo-ui-slider",!0),m_group=svg,m_plus=svg.append("g"),m_plus.append("circle").datum({fill:"white",stroke:null}).classed("geo-zoom-in",!0).attr("cx",m_xscale(0)).attr("cy",m_yscale(0)-m_width+2).attr("r",m_width/2).style({cursor:"pointer"}).on("click",function(){var z=map.zoom();map.transition({zoom:z+1,ease:d3.ease("cubic-in-out"),duration:500})}).on("mousedown",function(){d3.event.stopPropagation()}),put_icon(m_plusIcon,m_plus,m_xscale(0),m_yscale(0)-m_width+2,m_width+5).style("cursor","pointer").style("pointer-events","none").select("path").datum({fill:"black",stroke:null}),m_minus=svg.append("g"),m_minus.append("circle").datum({fill:"white",stroke:null}).classed("geo-zoom-out",!0).attr("cx",m_xscale(0)).attr("cy",m_yscale(1)+m_width-2).attr("r",m_width/2).style({cursor:"pointer"}).on("click",function(){var z=map.zoom();map.transition({zoom:z-1,ease:d3.ease("cubic-in-out"),duration:500})}).on("mousedown",function(){d3.event.stopPropagation()}),put_icon(m_minusIcon,m_minus,m_xscale(0),m_yscale(1)+m_width-2,m_width+5).style("cursor","pointer").style("pointer-events","none").select("path").datum({fill:"black",stroke:null}),m_track=svg.append("rect").datum({fill:"white",stroke:"black"}).classed("geo-zoom-track",!0).attr("x",m_xscale(0)-m_width/6).attr("y",m_yscale(0)).attr("rx",m_width/10).attr("ry",m_width/10).attr("width",m_width/3).attr("height",m_height).style({cursor:"pointer"}).on("click",function(){respond(d3.event,!0)}),m_nub=svg.append("rect").datum({fill:"black",stroke:null}).classed("geo-zoom-nub",!0).attr("x",m_xscale(-4)).attr("y",m_yscale(.5)-m_nubSize/2).attr("rx",3).attr("ry",3).attr("width",m_width).attr("height",m_nubSize).style({cursor:"pointer"}).on("mousedown",function(){d3.select(document).on("mousemove.geo.slider",function(){respond(d3.event)}),d3.select(document).on("mouseup.geo.slider",function(){respond(d3.event),d3.select(document).on(".geo.slider",null)}),d3.event.stopPropagation()});var mouseOver=function(){d3.select(this).attr("filter","url(#geo-highlight)"),m_group.selectAll("rect,path,circle").transition().duration(m_highlightDur).style("fill",function(d){return d.fill||null}).style("stroke",function(d){return d.stroke||null})},mouseOut=function(){d3.select(this).attr("filter",null),m_group.selectAll("circle,rect,path").transition().duration(m_highlightDur).style("fill",function(d){return m_lowContrast[d.fill]||null}).style("stroke",function(d){return m_lowContrast[d.stroke]||null})};m_group.selectAll("*").on("mouseover",mouseOver).on("mouseout",mouseOut),m_this.layer().geoOn(geo.event.zoom,function(){m_this._update()}),mouseOut(),m_this._update()},this._exit=function(){m_group.remove(),m_this.layer().geoOff(geo.event.zoom),s_exit()},this._update=function(obj){var map=m_this.layer().map(),zoomRange=map.zoomRange(),zoom=map.zoom(),zoomScale=d3.scale.linear();obj=obj||{},zoom=obj.value||zoom,zoomScale.domain([zoomRange.min,zoomRange.max]).range([1,0]).clamp(!0),m_nub.attr("y",m_yscale(zoomScale(zoom))-m_nubSize/2)}},inherit(geo.gui.sliderWidget,geo.gui.svgWidget),geo.registerWidget("dom","slider",geo.gui.sliderWidget),geo.gui.legendWidget=function(arg){"use strict";if(!(this instanceof geo.gui.legendWidget))return new geo.gui.legendWidget(arg);geo.gui.svgWidget.call(this,arg);var m_this=this,m_categories=[],m_top=null,m_group=null,m_border=null,m_spacing=20,m_padding=12,s_createCanvas=this._createCanvas,s_appendChild=this._appendChild;this.categories=function(arg){return void 0===arg?m_categories.slice():(m_categories=arg.slice().map(function(d){return"line"===d.type&&(d.style.fill=!1,d.style.stroke=!0),d}),m_this.draw(),m_this)},this.size=function(){var height,width=1,test=d3.select(m_this.canvas()).append("text").style("opacity",1e-6);return m_categories.forEach(function(d){test.text(d.name),width=Math.max(width,test.node().getBBox().width)}),test.remove(),height=m_spacing*(m_categories.length+1),{width:width+50,height:height}},this.draw=function(){function applyColor(selection){selection.style("fill",function(d){return d.style.fill||void 0===d.style.fill?d.style.fillColor:"none"}).style("fill-opacity",function(d){return void 0===d.style.fillOpacity?1:d.style.fillOpacity}).style("stroke",function(d){return d.style.stroke||void 0===d.style.stroke?d.style.strokeColor:"none"}).style("stroke-opacity",function(d){return void 0===d.style.strokeOpacity?1:d.style.strokeOpacity}).style("stroke-width",function(d){return void 0===d.style.strokeWidth?1.5:d.style.strokeWidth})}m_this._init(),m_border.attr("height",m_this.size().height+2*m_padding).style("display",null);var scale=m_this._scale(),labels=m_group.selectAll("g.geo-label").data(m_categories,function(d){return d.name}),g=labels.enter().append("g").attr("class","geo-label").attr("transform",function(d,i){return"translate(0,"+scale.y(i)+")"});return applyColor(g.filter(function(d){return"point"!==d.type&&"line"!==d.type}).append("rect").attr("x",0).attr("y",-6).attr("rx",5).attr("ry",5).attr("width",40).attr("height",12)),applyColor(g.filter(function(d){return"point"===d.type}).append("circle").attr("cx",20).attr("cy",0).attr("r",6)),applyColor(g.filter(function(d){return"line"===d.type}).append("line").attr("x1",0).attr("y1",0).attr("x2",40).attr("y2",0)),g.append("text").attr("x","50px").attr("y",0).attr("dy","0.3em").text(function(d){return d.name}),m_this.reposition(),m_this},this._scale=function(){return{x:d3.scale.linear().domain([0,1]).range([0,m_this.size().width]),y:d3.scale.linear().domain([0,m_categories.length-1]).range([m_padding/2,m_this.size().height-m_padding/2])}},this._init=function(){m_top||(s_createCanvas(),s_appendChild());var w=m_this.size().width+2*m_padding+4,h=m_this.size().height+2*m_padding+4;m_top&&m_top.remove(),d3.select(m_this.canvas()).attr("width",w).attr("height",h),m_top=d3.select(m_this.canvas()).append("g"),m_group=m_top.append("g").attr("transform","translate("+[m_padding+2,m_padding+2]+")"),m_border=m_group.append("rect").attr("x",-m_padding).attr("y",-m_padding).attr("width",w-4).attr("height",h-4).attr("rx",3).attr("ry",3).style({stroke:"black","stroke-width":"1.5px",fill:"white","fill-opacity":.75,display:"none"}),m_group.on("mousedown",function(){d3.event.stopPropagation()}),m_group.on("mouseover",function(){m_border.transition().duration(250).style("fill-opacity",1)}),m_group.on("mouseout",function(){m_border.transition().duration(250).style("fill-opacity",.75)}),m_this.reposition()},this.geoOn(geo.event.resize,function(){m_this.draw()})},inherit(geo.gui.legendWidget,geo.gui.svgWidget),geo.registerWidget("dom","legend",geo.gui.legendWidget),function($,geo,d3){"use strict";var load=function(){function isColorKey(key){return"color"===key.slice(key.length-5,key.length).toLowerCase()}function makeColorScale(data,acc){function wrap(func){return geo.util.isFunction(func)?function(){return func(acc.apply(this,arguments))}:func(acc)}if(!d3)return console.warn("d3 is unavailable, cannot apply color scales."),acc;var domain,cannotHandle=!1,doNotHandle=!0,categorical=!1,min=Number.POSITIVE_INFINITY,max=Number.NEGATIVE_INFINITY;return domain=geo.util.isFunction(acc)?d3.set(data.map(acc)).values():[acc],domain.forEach(function(v){("string"!=typeof v||"object"!=typeof geo.util.convertColor(v))&&(doNotHandle=!1),"string"==typeof v?categorical=!0:isFinite(v)?+v>max?max=+v:min>+v&&(min=+v):cannotHandle=!0}),cannotHandle?acc:doNotHandle?acc:wrap(categorical?domain.length<=10?d3.scale.category10().domain(domain):domain.length<=20?d3.scale.category20().domain(domain):d3.scale.category20().domain(domain):d3.scale.linear().range(["rgb(252,141,89)","rgb(255,255,191)","rgb(145,191,219)"]).domain([min,(min+max)/2,max]))}return $.widget?void $.widget("geojs.geojsMap",{ +options:{center:{latitude:0,longitude:0},zoom:0,width:null,height:null,layers:[],data:[],url:"http://tile.openstreetmap.org/{z}/{x}/{y}.png",attribution:void 0,baseLayer:"osm",baseRenderer:"vgl"},_create:function(){!this._map&&this.element.length&&(this._map=geo.map({width:this.options.width,height:this.options.height,zoom:this.options.zoom,center:this.options.center,node:this.element.get(0)}),this._baseLayer=this._map.createLayer(this.options.baseLayer,{renderer:this.options.baseRenderer,url:this.options.url,attribution:this.options.attribution}),this._resize({width:800,height:600}),this._layers=[],this.update())},update:function(layers){var m_this=this;return this.options.layers=layers||this.options.layers||[],this._layers.forEach(function(layer){layer.clear(),m_this._map.deleteLayer(layer)}),this._layers=this.options.layers.map(function(layer){return layer.data=layer.data||m_this.options.data,(layer.features||[]).forEach(function(feature){var scl,data=feature.data||layer.data||[];"point"===feature.type&&(feature.size?feature._size=geo.util.ensureFunction(feature.size):null===feature.size&&delete feature._size,data.length&&feature._size&&(scl=d3.scale.linear().domain(d3.extent(data,feature._size)).range([5,20]),feature.radius=function(){return scl(feature._size.apply(this,arguments))}),delete feature.size);var key;for(key in feature)feature.hasOwnProperty(key)&&isColorKey(key)&&(feature[key]=makeColorScale(data,feature[key]))}),geo.layer.create(m_this._map,layer)}),this.redraw(),this},map:function(){return this._map},url:function(url){return this._baseLayer.url(url),this},_resize:function(size){var width=this.options.width,height=this.options.height;size&&(width=size.width,height=size.height),width||(width=this.element.width()),height||(height=this.element.height()),this._map.resize(0,0,width,height)},redraw:function(){return this._resize(),this}}):void($.fn.geojsMap=function(){throw new Error("The geojs jquery plugin requires jquery ui to be available.")})};$(load)}($||window.$,geo||window.geo,d3||window.d3); \ No newline at end of file diff --git a/package.json b/package.json index 0aba10ee66..744721979d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geojs", - "version": "0.5.0", + "version": "0.6.0-rc.1", "description": "JavaScript Geo visualization and Analysis Library", "homepage": "https://github.com/OpenGeoscience/geojs", "license": "Apache-2.0",