From 2703460ecd63bfcd701db25eea33ca9dc24ef47e Mon Sep 17 00:00:00 2001 From: Jonathan Beezley Date: Thu, 31 Mar 2016 16:57:03 -0400 Subject: [PATCH] Update to version 0.9 --- bower.json | 2 +- docs/conf.py | 2 +- geo.js | 77380 ++++++++++++++++++++++++++++--------------------- package.json | 2 +- 4 files changed, 45023 insertions(+), 32363 deletions(-) diff --git a/bower.json b/bower.json index 6fefccc331..ec14574748 100644 --- a/bower.json +++ b/bower.json @@ -1,6 +1,6 @@ { "name": "geojs", - "version": "0.8.0", + "version": "0.9.0", "description": "JavaScript Geo visualization and Analysis Library", "homepage": "https://github.com/OpenGeoscience/geojs", "main": "geo.js", diff --git a/docs/conf.py b/docs/conf.py index 634a38f521..95c1a382cf 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -73,7 +73,7 @@ # built documents. # # The short X.Y version. -version = '0.8.0' +version = '0.9.0' # The full version, including alpha/beta/rc tags. release = version diff --git a/geo.js b/geo.js index 21fe60964b..13be3d87db 100644 --- a/geo.js +++ b/geo.js @@ -1,32360 +1,45020 @@ -/** @namespace */ -var geo = {}; -window.geo = geo; - -geo.renderers = {}; -geo.features = {}; -geo.fileReaders = {}; -geo.rendererLayerAdjustments = {}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Convenient function to define JS inheritance - */ -////////////////////////////////////////////////////////////////////////////// -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 () {}; -}; - -// 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'; - - if (geo.fileReaders === undefined) { - geo.fileReaders = {}; - } - - geo.fileReaders[name] = func; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new file reader - */ -////////////////////////////////////////////////////////////////////////////// -geo.createFileReader = function (name, opts) { - 'use strict'; - - if (geo.fileReaders.hasOwnProperty(name)) { - return geo.fileReaders[name](opts); - } - return null; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Register a new renderer type - */ -////////////////////////////////////////////////////////////////////////////// -geo.registerRenderer = function (name, func) { - 'use strict'; - - if (geo.renderers === undefined) { - geo.renderers = {}; - } - - geo.renderers[name] = func; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create new instance of the renderer - */ -////////////////////////////////////////////////////////////////////////////// -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} - ); - ren._init(); - return ren; - } - return null; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Check if the named renderer is supported. If not, display a warning and get - * the name of a fallback renderer. Ideally, we would pass a list of desired - * features, and, if the renderer is unavailable, this would choose a fallback - * that would support those features. - * - * @params {string|null} name name of the desired renderer - * @params {boolean} noFallack if true, don't recommend a fallback - * @return {string|null|false} the name of the renderer that should be used - * of false if no valid renderer can be determined. - */ -////////////////////////////////////////////////////////////////////////////// -geo.checkRenderer = function (name, noFallback) { - 'use strict'; - if (name === null) { - return name; - } - if (geo.renderers.hasOwnProperty(name)) { - var ren = geo.renderers[name]; - if (!ren.supported || ren.supported()) { - return name; - } - if (!ren.fallback || noFallback) { - return false; - } - var fallback = geo.checkRenderer(ren.fallback(), true); - if (fallback !== false) { - console.warn(name + ' renderer is unavailable, using ' + fallback + - ' renderer instead'); - } - return fallback; - } - return false; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Register a new feature type - */ -////////////////////////////////////////////////////////////////////////////// -geo.registerFeature = function (category, name, func) { - 'use strict'; - - if (geo.features === undefined) { - geo.features = {}; - } - - if (!(category in geo.features)) { - geo.features[category] = {}; - } - - // TODO Add warning if the name already exists - geo.features[category][name] = func; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create new instance of the renderer - */ -////////////////////////////////////////////////////////////////////////////// -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]) { - if (arg !== undefined) { - $.extend(true, options, arg); - } - 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'; - - if (geo.layers === undefined) { - geo.layers = {}; - } - - geo.layers[name] = func; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create new instance of the layer - */ -////////////////////////////////////////////////////////////////////////////// -geo.createLayer = function (name, map, arg) { - 'use strict'; - - /// Default renderer is vgl - var options = {'map': map, 'renderer': 'vgl'}, - layer = null; - - if (name in geo.layers) { - if (arg !== undefined) { - $.extend(true, options, arg); - } - layer = geo.layers[name](options); - layer._init(); - return layer; - } else { - return null; - } -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Register a new widget type - */ -////////////////////////////////////////////////////////////////////////////// -geo.registerWidget = function (category, name, func) { - 'use strict'; - - if (geo.widgets === undefined) { - geo.widgets = {}; - } - - if (!(category in geo.widgets)) { - geo.widgets[category] = {}; - } - - // TODO Add warning if the name already exists - geo.widgets[category][name] = func; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create new instance of the widget - */ -////////////////////////////////////////////////////////////////////////////// -geo.createWidget = function (name, layer, arg) { - 'use strict'; - - var options = { - layer: layer - }; - - if (name in geo.widgets.dom) { - if (arg !== undefined) { - $.extend(true, options, arg); - } - - return geo.widgets.dom[name](options); - } - - throw new Error('Cannot create unknown widget ' + name); -}; - -// Add a polyfill for window.requestAnimationFrame. -if (!window.requestAnimationFrame) { - window.requestAnimationFrame = function (func) { - 'use strict'; - - window.setTimeout(func, 15); - }; -} - -// Add a polyfill for Math.log2 -if (!Math.log2) { - Math.log2 = function () { - '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.8.0'; - -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define('vgl', [], function () { - return (root['vgl'] = factory()); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - root['vgl'] = factory(); - } -}(this, function () { - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl: true, ogs: true, inherit*/ -/*exported vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -if (typeof ogs === 'undefined') { - var ogs = {}; -} - -////////////////////////////////////////////////////////////////////////////// -/** - * Create namespace for the given name - * - * @param ns_string - * @returns {*|{}} - */ -////////////////////////////////////////////////////////////////////////////// -ogs.namespace = function (ns_string) { - 'use strict'; - - var parts = ns_string.split('.'), parent = ogs, i; - - // strip redundant leading global - if (parts[0] === 'ogs') { - parts = parts.slice(1); - } - for (i = 0; i < parts.length; i += 1) { - // create a property if it doesn't exist - if (typeof parent[parts[i]] === 'undefined') { - parent[parts[i]] = {}; - } - parent = parent[parts[i]]; - } - return parent; -}; - -/** vgl namespace */ -var vgl = ogs.namespace('gl'); - -////////////////////////////////////////////////////////////////////////////// -/** - * Convenient function to define JS inheritance - * - * @param C - * @param P - */ -////////////////////////////////////////////////////////////////////////////// -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; -} - -////////////////////////////////////////////////////////////////////////////// -/** - * Convenient function to get size of an object - * - * @param obj - * @returns {number} * - */ -////////////////////////////////////////////////////////////////////////////// -Object.size = function (obj) { - 'use strict'; - - var size = 0, key = null; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - size += 1; - } - } - return size; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Wrap GL enums. Currently to get values of the enums we need to create - * or access the context. - * - * Using enums from here: - * https://github.com/toji/dart-gl-enums/blob/master/lib/gl_enums.dart - * - * @class - */ -////////////////////////////////////////////////////////////////////////////// -vgl.GL = { - ACTIVE_ATTRIBUTES : 0x8B89, - ACTIVE_TEXTURE : 0x84E0, - ACTIVE_UNIFORMS : 0x8B86, - ALIASED_LINE_WIDTH_RANGE : 0x846E, - ALIASED_POINT_SIZE_RANGE : 0x846D, - ALPHA : 0x1906, - ALPHA_BITS : 0x0D55, - ALWAYS : 0x0207, - ARRAY_BUFFER : 0x8892, - ARRAY_BUFFER_BINDING : 0x8894, - ATTACHED_SHADERS : 0x8B85, - BACK : 0x0405, - BLEND : 0x0BE2, - BLEND_COLOR : 0x8005, - BLEND_DST_ALPHA : 0x80CA, - BLEND_DST_RGB : 0x80C8, - BLEND_EQUATION : 0x8009, - BLEND_EQUATION_ALPHA : 0x883D, - BLEND_EQUATION_RGB : 0x8009, - BLEND_SRC_ALPHA : 0x80CB, - BLEND_SRC_RGB : 0x80C9, - BLUE_BITS : 0x0D54, - BOOL : 0x8B56, - BOOL_VEC2 : 0x8B57, - BOOL_VEC3 : 0x8B58, - BOOL_VEC4 : 0x8B59, - BROWSER_DEFAULT_WEBGL : 0x9244, - BUFFER_SIZE : 0x8764, - BUFFER_USAGE : 0x8765, - BYTE : 0x1400, - CCW : 0x0901, - CLAMP_TO_EDGE : 0x812F, - COLOR_ATTACHMENT0 : 0x8CE0, - COLOR_BUFFER_BIT : 0x00004000, - COLOR_CLEAR_VALUE : 0x0C22, - COLOR_WRITEMASK : 0x0C23, - COMPILE_STATUS : 0x8B81, - COMPRESSED_TEXTURE_FORMATS : 0x86A3, - CONSTANT_ALPHA : 0x8003, - CONSTANT_COLOR : 0x8001, - CONTEXT_LOST_WEBGL : 0x9242, - CULL_FACE : 0x0B44, - CULL_FACE_MODE : 0x0B45, - CURRENT_PROGRAM : 0x8B8D, - CURRENT_VERTEX_ATTRIB : 0x8626, - CW : 0x0900, - DECR : 0x1E03, - DECR_WRAP : 0x8508, - DELETE_STATUS : 0x8B80, - DEPTH_ATTACHMENT : 0x8D00, - DEPTH_BITS : 0x0D56, - DEPTH_BUFFER_BIT : 0x00000100, - DEPTH_CLEAR_VALUE : 0x0B73, - DEPTH_COMPONENT : 0x1902, - DEPTH_COMPONENT16 : 0x81A5, - DEPTH_FUNC : 0x0B74, - DEPTH_RANGE : 0x0B70, - DEPTH_STENCIL : 0x84F9, - DEPTH_STENCIL_ATTACHMENT : 0x821A, - DEPTH_TEST : 0x0B71, - DEPTH_WRITEMASK : 0x0B72, - DITHER : 0x0BD0, - DONT_CARE : 0x1100, - DST_ALPHA : 0x0304, - DST_COLOR : 0x0306, - DYNAMIC_DRAW : 0x88E8, - ELEMENT_ARRAY_BUFFER : 0x8893, - ELEMENT_ARRAY_BUFFER_BINDING : 0x8895, - EQUAL : 0x0202, - FASTEST : 0x1101, - FLOAT : 0x1406, - FLOAT_MAT2 : 0x8B5A, - FLOAT_MAT3 : 0x8B5B, - FLOAT_MAT4 : 0x8B5C, - FLOAT_VEC2 : 0x8B50, - FLOAT_VEC3 : 0x8B51, - FLOAT_VEC4 : 0x8B52, - FRAGMENT_SHADER : 0x8B30, - FRAMEBUFFER : 0x8D40, - FRAMEBUFFER_ATTACHMENT_OBJECT_NAME : 0x8CD1, - FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE : 0x8CD0, - FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE : 0x8CD3, - FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL : 0x8CD2, - FRAMEBUFFER_BINDING : 0x8CA6, - FRAMEBUFFER_COMPLETE : 0x8CD5, - FRAMEBUFFER_INCOMPLETE_ATTACHMENT : 0x8CD6, - FRAMEBUFFER_INCOMPLETE_DIMENSIONS : 0x8CD9, - FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT : 0x8CD7, - FRAMEBUFFER_UNSUPPORTED : 0x8CDD, - FRONT : 0x0404, - FRONT_AND_BACK : 0x0408, - FRONT_FACE : 0x0B46, - FUNC_ADD : 0x8006, - FUNC_REVERSE_SUBTRACT : 0x800B, - FUNC_SUBTRACT : 0x800A, - GENERATE_MIPMAP_HINT : 0x8192, - GEQUAL : 0x0206, - GREATER : 0x0204, - GREEN_BITS : 0x0D53, - HIGH_FLOAT : 0x8DF2, - HIGH_INT : 0x8DF5, - INCR : 0x1E02, - INCR_WRAP : 0x8507, - INT : 0x1404, - INT_VEC2 : 0x8B53, - INT_VEC3 : 0x8B54, - INT_VEC4 : 0x8B55, - INVALID_ENUM : 0x0500, - INVALID_FRAMEBUFFER_OPERATION : 0x0506, - INVALID_OPERATION : 0x0502, - INVALID_VALUE : 0x0501, - INVERT : 0x150A, - KEEP : 0x1E00, - LEQUAL : 0x0203, - LESS : 0x0201, - LINEAR : 0x2601, - LINEAR_MIPMAP_LINEAR : 0x2703, - LINEAR_MIPMAP_NEAREST : 0x2701, - LINES : 0x0001, - LINE_LOOP : 0x0002, - LINE_STRIP : 0x0003, - LINE_WIDTH : 0x0B21, - LINK_STATUS : 0x8B82, - LOW_FLOAT : 0x8DF0, - LOW_INT : 0x8DF3, - LUMINANCE : 0x1909, - LUMINANCE_ALPHA : 0x190A, - MAX_COMBINED_TEXTURE_IMAGE_UNITS : 0x8B4D, - MAX_CUBE_MAP_TEXTURE_SIZE : 0x851C, - MAX_FRAGMENT_UNIFORM_VECTORS : 0x8DFD, - MAX_RENDERBUFFER_SIZE : 0x84E8, - MAX_TEXTURE_IMAGE_UNITS : 0x8872, - MAX_TEXTURE_SIZE : 0x0D33, - MAX_VARYING_VECTORS : 0x8DFC, - MAX_VERTEX_ATTRIBS : 0x8869, - MAX_VERTEX_TEXTURE_IMAGE_UNITS : 0x8B4C, - MAX_VERTEX_UNIFORM_VECTORS : 0x8DFB, - MAX_VIEWPORT_DIMS : 0x0D3A, - MEDIUM_FLOAT : 0x8DF1, - MEDIUM_INT : 0x8DF4, - MIRRORED_REPEAT : 0x8370, - NEAREST : 0x2600, - NEAREST_MIPMAP_LINEAR : 0x2702, - NEAREST_MIPMAP_NEAREST : 0x2700, - NEVER : 0x0200, - NICEST : 0x1102, - NONE : 0, - NOTEQUAL : 0x0205, - NO_ERROR : 0, - ONE : 1, - ONE_MINUS_CONSTANT_ALPHA : 0x8004, - ONE_MINUS_CONSTANT_COLOR : 0x8002, - ONE_MINUS_DST_ALPHA : 0x0305, - ONE_MINUS_DST_COLOR : 0x0307, - ONE_MINUS_SRC_ALPHA : 0x0303, - ONE_MINUS_SRC_COLOR : 0x0301, - OUT_OF_MEMORY : 0x0505, - PACK_ALIGNMENT : 0x0D05, - POINTS : 0x0000, - POLYGON_OFFSET_FACTOR : 0x8038, - POLYGON_OFFSET_FILL : 0x8037, - POLYGON_OFFSET_UNITS : 0x2A00, - RED_BITS : 0x0D52, - RENDERBUFFER : 0x8D41, - RENDERBUFFER_ALPHA_SIZE : 0x8D53, - RENDERBUFFER_BINDING : 0x8CA7, - RENDERBUFFER_BLUE_SIZE : 0x8D52, - RENDERBUFFER_DEPTH_SIZE : 0x8D54, - RENDERBUFFER_GREEN_SIZE : 0x8D51, - RENDERBUFFER_HEIGHT : 0x8D43, - RENDERBUFFER_INTERNAL_FORMAT : 0x8D44, - RENDERBUFFER_RED_SIZE : 0x8D50, - RENDERBUFFER_STENCIL_SIZE : 0x8D55, - RENDERBUFFER_WIDTH : 0x8D42, - RENDERER : 0x1F01, - REPEAT : 0x2901, - REPLACE : 0x1E01, - RGB : 0x1907, - RGB565 : 0x8D62, - RGB5_A1 : 0x8057, - RGBA : 0x1908, - RGBA4 : 0x8056, - SAMPLER_2D : 0x8B5E, - SAMPLER_CUBE : 0x8B60, - SAMPLES : 0x80A9, - SAMPLE_ALPHA_TO_COVERAGE : 0x809E, - SAMPLE_BUFFERS : 0x80A8, - SAMPLE_COVERAGE : 0x80A0, - SAMPLE_COVERAGE_INVERT : 0x80AB, - SAMPLE_COVERAGE_VALUE : 0x80AA, - SCISSOR_BOX : 0x0C10, - SCISSOR_TEST : 0x0C11, - SHADER_TYPE : 0x8B4F, - SHADING_LANGUAGE_VERSION : 0x8B8C, - SHORT : 0x1402, - SRC_ALPHA : 0x0302, - SRC_ALPHA_SATURATE : 0x0308, - SRC_COLOR : 0x0300, - STATIC_DRAW : 0x88E4, - STENCIL_ATTACHMENT : 0x8D20, - STENCIL_BACK_FAIL : 0x8801, - STENCIL_BACK_FUNC : 0x8800, - STENCIL_BACK_PASS_DEPTH_FAIL : 0x8802, - STENCIL_BACK_PASS_DEPTH_PASS : 0x8803, - STENCIL_BACK_REF : 0x8CA3, - STENCIL_BACK_VALUE_MASK : 0x8CA4, - STENCIL_BACK_WRITEMASK : 0x8CA5, - STENCIL_BITS : 0x0D57, - STENCIL_BUFFER_BIT : 0x00000400, - STENCIL_CLEAR_VALUE : 0x0B91, - STENCIL_FAIL : 0x0B94, - STENCIL_FUNC : 0x0B92, - STENCIL_INDEX : 0x1901, - STENCIL_INDEX8 : 0x8D48, - STENCIL_PASS_DEPTH_FAIL : 0x0B95, - STENCIL_PASS_DEPTH_PASS : 0x0B96, - STENCIL_REF : 0x0B97, - STENCIL_TEST : 0x0B90, - STENCIL_VALUE_MASK : 0x0B93, - STENCIL_WRITEMASK : 0x0B98, - STREAM_DRAW : 0x88E0, - SUBPIXEL_BITS : 0x0D50, - TEXTURE : 0x1702, - TEXTURE0 : 0x84C0, - TEXTURE1 : 0x84C1, - TEXTURE10 : 0x84CA, - TEXTURE11 : 0x84CB, - TEXTURE12 : 0x84CC, - TEXTURE13 : 0x84CD, - TEXTURE14 : 0x84CE, - TEXTURE15 : 0x84CF, - TEXTURE16 : 0x84D0, - TEXTURE17 : 0x84D1, - TEXTURE18 : 0x84D2, - TEXTURE19 : 0x84D3, - TEXTURE2 : 0x84C2, - TEXTURE20 : 0x84D4, - TEXTURE21 : 0x84D5, - TEXTURE22 : 0x84D6, - TEXTURE23 : 0x84D7, - TEXTURE24 : 0x84D8, - TEXTURE25 : 0x84D9, - TEXTURE26 : 0x84DA, - TEXTURE27 : 0x84DB, - TEXTURE28 : 0x84DC, - TEXTURE29 : 0x84DD, - TEXTURE3 : 0x84C3, - TEXTURE30 : 0x84DE, - TEXTURE31 : 0x84DF, - TEXTURE4 : 0x84C4, - TEXTURE5 : 0x84C5, - TEXTURE6 : 0x84C6, - TEXTURE7 : 0x84C7, - TEXTURE8 : 0x84C8, - TEXTURE9 : 0x84C9, - TEXTURE_2D : 0x0DE1, - TEXTURE_BINDING_2D : 0x8069, - TEXTURE_BINDING_CUBE_MAP : 0x8514, - TEXTURE_CUBE_MAP : 0x8513, - TEXTURE_CUBE_MAP_NEGATIVE_X : 0x8516, - TEXTURE_CUBE_MAP_NEGATIVE_Y : 0x8518, - TEXTURE_CUBE_MAP_NEGATIVE_Z : 0x851A, - TEXTURE_CUBE_MAP_POSITIVE_X : 0x8515, - TEXTURE_CUBE_MAP_POSITIVE_Y : 0x8517, - TEXTURE_CUBE_MAP_POSITIVE_Z : 0x8519, - TEXTURE_MAG_FILTER : 0x2800, - TEXTURE_MIN_FILTER : 0x2801, - TEXTURE_WRAP_S : 0x2802, - TEXTURE_WRAP_T : 0x2803, - TRIANGLES : 0x0004, - TRIANGLE_FAN : 0x0006, - TRIANGLE_STRIP : 0x0005, - UNPACK_ALIGNMENT : 0x0CF5, - UNPACK_COLORSPACE_CONVERSION_WEBGL : 0x9243, - UNPACK_FLIP_Y_WEBGL : 0x9240, - UNPACK_PREMULTIPLY_ALPHA_WEBGL : 0x9241, - UNSIGNED_BYTE : 0x1401, - UNSIGNED_INT : 0x1405, - UNSIGNED_SHORT : 0x1403, - UNSIGNED_SHORT_4_4_4_4 : 0x8033, - UNSIGNED_SHORT_5_5_5_1 : 0x8034, - UNSIGNED_SHORT_5_6_5 : 0x8363, - VALIDATE_STATUS : 0x8B83, - VENDOR : 0x1F00, - VERSION : 0x1F02, - VERTEX_ATTRIB_ARRAY_BUFFER_BINDING : 0x889F, - VERTEX_ATTRIB_ARRAY_ENABLED : 0x8622, - VERTEX_ATTRIB_ARRAY_NORMALIZED : 0x886A, - VERTEX_ATTRIB_ARRAY_POINTER : 0x8645, - VERTEX_ATTRIB_ARRAY_SIZE : 0x8623, - VERTEX_ATTRIB_ARRAY_STRIDE : 0x8624, - VERTEX_ATTRIB_ARRAY_TYPE : 0x8625, - VERTEX_SHADER : 0x8B31, - VIEWPORT : 0x0BA2, - ZERO : 0 -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class timestamp - * - * @class - * @returns {vgl.timestamp} - */ -////////////////////////////////////////////////////////////////////////////// -var m_globalModifiedTime = 0; - -vgl.timestamp = function () { - 'use strict'; - - if (!(this instanceof vgl.timestamp)) { - return new vgl.timestamp(); - } - - var m_modifiedTime = 0; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Update modified time - */ - ///////////////////////////////////////////////////////////////////////////// - this.modified = function () { - m_globalModifiedTime += 1; - m_modifiedTime = m_globalModifiedTime; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get modified time - * - * @returns {number} - */ - ///////////////////////////////////////////////////////////////////////////// - this.getMTime = function () { - return m_modifiedTime; - }; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class object - * - * @class - * @returns {vgl.object} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.object = function () { - 'use strict'; - - if (!(this instanceof vgl.object)) { - return new vgl.object(); - } - - /** @private */ - var m_modifiedTime = vgl.timestamp(); - m_modifiedTime.modified(); - - //////////////////////////////////////////////////////////////////////////// - /** - * Mark the object modified - */ - //////////////////////////////////////////////////////////////////////////// - this.modified = function () { - m_modifiedTime.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return modified time of the object - * - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.getMTime = function () { - return m_modifiedTime.getMTime(); - }; - - return this; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class event - * - * @class event - * @returns {vgl.event} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.event = function () { - 'use strict'; - - if (!(this instanceof vgl.event)) { - return new vgl.event(); - } - vgl.object.call(this); - - return this; -}; - -inherit(vgl.event, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * types - */ -////////////////////////////////////////////////////////////////////////////// -vgl.event.keyPress = 'vgl.event.keyPress'; -vgl.event.mousePress = 'vgl.event.mousePress'; -vgl.event.mouseRelease = 'vgl.event.mouseRelease'; -vgl.event.contextMenu = 'vgl.event.contextMenu'; -vgl.event.configure = 'vgl.event.configure'; -vgl.event.enable = 'vgl.event.enable'; -vgl.event.mouseWheel = 'vgl.event.mouseWheel'; -vgl.event.keyRelease = 'vgl.event.keyRelease'; -vgl.event.middleButtonPress = 'vgl.event.middleButtonPress'; -vgl.event.startInteraction = 'vgl.event.startInteraction'; -vgl.event.enter = 'vgl.event.enter'; -vgl.event.rightButtonPress = 'vgl.event.rightButtonPress'; -vgl.event.middleButtonRelease = 'vgl.event.middleButtonRelease'; -vgl.event.char = 'vgl.event.char'; -vgl.event.disable = 'vgl.event.disable'; -vgl.event.endInteraction = 'vgl.event.endInteraction'; -vgl.event.mouseMove = 'vgl.event.mouseMove'; -vgl.event.mouseOut = 'vgl.event.mouseOut'; -vgl.event.expose = 'vgl.event.expose'; -vgl.event.timer = 'vgl.event.timer'; -vgl.event.leftButtonPress = 'vgl.event.leftButtonPress'; -vgl.event.leave = 'vgl.event.leave'; -vgl.event.rightButtonRelease = 'vgl.event.rightButtonRelease'; -vgl.event.leftButtonRelease = 'vgl.event.leftButtonRelease'; -vgl.event.click = 'vgl.event.click'; -vgl.event.dblClick = 'vgl.event.dblClick'; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class boundingObject - * - * @class - * @return {vgl.boundingObject} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.boundingObject = function () { - 'use strict'; - - if (!(this instanceof vgl.boundingObject)) { - return new vgl.boundingObject(); - } - vgl.object.call(this); - - /** @private */ - var m_bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - m_computeBoundsTimestamp = vgl.timestamp(), - m_boundsDirtyTimestamp = vgl.timestamp(); - - m_computeBoundsTimestamp.modified(); - m_boundsDirtyTimestamp.modified(); - - //////////////////////////////////////////////////////////////////////////// - /** - * Get current bounds of the object - */ - //////////////////////////////////////////////////////////////////////////// - this.bounds = function () { - return m_bounds; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if bounds are valid - */ - //////////////////////////////////////////////////////////////////////////// - this.hasValidBounds = function (bounds) { - if (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) { - return false; - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set current bounds of the object - */ - //////////////////////////////////////////////////////////////////////////// - this.setBounds = function (minX, maxX, minY, maxY, minZ, maxZ) { - if (!this.hasValidBounds([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; - - this.modified(); - m_computeBoundsTimestamp.modified(); - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Reset bounds to default values - */ - //////////////////////////////////////////////////////////////////////////// - this.resetBounds = function () { - m_bounds[0] = Number.MAX_VALUE; - m_bounds[1] = -Number.MAX_VALUE; - m_bounds[2] = Number.MAX_VALUE; - m_bounds[3] = -Number.MAX_VALUE; - m_bounds[4] = Number.MAX_VALUE; - m_bounds[5] = -Number.MAX_VALUE; - - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute bounds of the object - * - * Should be implemented by the concrete class - */ - //////////////////////////////////////////////////////////////////////////// - this.computeBounds = function () { - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return bounds computation modification time - * - * @returns {vgl.timestamp} - */ - //////////////////////////////////////////////////////////////////////////// - this.computeBoundsTimestamp = function () { - return m_computeBoundsTimestamp; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return bounds dirty timestamp - * - * @returns {vgl.timestamp} - */ - //////////////////////////////////////////////////////////////////////////// - this.boundsDirtyTimestamp = function () { - return m_boundsDirtyTimestamp; - }; - - this.resetBounds(); - - return this; -}; - -vgl.boundingObject.ReferenceFrame = { - 'Relative' : 0, - 'Absolute' : 1 -}; - -inherit(vgl.boundingObject, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class node - * - * @class - * @returns {vgl.node} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.node = function () { - 'use strict'; - - if (!(this instanceof vgl.node)) { - return new vgl.node(); - } - vgl.boundingObject.call(this); - - /** @private */ - var m_parent = null, - m_material = null, - m_visible = true, - m_overlay = false; - - //////////////////////////////////////////////////////////////////////////// - /** - * Accept visitor for scene traversal - */ - //////////////////////////////////////////////////////////////////////////// - this.accept = function (visitor) { - visitor.visit(this); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return active material used by the node - */ - //////////////////////////////////////////////////////////////////////////// - this.material = function () { - return m_material; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set material to be used the node - * - * @param material - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setMaterial = function (material) { - if (material !== m_material) { - m_material = material; - this.modified(); - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if the node is visible or node - * - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.visible = function () { - return m_visible; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Turn ON/OFF visibility of the node - * - * @param flag - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setVisible = function (flag) { - if (flag !== m_visible) { - m_visible = flag; - this.modified(); - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return current parent of the node - * - * @returns {null} - */ - //////////////////////////////////////////////////////////////////////////// - this.parent = function () { - return m_parent; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set parent of the node - * - * @param parent - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setParent = function (parent) { - if (parent !== m_parent) { - if (m_parent !== null) { - m_parent.removeChild(this); - } - m_parent = parent; - this.modified(); - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if the node is an overlay node - * - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.overlay = function () { - return m_overlay; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set if the node is an overlay node or not - * - * @param flag - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setOverlay = function (flag) { - if (m_overlay !== flag) { - m_overlay = flag; - this.modified(); - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /* - * Traverse parent and their parent and so on - */ - //////////////////////////////////////////////////////////////////////////// - this.ascend = function (visitor) { - visitor = visitor; /* unused parameter */ - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Traverse children - */ - //////////////////////////////////////////////////////////////////////////// - this.traverse = function (visitor) { - visitor = visitor; /* unused parameter */ - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Mark that the bounds are modified - * - */ - //////////////////////////////////////////////////////////////////////////// - this.boundsModified = function () { - // @todo Implement this - this.boundsDirtyTimestamp().modified(); - - if (m_parent !== null) { - m_parent.boundsModified(); - } - }; - - return this; -}; - -inherit(vgl.node, vgl.boundingObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class groupNode - * - * @class - * @returns {vgl.groupNode} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.groupNode = function () { - 'use strict'; - - if (!(this instanceof vgl.groupNode)) { - return new vgl.groupNode(); - } - vgl.node.call(this); - - var m_children = []; - - // Reference to base class methods - this.b_setVisible = this.setVisible; - - //////////////////////////////////////////////////////////////////////////// - /** - * Turn on / off visibility - * - * @param flag - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setVisible = function (flag) { - var i; - - if (this.b_setVisible(flag) !== true) { - return false; - } - - for (i = 0; i < m_children.length; i += 1) { - m_children[i].setVisible(flag); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Make the incoming node as child of the group node - * - * @param childNode - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.addChild = function (childNode) { - if (childNode instanceof vgl.node) { - if (m_children.indexOf(childNode) === -1) { - childNode.setParent(this); - m_children.push(childNode); - this.boundsDirtyTimestamp().modified(); - return true; - } - return false; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove parent-child relationship between the group and incoming node - * - * @param childNode - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.removeChild = function (childNode) { - if (childNode.parent() === this) { - var index = m_children.indexOf(childNode); - m_children.splice(index, 1); - this.boundsDirtyTimestamp().modified(); - return true; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove parent-child relationship between child nodes and the group node - */ - //////////////////////////////////////////////////////////////////////////// - this.removeChildren = function () { - var i; - for (i = 0; i < m_children.length; i += 1) { - this.removeChild(m_children[i]); - } - - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return children of this group node - * - * @returns {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.children = function () { - return m_children; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return true if this group node has node as a child, false otherwise. - * - * @param node - * @returns {bool} - */ - //////////////////////////////////////////////////////////////////////////// - this.hasChild = function (node) { - var i = 0, child = false; - - for (i = 0; i < m_children.length; i += 1) { - if (m_children[i] === node) { - child = true; - break; - } - } - - return child; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Accept a visitor and traverse the scene tree - * - * @param visitor - */ - //////////////////////////////////////////////////////////////////////////// - this.accept = function (visitor) { - visitor.visit(this); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Traverse the scene - * - * @param visitor - */ - //////////////////////////////////////////////////////////////////////////// - this.traverse = function (visitor) { - switch (visitor.type()) { - case visitor.UpdateVisitor: - this.traverseChildrenAndUpdateBounds(visitor); - break; - case visitor.CullVisitor: - this.traverseChildren(visitor); - break; - default: - break; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Traverse all of the children and update the bounds for each - * - * @param visitor - */ - //////////////////////////////////////////////////////////////////////////// - this.traverseChildrenAndUpdateBounds = function (visitor) { - var i; - - if (this.m_parent && this.boundsDirtyTimestamp().getMTime() > - this.computeBoundsTimestamp().getMTime()) { - // Flag parents bounds dirty. - this.m_parent.boundsDirtyTimestamp.modified(); - } - - this.computeBounds(); - - if (visitor.mode() === visitor.TraverseAllChildren) { - for (i = 0; i < m_children.length(); i += 1) { - m_children[i].accept(visitor); - this.updateBounds(m_children[i]); - } - } - - this.computeBoundsTimestamp().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Traverse children of the group node - * - * @param visitor - */ - //////////////////////////////////////////////////////////////////////////// - this.traverseChildren = function (visitor) { - var i; - - if (visitor.mode() === vgl.vesVisitor.TraverseAllChildren) { - for (i = 0; i < m_children.length(); i += 1) { - m_children[i].accept(visitor); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute bounds for the group node - */ - //////////////////////////////////////////////////////////////////////////// - this.computeBounds = function () { - var i = 0; - - if (this.computeBoundsTimestamp().getMTime() > - this.boundsDirtyTimestamp().getMTime()) { - return; - } - - for (i = 0; i < m_children.length; i += 1) { - this.updateBounds(m_children[i]); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update bounds for the group node - * - * This method is used internally to update bounds of the group node by - * traversing each of its child. - * - * @param child - */ - //////////////////////////////////////////////////////////////////////////// - this.updateBounds = function (child) { - // FIXME: This check should not be required and possibly is incorrect - if (child.overlay()) { - return; - } - - // Make sure that child bounds are upto date - child.computeBounds(); - - var bounds = this.bounds(), - childBounds = child.bounds(), - istep = 0, - jstep = 0, - i; - - for (i = 0; i < 3; i += 1) { - istep = i * 2; - jstep = i * 2 + 1; - if (childBounds[istep] < bounds[istep]) { - bounds[istep] = childBounds[istep]; - } - if (childBounds[jstep] > bounds[jstep]) { - bounds[jstep] = childBounds[jstep]; - } - } - - this.setBounds(bounds[0], bounds[1], bounds[2], bounds[3], - bounds[4], bounds[5]); - }; - - return this; -}; - -inherit(vgl.groupNode, vgl.node); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, vec3, mat4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class actor - * - * @class - * @returns {vgl.actor} - */ -//////////////////////////////////////////////////////////////////////////// -vgl.actor = function () { - 'use strict'; - - if (!(this instanceof vgl.actor)) { - return new vgl.actor(); - } - vgl.node.call(this); - - /** @private */ - var m_this = this, - m_transformMatrix = mat4.create(), - m_referenceFrame = vgl.boundingObject.ReferenceFrame.Relative, - m_mapper = null; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get transformation matrix used by the actor - * - * @returns {mat4} - */ - //////////////////////////////////////////////////////////////////////////// - this.matrix = function () { - return m_transformMatrix; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set transformation matrix for the actor - * - * @param {mat4} 4X4 transformation matrix - */ - //////////////////////////////////////////////////////////////////////////// - this.setMatrix = function (tmatrix) { - if (tmatrix !== m_transformMatrix) { - m_transformMatrix = tmatrix; - m_this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get reference frame for the transformations - * - * @returns {String} Possible values are Absolute or Relative - */ - //////////////////////////////////////////////////////////////////////////// - this.referenceFrame = function () { - return m_referenceFrame; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set reference frame for the transformations - * - * @param {vgl.boundingObject.ReferenceFrame} - * referenceFrame Possible values are (Absolute | Relative) - */ - //////////////////////////////////////////////////////////////////////////// - this.setReferenceFrame = function (referenceFrame) { - if (referenceFrame !== m_referenceFrame) { - m_referenceFrame = referenceFrame; - m_this.modified(); - return true; - } - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return mapper where actor gets it behavior and data - * - * @returns {vgl.mapper} - */ - //////////////////////////////////////////////////////////////////////////// - this.mapper = function () { - return m_mapper; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Connect an actor to its data source - * - * @param {vgl.mapper} - */ - //////////////////////////////////////////////////////////////////////////// - this.setMapper = function (mapper) { - if (mapper !== m_mapper) { - m_mapper = mapper; - m_this.boundsModified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * @todo - */ - //////////////////////////////////////////////////////////////////////////// - this.accept = function (visitor) { - visitor = visitor; /* ignore this parameter */ - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * @todo - */ - //////////////////////////////////////////////////////////////////////////// - this.ascend = function (visitor) { - visitor = visitor; /* ignore this parameter */ - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute object space to world space matrix - * @todo - */ - //////////////////////////////////////////////////////////////////////////// - this.computeLocalToWorldMatrix = function (matrix, visitor) { - matrix = matrix; /* ignore this parameter */ - visitor = visitor; /* ignore this parameter */ - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute world space to object space matrix - * @todo - */ - //////////////////////////////////////////////////////////////////////////// - this.computeWorldToLocalMatrix = function (matrix, visitor) { - matrix = matrix; /* ignore this parameter */ - visitor = visitor; /* ignore this parameter */ - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute actor bounds - */ - //////////////////////////////////////////////////////////////////////////// - this.computeBounds = function () { - if (m_mapper === null || m_mapper === undefined) { - m_this.resetBounds(); - return; - } - - var computeBoundsTimestamp = m_this.computeBoundsTimestamp(), - mapperBounds, minPt, maxPt, newBounds; - - if (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(); - } - }; - - return m_this; -}; - -inherit(vgl.actor, vgl.node); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Freeze javascript object - * - * @param obj - */ -////////////////////////////////////////////////////////////////////////////// -vgl.freezeObject = function (obj) { - 'use strict'; - - /** - * Freezes an object, using Object.freeze if available, otherwise returns - * the object unchanged. This function should be used in setup code to prevent - * errors from completely halting JavaScript execution in legacy browsers. - * - * @exports freezeObject - */ - var freezedObject = Object.freeze(obj); - if (typeof freezedObject === 'undefined') { - freezedObject = function (o) { - return o; - }; - } - - return freezedObject; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Returns the first parameter if not undefined, - * otherwise the second parameter. - * - * @class - * @returns {vgl.defaultValue} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.defaultValue = function (a, b) { - 'use strict'; - - if (typeof a !== 'undefined') { - return a; - } - return b; -}; - -vgl.defaultValue.EMPTY_OBJECT = vgl.freezeObject({}); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class graphicsObject - * - * @class - * @param type - * @returns {vgl.graphicsObject} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.graphicsObject = function (type) { - 'use strict'; - - type = type; /* unused parameter */ - if (!(this instanceof vgl.graphicsObject)) { - return new vgl.graphicsObject(); - } - vgl.object.call(this); - - var m_this = this; - - //////////////////////////////////////////////////////////////////////////// - /** - * Setup (initialize) the object - * - * @param renderState - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this._setup = function (renderState) { - renderState = renderState; /* unused parameter */ - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove any resources acquired before deletion - * - * @param renderState - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this._cleanup = function (renderState) { - renderState = renderState; /* unused parameter */ - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bind and activate - * - * @param renderState - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.bind = function (renderState) { - renderState = renderState; /* unused parameter */ - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Undo bind and deactivate - * - * @param renderState - * @returns {boolean} - * - * TODO: Change it to unbind (simple) - */ - //////////////////////////////////////////////////////////////////////////// - this.undoBind = function (renderState) { - renderState = renderState; /* unused parameter */ - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render the object - */ - //////////////////////////////////////////////////////////////////////////// - this.render = function (renderState) { - renderState = renderState; /* unused parameter */ - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove the object and release its graphics resources - */ - //////////////////////////////////////////////////////////////////////////// - this.remove = function (renderState) { - m_this._cleanup(renderState); - }; - - return m_this; -}; - -inherit(vgl.graphicsObject, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, Uint16Array*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of geojson reader - * - * This contains code that reads a geoJSON file and produces rendering - * primitives from it. - * - * @class - * @returns {vgl.geojsonReader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.geojsonReader = function () { - 'use strict'; - - if (!(this instanceof vgl.geojsonReader)) { - return new vgl.geojsonReader(); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Read scalars - * - * @param coordinates - * @param geom - * @param size_estimate - * @param idx - */ - //////////////////////////////////////////////////////////////////////////// - this.readScalars = function (coordinates, geom, size_estimate, idx) { - var array = null, - s = null, - r = null, - g = null, - b = null; - - if (this.m_scalarFormat === 'values' && coordinates.length === 4) { - s = coordinates[3]; - array = geom.sourceData(vgl.vertexAttributeKeys.Scalar); - - if (!array) { - array = new vgl.sourceDataSf(); - if (this.m_scalarRange) { - array.setScalarRange(this.m_scalarRange[0], this.m_scalarRange[1]); - } - if (size_estimate !== undefined) { - //array.length = size_estimate; //no, slow on Safari - array.data().length = size_estimate; - } - geom.addSource(array); - } - if (size_estimate === undefined) { - array.pushBack(s); - } else { - array.insertAt(idx, s); - } - } else if (this.m_scalarFormat === 'rgb' && coordinates.length === 6) { - array = geom.sourceData(vgl.vertexAttributeKeys.Color); - if (!array) { - array = new vgl.sourceDataC3fv(); - if (size_estimate !== undefined) { - array.length = size_estimate * 3; - } - geom.addSource(array); - } - r = coordinates[3]; - g = coordinates[4]; - b = coordinates[5]; - if (size_estimate === undefined) { - array.pushBack([r, g, b]); - } else { - array.insertAt(idx, [r, g, b]); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Read point data - * - * @param coordinates - * @returns {vgl.geometryData} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - - geom.addSource(vglcoords); - for (i = 0; i < 1; i += 1) { - indices[i] = i; - - x = coordinates[0]; - y = coordinates[1]; - z = 0.0; - if (coordinates.length > 2) { - z = coordinates[2]; - } - - //console.log('read ' + x + ',' + y + ',' + z); - vglcoords.pushBack([x, y, z]); - - //attributes - this.readScalars(coordinates, geom); - } - - vglpoints.setIndices(indices); - geom.addPrimitive(vglpoints); - geom.setName('aPoint'); - return geom; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Read multipoint data - * - * @param coordinates - * @returns {vgl.geometryData} - */ - //////////////////////////////////////////////////////////////////////////// - this.readMultiPoint = function (coordinates) { - var 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, - i; - - //preallocate with size estimate - vglcoords.data().length = estpntcnt * 3; //x,y,z - - for (i = 0; i < coordinates.length; i += 1) { - indices[i] = i; - x = coordinates[i][0]; - y = coordinates[i][1]; - z = 0.0; - if (coordinates[i].length > 2) { - z = coordinates[i][2]; - } - - //console.log('read ' + x + ',' + y + ',' + z); - vglcoords.insertAt(pntcnt, [x, y, z]); - - //attributes - this.readScalars(coordinates[i], geom, estpntcnt, pntcnt); - - pntcnt += 1; - } - - vglpoints.setIndices(indices); - geom.addPrimitive(vglpoints); - geom.addSource(vglcoords); - geom.setName('manyPoints'); - return geom; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Read line string data - * - * @param coordinates - * @returns {vgl.geometryData} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - - vglline.setIndicesPerPrimitive(coordinates.length); - - for (i = 0; i < coordinates.length; i += 1) { - indices.push(i); - x = coordinates[i][0]; - y = coordinates[i][1]; - z = 0.0; - if (coordinates[i].length > 2) { - z = coordinates[i][2]; - } - - //console.log('read ' + x + ',' + y + ',' + z); - vglcoords.pushBack([x, y, z]); - - //attributes - this.readScalars(coordinates[i], geom); - } - - vglline.setIndices(indices); - geom.addPrimitive(vglline); - geom.addSource(vglcoords); - geom.setName('aLineString'); - return geom; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Read multi line string - * - * @param coordinates - * @returns {vgl.geometryData} - */ - //////////////////////////////////////////////////////////////////////////// - this.readMultiLineString = function (coordinates) { - var geom = new vgl.geometryData(), - vglcoords = new vgl.sourceDataP3fv(), - pntcnt = 0, - //lines should be at least 2 verts long, underest OK - estpntcnt = coordinates.length * 2, - i = null, - j = null, - x = null, - y = null, - z = null, - indices = null, - vglline = null, - thisLineLength = null; - - // Preallocate with size estimate - vglcoords.data().length = estpntcnt * 3; //x,y,z - - for (j = 0; j < coordinates.length; j += 1) { - indices = []; - //console.log('getting line ' + j); - vglline = new vgl.lineStrip(); - thisLineLength = coordinates[j].length; - vglline.setIndicesPerPrimitive(thisLineLength); - for (i = 0; i < thisLineLength; i += 1) { - indices.push(pntcnt); - x = coordinates[j][i][0]; - y = coordinates[j][i][1]; - z = 0.0; - if (coordinates[j][i].length > 2) { - z = coordinates[j][i][2]; - } - - //console.log('read ' + x + ',' + y + ',' + z); - vglcoords.insertAt(pntcnt, [x, y, z]); - - //attributes - this.readScalars(coordinates[j][i], geom, estpntcnt * 2, pntcnt); - - pntcnt += 1; - } - - vglline.setIndices(indices); - geom.addPrimitive(vglline); - } - - geom.setName('aMultiLineString'); - geom.addSource(vglcoords); - return geom; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Read polygon data - * - * @param coordinates - * @returns {vgl.geometryData} - */ - //////////////////////////////////////////////////////////////////////////// - this.readPolygon = function (coordinates) { - //TODO: ignoring holes given in coordinates[1...] - //TODO: ignoring convex - //TODO: implement ear clipping in VGL instead of this to handle both - 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; i < thisPolyLength; i += 1) { - x = coordinates[0][i][0]; - y = coordinates[0][i][1]; - z = 0.0; - if (coordinates[0][i].length > 2) { - z = coordinates[0][i][2]; - } - - //console.log('read ' + x + ',' + y + ',' + z); - vglcoords.pushBack([x, y, z]); - - //attributes - this.readScalars(coordinates[0][i], geom); - - if (i > 1) { - //console.log('Cutting new triangle 0,'+ vl+ ','+ i); - indices = new Uint16Array([0, vl, i]); - vgltriangle = new vgl.triangles(); - vgltriangle.setIndices(indices); - geom.addPrimitive(vgltriangle); - vl = i; - } - } - - geom.setName('POLY'); - geom.addSource(vglcoords); - return geom; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Read multi polygon data - * - * @param coordinates - * @returns {vgl.geometryData} - */ - //////////////////////////////////////////////////////////////////////////// - this.readMultiPolygon = function (coordinates) { - var geom = new vgl.geometryData(), - vglcoords = new vgl.sourceDataP3fv(), - ccount = 0, - numPolys = coordinates.length, - pntcnt = 0, - estpntcnt = numPolys * 3, // assume triangles, underest is fine - vgltriangle = new vgl.triangles(), - indexes = [], - i = null, - j = null, - x = null, - y = null, - z = null, - thisPolyLength = null, - vf = null, - vl = null, - flip = null, - flipped = false, - tcount = 0; - - - //var time1 = new Date().getTime() - //var a = 0; - //var b = 0; - //var c = 0; - //var d = 0; - - //preallocate with size estimate - vglcoords.data().length = numPolys * 3; //x,y,z - for (j = 0; j < numPolys; j += 1) { - //console.log('getting poly ' + j); - - thisPolyLength = coordinates[j][0].length; - vf = ccount; - vl = ccount + 1; - flip = [false, false, false]; - for (i = 0; i < thisPolyLength; i += 1) { - //var timea = new Date().getTime() - - x = coordinates[j][0][i][0]; - y = coordinates[j][0][i][1]; - z = 0.0; - if (coordinates[j][0][i].length > 2) { - z = coordinates[j][0][i][2]; - } - flipped = false; - if (x > 180) { - flipped = true; - x = x - 360; - } - if (i === 0) { - flip[0] = flipped; - } else { - flip[1 + (i - 1) % 2] = flipped; - } - //var timeb = new Date().getTime(); - //console.log('read ' + x + ',' + y + ',' + z); - - vglcoords.insertAt(pntcnt, [x, y, z]); - //var timec = new Date().getTime(); - - //attributes - this.readScalars(coordinates[j][0][i], geom, estpntcnt, pntcnt); - pntcnt += 1; - //var timed = new Date().getTime() - - if (i > 1) { - //if (vl < 50) { - //console.log('Cutting new triangle ' + tcount + ':' + vf + ',' + - // vl + ',' + ccount); - //console.log(indexes); - //} - if (flip[0] === flip[1] && flip[1] === flip[2]) { - //indexes = indexes.concat([vf,vl,ccount]); //no, very slow in Safari - indexes[tcount * 3 + 0] = vf; - indexes[tcount * 3 + 1] = vl; - indexes[tcount * 3 + 2] = ccount; - tcount += 1; - } - //else { - // //TODO: duplicate triangles that straddle boundary on either side - //} - - vl = ccount; - } - ccount += 1; - //var timee = new Date().getTime() - //a = a + (timeb-timea) - //b = b + (timec-timeb) - //c = c + (timed-timec) - //d = d + (timee-timed) - } - } - vgltriangle.setIndices(indexes); - geom.addPrimitive(vgltriangle); - - //console.log('NUMPOLYS ' + pntcnt); - //console.log('RMP: ', a, ',', b, ',', c, ',', d) - //var time2 = new Date().getTime() - - geom.setName('aMultiPoly'); - geom.addSource(vglcoords); - //var time3 = new Date().getTime() - //console.log('RMP: ', time2-time1, ',', time3-time2) - - return geom; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * @param object - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.readGJObjectInt = function (object) { - if (!object.hasOwnProperty('type')) { - //console.log('uh oh, not a geojson object'); - return null; - } - - //look for properties type annotation - if (object.properties && - object.properties.ScalarFormat && - object.properties.ScalarFormat === 'values') { - this.m_scalarFormat = 'values'; - if (object.properties.ScalarRange) { - this.m_scalarRange = object.properties.ScalarRange; - } - } - if (object.properties && - object.properties.ScalarFormat && - object.properties.ScalarFormat === 'rgb') { - this.m_scalarFormat = 'rgb'; - } - - //TODO: ignoring 'crs' and 'bbox' and misc meta data on all of these, - //best to handle as references into original probably - var ret, - type = object.type, - next = null, - nextset = null, - i = null; - - switch (type) { - case 'Point': - //console.log('parsed Point'); - ret = this.readPoint(object.coordinates); - break; - case 'MultiPoint': - //console.log('parsed MultiPoint'); - ret = this.readMultiPoint(object.coordinates); - break; - case 'LineString': - //console.log('parsed LineString'); - ret = this.readLineString(object.coordinates); - break; - case 'MultiLineString': - //console.log('parsed MultiLineString'); - ret = this.readMultiLineString(object.coordinates); - break; - case 'Polygon': - //console.log('parsed Polygon'); - ret = this.readPolygon(object.coordinates); - break; - case 'MultiPolygon': - //console.log('parsed MultiPolygon'); - ret = this.readMultiPolygon(object.coordinates); - break; - case 'GeometryCollection': - //console.log('parsed GeometryCollection'); - nextset = []; - for (i = 0; i < object.geometries.length; i += 1) { - next = this.readGJObject(object.geometries[i]); - nextset.push(next); - } - ret = nextset; - break; - case 'Feature': - //console.log('parsed Feature'); - next = this.readGJObject(object.geometry); - ret = next; - break; - case 'FeatureCollection': - //console.log('parsed FeatureCollection'); - nextset = []; - for (i = 0; i < object.features.length; i += 1) { - next = this.readGJObject(object.features[i]); - nextset.push(next); - } - ret = nextset; - break; - default: - console.log('Don\'t understand type ' + type); - ret = null; - break; - } - return ret; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * @param object - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.readGJObject = function (object) { - //var time1, time2; - var ret; - //time1 = new Date().getTime() - ret = this.readGJObjectInt(object); - //time2 = new Date().getTime() - //console.log('ELAPSED: ', time2-time1) - return ret; - }; - - /** - * Linearize geometries - * - * @param geoms - * @param geom - */ - this.linearizeGeoms = function (geoms, geom) { - var i = null; - - if (Object.prototype.toString.call(geom) === '[object Array]') { - for (i = 0; i < geom.length; i += 1) { - this.linearizeGeoms(geoms, geom[i]); - } - } else { - geoms.push(geom); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Read geometries from geojson object - * - * @param object - * @returns {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.readGeomObject = function (object) { - var geom, - geoms = []; - - geom = this.readGJObject(object); - this.linearizeGeoms(geoms, geom); - return geoms; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Given a buffer get rendering primitives - * - * @param buffer - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.getPrimitives = function (buffer) { - //console.log('Parsing geoJSON'); - if (!buffer) { - return []; - } - - var obj = JSON.parse(buffer), - geom = this.readGJObject(obj), - geoms = []; - - this.m_scalarFormat = 'none'; - this.m_scalarRange = null; - - this.linearizeGeoms(geoms, geom); - - return { 'geoms': geoms, - 'scalarFormat': this.m_scalarFormat, - 'scalarRange': this.m_scalarRange }; - }; - - return this; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl*/ -////////////////////////////////////////////////////////////////////////////// - -vgl.data = function () { - 'use strict'; - - if (!(this instanceof vgl.data)) { - return new vgl.data(); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Return data type. Should be implemented by the derived class - */ - //////////////////////////////////////////////////////////////////////////// - this.type = function () { - }; -}; - -vgl.data.raster = 0; -vgl.data.point = 1; -vgl.data.lineString = 2; -vgl.data.polygon = 3; -vgl.data.geometry = 10; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, Uint16Array, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class primitive - * - * @class - * @return {vgl.primitive} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.primitive = function () { - 'use strict'; - - if (!(this instanceof vgl.primitive)) { - return new vgl.primitive(); - } - - /** @private */ - var m_indicesPerPrimitive = 0, - m_primitiveType = 0, - m_indicesValueType = 0, - m_indices = null; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get indices of the primitive - * - * @returns {null} - */ - //////////////////////////////////////////////////////////////////////////// - this.indices = function () { - return m_indices; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create indices array for the primitive - * @param type - */ - //////////////////////////////////////////////////////////////////////////// - this.createIndices = function (type) { - type = type; /* unused parameters */ - // TODO Check for the type - m_indices = new Uint16Array(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the number of indices - */ - //////////////////////////////////////////////////////////////////////////// - this.numberOfIndices = function () { - return m_indices.length; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return size of indices in bytes - */ - //////////////////////////////////////////////////////////////////////////// - this.sizeInBytes = function () { - return m_indices.length * Uint16Array.BYTES_PER_ELEMENT; - }; - - //////////////////////////////////////////////////////////////////////////// - /* - * Return primitive type g - */ - //////////////////////////////////////////////////////////////////////////// - this.primitiveType = function () { - return m_primitiveType; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set primitive type - */ - //////////////////////////////////////////////////////////////////////////// - this.setPrimitiveType = function (type) { - m_primitiveType = type; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return count of indices that form a primitives - */ - //////////////////////////////////////////////////////////////////////////// - this.indicesPerPrimitive = function () { - return m_indicesPerPrimitive; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set count of indices that form a primitive - */ - //////////////////////////////////////////////////////////////////////////// - this.setIndicesPerPrimitive = function (count) { - m_indicesPerPrimitive = count; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return indices value type - */ - //////////////////////////////////////////////////////////////////////////// - this.indicesValueType = function () { - return m_indicesValueType; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set indices value type - */ - //////////////////////////////////////////////////////////////////////////// - this.setIndicesValueType = function (type) { - m_indicesValueType = type; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set indices from a array - */ - //////////////////////////////////////////////////////////////////////////// - this.setIndices = function (indicesArray) { - // TODO Check for the type - m_indices = new Uint16Array(indicesArray); - }; - - return this; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class triangleStrip - * - * @returns {vgl.triangleStrip} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.triangleStrip = function () { - 'use strict'; - - if (!(this instanceof vgl.triangleStrip)) { - return new vgl.triangleStrip(); - } - - vgl.primitive.call(this); - - this.setPrimitiveType(vgl.GL.TRIANGLE_STRIP); - this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); - this.setIndicesPerPrimitive(3); - - return this; -}; - -inherit(vgl.triangleStrip, vgl.primitive); - -//////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class triangles - * - * @returns {vgl.triangles} - */ -//////////////////////////////////////////////////////////////////////////// -vgl.triangles = function () { - 'use strict'; - - if (!(this instanceof vgl.triangles)) { - return new vgl.triangles(); - } - vgl.primitive.call(this); - - this.setPrimitiveType(vgl.GL.TRIANGLES); - this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); - this.setIndicesPerPrimitive(3); - - return this; -}; - -inherit(vgl.triangles, vgl.primitive); - -////////////////////////////////////////////////////////////////////////////// -/** - * create a instance of lines primitive type - * - * @returns {vgl.lines} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.lines = function () { - 'use strict'; - - if (!(this instanceof vgl.lines)) { - return new vgl.lines(); - } - vgl.primitive.call(this); - - this.setPrimitiveType(vgl.GL.LINES); - this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); - this.setIndicesPerPrimitive(2); - - return this; -}; -inherit(vgl.lines, vgl.primitive); - -////////////////////////////////////////////////////////////////////////////// -/** - * create a instance of line strip primitive type - * - * @returns {vgl.lineStrip} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.lineStrip = function () { - 'use strict'; - - if (!(this instanceof vgl.lineStrip)) { - return new vgl.lineStrip(); - } - vgl.primitive.call(this); - - this.setPrimitiveType(vgl.GL.LINE_STRIP); - this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); - this.setIndicesPerPrimitive(2); - - return this; -}; -inherit(vgl.lineStrip, vgl.primitive); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class points - * - * @returns {vgl.points} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.points = function () { - 'use strict'; - - if (!(this instanceof vgl.points)) { - return new vgl.points(); - } - vgl.primitive.call(this); - - this.setPrimitiveType(vgl.GL.POINTS); - this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); - this.setIndicesPerPrimitive(1); - - return this; -}; - -inherit(vgl.points, vgl.primitive); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class vertexDataP3f - * - * @returns {vgl.vertexDataP3f} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.vertexDataP3f = function () { - 'use strict'; - - if (!(this instanceof vgl.vertexDataP3f)) { - return new vgl.vertexDataP3f(); - } - - /** @private */ - this.m_position = []; - - return this; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class vertexDataP3N3f - * - * @class - * @returns {vgl.vertexDataP3N3f} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.vertexDataP3N3f = function () { - 'use strict'; - - if (!(this instanceof vgl.vertexDataP3N3f)) { - return new vgl.vertexDataP3N3f(); - } - - this.m_position = []; - this.m_normal = []; - - return this; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class vertexDataP3T3f - * - * @class - * @returns {vgl.vertexDataP3T3f} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.vertexDataP3T3f = function () { - 'use strict'; - - if (!(this instanceof vgl.vertexDataP3T3f)) { - return new vgl.vertexDataP3T3f(); - } - - this.m_position = []; - this.m_texCoordinate = []; - - return this; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceData - * @class - * @returns {vgl.sourceData} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceData = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceData)) { - return new vgl.sourceData(arg); - } - - arg = arg || {}; - var m_attributesMap = {}, - m_data = [], - m_name = arg.name || 'Source ' + new Date().toISOString(), - - //////////////////////////////////////////////////////////////////////////// - /** - * Attribute data for the source - */ - //////////////////////////////////////////////////////////////////////////// - vglAttributeData = function () { - // Number of components per group - // Type of data type (GL_FLOAT etc) - this.m_numberOfComponents = 0; - // Size of data type - this.m_dataType = 0; - this.m_dataTypeSize = 0; - // Specifies whether fixed-point data values should be normalized - // (true) or converted directly as fixed-point values (false) - // when they are accessed. - this.m_normalized = false; - // Strides for each attribute. - this.m_stride = 0; - // Offset - this.m_offset = 0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return raw data for this source - * - * @returns {Array or Float32Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.data = function () { - return m_data; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return raw data for this source - * - * @returns {Array or Float32Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.getData = function () { - return this.data(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * If the raw data is not a Float32Array, convert it to one. Then, return - * raw data for this source - * - * @returns {Float32Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.dataToFloat32Array = function () { - if (!(m_data instanceof Float32Array)) { - m_data = new Float32Array(m_data); - } - return m_data; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set data for this source - * - */ - //////////////////////////////////////////////////////////////////////////// - this.setData = function (data) { - if (!(data instanceof Array) && !(data instanceof Float32Array)) { - console.log('[error] Requires array'); - return; - } - if (data instanceof Float32Array) { - m_data = data; - } else { - m_data = data.slice(0); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add new attribute data to the source - */ - //////////////////////////////////////////////////////////////////////////// - this.addAttribute = function (key, dataType, sizeOfDataType, offset, stride, - noOfComponents, normalized) { - - if (!m_attributesMap.hasOwnProperty(key)) { - /* jshint newcap: false */ - //jscs:disable requireCapitalizedConstructors - var newAttr = new vglAttributeData(); - //jscs:enable requireCapitalizedConstructors - /* jshint newcap: true */ - newAttr.m_dataType = dataType; - newAttr.m_dataTypeSize = sizeOfDataType; - newAttr.m_offset = offset; - newAttr.m_stride = stride; - newAttr.m_numberOfComponents = noOfComponents; - newAttr.m_normalized = normalized; - m_attributesMap[key] = newAttr; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return size of the source data - */ - //////////////////////////////////////////////////////////////////////////// - this.sizeOfArray = function () { - return Object.size(m_data); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return length of array - */ - //////////////////////////////////////////////////////////////////////////// - this.lengthOfArray = function () { - return m_data.length; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return size of the source data in bytes - */ - //////////////////////////////////////////////////////////////////////////// - /* - * TODO: code below is probably wrong. - * Example: - * format P3N3f - * m_data = [ 1, 2, 3, 4, 5, 6 ]; // contains one vertex, - * // one normal, m_data.length == 6 - * - * The inner loop computes: - * sizeInBytes += 3 * 4; // for position - * sizeInBytes += 3 * 4; // for normal - * - * Then sizeInBytes *= 6; // m_data.length == 6 - * which gives sizeInBytes == 144 bytes when it should have been 4*6 = 24 - */ - this.sizeInBytes = function () { - var sizeInBytes = 0, - keys = this.keys(), i; - - for (i = 0; i < keys.length(); i += 1) { - sizeInBytes += this.numberOfComponents(keys[i]) * - this.sizeOfAttributeDataType(keys[i]); - } - - sizeInBytes *= this.sizeOfArray(); - - return sizeInBytes; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if there is attribute exists of a given key type - */ - //////////////////////////////////////////////////////////////////////////// - this.hasKey = function (key) { - return m_attributesMap.hasOwnProperty(key); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return keys of all attributes - */ - //////////////////////////////////////////////////////////////////////////// - this.keys = function () { - return Object.keys(m_attributesMap); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return number of attributes of source data - */ - //////////////////////////////////////////////////////////////////////////// - this.numberOfAttributes = function () { - return Object.size(m_attributesMap); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return number of components of the attribute data - */ - //////////////////////////////////////////////////////////////////////////// - this.attributeNumberOfComponents = function (key) { - if (m_attributesMap.hasOwnProperty(key)) { - return m_attributesMap[key].m_numberOfComponents; - } - - return 0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return if the attribute data is normalized - */ - //////////////////////////////////////////////////////////////////////////// - this.normalized = function (key) { - if (m_attributesMap.hasOwnProperty(key)) { - return m_attributesMap[key].m_normalized; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return size of the attribute data type - */ - //////////////////////////////////////////////////////////////////////////// - this.sizeOfAttributeDataType = function (key) { - if (m_attributesMap.hasOwnProperty(key)) { - return m_attributesMap[key].m_dataTypeSize; - } - - return 0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return attribute data type - */ - //////////////////////////////////////////////////////////////////////////// - this.attributeDataType = function (key) { - if (m_attributesMap.hasOwnProperty(key)) { - return m_attributesMap[key].m_dataType; - } - - return undefined; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return attribute offset - */ - //////////////////////////////////////////////////////////////////////////// - this.attributeOffset = function (key) { - if (m_attributesMap.hasOwnProperty(key)) { - return m_attributesMap[key].m_offset; - } - - return 0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return attribute stride - */ - //////////////////////////////////////////////////////////////////////////// - this.attributeStride = function (key) { - if (m_attributesMap.hasOwnProperty(key)) { - return m_attributesMap[key].m_stride; - } - - return 0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Virtual function to insert new vertex data at the end - */ - //////////////////////////////////////////////////////////////////////////// - this.pushBack = function (vertexData) { - vertexData = vertexData; /* unused parameter */ - // Should be implemented by the base class - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Insert new data block to the raw data - */ - //////////////////////////////////////////////////////////////////////////// - this.insert = function (data) { - var i; - - //m_data = m_data.concat(data); //no, slow on Safari - /* If we will are given a Float32Array and don't have any other data, use - * it directly. */ - if (!m_data.length && data.length && data instanceof Float32Array) { - m_data = data; - return; - } - /* If our internal array is immutable and we will need to change it, create - * a regular mutable array from it. */ - if (!m_data.slice && (m_data.length || !data.slice)) { - m_data = Array.prototype.slice.call(m_data); - } - if (!data.length) { - /* data is a singular value, so append it to our array */ - m_data[m_data.length] = data; - } else { - /* We don't have any data currently, so it is faster to copy the data - * using slice. */ - if (!m_data.length && data.slice) { - m_data = data.slice(0); - } else { - for (i = 0; i < data.length; i += 1) { - m_data[m_data.length] = data[i]; - } - } - } - }; - - this.insertAt = function (index, data) { - var i; - - if (!data.length) { - m_data[index] = data; - } else { - for (i = 0; i < data.length; i += 1) { - m_data[index * data.length + i] = data[i]; - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return name of the source data - */ - //////////////////////////////////////////////////////////////////////////// - this.name = function () { - return m_name; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set name of the source data - */ - //////////////////////////////////////////////////////////////////////////// - this.setName = function (name) { - m_name = name; - }; - - - return this; -}; - - -vgl.sourceDataAnyfv = function (size, key, arg) { - 'use strict'; - if (!(this instanceof vgl.sourceDataAnyfv)) { - return new vgl.sourceDataAnyfv(size, key, arg); - } - - vgl.sourceData.call(this, arg); - this.addAttribute(key, vgl.GL.FLOAT, - 4, 0, size * 4, size, false); - - this.pushBack = function (value) { - this.insert(value); - }; - - return this; -}; -inherit(vgl.sourceDataAnyfv, vgl.sourceData); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceDataP3T3f - * - * @returns {vgl.sourceDataP3T3f} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceDataP3T3f = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceDataP3T3f)) { - return new vgl.sourceDataP3T3f(arg); - } - vgl.sourceData.call(this, arg); - - this.addAttribute(vgl.vertexAttributeKeys.Position, vgl.GL.FLOAT, 4, 0, 6 * 4, 3, - false); - this.addAttribute(vgl.vertexAttributeKeys.TextureCoordinate, vgl.GL.FLOAT, 4, 12, - 6 * 4, 3, false); - - this.pushBack = function (value) { - this.insert(value.m_position); - this.insert(value.m_texCoordinate); - }; - - return this; -}; - -inherit(vgl.sourceDataP3T3f, vgl.sourceData); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceDataP3N3f - * - * @returns {vgl.sourceDataP3N3f} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceDataP3N3f = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceDataP3N3f)) { - return new vgl.sourceDataP3N3f(arg); - } - - vgl.sourceData.call(this, arg); - - this.addAttribute(vgl.vertexAttributeKeys.Position, vgl.GL.FLOAT, 4, 0, 6 * 4, 3, - false); - this.addAttribute(vgl.vertexAttributeKeys.Normal, vgl.GL.FLOAT, 4, 12, 6 * 4, 3, - false); - - this.pushBack = function (value) { - this.insert(value.m_position); - this.insert(value.m_normal); - }; - - return this; -}; - -inherit(vgl.sourceDataP3N3f, vgl.sourceData); - -///////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceDataP3fv - * - * @returns {vgl.sourceDataP3fv} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceDataP3fv = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceDataP3fv)) { - return new vgl.sourceDataP3fv(arg); - } - - vgl.sourceData.call(this, arg); - - this.addAttribute(vgl.vertexAttributeKeys.Position, vgl.GL.FLOAT, 4, 0, 3 * 4, 3, - false); - - this.pushBack = function (value) { - this.insert(value); - }; - - return this; -}; - -inherit(vgl.sourceDataP3fv, vgl.sourceData); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceDataT2fv - * - * @returns {vgl.sourceDataT2fv} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceDataT2fv = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceDataT2fv)) { - return new vgl.sourceDataT2fv(arg); - } - - vgl.sourceData.call(this, arg); - - this.addAttribute(vgl.vertexAttributeKeys.TextureCoordinate, vgl.GL.FLOAT, 4, 0, - 2 * 4, 2, false); - - this.pushBack = function (value) { - this.insert(value); - }; - - return this; -}; - -inherit(vgl.sourceDataT2fv, vgl.sourceData); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceDataC3fv - * - * @returns {vgl.sourceDataC3fv} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceDataC3fv = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceDataC3fv)) { - return new vgl.sourceDataC3fv(arg); - } - - vgl.sourceData.call(this, arg); - - this.addAttribute(vgl.vertexAttributeKeys.Color, vgl.GL.FLOAT, 4, 0, 3 * 4, 3, false); - - this.pushBack = function (value) { - this.insert(value); - }; - - return this; -}; - -inherit(vgl.sourceDataC3fv, vgl.sourceData); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceDataSf meant to hold scalar float values - * - * @class - * @returns {vgl.sourceDataSf} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceDataSf = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceDataSf)) { - return new vgl.sourceDataSf(arg); - } - - var m_min = null, - m_max = null, - m_fixedmin = null, - m_fixedmax = null; - - vgl.sourceData.call(this, arg); - - this.addAttribute(vgl.vertexAttributeKeys.Scalar, vgl.GL.FLOAT, 4, 0, 4, 1, false); - - this.pushBack = function (value) { - if (m_max === null || value > m_max) { - m_max = value; - } - if (m_min === null || value < m_min) { - m_min = value; - } - //this.insert(value); //no, slow on Safari - this.data()[this.data().length] = value; - }; - - this.insertAt = function (index, value) { - if (m_max === null || value > m_max) { - m_max = value; - } - if (m_min === null || value < m_min) { - m_min = value; - } - //call superclass ?? - //vgl.sourceData.insertAt.call(this, index, value); - this.data()[index] = value; - }; - - this.scalarRange = function () { - if (m_fixedmin === null || m_fixedmax === null) { - return [m_min, m_max]; - } - - return [m_fixedmin, m_fixedmax]; - }; - - this.setScalarRange = function (min, max) { - m_fixedmin = min; - m_fixedmax = max; - }; - - return this; -}; - -inherit(vgl.sourceDataSf, vgl.sourceData); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sourceDataDf meant to hold data float values - * - * This source array is the best way to pass a array of floats to the shader - * that has one entry for each of the vertices. - * - * @class - * @returns {vgl.sourceDataDf} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.sourceDataDf = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.sourceDataDf)) { - return new vgl.sourceDataDf(arg); - } - - vgl.sourceData.call(this, arg); - - this.addAttribute(vgl.vertexAttributeKeys.Scalar, vgl.GL.FLOAT, - 4, 0, 4, 1, false); - - this.pushBack = function (value) { - this.data()[this.data().length] = value; - }; - - this.insertAt = function (index, value) { - this.data()[index] = value; - }; - - return this; -}; - -inherit(vgl.sourceDataDf, vgl.sourceData); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class geometryData - * - * @class - * @returns {vgl.geometryData} - */ -///////////////////////////////////////////////////////////////////////////// -vgl.geometryData = function () { - 'use strict'; - - if (!(this instanceof vgl.geometryData)) { - return new vgl.geometryData(); - } - vgl.data.call(this); - - /** @private */ - var m_name = '', - m_primitives = [], - m_sources = [], - m_bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - m_computeBoundsTimestamp = vgl.timestamp(), - m_boundsDirtyTimestamp = vgl.timestamp(); - - //////////////////////////////////////////////////////////////////////////// - /** - * Return type - */ - //////////////////////////////////////////////////////////////////////////// - this.type = function () { - return vgl.data.geometry; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return ID of the geometry data - */ - //////////////////////////////////////////////////////////////////////////// - this.name = function () { - return m_name; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set name of the geometry data - */ - //////////////////////////////////////////////////////////////////////////// - this.setName = function (name) { - m_name = name; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add new source - */ - //////////////////////////////////////////////////////////////////////////// - this.addSource = function (source, sourceName) { - // @todo Check if the incoming source has duplicate keys - - if (sourceName !== undefined) { - source.setName(sourceName); - } - // NOTE This might not work on IE8 or lower - if (m_sources.indexOf(source) === -1) { - m_sources.push(source); - - if (source.hasKey(vgl.vertexAttributeKeys.Position)) { - m_boundsDirtyTimestamp.modified(); - } - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return source for a given index. Returns 0 if not found. - */ - //////////////////////////////////////////////////////////////////////////// - this.source = function (index) { - if (index < m_sources.length) { - return m_sources[index]; - } - - return 0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return source with a specified name. Returns 0 if not found. - */ - //////////////////////////////////////////////////////////////////////////// - this.sourceByName = function (sourceName) { - for (var i = 0; i < m_sources.length; i += 1) { - if (m_sources[i].name() === sourceName) { - return m_sources[i]; - } - } - return 0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return number of sources - */ - //////////////////////////////////////////////////////////////////////////// - this.numberOfSources = function () { - return m_sources.length; - }; - - /** - * Return source data given a key - */ - this.sourceData = function (key) { - var i; - - for (i = 0; i < m_sources.length; i += 1) { - if (m_sources[i].hasKey(key)) { - return m_sources[i]; - } - } - - return null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add new primitive - */ - //////////////////////////////////////////////////////////////////////////// - this.addPrimitive = function (primitive) { - m_primitives.push(primitive); - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return primitive for a given index. Returns null if not found. - */ - //////////////////////////////////////////////////////////////////////////// - this.primitive = function (index) { - if (index < m_primitives.length) { - return m_primitives[index]; - } - - return null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return number of primitives - */ - //////////////////////////////////////////////////////////////////////////// - this.numberOfPrimitives = function () { - return m_primitives.length; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return bounds [minX, maxX, minY, maxY, minZ, maxZ] - */ - //////////////////////////////////////////////////////////////////////////// - this.bounds = function () { - if (m_boundsDirtyTimestamp.getMTime() > m_computeBoundsTimestamp.getMTime()) { - this.computeBounds(); - } - return m_bounds; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if bounds are dirty or mark them as such. - * - * @param dirty: true to set bounds as dirty. - * Return true if bounds are dirty. - */ - //////////////////////////////////////////////////////////////////////////// - this.boundsDirty = function (dirty) { - if (dirty) { - m_boundsDirtyTimestamp.modified(); - } - return m_boundsDirtyTimestamp.getMTime() > m_computeBoundsTimestamp.getMTime(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Reset bounds - */ - //////////////////////////////////////////////////////////////////////////// - this.resetBounds = function () { - m_bounds[0] = 0.0; - m_bounds[1] = 0.0; - m_bounds[2] = 0.0; - m_bounds[3] = 0.0; - m_bounds[4] = 0.0; - m_bounds[5] = 0.0; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set bounds - */ - //////////////////////////////////////////////////////////////////////////// - this.setBounds = function (minX, maxX, minY, maxY, minZ, maxZ) { - 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(); - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute bounds - */ - //////////////////////////////////////////////////////////////////////////// - this.computeBounds = function () { - if (m_boundsDirtyTimestamp.getMTime() > m_computeBoundsTimestamp.getMTime()) { - var 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, - j, ib, jb, maxv, minv, - value = null, - vertexIndex; - - // We advance by index, not by byte - stride /= sizeOfDataType; - offset /= sizeOfDataType; - - this.resetBounds(); - - for (j = 0; j < numberOfComponents; j += 1) { - ib = j * 2; - jb = j * 2 + 1; - if (count) { - maxv = minv = m_bounds[jb] = data[offset + j]; - } else { - maxv = minv = 0; - } - for (vertexIndex = offset + stride + j; vertexIndex < count; - vertexIndex += stride) { - value = data[vertexIndex]; - if (value > maxv) { - maxv = value; - } - if (value < minv) { - minv = value; - } - } - m_bounds[ib] = minv; m_bounds[jb] = maxv; - } - - m_computeBoundsTimestamp.modified(); - } - }; - - - //////////////////////////////////////////////////////////////////////////// - /** - * Returns the vertex closest to a given position - */ - //////////////////////////////////////////////////////////////////////////// - this.findClosestVertex = function (point) { - 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, - minDist = Number.MAX_VALUE, - minIndex = null, - vi, vPos, dx, dy, dz, dist, i; - - // assume positions are always triplets - if (numberOfComponents !== 3) { - console.log('[warning] Find closest vertex assumes three' + - 'component vertex '); - } - - if (!point.z) { - point = {x: point.x, y: point.y, z: 0}; - } - - for (vi = offset, i = 0; vi < data.length; vi += stride, i += 1) { - vPos = [data[vi], - data[vi + 1], - data[vi + 2]]; - - dx = vPos[0] - point.x; - dy = vPos[1] - point.y; - dz = vPos[2] - point.z; - dist = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (dist < minDist) { - minDist = dist; - minIndex = i; - } - } - return minIndex; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Returns the requested vertex position - */ - //////////////////////////////////////////////////////////////////////////// - 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; - - // assume positions are always triplets - if (numberOfComponents !== 3) { - console.log('[warning] getPosition assumes three component data'); - } - - return [data[offset + index * stride], - data[offset + index * stride + 1], - data[offset + index * stride + 2]]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Returns the scalar corresponding to a given vertex index - */ - //////////////////////////////////////////////////////////////////////////// - this.getScalar = function (index) { - var attr = vgl.vertexAttributeKeys.Scalar, - sourceData = this.sourceData(attr), - numberOfComponents, sizeOfDataType, data, stride, offset; - - if (!sourceData) { - return null; - } - - numberOfComponents = sourceData.attributeNumberOfComponents(attr); - sizeOfDataType = sourceData.sizeOfAttributeDataType(attr); - data = sourceData.data(); - stride = sourceData.attributeStride(attr) / sizeOfDataType; - offset = sourceData.attributeOffset(attr) / sizeOfDataType; - - //console.log('index for scalar is ' + index); - //console.log('offset for scalar is ' + offset); - //console.log('stride for scalar is ' + stride); - - //console.log('have ' + data.length + ' scalars'); - - if (index * stride + offset >= data.length) { - console.log('access out of bounds in getScalar'); - } - - return data[index * stride + offset]; - }; - - return this; -}; - -inherit(vgl.geometryData, vgl.data); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, Float32Array, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class mapper - * - * @class - * @returns {vgl.mapper} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.mapper = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.mapper)) { - return new vgl.mapper(arg); - } - vgl.boundingObject.call(this); - - /** @private */ - arg = arg || {}; - - var m_dirty = true, - m_color = [0.0, 1.0, 1.0], - m_geomData = null, - m_buffers = [], - m_bufferVertexAttributeMap = {}, - m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, - m_glCompileTimestamp = vgl.timestamp(), - m_context = null, - m_this = this; - - //////////////////////////////////////////////////////////////////////////// - /** - * Delete cached VBO if any - */ - //////////////////////////////////////////////////////////////////////////// - this.deleteVertexBufferObjects = function (renderState) { - var i; - var context = m_context; - if (renderState) { - context = renderState.m_context; - } - if (context) { - for (i = 0; i < m_buffers.length; i += 1) { - context.deleteBuffer(m_buffers[i]); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create new VBO for all its geometryData sources and primitives - * - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function createVertexBufferObjects(renderState) { - if (m_geomData) { - if (renderState) { - m_context = renderState.m_context; - } - var numberOfSources = m_geomData.numberOfSources(), - i, j, k, bufferId = null, keys, ks, numberOfPrimitives, data; - - for (i = 0; i < numberOfSources; i += 1) { - bufferId = m_context.createBuffer(); - m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, bufferId); - data = m_geomData.source(i).data(); - if (!(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 = []; - - for (j = 0; j < keys.length; j += 1) { - ks.push(keys[j]); - } - - m_bufferVertexAttributeMap[i] = ks; - m_buffers[i] = bufferId; - } - - numberOfPrimitives = m_geomData.numberOfPrimitives(); - for (k = 0; k < numberOfPrimitives; 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(); - } - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Clear cache related to buffers - * - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function cleanUpDrawObjects(renderState) { - renderState = renderState; /* avoid unused warning */ - m_bufferVertexAttributeMap = {}; - m_buffers = []; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Setup draw objects; Delete old ones and create new ones - * - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function setupDrawObjects(renderState) { - // Delete buffer objects from past if any. - m_this.deleteVertexBufferObjects(renderState); - - // Clear any cache related to buffers - cleanUpDrawObjects(renderState); - - // Now construct the new ones. - createVertexBufferObjects(renderState); - - m_dirty = false; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute bounds of the data - */ - //////////////////////////////////////////////////////////////////////////// - this.computeBounds = function () { - if (m_geomData === null || typeof m_geomData === 'undefined') { - this.resetBounds(); - return; - } - - var computeBoundsTimestamp = this.computeBoundsTimestamp(), - boundsDirtyTimestamp = this.boundsDirtyTimestamp(), - geomBounds = null; - - if (boundsDirtyTimestamp.getMTime() > computeBoundsTimestamp.getMTime()) { - geomBounds = m_geomData.bounds(); - - this.setBounds(geomBounds[0], geomBounds[1], geomBounds[2], - geomBounds[3], geomBounds[4], geomBounds[5]) ; - - computeBoundsTimestamp.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get solid color of the geometry - */ - //////////////////////////////////////////////////////////////////////////// - this.color = function () { - return m_color; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set solid color of the geometry. Default is teal [1.0, 1.0, 1.0] - * - * @param r Red component of the color [0.0 - 1.0] - * @param g Green component of the color [0.0 - 1.0] - * @param b Blue component of the color [0.0 - 1.0] - */ - //////////////////////////////////////////////////////////////////////////// - this.setColor = function (r, g, b) { - m_color[0] = r; - m_color[1] = g; - m_color[2] = b; - - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return stored geometry data if any - */ - //////////////////////////////////////////////////////////////////////////// - this.geometryData = function () { - return m_geomData; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Connect mapper to its geometry data - */ - //////////////////////////////////////////////////////////////////////////// - this.setGeometryData = function (geom) { - if (m_geomData !== geom) { - m_geomData = geom; - - this.modified(); - this.boundsDirtyTimestamp().modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update the buffer used for a named source. - * - * @param {String} sourceName The name of the source to update. - * @param {Object[] or Float32Array} values The values to use for the source. - * If not specified, use the source's own buffer. - */ - //////////////////////////////////////////////////////////////////////////// - this.updateSourceBuffer = function (sourceName, values, renderState) { - if (renderState) { - m_context = renderState.m_context; - } - if (!m_context) { - return false; - } - var bufferIndex = -1; - for (var i = 0; i < m_geomData.numberOfSources(); i += 1) { - if (m_geomData.source(i).name() === sourceName) { - bufferIndex = i; - break; - } - } - if (bufferIndex < 0 || bufferIndex >= m_buffers.length) { - return false; - } - if (!values) { - values = m_geomData.source(i).dataToFloat32Array(); - } - m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_buffers[bufferIndex]); - if (values instanceof Float32Array) { - m_context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, values); - } else { - m_context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, - new Float32Array(values)); - } - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the buffer used for a named source. If the current buffer isn't a - * Float32Array, it is converted to one. This array can then be modified - * directly, after which updateSourceBuffer can be called to update the - * GL array. - * - * @param {String} sourceName The name of the source to update. - * @returns {Float32Array} An array used for this source. - */ - //////////////////////////////////////////////////////////////////////////// - this.getSourceBuffer = function (sourceName) { - var source = m_geomData.sourceByName(sourceName); - if (!source) { - return new Float32Array(); - } - return source.dataToFloat32Array(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render the mapper - */ - //////////////////////////////////////////////////////////////////////////// - this.render = function (renderState) { - if (this.getMTime() > m_glCompileTimestamp.getMTime() || - renderState.m_contextChanged) { - setupDrawObjects(renderState); - } - m_context = renderState.m_context; - - // Fixed vertex color - m_context.vertexAttrib3fv(vgl.vertexAttributeKeys.Color, this.color()); - - // TODO Use renderState - var bufferIndex = 0, - j = 0, i, noOfPrimitives = null, primitive = null; - - for (i in m_bufferVertexAttributeMap) { - if (m_bufferVertexAttributeMap.hasOwnProperty(i)) { - m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, - m_buffers[bufferIndex]); - for (j = 0; j < m_bufferVertexAttributeMap[i].length; j += 1) { - renderState.m_material - .bindVertexData(renderState, m_bufferVertexAttributeMap[i][j]); - } - bufferIndex += 1; - } - } - - noOfPrimitives = m_geomData.numberOfPrimitives(); - for (j = 0; j < noOfPrimitives; j += 1) { - m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_buffers[bufferIndex]); - bufferIndex += 1; - primitive = m_geomData.primitive(j); - switch (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()); - break; - } - m_context.bindBuffer (vgl.GL.ARRAY_BUFFER, null); - } - }; - - return this; -}; - -inherit(vgl.mapper, vgl.boundingObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -vgl.groupMapper = function () { - 'use strict'; - - if (!(this instanceof vgl.groupMapper)) { - return new vgl.groupMapper(); - } - vgl.mapper.call(this); - - /** @private */ - var m_createMappersTimestamp = vgl.timestamp(), - m_mappers = [], - m_geomDataArray = []; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return stored geometry data if any - * - * @param index optional - */ - //////////////////////////////////////////////////////////////////////////// - this.geometryData = function (index) { - if (index !== undefined && index < m_geomDataArray.length) { - return m_geomDataArray[index]; - } - - if (m_geomDataArray.length > 0) { - return m_geomDataArray[0]; - } - - return null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Connect mapper to its geometry data - * - * @param geom {vgl.geomData} - */ - //////////////////////////////////////////////////////////////////////////// - this.setGeometryData = function (geom) { - if (m_geomDataArray.length === 1) { - if (m_geomDataArray[0] === geom) { - return; - } - } - m_geomDataArray = []; - m_geomDataArray.push(geom); - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return stored geometry data array if any - */ - //////////////////////////////////////////////////////////////////////////// - this.geometryDataArray = function () { - return m_geomDataArray; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Connect mapper to its geometry data - * - * @param geoms {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.setGeometryDataArray = function (geoms) { - if (geoms instanceof Array) { - if (m_geomDataArray !== geoms) { - m_geomDataArray = []; - m_geomDataArray = geoms; - this.modified(); - return true; - } - } else { - console.log('[error] Requies array of geometry data'); - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute bounds of the data - */ - //////////////////////////////////////////////////////////////////////////// - this.computeBounds = function () { - if (m_geomDataArray === null || - m_geomDataArray === undefined) { - this.resetBounds(); - return; - } - - var computeBoundsTimestamp = this.computeBoundsTimestamp(), - boundsDirtyTimestamp = this.boundsDirtyTimestamp(), - m_bounds = this.bounds(), - geomBounds = null, - i = null; - - if (boundsDirtyTimestamp.getMTime() > - computeBoundsTimestamp.getMTime()) { - - for (i = 0; i < m_geomDataArray.length; i += 1) { - geomBounds = m_geomDataArray[i].bounds(); - - if (m_bounds[0] > geomBounds[0]) { - m_bounds[0] = geomBounds[0]; - } - if (m_bounds[1] < geomBounds[1]) { - m_bounds[1] = geomBounds[1]; - } - if (m_bounds[2] > geomBounds[2]) { - m_bounds[2] = geomBounds[2]; - } - if (m_bounds[3] < geomBounds[3]) { - m_bounds[3] = geomBounds[3]; - } - if (m_bounds[4] > geomBounds[4]) { - m_bounds[4] = geomBounds[4]; - } - if (m_bounds[5] < geomBounds[5]) { - m_bounds[5] = geomBounds[5]; - } - } - - this.modified(); - computeBoundsTimestamp.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render the mapper - */ - //////////////////////////////////////////////////////////////////////////// - this.render = function (renderState) { - var i = null; - - if (this.getMTime() > m_createMappersTimestamp.getMTime()) { - // NOTE Hoping that it will release the graphics resources - for (i = 0; i < m_geomDataArray.length; i += 1) { - m_mappers.push(vgl.mapper()); - m_mappers[i].setGeometryData(m_geomDataArray[i]); - } - m_createMappersTimestamp.modified(); - } - - for (i = 0; i < m_mappers.length; i += 1) { - m_mappers[i].render(renderState); - } - }; - - return this; -}; - -inherit(vgl.groupMapper, vgl.mapper); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -vgl.materialAttributeType = { - 'Undefined' : 0x0, - 'ShaderProgram' : 0x1, - 'Texture' : 0x2, - 'Blend' : 0x3, - 'Depth' : 0x4 -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class materialAttribute - * - * @class - * @param type - * @returns {vgl.materialAttribute} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.materialAttribute = function (type) { - 'use strict'; - - if (!(this instanceof vgl.materialAttribute)) { - return new vgl.materialAttribute(); - } - vgl.graphicsObject.call(this); - - /** @private */ - var m_this = this, - m_type = type, - m_enabled = true; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return tyep of the material attribute - * - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.type = function () { - return m_type; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return if material attribute is enabled or not - * - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.enabled = function () { - return m_enabled; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bind and activate vertex specific data - * - * @param renderState - * @param key - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.bindVertexData = function (renderState, key) { - renderState = renderState; /* unused parameter */ - key = key /* unused parameter */; - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Undo bind and deactivate vertex specific data - * - * @param renderState - * @param key - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.undoBindVertexData = function (renderState, key) { - renderState = renderState; /* unused parameter */ - key = key /* unused parameter */; - return false; - }; - - return m_this; -}; - -inherit(vgl.materialAttribute, vgl.graphicsObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of clas blendFunction - * - * @class - * @param source - * @param destination - * @returns {vgl.blendFunction} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.blendFunction = function (source, destination) { - 'use strict'; - - if (!(this instanceof vgl.blendFunction)) { - return new vgl.blendFunction(source, destination); - } - - /** @private */ - var m_source = source, - m_destination = destination; - - //////////////////////////////////////////////////////////////////////////// - /** - * Apply blend function to the current state - * - * @param {vgl.renderState} - */ - //////////////////////////////////////////////////////////////////////////// - this.apply = function (renderState) { - renderState.m_context.blendFuncSeparate(m_source, m_destination, - vgl.GL.ONE, vgl.GL.ONE_MINUS_SRC_ALPHA); - }; - - return this; -}; - -//////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class blend - * - * @returns {vgl.blend} - */ -//////////////////////////////////////////////////////////////////////////// -vgl.blend = function () { - 'use strict'; - - if (!(this instanceof vgl.blend)) { - return new vgl.blend(); - } - vgl.materialAttribute.call( - this, vgl.materialAttributeType.Blend); - - /** @private */ - var m_wasEnabled = false, - m_blendFunction = vgl.blendFunction(vgl.GL.SRC_ALPHA, - vgl.GL.ONE_MINUS_SRC_ALPHA); - - //////////////////////////////////////////////////////////////////////////// - /** - * Bind blend attribute - * - * @param {vgl.renderState} - */ - //////////////////////////////////////////////////////////////////////////// - this.bind = function (renderState) { - m_wasEnabled = renderState.m_context.isEnabled(vgl.GL.BLEND); - - if (this.enabled()) { - renderState.m_context.enable(vgl.GL.BLEND); - m_blendFunction.apply(renderState); - } else { - renderState.m_context.disable(vgl.GL.BLEND); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Undo bind blend attribute - * - * @param {vgl.renderState} - */ - //////////////////////////////////////////////////////////////////////////// - this.undoBind = function (renderState) { - if (m_wasEnabled) { - renderState.m_context.enable(vgl.GL.BLEND); - } else { - renderState.m_context.disable(vgl.GL.BLEND); - } - - return true; - }; - - return this; -}; - -inherit(vgl.blend, vgl.materialAttribute); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class material - * - * @class - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.material = function () { - 'use strict'; - - if (!(this instanceof vgl.material)) { - return new vgl.material(); - } - vgl.graphicsObject.call(this); - - // / Private member variables - var m_this = this, - m_shaderProgram = new vgl.shaderProgram(), - m_binNumber = 100, - m_textureAttributes = {}, - m_attributes = {}; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return bin number for the material - * - * @default 100 - * @returns {number} - */ - //////////////////////////////////////////////////////////////////////////// - this.binNumber = function () { - return m_binNumber; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set bin number for the material - * - * @param binNo - */ - //////////////////////////////////////////////////////////////////////////// - this.setBinNumber = function (binNo) { - m_binNumber = binNo; - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if incoming attribute already exists in the material - * - * @param attr - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.exists = function (attr) { - if (attr.type() === vgl.materialAttribute.Texture) { - return m_textureAttributes.hasOwnProperty(attr); - } - - return m_attributes.hasOwnProperty(attr); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get uniform given a name - - * @param name Uniform name - * @returns {vgl.uniform} - */ - //////////////////////////////////////////////////////////////////////////// - this.uniform = function (name) { - if (m_shaderProgram) { - return m_shaderProgram.uniform(name); - } - - return null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get material attribute - - * @param attr Attribute name - * @returns {vgl.materialAttribute} - */ - //////////////////////////////////////////////////////////////////////////// - this.attribute = function (name) { - if (m_attributes.hasOwnProperty(name)) { - return m_attributes[name]; - } - - if (m_textureAttributes.hasOwnProperty(name)) { - return m_textureAttributes[name]; - } - - return null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set a new attribute for the material - * - * This method replace any existing attribute except for textures as - * materials can have multiple textures. - * - * @param attr - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setAttribute = function (attr) { - if (attr.type() === vgl.materialAttributeType.Texture && - m_textureAttributes[attr.textureUnit()] !== attr) { - m_textureAttributes[attr.textureUnit()] = attr; - m_this.modified(); - return true; - } - - if (m_attributes[attr.type()] === attr) { - return false; - } - - // Shader is a very special attribute - if (attr.type() === vgl.materialAttributeType.ShaderProgram) { - m_shaderProgram = attr; - } - - m_attributes[attr.type()] = attr; - m_this.modified(); - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add a new attribute to the material. - * - * @param attr - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.addAttribute = function (attr) { - if (m_this.exists(attr)) { - return false; - } - - if (attr.type() === vgl.materialAttributeType.Texture) { - // TODO Currently we don't check if we are replacing or not. - // It would be nice to have a flag for it. - m_textureAttributes[attr.textureUnit()] = attr; - m_this.modified(); - return true; - } - - // Shader is a very special attribute - if (attr.type() === vgl.materialAttributeType.ShaderProgram) { - m_shaderProgram = attr; - } - - m_attributes[attr.type()] = attr; - m_this.modified(); - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return shader program used by the material - * - * @returns {vgl.shaderProgram} - */ - //////////////////////////////////////////////////////////////////////////// - this.shaderProgram = function () { - return m_shaderProgram; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Setup (initialize) the material attribute - * - * @param renderState - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this._setup = function (renderState) { - renderState = renderState; /* unused parameter */ - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove any resources acquired before deletion - * - * @param renderState - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this._cleanup = function (renderState) { - for (var key in m_attributes) { - if (m_attributes.hasOwnProperty(key)) { - m_attributes[key]._cleanup(renderState); - } - } - - for (key in m_textureAttributes) { - if (m_textureAttributes.hasOwnProperty(key)) { - m_textureAttributes[key]._cleanup(renderState); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bind and activate material states - * - * @param renderState - */ - //////////////////////////////////////////////////////////////////////////// - this.bind = function (renderState) { - var key = null; - - m_shaderProgram.bind(renderState); - - for (key in m_attributes) { - if (m_attributes.hasOwnProperty(key)) { - if (m_attributes[key] !== m_shaderProgram) { - m_attributes[key].bind(renderState); - } - } - } - - for (key in m_textureAttributes) { - if (m_textureAttributes.hasOwnProperty(key)) { - m_textureAttributes[key].bind(renderState); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Undo-bind and de-activate material states - * - * @param renderState - */ - //////////////////////////////////////////////////////////////////////////// - this.undoBind = function (renderState) { - var key = null; - for (key in m_attributes) { - if (m_attributes.hasOwnProperty(key)) { - m_attributes[key].undoBind(renderState); - } - } - - for (key in m_textureAttributes) { - if (m_textureAttributes.hasOwnProperty(key)) { - m_textureAttributes[key].undoBind(renderState); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bind vertex data - * - * @param renderState - * @param key - */ - //////////////////////////////////////////////////////////////////////////// - this.bindVertexData = function (renderState, key) { - var i = null; - - for (i in m_attributes) { - if (m_attributes.hasOwnProperty(i)) { - m_attributes[i].bindVertexData(renderState, key); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Undo bind vertex data - * - * @param renderState - * @param key - */ - //////////////////////////////////////////////////////////////////////////// - this.undoBindVertexData = function (renderState, key) { - var i = null; - - for (i in m_attributes) { - if (m_attributes.hasOwnProperty(i)) { - m_attributes.undoBindVertexData(renderState, key); - } - } - }; - - return m_this; -}; - -vgl.material.RenderBin = { - 'Base' : 0, - 'Default' : 100, - 'Opaque' : 100, - 'Transparent' : 1000, - 'Overlay' : 10000 -}; - -inherit(vgl.material, vgl.graphicsObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, vec2, vec3, vec4, mat4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class renderState - * - * @returns {vgl.renderState} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.renderState = function () { - 'use strict'; - - this.m_context = null; - this.m_modelViewMatrix = mat4.create(); - this.m_normalMatrix = mat4.create(); - this.m_projectionMatrix = null; - this.m_material = null; - this.m_mapper = null; -}; - -//////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class renderer * - * - * @returns {vgl.renderer} - */ -//////////////////////////////////////////////////////////////////////////// -vgl.renderer = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.renderer)) { - return new vgl.renderer(arg); - } - vgl.graphicsObject.call(this); - arg = arg || {}; - - /** @private */ - var m_this = this; - m_this.m_renderWindow = null; - m_this.m_contextChanged = false; - m_this.m_sceneRoot = new vgl.groupNode(); - m_this.m_camera = new vgl.camera(arg); - m_this.m_nearClippingPlaneTolerance = null; - m_this.m_x = 0; - m_this.m_y = 0; - m_this.m_width = 0; - m_this.m_height = 0; - m_this.m_resizable = true; - m_this.m_resetScene = true; - m_this.m_layer = 0; - m_this.m_renderPasses = null; - m_this.m_resetClippingRange = true; - m_this.m_depthBits = null; - - m_this.m_camera.addChild(m_this.m_sceneRoot); - - //////////////////////////////////////////////////////////////////////////// - /** - * Get width of the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.width = function () { - return m_this.m_width; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get height of the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.height = function () { - return m_this.m_height; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get layer this renderer is associated with - * - * @return {Number} - */ - //////////////////////////////////////////////////////////////////////////// - this.layer = function () { - return m_this.m_layer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set the layer this renderer is associated with. - * - * @param layerNo - */ - //////////////////////////////////////////////////////////////////////////// - this.setLayer = function (layerNo) { - m_this.m_layer = layerNo; - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * - */ - //////////////////////////////////////////////////////////////////////////// - this.isResizable = function () { - return m_this.m_resizable; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * - */ - //////////////////////////////////////////////////////////////////////////// - this.setResizable = function (r) { - m_this.m_resizable = r; - }; - - - //////////////////////////////////////////////////////////////////////////// - /** - * Return render window (owner) of the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.renderWindow = function () { - return m_this.m_renderWindow; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set render window for the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.setRenderWindow = function (renWin) { - if (m_this.m_renderWindow !== renWin) { - if (m_this.m_renderWindow) { - m_this.m_renderWindow.removeRenderer(this); - } - m_this.m_renderWindow = renWin; - m_this.m_contextChanged = true; - m_this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get background color - */ - //////////////////////////////////////////////////////////////////////////// - this.backgroundColor = function () { - return m_this.m_camera.clearColor(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set background color of the renderer - * - * @param r - * @param g - * @param b - * @param a - */ - //////////////////////////////////////////////////////////////////////////// - this.setBackgroundColor = function (r, g, b, a) { - m_this.m_camera.setClearColor(r, g, b, a); - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get scene root - * - * @returns {vgl.groupNode} - */ - //////////////////////////////////////////////////////////////////////////// - this.sceneRoot = function () { - return m_this.m_sceneRoot; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get main camera of the renderer - * - * @returns {vgl.camera} - */ - //////////////////////////////////////////////////////////////////////////// - this.camera = function () { - return m_this.m_camera; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render the scene - */ - //////////////////////////////////////////////////////////////////////////// - this.render = function () { - var i, renSt, children, actor = null, sortedActors = [], - mvMatrixInv = mat4.create(), clearColor = null; - - renSt = new vgl.renderState(); - renSt.m_renderer = m_this; - renSt.m_context = m_this.renderWindow().context(); - if (!m_this.m_depthBits || m_this.m_contextChanged) { - m_this.m_depthBits = renSt.m_context.getParameter(vgl.GL.DEPTH_BITS); - } - renSt.m_contextChanged = m_this.m_contextChanged; - - if (m_this.m_renderPasses) { - for (i = 0; i < m_this.m_renderPasses.length; i += 1) { - if (m_this.m_renderPasses[i].render(renSt)) { - // Stop the rendering if render pass returns false - console.log('returning'); - m_this.m_renderPasses[i].remove(renSt); - return; - } - m_this.m_renderPasses[i].remove(renSt); - } - } - - renSt.m_context.enable(vgl.GL.DEPTH_TEST); - renSt.m_context.depthFunc(vgl.GL.LEQUAL); - - /*jshint bitwise: false */ - if (m_this.m_camera.clearMask() & vgl.GL.COLOR_BUFFER_BIT) { - clearColor = m_this.m_camera.clearColor(); - renSt.m_context.clearColor(clearColor[0], clearColor[1], - clearColor[2], clearColor[3]); - } - - if (m_this.m_camera.clearMask() & vgl.GL.DEPTH_BUFFER_BIT) { - renSt.m_context.clearDepth(m_this.m_camera.clearDepth()); - } - /*jshint bitwise: true */ - - renSt.m_context.clear(m_this.m_camera.clearMask()); - - // Set the viewport for this renderer - renSt.m_context.viewport(m_this.m_x, m_this.m_y, - m_this.m_width, m_this.m_height); - - children = m_this.m_sceneRoot.children(); - - if (children.length > 0 && m_this.m_resetScene) { - m_this.resetCamera(); - m_this.m_resetScene = false; - } - - for (i = 0; i < children.length; i += 1) { - actor = children[i]; - - // Compute the bounds even if the actor is not visible - actor.computeBounds(); - - // If bin number is < 0, then don't even bother - // rendering the data - if (actor.visible() && actor.material().binNumber() >= 0) { - sortedActors.push([actor.material().binNumber(), actor]); - } - } - - // Now perform sorting - sortedActors.sort(function (a, b) {return a[0] - b[0];}); - - for (i = 0; i < sortedActors.length; i += 1) { - actor = sortedActors[i][1]; - if (actor.referenceFrame() === - vgl.boundingObject.ReferenceFrame.Relative) { - var view = m_this.m_camera.viewMatrix(); - /* If the view matrix is a plain array, keep it as such. This is - * intended to preserve precision, and will only be the case if the - * view matrix was created by delibrately setting it as an array. */ - if (view instanceof Array) { - renSt.m_modelViewMatrix = new Array(16); - } - mat4.multiply(renSt.m_modelViewMatrix, view, actor.matrix()); - renSt.m_projectionMatrix = m_this.m_camera.projectionMatrix(); - renSt.m_modelViewAlignment = m_this.m_camera.viewAlignment(); - } else { - renSt.m_modelViewMatrix = actor.matrix(); - renSt.m_modelViewAlignment = null; - renSt.m_projectionMatrix = mat4.create(); - mat4.ortho(renSt.m_projectionMatrix, - 0, m_this.m_width, 0, m_this.m_height, -1, 1); - } - - mat4.invert(mvMatrixInv, renSt.m_modelViewMatrix); - mat4.transpose(renSt.m_normalMatrix, mvMatrixInv); - renSt.m_material = actor.material(); - renSt.m_mapper = actor.mapper(); - - // TODO Fix this shortcut - renSt.m_material.bind(renSt); - renSt.m_mapper.render(renSt); - renSt.m_material.undoBind(renSt); - } - - renSt.m_context.finish(); - m_this.m_contextChanged = false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Automatically set up the camera based on visible actors - */ - //////////////////////////////////////////////////////////////////////////// - this.resetCamera = function () { - m_this.m_camera.computeBounds(); - - var vn = m_this.m_camera.directionOfProjection(), - visibleBounds = m_this.m_camera.bounds(), - center = [ - (visibleBounds[0] + visibleBounds[1]) / 2.0, - (visibleBounds[2] + visibleBounds[3]) / 2.0, - (visibleBounds[4] + visibleBounds[5]) / 2.0 - ], - diagonals = [ - visibleBounds[1] - visibleBounds[0], - visibleBounds[3] - visibleBounds[2], - visibleBounds[5] - visibleBounds[4] - ], - radius = 0.0, - aspect = m_this.m_camera.viewAspect(), - angle = m_this.m_camera.viewAngle(), - distance = null, - vup = null; - - if (diagonals[0] > diagonals[1]) { - if (diagonals[0] > diagonals[2]) { - radius = diagonals[0] / 2.0; - } else { - radius = diagonals[2] / 2.0; - } - } else { - if (diagonals[1] > diagonals[2]) { - radius = diagonals[1] / 2.0; - } else { - radius = diagonals[2] / 2.0; - } - } - - // @todo Need to figure out what's happening here - if (aspect >= 1.0) { - angle = 2.0 * Math.atan(Math.tan(angle * 0.5) / aspect); - } else { - angle = 2.0 * Math.atan(Math.tan(angle * 0.5) * aspect); - } - - distance = radius / Math.sin(angle * 0.5); - vup = m_this.m_camera.viewUpDirection(); - - if (Math.abs(vec3.dot(vup, vn)) > 0.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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check whether or not whether or not the bounds are valid - */ - //////////////////////////////////////////////////////////////////////////// - this.hasValidBounds = function (bounds) { - if (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) { - return false; - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Recalculate camera's clipping range - */ - //////////////////////////////////////////////////////////////////////////// - this.resetCameraClippingRange = function (bounds) { - if (typeof bounds === 'undefined') { - m_this.m_camera.computeBounds(); - bounds = m_this.m_camera.bounds(); - } - - if (!m_this.hasValidBounds(bounds)) { - return; - } - - 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) { - return; - } - - // Set the max near clipping plane and the min far clipping plane - range[0] = a * bounds[0] + b * bounds[2] + c * bounds[4] + d; - range[1] = 1e-18; - - // Find the closest / farthest bounding box vertex - for (k = 0; k < 2; k += 1) { - for (j = 0; j < 2; j += 1) { - for (i = 0; i < 2; i += 1) { - dist = a * bounds[i] + b * bounds[2 + j] + c * bounds[4 + k] + d; - range[0] = (dist < range[0]) ? (dist) : (range[0]); - range[1] = (dist > range[1]) ? (dist) : (range[1]); - } - } - } - - // Do not let the range behind the camera throw off the calculation. - if (range[0] < 0.0) { - range[0] = 0.0; - } - - // Give ourselves a little breathing room - range[0] = 0.99 * range[0] - (range[1] - range[0]) * 0.5; - range[1] = 1.01 * range[1] + (range[1] - range[0]) * 0.5; - - // Make sure near is not bigger than far - range[0] = (range[0] >= range[1]) ? (0.01 * range[1]) : (range[0]); - - // Make sure near is at least some fraction of far - this prevents near - // from being behind the camera or too close in front. How close is too - // close depends on the resolution of the depth buffer. - if (!m_this.m_nearClippingPlaneTolerance) { - m_this.m_nearClippingPlaneTolerance = 0.01; - - if (m_this.m_depthBits && m_this.m_depthBits > 16) { - m_this.m_nearClippingPlaneTolerance = 0.001; - } - } - - // make sure the front clipping range is not too far from the far clippnig - // range, this is to make sure that the zbuffer resolution is effectively - // used. - if (range[0] < m_this.m_nearClippingPlaneTolerance * range[1]) { - range[0] = m_this.m_nearClippingPlaneTolerance * range[1]; - } - - m_this.m_camera.setClippingRange(range[0], range[1]); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Resize viewport given a width and height - */ - //////////////////////////////////////////////////////////////////////////// - this.resize = function (width, height) { - if (!width || !height) { - return; - } - // @note: where do m_this.m_x and m_this.m_y come from? - m_this.positionAndResize(m_this.m_x, m_this.m_y, width, height); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Resize viewport given a position, width and height - */ - //////////////////////////////////////////////////////////////////////////// - this.positionAndResize = function (x, y, width, height) { - var i; - - // TODO move this code to camera - if (x < 0 || y < 0 || width <= 0 || height <= 0) { - console.log('[error] Invalid position and resize values', - x, y, width, height); - return; - } - - //If we're allowing this renderer to resize ... - 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(); - } - - if (m_this.m_renderPasses) { - for (i = 0; i < m_this.m_renderPasses.length; i += 1) { - m_this.m_renderPasses[i].resize(width, height); - m_this.m_renderPasses[i].renderer().positionAndResize(x, y, width, height); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add new actor to the collection - * - * @param actor - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.addActor = function (actor) { - if (actor instanceof vgl.actor) { - m_this.m_sceneRoot.addChild(actor); - m_this.modified(); - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return true if this renderer has this actor attached, false otherwise. - * - * @param actor - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.hasActor = function (actor) { - return m_this.m_sceneRoot.hasChild(actor); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add an array of actors to the collection - */ - //////////////////////////////////////////////////////////////////////////// - this.addActors = function (actors) { - var i = null; - if (actors instanceof Array) { - for (i = 0; i < actors.length; i += 1) { - m_this.m_sceneRoot.addChild(actors[i]); - } - m_this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove the actor from the collection - * - * @param actor - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.removeActor = function (actor) { - if (m_this.m_sceneRoot.children().indexOf(actor) !== -1) { - /* When we remove an actor, free the VBOs of the mapper and mark the - * mapper as modified; it will reallocate VBOs as necessary. */ - if (actor.mapper()) { - actor.mapper().deleteVertexBufferObjects(); - actor.mapper().modified(); - } - m_this.m_sceneRoot.removeChild(actor); - m_this.modified(); - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove actors from the collection - * - * @param actors - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.removeActors = function (actors) { - if (!(actors instanceof Array)) { - return false; - } - - var i; - for (i = 0; i < actors.length; i += 1) { - m_this.m_sceneRoot.removeChild(actors[i]); - } - m_this.modified(); - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove all actors for a renderer - * - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.removeAllActors = function () { - return m_this.m_sceneRoot.removeChildren(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Transform a point in the world space to display space - */ - //////////////////////////////////////////////////////////////////////////// - this.worldToDisplay = function (worldPt, viewMatrix, projectionMatrix, width, - height) { - var viewProjectionMatrix = mat4.create(), - winX = null, - winY = null, - winZ = null, - winW = null, - clipPt = null; - - - mat4.multiply(viewProjectionMatrix, projectionMatrix, viewMatrix); - - // Transform world to clipping coordinates - clipPt = vec4.create(); - vec4.transformMat4(clipPt, worldPt, viewProjectionMatrix); - - if (clipPt[3] !== 0.0) { - clipPt[0] = clipPt[0] / clipPt[3]; - clipPt[1] = clipPt[1] / clipPt[3]; - clipPt[2] = clipPt[2] / clipPt[3]; - clipPt[3] = 1.0; - } - - winX = (((clipPt[0]) + 1) / 2.0) * width; - // We calculate -point3D.getY() because the screen Y axis is - // oriented top->down - winY = ((1 - clipPt[1]) / 2.0) * height; - winZ = clipPt[2]; - winW = clipPt[3]; - - return vec4.fromValues(winX, winY, winZ, winW); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Transform a point in display space to world space - * @param displayPt - * @param viewMatrix - * @param projectionMatrix - * @param width - * @param height - * @returns {vec4} - */ - //////////////////////////////////////////////////////////////////////////// - this.displayToWorld = function (displayPt, viewMatrix, projectionMatrix, - width, height) { - var x = (2.0 * displayPt[0] / width) - 1, - y = -(2.0 * displayPt[1] / height) + 1, - z = displayPt[2], - viewProjectionInverse = mat4.create(), - worldPt = null; - - mat4.multiply(viewProjectionInverse, projectionMatrix, viewMatrix); - mat4.invert(viewProjectionInverse, viewProjectionInverse); - - worldPt = vec4.fromValues(x, y, z, 1); - vec4.transformMat4(worldPt, worldPt, viewProjectionInverse); - if (worldPt[3] !== 0.0) { - worldPt[0] = worldPt[0] / worldPt[3]; - worldPt[1] = worldPt[1] / worldPt[3]; - worldPt[2] = worldPt[2] / worldPt[3]; - worldPt[3] = 1.0; - } - - return worldPt; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the focusDisplayPoint - * @returns {vec4} - */ - //////////////////////////////////////////////////////////////////////////// - this.focusDisplayPoint = function () { - var focalPoint = m_this.m_camera.focalPoint(), - focusWorldPt = vec4.fromValues( - focalPoint[0], focalPoint[1], focalPoint[2], 1); - - return m_this.worldToDisplay( - focusWorldPt, m_this.m_camera.viewMatrix(), - m_this.m_camera.projectionMatrix(), m_this.m_width, m_this.m_height); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Will the scene be reset. - * @returns {bool} - */ - //////////////////////////////////////////////////////////////////////////// - this.resetScene = function () { - return m_this.m_resetScene; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * If true the scene will be reset, otherwise the scene will not be - * automatically reset. - * - * @param reset - */ - //////////////////////////////////////////////////////////////////////////// - this.setResetScene = function (reset) { - if (m_this.m_resetScene !== reset) { - m_this.m_resetScene = reset; - m_this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Will the clipping range be reset - * @returns {bool} - */ - //////////////////////////////////////////////////////////////////////////// - this.resetClippingRange = function () { - return m_this.m_resetClippingRange; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * If true the camera clipping range will be reset, otherwise the scene will - * not be automatically reset. - * - * @param reset - */ - //////////////////////////////////////////////////////////////////////////// - this.setResetClippingRange = function (reset) { - if (m_this.m_resetClippingRange !== reset) { - m_this.m_resetClippingRange = reset; - m_this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * - */ - //////////////////////////////////////////////////////////////////////////// - this.addRenderPass = function (renPass) { - var i; - - if (m_this.m_renderPasses) { - for (i = 0; i < m_this.m_renderPasses.length; i += 1) { - if (renPass === m_this.m_renderPasses[i]) { - return; - } - } - } - - m_this.m_renderPasses = []; - m_this.m_renderPasses.push(renPass); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * - */ - //////////////////////////////////////////////////////////////////////////// - this.removeRenderPass = function (renPass) { - renPass = renPass; // TODO Implement this - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * - */ - //////////////////////////////////////////////////////////////////////////// - this._cleanup = function (renderState) { - var children = m_this.m_sceneRoot.children(); - for (var i = 0; i < children.length; i += 1) { - var actor = children[i]; - actor.material()._cleanup(renderState); - actor.mapper()._cleanup(renderState); - } - - m_this.m_sceneRoot.removeChildren(); - }; - - return m_this; -}; - -inherit(vgl.renderer, vgl.graphicsObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, vec4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class renderWindow - * - * @class - * @returns {vgl.renderWindow} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.renderWindow = function (canvas) { - 'use strict'; - - if (!(this instanceof vgl.renderWindow)) { - return new vgl.renderWindow(canvas); - } - vgl.graphicsObject.call(this); - - /** @private */ - var m_this = this, - m_x = 0, - m_y = 0, - m_width = 400, - m_height = 400, - m_canvas = canvas, - m_activeRender = null, - m_renderers = [], - m_context = null; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get size of the render window - * - * @returns {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.windowSize = function () { - return [m_width, m_height]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set size of the render window - * - * @param width - * @param height - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setWindowSize = function (width, height) { - - if (m_width !== width || m_height !== height) { - m_width = width; - m_height = height; - - m_this.modified(); - - return true; - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get window position (top left coordinates) - * - * @returns {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.windowPosition = function () { - return [m_x, m_y]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set window position (top left coordinates) - * - * @param x - * @param y - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.setWindowPosition = function (x, y) { - if ((m_x !== x) || (m_y !== y)) { - m_x = x; - m_y = y; - m_this.modified(); - return true; - } - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return all renderers contained in the render window - * @returns {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.renderers = function () { - return m_renderers; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get active renderer of the the render window - * - * @returns vgl.renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.activeRenderer = function () { - return m_activeRender; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add renderer to the render window - * - * @param ren - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.addRenderer = function (ren) { - if (m_this.hasRenderer(ren) === false) { - m_renderers.push(ren); - ren.setRenderWindow(m_this); - if (m_activeRender === null) { - m_activeRender = ren; - } - if (ren.layer() !== 0) { - ren.camera().setClearMask(vgl.GL.DepthBufferBit); - } - m_this.modified(); - return true; - } - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove renderer from the render window - * - * @param ren - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.removeRenderer = function (ren) { - var index = m_renderers.indexOf(ren); - if (index !== -1) { - if (m_activeRender === ren) { - m_activeRender = null; - } - m_renderers.splice(index, 1); - m_this.modified(); - return true; - } - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return a renderer at a given index - * - * @param index - * @returns {vgl.renderer} - */ - //////////////////////////////////////////////////////////////////////////// - this.getRenderer = function (index) { - if (index < m_renderers.length) { - return m_renderers[index]; - } - - console.log('[WARNING] Out of index array'); - return null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if the renderer exists - * - * @param ren - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.hasRenderer = function (ren) { - var i; - for (i = 0; i < m_renderers.length; i += 1) { - if (ren === m_renderers[i]) { - return true; - } - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Resize window - * - * @param width - * @param height - */ - //////////////////////////////////////////////////////////////////////////// - this.resize = function (width, height) { - m_this.positionAndResize(m_x, m_y, width, height); - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Resize and reposition the window - * - * @param x - * @param y - * @param width - * @param height - */ - //////////////////////////////////////////////////////////////////////////// - this.positionAndResize = function (x, y, width, height) { - m_x = x; - m_y = y; - m_width = width; - m_height = height; - var i; - for (i = 0; i < m_renderers.length; i += 1) { - m_renderers[i].positionAndResize(m_x, m_y, m_width, m_height); - } - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create the window - * - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this._setup = function (renderState) { - renderState = renderState; /* unused parameter */ - m_context = null; - - try { - // Try to grab the standard context. If it fails, fallback to - // experimental. - m_context = m_canvas.getContext('webgl') || - m_canvas.getContext('experimental-webgl'); - - // Set width and height of renderers if not set already - var i; - for (i = 0; i < m_renderers.length; i += 1) { - if ((m_renderers[i].width() > m_width) || - m_renderers[i].width() === 0 || - (m_renderers[i].height() > m_height) || - m_renderers[i].height() === 0) { - m_renderers[i].resize(m_x, m_y, m_width, m_height); - } - } - - return true; - } - catch (e) { - } - - // If we don't have a GL context, give up now - if (!m_context) { - console('[ERROR] Unable to initialize WebGL. Your browser may not support it.'); - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return current GL context - */ - //////////////////////////////////////////////////////////////////////////// - this.context = function () { - return m_context; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Delete this window and release any graphics resources - */ - //////////////////////////////////////////////////////////////////////////// - this._cleanup = function (renderState) { - var i; - for (i = 0; i < m_renderers.length; i += 1) { - m_renderers[i]._cleanup(renderState); - } - vgl.clearCachedShaders(renderState ? renderState.m_context : null); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render the scene - */ - //////////////////////////////////////////////////////////////////////////// - this.render = function () { - var i; - m_renderers.sort(function (a, b) {return a.layer() - b.layer();}); - for (i = 0; i < m_renderers.length; i += 1) { - m_renderers[i].render(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the focusDisplayPoint from the activeRenderer - * @returns {vec4} - */ - //////////////////////////////////////////////////////////////////////////// - this.focusDisplayPoint = function () { - return m_activeRender.focusDisplayPoint(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Transform a point in display space to world space - * @param {Number} x - * @param {Number} y - * @param {vec4} focusDisplayPoint - * @returns {vec4} - */ - //////////////////////////////////////////////////////////////////////////// - this.displayToWorld = function (x, y, focusDisplayPoint, ren) { - ren = ren === undefined ? ren = m_activeRender : ren; - - var camera = ren.camera(); - if (!focusDisplayPoint) { - focusDisplayPoint = ren.focusDisplayPoint(); - } - - return ren.displayToWorld( - vec4.fromValues(x, y, focusDisplayPoint[2], 1.0), camera.viewMatrix(), - camera.projectionMatrix(), m_width, m_height); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Transform a point in display space to world space - * @param {Number} x - * @param {Number} y - * @param {vec4} focusDisplayPoint - * @returns {vec4} - */ - //////////////////////////////////////////////////////////////////////////// - this.worldToDisplay = function (x, y, z, ren) { - ren = ren === undefined ? ren = m_activeRender : ren; - var camera = ren.camera(); - return ren.worldToDisplay( - vec4.fromValues(x, y, z, 1.0), camera.viewMatrix(), - camera.projectionMatrix(), m_width, m_height); - }; - - return m_this; -}; - -inherit(vgl.renderWindow, vgl.graphicsObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, vec3, vec4, mat4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class camera - * - * @class - * @returns {vgl.camera} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.camera = function (arg) { - 'use strict'; - - if (!(this instanceof vgl.camera)) { - return new vgl.camera(arg); - } - vgl.groupNode.call(this); - arg = arg || {}; - - /** @private */ - var m_viewAngle = (Math.PI * 30) / 180.0, - m_position = vec4.fromValues(0.0, 0.0, 1.0, 1.0), - m_focalPoint = vec4.fromValues(0.0, 0.0, 0.0, 1.0), - m_centerOfRotation = vec3.fromValues(0.0, 0.0, 0.0), - m_viewUp = vec4.fromValues(0.0, 1.0, 0.0, 0.0), - m_rightDir = vec4.fromValues(1.0, 0.0, 0.0, 0.0), - m_near = 0.01, - m_far = 10000.0, - m_viewAspect = 1.0, - m_directionOfProjection = vec4.fromValues(0.0, 0.0, -1.0, 0.0), - m_viewPlaneNormal = vec4.fromValues(0.0, 0.0, 1.0, 0.0), - m_viewMatrix = mat4.create(), - m_projectionMatrix = mat4.create(), - m_computeModelViewMatrixTime = vgl.timestamp(), - m_computeProjectMatrixTime = vgl.timestamp(), - m_left = -1.0, - m_right = 1.0, - m_top = +1.0, - m_bottom = -1.0, - m_parallelExtents = {zoom: 1, tilesize: 256}, - m_enableTranslation = true, - m_enableRotation = true, - m_enableScale = true, - m_enableParallelProjection = false, - m_clearColor = [0.0, 0.0, 0.0, 0.0], - m_clearDepth = 1.0, - /*jshint bitwise: false */ - m_clearMask = vgl.GL.COLOR_BUFFER_BIT | - vgl.GL.DEPTH_BUFFER_BIT; - /*jshint bitwise: true */ - - if (arg.parallelProjection !== undefined) { - m_enableParallelProjection = arg.parallelProjection ? true : false; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Get view angle of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.viewAngle = function () { - return m_viewAngle; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set view angle of the camera in degrees, which is converted to radians. - */ - //////////////////////////////////////////////////////////////////////////// - this.setViewAngleDegrees = function (a) { - m_viewAngle = (Math.PI * a) / 180.0; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set view angle of the camera in degrees, which is converted to radians. - */ - //////////////////////////////////////////////////////////////////////////// - this.setViewAngle = function (a) { - if (m_enableScale) { - m_viewAngle = a; - this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get position of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.position = function () { - return m_position; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set position of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.setPosition = function (x, y, z) { - if (m_enableTranslation) { - m_position = vec4.fromValues(x, y, z, 1.0); - this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get focal point of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.focalPoint = function () { - return m_focalPoint; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set focal point of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.setFocalPoint = function (x, y, z) { - if (m_enableRotation && m_enableTranslation) { - m_focalPoint = vec4.fromValues(x, y, z, 1.0); - this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get view-up direction of camera - */ - //////////////////////////////////////////////////////////////////////////// - this.viewUpDirection = function () { - return m_viewUp; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set view-up direction of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.setViewUpDirection = function (x, y, z) { - m_viewUp = vec4.fromValues(x, y, z, 0); - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get center of rotation for camera - */ - //////////////////////////////////////////////////////////////////////////// - this.centerOfRotation = function () { - return m_centerOfRotation; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set center of rotation for camera - */ - //////////////////////////////////////////////////////////////////////////// - this.setCenterOfRotation = function (centerOfRotation) { - m_centerOfRotation = centerOfRotation; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get clipping range of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.clippingRange = function () { - return [m_near, m_far]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set clipping range of the camera - */ - //////////////////////////////////////////////////////////////////////////// - this.setClippingRange = function (near, far) { - m_near = near; - m_far = far; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get view aspect - */ - //////////////////////////////////////////////////////////////////////////// - this.viewAspect = function () { - return m_viewAspect; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set view aspect - */ - //////////////////////////////////////////////////////////////////////////// - this.setViewAspect = function (aspect) { - m_viewAspect = aspect; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return active mode for scaling (enabled / disabled) - */ - //////////////////////////////////////////////////////////////////////////// - this.enableScale = function () { - return m_enableScale; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Enable/disable scaling - */ - //////////////////////////////////////////////////////////////////////////// - this.setEnableScale = function (flag) { - if (flag !== m_enableScale) { - m_enableScale = flag; - this.modified(); - return true; - } - - return m_enableScale; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return active mode for rotation (enabled / disabled) - */ - //////////////////////////////////////////////////////////////////////////// - this.enableRotation = function () { - return m_enableRotation; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Enable / disable rotation - */ - //////////////////////////////////////////////////////////////////////////// - this.setEnableRotation = function (flag) { - if (flag !== m_enableRotation) { - m_enableRotation = flag; - this.modified(); - return true; - } - - return m_enableRotation; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return active mode for translation (enabled/disabled) - */ - //////////////////////////////////////////////////////////////////////////// - this.enableTranslation = function () { - return m_enableTranslation; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Enable / disable translation - */ - //////////////////////////////////////////////////////////////////////////// - this.setEnableTranslation = function (flag) { - if (flag !== m_enableTranslation) { - m_enableTranslation = flag; - this.modified(); - return true; - } - - return m_enableTranslation; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return if parallel projection is enabled - */ - //////////////////////////////////////////////////////////////////////////// - this.isEnabledParallelProjection = function () { - return m_enableParallelProjection; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Enable / disable parallel projection - */ - //////////////////////////////////////////////////////////////////////////// - this.enableParallelProjection = function (flag) { - if (flag !== m_enableParallelProjection) { - m_enableParallelProjection = flag; - this.modified(); - return true; - } - - return m_enableParallelProjection; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Enable / disable parallel projection - */ - //////////////////////////////////////////////////////////////////////////// - this.setEnableParallelProjection = function (flag) { - return this.enableParallelProjection(flag); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get parallel projection parameters - */ - //////////////////////////////////////////////////////////////////////////// - this.parallelProjection = function () { - return {left: m_left, right: m_right, top: m_top, bottom: m_bottom}; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set parallel projection parameters - */ - //////////////////////////////////////////////////////////////////////////// - this.setParallelProjection = function (left, right, top, bottom) { - m_left = left; - m_right = right; - m_top = top; - m_bottom = bottom; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get parallel projection extents parameters - */ - //////////////////////////////////////////////////////////////////////////// - this.parallelExtents = function () { - return m_parallelExtents; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set parallel projection extents parameters - */ - //////////////////////////////////////////////////////////////////////////// - this.setParallelExtents = function (extents) { - var prop = ['width', 'height', 'zoom', 'tilesize'], mod = false, i, key; - for (i = 0; i < prop.length; i += 1) { - key = prop[i]; - if (extents[key] !== undefined && - extents[key] !== m_parallelExtents[key]) { - m_parallelExtents[key] = extents[key]; - mod = true; - } - } - if (mod && m_parallelExtents.width && m_parallelExtents.height && - m_parallelExtents.zoom !== undefined && m_parallelExtents.tilesize) { - var unitsPerPixel = this.unitsPerPixel(m_parallelExtents.zoom, - m_parallelExtents.tilesize); - m_right = unitsPerPixel * m_parallelExtents.width / 2; - m_left = -m_right; - m_top = unitsPerPixel * m_parallelExtents.height / 2; - m_bottom = -m_top; - this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute the units per pixel. - * - * @param zoom: tile zoom level. - * @param tilesize: number of pixels per tile (defaults to 256). - * @returns: unitsPerPixel. - */ - //////////////////////////////////////////////////////////////////////////// - this.unitsPerPixel = function (zoom, tilesize) { - tilesize = tilesize || 256; - return 360.0 * Math.pow(2, -zoom) / tilesize; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return direction of projection - */ - //////////////////////////////////////////////////////////////////////////// - this.directionOfProjection = function () { - this.computeDirectionOfProjection(); - return m_directionOfProjection; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return view plane normal direction - */ - //////////////////////////////////////////////////////////////////////////// - this.viewPlaneNormal = function () { - this.computeViewPlaneNormal(); - return m_viewPlaneNormal; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return view-matrix for the camera This method does not compute the - * view-matrix for the camera. It is assumed that a call to computeViewMatrix - * has been made earlier. - * - * @returns {mat4} - */ - //////////////////////////////////////////////////////////////////////////// - this.viewMatrix = function () { - 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. - * @param {boolean} preserveType: if true, clone the input using slice. This - * can be used to ensure the array is a specific precision. - */ - //////////////////////////////////////////////////////////////////////////// - this.setViewMatrix = function (view, preserveType) { - if (!preserveType) { - mat4.copy(m_viewMatrix, view); - } else { - m_viewMatrix = view.slice(); - } - m_computeModelViewMatrixTime.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return camera projection matrix This method does not compute the - * projection-matrix for the camera. It is assumed that a call to - * computeProjectionMatrix has been made earlier. - * - * @returns {mat4} - */ - //////////////////////////////////////////////////////////////////////////// - this.projectionMatrix = function () { - 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 - * - * @returns {number} - */ - //////////////////////////////////////////////////////////////////////////// - this.clearMask = function () { - return m_clearMask; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set clear mask for camera - * - * @param mask - */ - //////////////////////////////////////////////////////////////////////////// - this.setClearMask = function (mask) { - m_clearMask = mask; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get clear color (background color) of the camera - * - * @returns {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.clearColor = function () { - return m_clearColor; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set clear color (background color) for the camera - * - * @param color RGBA - */ - //////////////////////////////////////////////////////////////////////////// - this.setClearColor = function (r, g, b, a) { - m_clearColor[0] = r; - m_clearColor[1] = g; - m_clearColor[2] = b; - m_clearColor[3] = a; - - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * - * @returns {{1.0: null}} - */ - //////////////////////////////////////////////////////////////////////////// - this.clearDepth = function () { - return m_clearDepth; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * - * @param depth - */ - //////////////////////////////////////////////////////////////////////////// - this.setClearDepth = function (depth) { - m_clearDepth = depth; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute direction of projection - */ - //////////////////////////////////////////////////////////////////////////// - this.computeDirectionOfProjection = function () { - vec3.subtract(m_directionOfProjection, m_focalPoint, m_position); - vec3.normalize(m_directionOfProjection, m_directionOfProjection); - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute view plane normal - */ - //////////////////////////////////////////////////////////////////////////// - this.computeViewPlaneNormal = function () { - m_viewPlaneNormal[0] = -m_directionOfProjection[0]; - m_viewPlaneNormal[1] = -m_directionOfProjection[1]; - m_viewPlaneNormal[2] = -m_directionOfProjection[2]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Move camera closer or further away from the scene - */ - //////////////////////////////////////////////////////////////////////////// - this.zoom = function (d, dir) { - if (d === 0) { - return; - } - - if (!m_enableTranslation) { - return; - } - - d = d * vec3.distance(m_focalPoint, m_position); - if (!dir) { - dir = m_directionOfProjection; - m_position[0] = m_focalPoint[0] - d * dir[0]; - m_position[1] = m_focalPoint[1] - d * dir[1]; - m_position[2] = m_focalPoint[2] - d * dir[2]; - } else { - m_position[0] = m_position[0] + d * dir[0]; - m_position[1] = m_position[1] + d * dir[1]; - m_position[2] = m_position[2] + d * dir[2]; - } - - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Move camera sideways - */ - //////////////////////////////////////////////////////////////////////////// - this.pan = function (dx, dy, dz) { - if (!m_enableTranslation) { - return; - } - - m_position[0] += dx; - m_position[1] += dy; - m_position[2] += dz; - - m_focalPoint[0] += dx; - m_focalPoint[1] += dy; - m_focalPoint[2] += dz; - - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute camera coordinate axes - */ - //////////////////////////////////////////////////////////////////////////// - this.computeOrthogonalAxes = function () { - this.computeDirectionOfProjection(); - vec3.cross(m_rightDir, m_directionOfProjection, m_viewUp); - vec3.normalize(m_rightDir, m_rightDir); - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Rotate camera around center of rotation - * @param dx Rotation around vertical axis in degrees - * @param dy Rotation around horizontal axis in degrees - */ - //////////////////////////////////////////////////////////////////////////// - this.rotate = function (dx, dy) { - if (!m_enableRotation) { - return; - } - - // Convert degrees into radians - dx = 0.5 * dx * (Math.PI / 180.0); - dy = 0.5 * dy * (Math.PI / 180.0); - - var mat = mat4.create(), - inverseCenterOfRotation = new vec3.create(); - - mat4.identity(mat); - - inverseCenterOfRotation[0] = -m_centerOfRotation[0]; - inverseCenterOfRotation[1] = -m_centerOfRotation[1]; - inverseCenterOfRotation[2] = -m_centerOfRotation[2]; - - mat4.translate(mat, mat, m_centerOfRotation); - mat4.rotate(mat, mat, dx, m_viewUp); - mat4.rotate(mat, mat, dy, m_rightDir); - mat4.translate(mat, mat, inverseCenterOfRotation); - - vec4.transformMat4(m_position, m_position, mat); - vec4.transformMat4(m_focalPoint, m_focalPoint, mat); - - // Update viewup vector - vec4.transformMat4(m_viewUp, m_viewUp, mat); - vec4.normalize(m_viewUp, m_viewUp); - - // Update direction of projection - this.computeOrthogonalAxes(); - - // Mark modified - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute camera view matrix - */ - //////////////////////////////////////////////////////////////////////////// - this.computeViewMatrix = function () { - if (m_computeModelViewMatrixTime.getMTime() < this.getMTime()) { - mat4.lookAt(m_viewMatrix, m_position, m_focalPoint, m_viewUp); - m_computeModelViewMatrixTime.modified(); - } - return m_viewMatrix; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Check if the texture should be aligned to the screen. Alignment only - * occurs if the parallel extents contain width, height, and a - * close-to-integer zoom level, and if the units-per-pixel value has been - * computed. The camera must either be in parallel projection mode OR must - * have a perspective camera which is oriented along the z-axis without any - * rotation. - * - * @returns: either null if no alignment should occur, or an alignment object - * with the rounding value and offsets. - */ - //////////////////////////////////////////////////////////////////////////// - this.viewAlignment = function () { - if (!m_enableParallelProjection) { - /* If we aren't in parallel projection mode, ensure that the projection - * matrix meets strict specifications */ - var proj = this.projectionMatrix(); - if (proj[1] || proj[2] || proj[3] || proj[4] || proj[6] || proj[7] || - proj[8] || proj[9] || proj[12] || proj[13] || proj[15]) { - return null; - } - } - var unitsPerPixel = this.unitsPerPixel(m_parallelExtents.zoom, - m_parallelExtents.tilesize); - /* If we don't have screen dimensions, we can't know how to align pixels */ - if (!m_parallelExtents.width || !m_parallelExtents.height || - !unitsPerPixel) { - return null; - } - /* If we aren't at an integer zoom level, we shouldn't align pixels. If - * we are really close to an integer zoom level, that is good enough. */ - if (parseFloat(m_parallelExtents.zoom.toFixed(6)) !== - parseFloat(m_parallelExtents.zoom.toFixed(0))) { - return null; - } - 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 - * the same reason. */ - if (m_parallelExtents.width % 2) { - align.dx = unitsPerPixel * 0.5; - } - if (m_parallelExtents.height % 2) { - align.dy = unitsPerPixel * 0.5; - } - return align; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute camera projection matrix - */ - //////////////////////////////////////////////////////////////////////////// - this.computeProjectionMatrix = function () { - if (m_computeProjectMatrixTime.getMTime() < this.getMTime()) { - if (!m_enableParallelProjection) { - mat4.perspective(m_projectionMatrix, m_viewAngle, m_viewAspect, m_near, m_far); - } else { - mat4.ortho(m_projectionMatrix, - m_left, m_right, m_bottom, m_top, m_near, m_far); - } - - m_computeProjectMatrixTime.modified(); - } - - return m_projectionMatrix; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert a zoom level and window size to a camera height. - */ - //////////////////////////////////////////////////////////////////////////// - this.zoomToHeight = function (zoom, width, height) { - return vgl.zoomToHeight(zoom, width, height, m_viewAngle); - }; - - this.computeDirectionOfProjection(); - - return this; -}; - -inherit(vgl.camera, vgl.groupNode); - -//////////////////////////////////////////////////////////////////////////// -/** - * Convert a zoom level and window size to a camera height. - * - * @param zoom: Zoom level, as used in OSM maps. - * @param width: width of the window. - * @param height: height of the window. - * @returns: perspective camera height. - */ -//////////////////////////////////////////////////////////////////////////// -vgl.zoomToHeight = function (zoom, width, height, viewAngle) { - 'use strict'; - viewAngle = viewAngle || (30 * Math.PI / 180.0); - var newZ = 360 * Math.pow(2, -zoom); - newZ /= Math.tan(viewAngle / 2) * 2 * 256 / height; - return newZ; -}; - -//////////////////////////////////////////////////////////////////////////// -/** - * Convert a camera height and window size to a zoom level. - * - * @param z: perspective camera height. - * @param width: width of the window. - * @param height: height of the window. - * @returns: zoom level. - */ -//////////////////////////////////////////////////////////////////////////// -vgl.heightToZoom = function (z, width, height, viewAngle) { - 'use strict'; - viewAngle = viewAngle || (30 * Math.PI / 180.0); - z *= Math.tan(viewAngle / 2) * 2 * 256 / height; - var zoom = -Math.log2(z / 360); - return zoom; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit, $*/ -////////////////////////////////////////////////////////////////////////////// - -//////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class interactorStyle - * - * @class vgl.interactorStyle - * interactorStyle is a base class for all interactor styles - * @returns {vgl.interactorStyle} - */ -//////////////////////////////////////////////////////////////////////////// -vgl.interactorStyle = function () { - 'use strict'; - - if (!(this instanceof vgl.interactorStyle)) { - return new vgl.interactorStyle(); - } - vgl.object.call(this); - - // Private member variables - var m_that = this, - m_viewer = null; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return viewer referenced by the interactor style - * - * @returns {null} - */ - //////////////////////////////////////////////////////////////////////////// - this.viewer = function () { - return m_viewer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set viewer for the interactor style - * - * @param viewer - */ - //////////////////////////////////////////////////////////////////////////// - this.setViewer = function (viewer) { - if (viewer !== m_viewer) { - m_viewer = viewer; - $(m_viewer).on(vgl.event.mousePress, m_that.handleMouseDown); - $(m_viewer).on(vgl.event.mouseRelease, m_that.handleMouseUp); - $(m_viewer).on(vgl.event.mouseMove, m_that.handleMouseMove); - $(m_viewer).on(vgl.event.mouseOut, m_that.handleMouseOut); - $(m_viewer).on(vgl.event.mouseWheel, m_that.handleMouseWheel); - $(m_viewer).on(vgl.event.keyPress, m_that.handleKeyPress); - $(m_viewer).on(vgl.event.mouseContextMenu, m_that.handleContextMenu); - $(m_viewer).on(vgl.event.click, m_that.handleClick); - $(m_viewer).on(vgl.event.dblClick, m_that.handleDoubleClick); - this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse down event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseDown = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse up event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseUp = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseMove = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseOut = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse wheel event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseWheel = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle click event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleClick = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle double click event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleDoubleClick = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle key press event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleKeyPress = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle context menu event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleContextMenu = function (event) { - event = event; /* unused parameter */ - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Reset to default - */ - //////////////////////////////////////////////////////////////////////////// - this.reset = function () { - return true; - }; - - return this; -}; - -inherit(vgl.interactorStyle, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, vec4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of trackballInteractorStyle - * - * @class vgl.trackballInteractorStyle - * @returns {vgl.trackballInteractorStyle} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.trackballInteractorStyle = function () { - 'use strict'; - - if (!(this instanceof vgl.trackballInteractorStyle)) { - return new vgl.trackballInteractorStyle(); - } - vgl.interactorStyle.call(this); - var m_that = this, - m_leftMouseBtnDown = false, - m_rightMouseBtnDown = false, - m_midMouseBtnDown = false, - m_outsideCanvas, - m_currPos = {x: 0, y: 0}, - m_lastPos = {x: 0, y: 0}; - - - ///////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event - * - * @param event - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.handleMouseMove = function (event) { - var width = m_that.viewer().renderWindow().windowSize()[0], - height = m_that.viewer().renderWindow().windowSize()[1], - ren = m_that.viewer().renderWindow().activeRenderer(), - cam = ren.camera(), coords = m_that.viewer().relMouseCoords(event), - fp, fdp, fwp, dp1, dp2, wp1, wp2, dx, dy, dz, m_zTrans; - - m_outsideCanvas = false; - m_currPos = {x: 0, y: 0}; - - if ((coords.x < 0) || (coords.x > width)) { - m_currPos.x = 0; - m_outsideCanvas = true; - } else { - m_currPos.x = coords.x; - } - if ((coords.y < 0) || (coords.y > height)) { - m_currPos.y = 0; - m_outsideCanvas = true; - } else { - m_currPos.y = coords.y; - } - if (m_outsideCanvas === true) { - return; - } - - 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.0); - dp2 = vec4.fromValues(m_lastPos.x, m_lastPos.y, fdp[2], 1.0); - - 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]; - - if (m_midMouseBtnDown) { - cam.pan(-dx, -dy, -dz); - m_that.viewer().render(); - } - if (m_leftMouseBtnDown) { - cam.rotate((m_lastPos.x - m_currPos.x), - (m_lastPos.y - m_currPos.y)); - ren.resetCameraClippingRange(); - m_that.viewer().render(); - } - if (m_rightMouseBtnDown) { - /// 2.0 is the speed up factor - m_zTrans = 2.0 * (m_currPos.y - m_lastPos.y) / height; - - // Calculate zoom scale here - if (m_zTrans > 0) { - cam.zoom(1 - Math.abs(m_zTrans)); - } else { - 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; - return false; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse down event - * - * @param event - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.handleMouseDown = function (event) { - var coords; - - if (event.button === 0) { - m_leftMouseBtnDown = true; - } - if (event.button === 1) { - m_midMouseBtnDown = true; - } - if (event.button === 2) { - m_rightMouseBtnDown = true; - } - coords = m_that.viewer().relMouseCoords(event); - if (coords.x < 0) { - m_lastPos.x = 0; - } else { - m_lastPos.x = coords.x; - } - if (coords.y < 0) { - m_lastPos.y = 0; - } else { - m_lastPos.y = coords.y; - } - return false; - }; - - // @note We never get mouse up from scroll bar: See the bug report here - // http://bugs.jquery.com/ticket/8184 - ///////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse up event - * - * @param event - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.handleMouseUp = function (event) { - if (event.button === 0) { - m_leftMouseBtnDown = false; - } - if (event.button === 1) { - m_midMouseBtnDown = false; - } - if (event.button === 2) { - m_rightMouseBtnDown = false; - } - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse wheel event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseWheel = function (event) { - var ren = m_that.viewer().renderWindow().activeRenderer(), - cam = ren.camera(); - - // TODO Compute zoom factor intelligently - if (event.originalEvent.wheelDelta < 0) { - cam.zoom(0.9); - } else { - cam.zoom(1.1); - } - ren.resetCameraClippingRange(); - m_that.viewer().render(); - return true; - }; - - return this; -}; -inherit(vgl.trackballInteractorStyle, vgl.interactorStyle); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, vec4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of pvwInteractorStyle (for ParaViewWeb) - * - * @class vgl.pvwInteractorStyle - * @returns {vgl.pvwInteractorStyle} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.pvwInteractorStyle = function () { - 'use strict'; - - if (!(this instanceof vgl.pvwInteractorStyle)) { - return new vgl.pvwInteractorStyle(); - } - vgl.trackballInteractorStyle.call(this); - var m_that = this, - m_leftMouseButtonDown = false, - m_rightMouseButtonDown = false, - m_middleMouseButtonDown = false, - 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_mouseLastPos = { - x: 0, - y: 0 - }; - - function render() { - m_renderer.resetCameraClippingRange(); - m_that.viewer().render(); - } - - ///////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event - * - * @param event - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.handleMouseMove = function (event) { - var rens = [], i = null, secCameras = [], deltaxy = null; - 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 = false; - m_coords = m_that.viewer().relMouseCoords(event); - m_currentMousePos = { - x: 0, - y: 0 - }; - - // Get secondary cameras - rens = m_that.viewer().renderWindow().renderers(); - for (i = 0; i < rens.length; i += 1) { - if (m_renderer !== rens[i]) { - secCameras.push(rens[i].camera()); - } - } - - if ((m_coords.x < 0) || (m_coords.x > m_width)) { - m_currentMousePos.x = 0; - m_outsideCanvas = true; - } else { - m_currentMousePos.x = m_coords.x; - } - if ((m_coords.y < 0) || (m_coords.y > m_height)) { - m_currentMousePos.y = 0; - m_outsideCanvas = true; - } else { - m_currentMousePos.y = m_coords.y; - } - if (m_outsideCanvas === true) { - return; - } - 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.0); - m_displayPt2 = vec4.fromValues( - m_mouseLastPos.x, m_mouseLastPos.y, m_focusDisplayPt[2], 1.0); - 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]; - - if (m_middleMouseButtonDown) { - m_camera.pan(-m_dx, -m_dy, -m_dz); - render(); - } - if (m_leftMouseButtonDown) { - deltaxy = [(m_mouseLastPos.x - m_currentMousePos.x), - (m_mouseLastPos.y - m_currentMousePos.y)]; - m_camera.rotate(deltaxy[0], deltaxy[1]); - - // Apply rotation to all other cameras - for (i = 0; i < secCameras.length; i += 1) { - secCameras[i].rotate(deltaxy[0], deltaxy[1]); - } - - // Apply rotation to all other cameras - for (i = 0; i < rens.length; i += 1) { - rens[i].resetCameraClippingRange(); - } - render(); - } - if (m_rightMouseButtonDown) { - /// 2.0 is the speed up factor. - m_zTrans = 2.0 * (m_currentMousePos.y - m_mouseLastPos.y) / m_height; - - // Calculate zoom scale here - if (m_zTrans > 0) { - m_camera.zoom(1 - Math.abs(m_zTrans)); - } else { - m_camera.zoom(1 + Math.abs(m_zTrans)); - } - render(); - } - m_mouseLastPos.x = m_currentMousePos.x; - m_mouseLastPos.y = m_currentMousePos.y; - return false; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse down event - * - * @param event - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.handleMouseDown = function (event) { - if (event.button === 0) { - m_leftMouseButtonDown = true; - } - if (event.button === 1) { - m_middleMouseButtonDown = true; - } - if (event.button === 2) { - m_rightMouseButtonDown = true; - } - m_coords = m_that.viewer().relMouseCoords(event); - if (m_coords.x < 0) { - m_mouseLastPos.x = 0; - } else { - m_mouseLastPos.x = m_coords.x; - } - if (m_coords.y < 0) { - m_mouseLastPos.y = 0; - } else { - m_mouseLastPos.y = m_coords.y; - } - return false; - }; - - // @note We never get mouse up from scroll bar: See the bug report here - // http://bugs.jquery.com/ticket/8184 - ///////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse up event - * - * @param event - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.handleMouseUp = function (event) { - if (event.button === 0) { - m_leftMouseButtonDown = false; - } - if (event.button === 1) { - m_middleMouseButtonDown = false; - } - if (event.button === 2) { - m_rightMouseButtonDown = false; - } - return false; - }; - - return this; -}; -inherit(vgl.pvwInteractorStyle, vgl.trackballInteractorStyle); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global window, vgl, inherit, $*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class viewer - * - * @param canvas - * @returns {vgl.viewer} - */ -////////////////////////////////////////////////////////////////////////////// -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 = true, - m_interactorStyle = null, - m_renderer = vgl.renderer(options), - m_renderWindow = vgl.renderWindow(m_canvas); - - //////////////////////////////////////////////////////////////////////////// - /** - * Get canvas of the viewer - * - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.canvas = function () { - return m_canvas; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return render window of the viewer - * - * @returns {vgl.renderWindow} - */ - //////////////////////////////////////////////////////////////////////////// - this.renderWindow = function () { - return m_renderWindow; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize the viewer - * - * This is a must call or otherwise render context may not initialized - * properly. - */ - //////////////////////////////////////////////////////////////////////////// - this.init = function () { - if (m_renderWindow !== null) { - m_renderWindow._setup(); - } else { - console.log('[ERROR] No render window attached'); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove the viewer - */ - //////////////////////////////////////////////////////////////////////////// - this.exit = function (renderState) { - if (m_renderWindow !== null) { - m_renderWindow._cleanup(renderState); - } else { - console.log('[ERROR] No render window attached'); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get interactor style of the viewer - * - * @returns {vgl.interactorStyle} - */ - //////////////////////////////////////////////////////////////////////////// - this.interactorStyle = function () { - return m_interactorStyle; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set interactor style to be used by the viewer - * - * @param {vgl.interactorStyle} style - */ - //////////////////////////////////////////////////////////////////////////// - this.setInteractorStyle = function (style) { - if (style !== m_interactorStyle) { - m_interactorStyle = style; - m_interactorStyle.setViewer(this); - this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse down event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseDown = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - // Only prevent default action for right mouse button - if (event.button === 2) { - fixedEvent.preventDefault(); - } - fixedEvent.state = 'down'; - fixedEvent.type = vgl.event.mousePress; - $(m_that).trigger(fixedEvent); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse up event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseUp = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.state = 'up'; - fixedEvent.type = vgl.event.mouseRelease; - $(m_that).trigger(fixedEvent); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseMove = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.type = vgl.event.mouseMove; - $(m_that).trigger(fixedEvent); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse wheel scroll - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseWheel = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.type = vgl.event.mouseWheel; - $(m_that).trigger(fixedEvent); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleMouseOut = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.type = vgl.event.mouseOut; - $(m_that).trigger(fixedEvent); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle key press event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleKeyPress = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.type = vgl.event.keyPress; - $(m_that).trigger(fixedEvent); - } - - return true; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle context menu event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleContextMenu = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.type = vgl.event.contextMenu; - $(m_that).trigger(fixedEvent); - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle click event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleClick = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.type = vgl.event.click; - $(m_that).trigger(fixedEvent); - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle double click event - * - * @param event - * @returns {boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.handleDoubleClick = function (event) { - if (m_ready === true) { - var fixedEvent = $.event.fix(event || window.event); - fixedEvent.preventDefault(); - fixedEvent.type = vgl.event.dblClick; - $(m_that).trigger(fixedEvent); - } - - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get mouse coodinates related to canvas - * - * @param event - * @returns {{x: number, y: number}} - */ - //////////////////////////////////////////////////////////////////////////// - this.relMouseCoords = function (event) { - if (event.pageX === undefined || event.pageY === undefined) { - 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); - - canvasX = event.pageX - totalOffsetX; - canvasY = event.pageY - totalOffsetY; - - return { - x: canvasX, - y: canvasY - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render - */ - //////////////////////////////////////////////////////////////////////////// - this.render = function () { - m_renderWindow.render(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bind canvas mouse events to their default handlers - */ - //////////////////////////////////////////////////////////////////////////// - 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Undo earlier binded handlers for canvas mouse events - */ - //////////////////////////////////////////////////////////////////////////// - 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function () { - this.bindEventHandlers(); - m_renderWindow.addRenderer(m_renderer); - }; - - this._init(); - return this; -}; - -inherit(vgl.viewer, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class shader - * - * @param type - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.shader = function (type) { - 'use strict'; - - if (!(this instanceof vgl.shader)) { - return new vgl.shader(type); - } - vgl.object.call(this); - - var m_shaderContexts = [], - m_shaderType = type, - m_shaderSource = ''; - - /** - * A shader can be associated with multiple contexts. Each context needs to - * be compiled and attached separately. These are tracked in the - * m_shaderContexts array. - * - * @param renderState a renderState that includes a m_context value. - * @return an object with context, compileTimestamp, and, if compiled, a - * shaderHandle entry. - */ - this._getContextEntry = function (renderState) { - var context = renderState.m_context, i, entry; - for (i = 0; i < m_shaderContexts.length; i += 1) { - if (m_shaderContexts[i].context === context) { - return m_shaderContexts[i]; - } - } - entry = { - context: context, - compileTimestamp: vgl.timestamp() - }; - m_shaderContexts.push(entry); - return entry; - }; - - /** - * Remove the context from the list of tracked contexts. This allows the - * associated shader handle to be GCed. Does nothing if the context is not - * in the list of tracked contexts. - * - * @param renderState a renderState that includes a m_context value. - */ - this.removeContext = function (renderState) { - var context = renderState.m_context, i; - for (i = 0; i < m_shaderContexts.length; i += 1) { - if (m_shaderContexts[i].context === context) { - m_shaderContexts.splice(i, 1); - return; - } - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get shader handle - */ - ///////////////////////////////////////////////////////////////////////////// - this.shaderHandle = function (renderState) { - var entry = this._getContextEntry(renderState); - return entry.shaderHandle; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get type of the shader - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.shaderType = function () { - return m_shaderType; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get shader source - * - * @returns {string} - */ - ///////////////////////////////////////////////////////////////////////////// - this.shaderSource = function () { - return m_shaderSource; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set shader source - * - * @param {string} source - */ - ///////////////////////////////////////////////////////////////////////////// - this.setShaderSource = function (source) { - m_shaderSource = source; - this.modified(); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Compile the shader - * - * @returns {null} - */ - ///////////////////////////////////////////////////////////////////////////// - this.compile = function (renderState) { - var entry = this._getContextEntry(renderState); - if (this.getMTime() < entry.compileTimestamp.getMTime()) { - return entry.shaderHandle; - } - - renderState.m_context.deleteShader(entry.shaderHandle); - entry.shaderHandle = renderState.m_context.createShader(m_shaderType); - renderState.m_context.shaderSource(entry.shaderHandle, m_shaderSource); - renderState.m_context.compileShader(entry.shaderHandle); - - // See if it compiled successfully - if (!renderState.m_context.getShaderParameter(entry.shaderHandle, - vgl.GL.COMPILE_STATUS)) { - console.log('[ERROR] An error occurred compiling the shaders: ' + - renderState.m_context.getShaderInfoLog(entry.shaderHandle)); - console.log(m_shaderSource); - renderState.m_context.deleteShader(entry.shaderHandle); - return null; - } - - entry.compileTimestamp.modified(); - - return entry.shaderHandle; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Attach shader to the program - * - * @param programHandle - */ - ///////////////////////////////////////////////////////////////////////////// - this.attachShader = function (renderState, programHandle) { - renderState.m_context.attachShader( - programHandle, this.shaderHandle(renderState)); - }; -}; - -inherit(vgl.shader, vgl.object); - - -/* We can use the same shader multiple times if it is identical. This caches - * the last N shaders and will reuse them when possible. The cache keeps the - * most recently requested shader at the front. If you are doing anything more - * to a shader then creating it and setting its source once, do not use this - * cache. - */ -(function () { - 'use strict'; - var m_shaderCache = [], - m_shaderCacheMaxSize = 10; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get a shader from the cache. Create a new shader if necessary using a - * specific source. - * - * @param type One of vgl.GL.*_SHADER - * @param context the GL context for the shader. - * @param {string} source the source code of the shader. - */ - ///////////////////////////////////////////////////////////////////////////// - vgl.getCachedShader = function (type, context, source) { - for (var i = 0; i < m_shaderCache.length; i += 1) { - if (m_shaderCache[i].type === type && - m_shaderCache[i].context === context && - m_shaderCache[i].source === source) { - if (i) { - m_shaderCache.splice(0, 0, m_shaderCache.splice(i, 1)[0]); - } - return m_shaderCache[0].shader; - } - } - var shader = new vgl.shader(type); - shader.setShaderSource(source); - m_shaderCache.unshift({ - type: type, - context: context, - source: source, - shader: shader - }); - if (m_shaderCache.length >= m_shaderCacheMaxSize) { - m_shaderCache.splice(m_shaderCacheMaxSize, - m_shaderCache.length - m_shaderCacheMaxSize); - } - return shader; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Clear the shader cache. - * - * @param context the GL context to clear, or null for clear all. - */ - ///////////////////////////////////////////////////////////////////////////// - vgl.clearCachedShaders = function (context) { - for (var i = m_shaderCache.length - 1; i >= 0; i -= 1) { - if (context === null || context === undefined || - m_shaderCache[i].context === context) { - m_shaderCache.splice(i, 1); - } - } - }; -})(); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit, $*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instace of class shaderProgram - * - * @class - * @returns {vgl.shaderProgram} - */ -////////////////////////////////////////////////////////////////////////////// - -var getBaseUrl = (function () { - 'use strict'; - var baseUrl = '.'; - var scripts = document.getElementsByTagName('script'); - /* When run in certain environments, there may be no scripts loaded. For - * instance, jQuery's $.getScript won't add it to a script tag. */ - if (scripts.length > 0) { - var index = scripts.length - 1; - var vglScript = scripts[index]; - index = vglScript.src.lastIndexOf('/'); - baseUrl = vglScript.src.substring(0, index); - } - return function () { return baseUrl; }; -})(); - - -vgl.shaderProgram = function () { - 'use strict'; - - if (!(this instanceof vgl.shaderProgram)) { - return new vgl.shaderProgram(); - } - vgl.materialAttribute.call( - this, vgl.materialAttributeType.ShaderProgram); - - /** @private */ - var m_this = this, - m_programHandle = 0, - m_compileTimestamp = vgl.timestamp(), - m_bindTimestamp = vgl.timestamp(), - m_shaders = [], - m_uniforms = [], - m_vertexAttributes = {}, - m_uniformNameToLocation = {}, - m_vertexAttributeNameToLocation = {}; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Create a particular shader type using GLSL shader strings from a file - */ - ///////////////////////////////////////////////////////////////////////////// - this.loadFromFile = function (type, sourceUrl) { - var shader; - $.ajax({ - url: sourceUrl, - type: 'GET', - async: false, - success: function (result) { - //console.log(result); - shader = vgl.shader(type); - shader.setShaderSource(result); - m_this.addShader(shader); - } - }); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Create a particular shader type using GLSL shader strings from a file - * relative to VGL load URL. - */ - ///////////////////////////////////////////////////////////////////////////// - this.loadShader = function (type, file) { - this.loadFromFile(type, getBaseUrl() + '/shaders/' + file); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Query uniform location in the program - * - * @param name - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.queryUniformLocation = function (renderState, name) { - return renderState.m_context.getUniformLocation(m_programHandle, name); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Query attribute location in the program - * - * @param name - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.queryAttributeLocation = function (renderState, name) { - return renderState.m_context.getAttribLocation(m_programHandle, name); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Add a new shader to the program - * - * @param shader - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.addShader = function (shader) { - if (m_shaders.indexOf(shader) > -1) { - return false; - } - - var i; - for (i = 0; i < m_shaders.length; i += 1) { - if (m_shaders[i].shaderType() === shader.shaderType()) { - m_shaders.splice(m_shaders.indexOf(shader), 1); - } - } - - m_shaders.push(shader); - m_this.modified(); - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Add a new uniform to the program - * - * @param uniform - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.addUniform = function (uniform) { - if (m_uniforms.indexOf(uniform) > -1) { - return false; - } - - m_uniforms.push(uniform); - m_this.modified(); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Add a new vertex attribute to the program - * - * @param attr - * @param key - */ - ///////////////////////////////////////////////////////////////////////////// - this.addVertexAttribute = function (attr, key) { - m_vertexAttributes[key] = attr; - m_this.modified(); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get uniform location - * - * This method does not perform any query into the program but relies on - * the fact that it depends on a call to queryUniformLocation earlier. - * - * @param name - * @returns {number} - */ - ///////////////////////////////////////////////////////////////////////////// - this.uniformLocation = function (name) { - return m_uniformNameToLocation[name]; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get attribute location - * - * This method does not perform any query into the program but relies on the - * fact that it depends on a call to queryUniformLocation earlier. - * - * @param name - * @returns {number} - */ - ///////////////////////////////////////////////////////////////////////////// - this.attributeLocation = function (name) { - return m_vertexAttributeNameToLocation[name]; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get uniform object using name as the key - * - * @param name - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.uniform = function (name) { - var i; - for (i = 0; i < m_uniforms.length; i += 1) { - if (m_uniforms[i].name() === name) { - return m_uniforms[i]; - } - } - - return null; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Update all uniforms - * - * This method should be used directly unless required - */ - ///////////////////////////////////////////////////////////////////////////// - this.updateUniforms = function (renderState) { - var i; - - for (i = 0; i < m_uniforms.length; i += 1) { - m_uniforms[i].callGL(renderState, - m_uniformNameToLocation[m_uniforms[i].name()]); - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Link shader program - * - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.link = function (renderState) { - renderState.m_context.linkProgram(m_programHandle); - - // If creating the shader program failed, alert - if (!renderState.m_context.getProgramParameter(m_programHandle, - vgl.GL.LINK_STATUS)) { - console.log('[ERROR] Unable to initialize the shader program.'); - return false; - } - - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Use the shader program - */ - ///////////////////////////////////////////////////////////////////////////// - this.use = function (renderState) { - renderState.m_context.useProgram(m_programHandle); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Peform any initialization required - */ - ///////////////////////////////////////////////////////////////////////////// - this._setup = function (renderState) { - if (m_programHandle === 0) { - m_programHandle = renderState.m_context.createProgram(); - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Peform any clean up required when the program gets deleted - */ - ///////////////////////////////////////////////////////////////////////////// - this._cleanup = function (renderState) { - m_this.deleteVertexAndFragment(renderState); - m_this.deleteProgram(renderState); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Delete the shader program - */ - ///////////////////////////////////////////////////////////////////////////// - this.deleteProgram = function (renderState) { - renderState.m_context.deleteProgram(m_programHandle); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Delete vertex and fragment shaders - */ - ///////////////////////////////////////////////////////////////////////////// - this.deleteVertexAndFragment = function (renderState) { - var i; - for (i = 0; i < m_shaders.length; i += 1) { - renderState.m_context.detachShader(m_shaders[i].shaderHandle(renderState)); - renderState.m_context.deleteShader(m_shaders[i].shaderHandle(renderState)); - m_shaders[i].removeContext(renderState); - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Compile and link a shader - */ - ///////////////////////////////////////////////////////////////////////////// - this.compileAndLink = function (renderState) { - var i; - - if (m_compileTimestamp.getMTime() >= this.getMTime()) { - return; - } - - m_this._setup(renderState); - - // Compile shaders - for (i = 0; i < m_shaders.length; i += 1) { - m_shaders[i].compile(renderState); - m_shaders[i].attachShader(renderState, m_programHandle); - } - - m_this.bindAttributes(renderState); - - // link program - if (!m_this.link(renderState)) { - console.log('[ERROR] Failed to link Program'); - m_this._cleanup(renderState); - } - - m_compileTimestamp.modified(); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Bind the program with its shaders - * - * @param renderState - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.bind = function (renderState) { - var i = 0; - - if (m_bindTimestamp.getMTime() < m_this.getMTime()) { - - // Compile shaders - m_this.compileAndLink(renderState); - - m_this.use(renderState); - m_this.bindUniforms(renderState); - m_bindTimestamp.modified(); - } else { - m_this.use(renderState); - } - - // Call update callback. - for (i = 0; i < m_uniforms.length; i += 1) { - m_uniforms[i].update(renderState, m_this); - } - - // Now update values to GL. - m_this.updateUniforms(renderState); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Undo binding of the shader program - * - * @param renderState - */ - ///////////////////////////////////////////////////////////////////////////// - this.undoBind = function (renderState) { - // REF https://www.khronos.org/opengles/sdk/docs/man/xhtml/glUseProgram.xml - // If program is 0, then the current rendering state refers to an invalid - // program object, and the results of vertex and fragment shader execution - // due to any glDrawArrays or glDrawElements commands are undefined - renderState.m_context.useProgram(null); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Bind vertex data - * - * @param renderState - * @param key - */ - ///////////////////////////////////////////////////////////////////////////// - this.bindVertexData = function (renderState, key) { - if (m_vertexAttributes.hasOwnProperty(key)) { - m_vertexAttributes[key].bindVertexData(renderState, key); - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Undo bind vetex data - * - * @param renderState - * @param key - */ - ///////////////////////////////////////////////////////////////////////////// - this.undoBindVertexData = function (renderState, key) { - if (m_vertexAttributes.hasOwnProperty(key)) { - m_vertexAttributes[key].undoBindVertexData(renderState, key); - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Bind uniforms - */ - ///////////////////////////////////////////////////////////////////////////// - this.bindUniforms = function (renderState) { - var i; - for (i = 0; i < m_uniforms.length; i += 1) { - m_uniformNameToLocation[m_uniforms[i].name()] = this - .queryUniformLocation(renderState, m_uniforms[i].name()); - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Bind vertex attributes - */ - ///////////////////////////////////////////////////////////////////////////// - this.bindAttributes = function (renderState) { - var key, name; - for (key in m_vertexAttributes) { - if (m_vertexAttributes.hasOwnProperty(key)) { - name = m_vertexAttributes[key].name(); - renderState.m_context.bindAttribLocation(m_programHandle, key, name); - m_vertexAttributeNameToLocation[name] = key; - } - } - }; - - return m_this; -}; - -inherit(vgl.shaderProgram, vgl.materialAttribute); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global Uint8Array, vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class texture - * - * @class - * @returns {vgl.texture} - */ -/////////////////////////////////////////////////////////////////////////////// -vgl.texture = function () { - 'use strict'; - - if (!(this instanceof vgl.texture)) { - return new vgl.texture(); - } - vgl.materialAttribute.call( - this, vgl.materialAttributeType.Texture); - - this.m_width = 0; - this.m_height = 0; - this.m_depth = 0; - - this.m_textureHandle = null; - this.m_textureUnit = 0; - - this.m_pixelFormat = vgl.GL.RGBA; - this.m_pixelDataType = vgl.GL.UNSIGNED_BYTE; - this.m_internalFormat = vgl.GL.RGBA; - this.m_nearestPixel = false; - - this.m_image = null; - - var m_setupTimestamp = vgl.timestamp(), - m_that = this; - - function activateTextureUnit(renderState) { - switch (m_that.m_textureUnit) { - case 0: - renderState.m_context.activeTexture(vgl.GL.TEXTURE0); - break; - case 1: - renderState.m_context.activeTexture(vgl.GL.TEXTURE1); - break; - case 2: - renderState.m_context.activeTexture(vgl.GL.TEXTURE2); - break; - case 3: - renderState.m_context.activeTexture(vgl.GL.TEXTURE3); - break; - case 4: - renderState.m_context.activeTexture(vgl.GL.TEXTURE4); - break; - case 5: - renderState.m_context.activeTexture(vgl.GL.TEXTURE5); - break; - case 6: - renderState.m_context.activeTexture(vgl.GL.TEXTURE6); - break; - case 7: - renderState.m_context.activeTexture(vgl.GL.TEXTURE7); - break; - case 8: - renderState.m_context.activeTexture(vgl.GL.TEXTURE8); - break; - case 9: - renderState.m_context.activeTexture(vgl.GL.TEXTURE9); - break; - case 10: - renderState.m_context.activeTexture(vgl.GL.TEXTURE10); - break; - case 11: - renderState.m_context.activeTexture(vgl.GL.TEXTURE11); - break; - case 12: - renderState.m_context.activeTexture(vgl.GL.TEXTURE12); - break; - case 13: - renderState.m_context.activeTexture(vgl.GL.TEXTURE13); - break; - case 14: - renderState.m_context.activeTexture(vgl.GL.TEXTURE14); - break; - case 15: - renderState.m_context.activeTexture(vgl.GL.TEXTURE15); - break; - default: - throw '[error] Texture unit ' + m_that.m_textureUnit + - ' is not supported'; - } - } - - ///////////////////////////////////////////////////////////////////////////// - /** - * Create texture, update parameters, and bind data - * - * @param renderState - */ - ///////////////////////////////////////////////////////////////////////////// - this.setup = function (renderState) { - // Activate the texture unit first - activateTextureUnit(renderState); - - 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, - this.m_nearestPixel ? vgl.GL.NEAREST : vgl.GL.LINEAR); - renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D, - vgl.GL.TEXTURE_MAG_FILTER, - this.m_nearestPixel ? vgl.GL.NEAREST : 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); - - if (this.m_image !== null) { - renderState.m_context.pixelStorei(vgl.GL.UNPACK_ALIGNMENT, 1); - renderState.m_context.pixelStorei(vgl.GL.UNPACK_FLIP_Y_WEBGL, true); - - this.updateDimensions(); - this.computeInternalFormatUsingImage(); - - // console.log('m_internalFormat ' + this.m_internalFormat); - // console.log('m_pixelFormat ' + this.m_pixelFormat); - // console.log('m_pixelDataType ' + this.m_pixelDataType); - - // FOR now support only 2D textures - renderState.m_context.texImage2D(vgl.GL.TEXTURE_2D, 0, this.m_internalFormat, - this.m_pixelFormat, this.m_pixelDataType, this.m_image); - } else { - renderState.m_context.texImage2D(vgl.GL.TEXTURE_2D, 0, this.m_internalFormat, - this.m_width, this.m_height, 0, this.m_pixelFormat, this.m_pixelDataType, null); - } - - renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, null); - m_setupTimestamp.modified(); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Create texture and if already created use it - * - * @param renderState - */ - ///////////////////////////////////////////////////////////////////////////// - this.bind = function (renderState) { - // TODO Call setup via material setup - if (this.getMTime() > m_setupTimestamp.getMTime()) { - this.setup(renderState); - } - - activateTextureUnit(renderState); - renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, this.m_textureHandle); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Turn off the use of this texture - * - * @param renderState - */ - ///////////////////////////////////////////////////////////////////////////// - this.undoBind = function (renderState) { - renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, null); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get image used by the texture - * - * @returns {vgl.image} - */ - ///////////////////////////////////////////////////////////////////////////// - this.image = function () { - return this.m_image; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set image for the texture - * - * @param {vgl.image} image - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setImage = function (image) { - if (image !== null) { - this.m_image = image; - this.updateDimensions(); - this.modified(); - return true; - } - - return false; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get nearest pixel flag for the texture - * - * @returns boolean - */ - ///////////////////////////////////////////////////////////////////////////// - this.nearestPixel = function () { - return this.m_nearestPixel; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set nearest pixel flag for the texture - * - * @param {boolean} nearest pixel flag - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setNearestPixel = function (nearest) { - nearest = nearest ? true : false; - if (nearest !== this.m_nearestPixel) { - this.m_nearestPixel = nearest; - this.modified(); - return true; - } - return false; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get texture unit of the texture - * - * @returns {number} - */ - ///////////////////////////////////////////////////////////////////////////// - this.textureUnit = function () { - return this.m_textureUnit; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set texture unit of the texture. Default is 0. - * - * @param {number} unit - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setTextureUnit = function (unit) { - if (this.m_textureUnit === unit) { - return false; - } - - this.m_textureUnit = unit; - this.modified(); - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get width of the texture - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.width = function () { - return this.m_width; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set width of the texture - * - * @param {number} width - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setWidth = function (width) { - if (m_that.m_width !== width) { - m_that.m_width = width; - m_that.modified(); - return true; - } - - return false; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get width of the texture - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.height = function () { - return m_that.m_height; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set height of the texture - * - * @param {number} height - * @returns {vgl.texture} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setHeight = function (height) { - if (m_that.m_height !== height) { - m_that.m_height = height; - m_that.modified(); - return true; - } - - return false; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get depth of the texture - * - * @returns {number} - */ - ///////////////////////////////////////////////////////////////////////////// - this.depth = function () { - return this.m_depth; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set depth of the texture - * - * @param {number} depth - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setDepth = function (depth) { - if (this.m_image === null) { - return false; - } - - this.m_depth = depth; - this.modified(); - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get the texture handle (id) of the texture - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.textureHandle = function () { - return this.m_textureHandle; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get internal format of the texture - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.internalFormat = function () { - return this.m_internalFormat; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set internal format of the texture - * - * @param internalFormat - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setInternalFormat = function (internalFormat) { - if (this.m_internalFormat !== internalFormat) { - this.m_internalFormat = internalFormat; - this.modified(); - return true; - } - - return false; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get pixel format of the texture - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.pixelFormat = function () { - return this.m_pixelFormat; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set pixel format of the texture - * - * @param pixelFormat - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setPixelFormat = function (pixelFormat) { - if (this.m_image === null) { - return false; - } - - this.m_pixelFormat = pixelFormat; - this.modified(); - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get pixel data type - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.pixelDataType = function () { - return this.m_pixelDataType; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set pixel data type - * - * @param pixelDataType - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setPixelDataType = function (pixelDataType) { - if (this.m_image === null) { - return false; - } - - this.m_pixelDataType = pixelDataType; - - this.modified(); - - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Compute internal format of the texture - */ - ///////////////////////////////////////////////////////////////////////////// - this.computeInternalFormatUsingImage = function () { - // Currently image does not define internal format - // and hence it's pixel format is the only way to query - // information on how color has been stored. - // switch (this.m_image.pixelFormat()) { - // case vgl.GL.RGB: - // this.m_internalFormat = vgl.GL.RGB; - // break; - // case vgl.GL.RGBA: - // this.m_internalFormat = vgl.GL.RGBA; - // break; - // case vgl.GL.Luminance: - // this.m_internalFormat = vgl.GL.Luminance; - // break; - // case vgl.GL.LuminanceAlpha: - // this.m_internalFormat = vgl.GL.LuminanceAlpha; - // break; - // // Do nothing when image pixel format is none or undefined. - // default: - // break; - // }; - - // TODO Fix this - this.m_internalFormat = vgl.GL.RGBA; - this.m_pixelFormat = vgl.GL.RGBA; - this.m_pixelDataType = vgl.GL.UNSIGNED_BYTE; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Update texture dimensions - */ - ///////////////////////////////////////////////////////////////////////////// - this.updateDimensions = function () { - if (this.m_image !== null) { - this.m_width = this.m_image.width; - this.m_height = this.m_image.height; - this.m_depth = 0; // Only 2D images are supported now - } - }; - - return this; -}; - -inherit(vgl.texture, vgl.materialAttribute); - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class lookupTable - * - * @class - * @returns {vgl.lookupTable} - */ -/////////////////////////////////////////////////////////////////////////////// -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]; - - this.m_colorTable = //paraview bwr colortable - [0.07514311, 0.468049805, 1, 1, - 0.247872569, 0.498782363, 1, 1, - 0.339526309, 0.528909511, 1, 1, - 0.409505078, 0.558608486, 1, 1, - 0.468487184, 0.588057293, 1, 1, - 0.520796675, 0.617435078, 1, 1, - 0.568724526, 0.646924167, 1, 1, - 0.613686735, 0.676713218, 1, 1, - 0.656658579, 0.707001303, 1, 1, - 0.698372844, 0.738002964, 1, 1, - 0.739424025, 0.769954435, 1, 1, - 0.780330104, 0.803121429, 1, 1, - 0.821573924, 0.837809045, 1, 1, - 0.863634967, 0.874374691, 1, 1, - 0.907017747, 0.913245283, 1, 1, - 0.936129275, 0.938743558, 0.983038586, 1, - 0.943467973, 0.943498599, 0.943398095, 1, - 0.990146732, 0.928791426, 0.917447482, 1, - 1, 0.88332677, 0.861943246, 1, - 1, 0.833985467, 0.803839606, 1, - 1, 0.788626485, 0.750707739, 1, - 1, 0.746206642, 0.701389973, 1, - 1, 0.70590052, 0.654994046, 1, - 1, 0.667019783, 0.610806959, 1, - 1, 0.6289553, 0.568237474, 1, - 1, 0.591130233, 0.526775617, 1, - 1, 0.552955184, 0.485962266, 1, - 1, 0.513776083, 0.445364274, 1, - 1, 0.472800903, 0.404551679, 1, - 1, 0.428977855, 0.363073592, 1, - 1, 0.380759558, 0.320428137, 1, - 0.961891484, 0.313155629, 0.265499262, 1, - 0.916482116, 0.236630659, 0.209939162, 1].map( - function (x) {return x * 255;}); - - ///////////////////////////////////////////////////////////////////////////// - /** - * Create lookup table, initialize parameters, and bind data to it - * - * @param {vgl.renderState} renderState - */ - ///////////////////////////////////////////////////////////////////////////// - this.setup = function (renderState) { - if (this.textureUnit() === 0) { - renderState.m_context.activeTexture(vgl.GL.TEXTURE0); - } else if (this.textureUnit() === 1) { - 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(); - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get color table used by the lookup table - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.colorTable = function () { - return this.m_colorTable; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set color table used by the lookup table - * - * @param colors - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setColorTable = function (colors) { - if (this.m_colorTable === colors) { - return false; - } - - this.m_colorTable = colors; - this.modified(); - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get scalar range - * - * @returns {Array} - */ - ///////////////////////////////////////////////////////////////////////////// - this.range = function () { - return m_range; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set scalar range for the lookup table - * - * @param range - * @returns {boolean} - */ - ///////////////////////////////////////////////////////////////////////////// - this.setRange = function (range) { - if (m_range === range) { - return false; - } - m_range = range; - this.modified(); - return true; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Given a [min,max] range update the lookup table range - * - * @param range - */ - ///////////////////////////////////////////////////////////////////////////// - this.updateRange = function (range) { - if (!(range instanceof Array)) { - console.log('[error] Invalid data type for range. Requires array [min,max]'); - } - - if (range[0] < m_range[0]) { - m_range[0] = range[0]; - this.modified(); - } - - if (range[1] > m_range[1]) { - m_range[1] = range[1]; - this.modified(); - } - }; - - return this; -}; - -inherit(vgl.lookupTable, vgl.texture); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, mat4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class uniform - * - * @param type - * @param name - * @returns {vgl.uniform} OpenGL uniform encapsulation - */ -/////////////////////////////////////////////////////////////////////////////// -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 = []; - - m_dataArray.length = this.getTypeNumberOfComponents(m_type); - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get name of the uniform - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.name = function () { - return m_name; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get type of the uniform - * - * @returns {*} - */ - ///////////////////////////////////////////////////////////////////////////// - this.type = function () { - return m_type; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Get value of the uniform - * - * @returns {Array} - */ - ///////////////////////////////////////////////////////////////////////////// - this.get = function () { - return m_dataArray; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Set value of the uniform - * - * @param value - */ - ///////////////////////////////////////////////////////////////////////////// - this.set = function (value) { - var i = 0; - if (m_dataArray.length === 16) { - for (i = 0; i < 16; i += 1) { - m_dataArray[i] = value[i]; - } - } else if (m_dataArray.length === 9) { - for (i = 0; i < 9; i += 1) { - m_dataArray[i] = value[i]; - } - } else if (m_dataArray.length === 4) { - for (i = 0; i < 4; i += 1) { - m_dataArray[i] = value[i]; - } - } else if (m_dataArray.length === 3) { - for (i = 0; i < 3; i += 1) { - m_dataArray[i] = value[i]; - } - } else if (m_dataArray.length === 2) { - for (i = 0; i < 2; i += 1) { - m_dataArray[i] = value[i]; - } - } else { - m_dataArray[0] = value; - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Call GL and pass updated values to the current shader - * - * @param location - */ - ///////////////////////////////////////////////////////////////////////////// - this.callGL = function (renderState, location) { - if (this.m_numberElements < 1) { - return; - } - - 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); - break; - default: - break; - } - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Virtual method to update the uniform - * - * Should be implemented by the derived class. - * - * @param renderState - * @param program - */ - ///////////////////////////////////////////////////////////////////////////// - this.update = function (renderState, program) { - renderState = renderState; /* unused parameter */ - program = program; /* unused parameter */ - // Should be implemented by the derived class - }; - - return this; -}; - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create new instance of class modelViewUniform - * - * @param name - * @returns {vgl.modelViewUniform} - */ -/////////////////////////////////////////////////////////////////////////////// -vgl.modelViewUniform = function (name) { - 'use strict'; - - if (!(this instanceof vgl.modelViewUniform)) { - return new vgl.modelViewUniform(name); - } - - if (name.length === 0) { - name = 'modelViewMatrix'; - } - - vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); - - this.set(mat4.create()); - - ///////////////////////////////////////////////////////////////////////////// - /** - * Update the uniform given a render state and shader program - * - * @param {vgl.renderState} renderState - * @param {vgl.shaderProgram} program - */ - ///////////////////////////////////////////////////////////////////////////// - this.update = function (renderState, program) { - program = program; /* unused parameter */ - this.set(renderState.m_modelViewMatrix); - }; - - return this; -}; - -inherit(vgl.modelViewUniform, vgl.uniform); - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create new instance of class modelViewOriginUniform. - * - * @param name - * @param {array} origin a triplet of floats. - * @returns {vgl.modelViewUniform} - */ -/////////////////////////////////////////////////////////////////////////////// -vgl.modelViewOriginUniform = function (name, origin) { - 'use strict'; - - if (!(this instanceof vgl.modelViewOriginUniform)) { - return new vgl.modelViewOriginUniform(name, origin); - } - - if (name.length === 0) { - name = 'modelViewMatrix'; - } - origin = origin || [0, 0, 0]; - - var m_origin = [origin[0], origin[1], origin[2] || 0]; - - vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); - - this.set(mat4.create()); - - /** - * Change the origin used by the uniform view matrix. - * - * @param {array} origin a triplet of floats. - */ - this.setOrigin = function (origin) { - origin = origin || [0, 0, 0]; - m_origin = [origin[0], origin[1], origin[2] || 0]; - }; - - ///////////////////////////////////////////////////////////////////////////// - /** - * Update the uniform given a render state and shader program. This offsets - * the modelViewMatrix by the origin, and, if the model view should be - * aligned, aligns it appropriately. The alignment must be done after the - * origin offset to maintain precision. - * - * @param {vgl.renderState} renderState - * @param {vgl.shaderProgram} program - */ - ///////////////////////////////////////////////////////////////////////////// - this.update = function (renderState, program) { - program = program; /* unused parameter */ - var view = mat4.create(); - mat4.translate(view, renderState.m_modelViewMatrix, m_origin); - if (renderState.m_modelViewAlignment) { - var align = renderState.m_modelViewAlignment; - /* view[12] and view[13] are the x and y offsets. align.round is the - * 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.roundx) * align.roundx + align.dx; - view[13] = Math.round(view[13] / align.roundy) * align.roundy + align.dy; - } - this.set(view); - }; - - return this; -}; - -inherit(vgl.modelViewOriginUniform, vgl.uniform); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class projectionUniform - * - * @param name - * @returns {vgl.projectionUniform} - */ -/////////////////////////////////////////////////////////////////////////////// -vgl.projectionUniform = function (name) { - 'use strict'; - - if (!(this instanceof vgl.projectionUniform)) { - return new vgl.projectionUniform(name); - } - - if (name.length === 0) { - name = 'projectionMatrix'; - } - - vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); - - this.set(mat4.create()); - - ///////////////////////////////////////////////////////////////////////////// - /** - * Update the uniform given a render state and shader program - * - * @param renderState - * @param program - */ - ///////////////////////////////////////////////////////////////////////////// - this.update = function (renderState, program) { - program = program; /* unused parameter */ - this.set(renderState.m_projectionMatrix); - }; - - return this; -}; - -inherit(vgl.projectionUniform, vgl.uniform); - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class floatUniform - * - * @param name - * @param value - * @returns {vgl.floatUniform} - */ -/////////////////////////////////////////////////////////////////////////////// -vgl.floatUniform = function (name, value) { - 'use strict'; - - if (!(this instanceof vgl.floatUniform)) { - return new vgl.floatUniform(name, value); - } - - if (name.length === 0) { - name = 'floatUniform'; - } - - value = value === undefined ? 1.0 : value; - - vgl.uniform.call(this, vgl.GL.FLOAT, name); - - this.set(value); -}; - -inherit(vgl.floatUniform, vgl.uniform); - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create new instance of class normalMatrixUniform - * - * @param name - * @returns {vgl.normalMatrixUniform} - */ -/////////////////////////////////////////////////////////////////////////////// -vgl.normalMatrixUniform = function (name) { - 'use strict'; - - if (!(this instanceof vgl.normalMatrixUniform)) { - return new vgl.normalMatrixUniform(name); - } - - if (name.length === 0) { - name = 'normalMatrix'; - } - - vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); - - this.set(mat4.create()); - - ///////////////////////////////////////////////////////////////////////////// - /** - * Update the uniform given a render state and shader program - * - * @param {vgl.renderState} renderState - * @param {vgl.shaderProgram} program - */ - ///////////////////////////////////////////////////////////////////////////// - this.update = function (renderState, program) { - program = program; /* unused parameter */ - this.set(renderState.m_normalMatrix); - }; - - return this; -}; - -inherit(vgl.normalMatrixUniform, vgl.uniform); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Keys to identify vertex attributes - * - * @type {{Position: number, Normal: number, TextureCoordinate: number, - * Color: number, Scalar: number, Scalar2: number, Scalar3: number, - * Scalar4: number, Scalar5: number, Scalar6: number, Scalar7: number, - * CountAttributeIndex: number}} - */ -////////////////////////////////////////////////////////////////////////////// -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 -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of vertexAttribute - * - * @param {string} name - * @returns {vgl.vertexAttribute} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.vertexAttribute = function (name) { - 'use strict'; - - if (!(this instanceof vgl.vertexAttribute)) { - return new vgl.vertexAttribute(name); - } - - var m_name = name; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get name of the vertex attribute - * - * @returns {string} - */ - ////////////////////////////////////////////////////////////////////////////// - this.name = function () { - return m_name; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Bind vertex data to the given render state - * - * @param {vgl.renderState} renderState - * @param {vgl.vertexAttributeKeys} key - */ - ////////////////////////////////////////////////////////////////////////////// - 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)); - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Undo bind vertex data for a given render state - * - * @param {vgl.renderState} renderState - * @param {vgl.vertexAttributeKeys} key - */ - ////////////////////////////////////////////////////////////////////////////// - this.undoBindVertexData = function (renderState, key) { - key = key; /* unused parameter */ - - var program = renderState.m_material.shaderProgram(); - - renderState.m_context.disableVertexAttribArray(program.attributeLocation(m_name)); - }; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -/////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class source - * - * @returns {vgl.source} - */ -/////////////////////////////////////////////////////////////////////////////// -vgl.source = function () { - 'use strict'; - - if (!(this instanceof vgl.source)) { - return new vgl.source(); - } - - vgl.object.call(this); - - ///////////////////////////////////////////////////////////////////////////// - /** - * Virtual function to create a source instance - */ - ///////////////////////////////////////////////////////////////////////////// - this.create = function () { - }; - - return this; -}; - -inherit(vgl.source, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class planeSource - * - * @class - * @returns {vgl.planeSource} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.planeSource = function () { - 'use strict'; - - if (!(this instanceof vgl.planeSource)) { - return new vgl.planeSource(); - } - vgl.source.call(this); - - var m_origin = [0.0, 0.0, 0.0], - m_point1 = [1.0, 0.0, 0.0], - m_point2 = [0.0, 1.0, 0.0], - m_normal = [0.0, 0.0, 1.0], - m_xresolution = 1, - m_yresolution = 1, - m_geom = null; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set origin of the plane - * - * @param x - * @param y - * @param z - */ - //////////////////////////////////////////////////////////////////////////// - this.setOrigin = function (x, y, z) { - m_origin[0] = x; - m_origin[1] = y; - m_origin[2] = z; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set point that defines the first axis of the plane - * - * @param x - * @param y - * @param z - */ - //////////////////////////////////////////////////////////////////////////// - this.setPoint1 = function (x, y, z) { - m_point1[0] = x; - m_point1[1] = y; - m_point1[2] = z; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set point that defines the first axis of the plane - * - * @param x - * @param y - * @param z - */ - //////////////////////////////////////////////////////////////////////////// - this.setPoint2 = function (x, y, z) { - m_point2[0] = x; - m_point2[1] = y; - m_point2[2] = z; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create a plane geometry given input parameters - * - * @returns {null} - */ - //////////////////////////////////////////////////////////////////////////// - this.create = function () { - m_geom = new vgl.geometryData(); - - var x = [], tc = [], v1 = [], v2 = [], - pts = [], i, j, k, ii, numPts, numPolys, - posIndex = 0, normIndex = 0, colorIndex = 0, texCoordIndex = 0, - positions = [], normals = [], colors = [], - texCoords = [], indices = [], tristrip = null, - sourcePositions = null, sourceColors = null, sourceTexCoords; - - x.length = 3; - tc.length = 2; - v1.length = 3; - v2.length = 3; - pts.length = 3; - - // Check input - for (i = 0; i < 3; i += 1) { - v1[i] = m_point1[i] - m_origin[i]; - v2[i] = m_point2[i] - m_origin[i]; - } - - // TODO Compute center and normal - // Set things up; allocate memory - 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; - - for (k = 0, i = 0; i < (m_yresolution + 1); i += 1) { - tc[1] = i / m_yresolution; - - for (j = 0; j < (m_xresolution + 1); j += 1) { - tc[0] = j / m_xresolution; - - for (ii = 0; ii < 3; ii += 1) { - x[ii] = m_origin[ii] + tc[0] * v1[ii] + tc[1] * v2[ii]; - } - - //jshint plusplus: false - positions[posIndex++] = x[0]; - positions[posIndex++] = x[1]; - positions[posIndex++] = x[2]; - - colors[colorIndex++] = 1.0; - colors[colorIndex++] = 1.0; - colors[colorIndex++] = 1.0; - - normals[normIndex++] = m_normal[0]; - normals[normIndex++] = m_normal[1]; - normals[normIndex++] = m_normal[2]; - - texCoords[texCoordIndex++] = tc[0]; - texCoords[texCoordIndex++] = tc[1]; - //jshint plusplus: true - } - } - - /// Generate polygon connectivity - for (i = 0; i < m_yresolution; i += 1) { - for (j = 0; j < m_xresolution; 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; i < numPts; i += 1) { - indices[i] = i; - } - - 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); - - return m_geom; - }; -}; - -inherit(vgl.planeSource, vgl.source); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class pointSource - * - * @class - * @returns {vgl.pointSource} - */ -////////////////////////////////////////////////////////////////////////////// -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; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get positions for the points - */ - //////////////////////////////////////////////////////////////////////////// - this.getPositions = function () { - return m_positions; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set positions for the source - * - * @param positions - */ - //////////////////////////////////////////////////////////////////////////// - this.setPositions = function (positions) { - if (positions instanceof Array) { - m_positions = positions; - } else { - console - .log('[ERROR] Invalid data type for positions. Array is required.'); - } - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get colors for the points - */ - //////////////////////////////////////////////////////////////////////////// - this.getColors = function () { - return m_colors; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set colors for the points - * - * @param colors - */ - //////////////////////////////////////////////////////////////////////////// - this.setColors = function (colors) { - if (colors instanceof Array) { - m_colors = colors; - } else { - console.log('[ERROR] Invalid data type for colors. Array is required.'); - } - - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get size for the points - */ - //////////////////////////////////////////////////////////////////////////// - this.getSize = function () { - return m_size; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set colors for the points - * - * @param colors - */ - //////////////////////////////////////////////////////////////////////////// - this.setSize = function (size) { - m_size = size; - this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set texture coordinates for the points - * - * @param texcoords - */ - //////////////////////////////////////////////////////////////////////////// - this.setTextureCoordinates = function (texcoords) { - if (texcoords instanceof Array) { - m_textureCoords = texcoords; - } else { - console.log('[ERROR] Invalid data type for ' + - 'texture coordinates. Array is required.'); - } - m_this.modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create a point geometry given input parameters - */ - //////////////////////////////////////////////////////////////////////////// - this.create = function () { - m_geom = new vgl.geometryData(); - - if (m_positions.length % 3 !== 0) { - console.log('[ERROR] Invalid length of the points array'); - return; - } - - var numPts = m_positions.length / 3, - i = 0, - indices = [], - pointsPrimitive, - sourcePositions, - sourceColors, - sourceTexCoords, - sourceSize; - - indices.length = numPts; - for (i = 0; i < numPts; i += 1) { - indices[i] = i; - } - - /// Generate array of size if needed - sourceSize = vgl.sourceDataDf(); - if (numPts !== m_size.length) { - for (i = 0; i < numPts; i += 1) { - sourceSize.pushBack(m_size); - } - } else { - sourceSize.setData(m_size); - } - m_geom.addSource(sourceSize); - - pointsPrimitive = new vgl.points(); - pointsPrimitive.setIndices(indices); - - sourcePositions = vgl.sourceDataP3fv(); - sourcePositions.pushBack(m_positions); - m_geom.addSource(sourcePositions); - - if ((m_colors.length > 0) && m_colors.length === m_positions.length) { - sourceColors = vgl.sourceDataC3fv(); - sourceColors.pushBack(m_colors); - m_geom.addSource(sourceColors); - } else if ((m_colors.length > 0) && m_colors.length !== m_positions.length) { - console - .log('[ERROR] Number of colors are different than number of points'); - } - - if (m_textureCoords.length > 0 && - m_textureCoords.length === m_positions.length) { - sourceTexCoords = vgl.sourceDataT2fv(); - sourceTexCoords.pushBack(m_textureCoords); - m_geom.addSource(sourceTexCoords); - } else if (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); - - return m_geom; - }; -}; - -inherit(vgl.pointSource, vgl.source); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class lineSource - * - * @class - * @returns {vgl.lineSource} - */ -////////////////////////////////////////////////////////////////////////////// -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; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set start positions for the lines - * - * @param positions - */ - //////////////////////////////////////////////////////////////////////////// - this.setPositions = function (positions) { - if (positions instanceof Array) { - m_positions = positions; - this.modified(); - return true; - } - - console - .log('[ERROR] Invalid data type for positions. Array is required.'); - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Set colors for the lines - * - * @param colors - */ - //////////////////////////////////////////////////////////////////////////// - this.setColors = function (colors) { - if (colors instanceof Array) { - m_colors = colors; - this.modified(); - return true; - } - - console.log('[ERROR] Invalid data type for colors. Array is required.'); - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create a point geometry given input parameters - */ - //////////////////////////////////////////////////////////////////////////// - this.create = function () { - if (!m_positions) { - console.log('[error] Invalid positions'); - return; - } - - if (m_positions.length % 3 !== 0) { - console.log('[error] Line source requires 3d points'); - return; - } - - if (m_positions.length % 3 !== 0) { - console.log('[ERROR] Invalid length of the points array'); - return; - } - - var m_geom = new vgl.geometryData(), - numPts = m_positions.length / 3, - i, - indices = [], - linesPrimitive, - sourcePositions, - sourceColors; - - indices.length = numPts; - - for (i = 0; i < numPts; i += 1) { - indices[i] = i; - } - - linesPrimitive = new vgl.lines(); - linesPrimitive.setIndices(indices); - - sourcePositions = vgl.sourceDataP3fv(); - sourcePositions.pushBack(m_positions); - m_geom.addSource(sourcePositions); - - if (m_colors && (m_colors.length > 0) && - m_colors.length === m_positions.length) { - sourceColors = vgl.sourceDataC3fv(); - sourceColors.pushBack(m_colors); - m_geom.addSource(sourceColors); - } else if (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); - - return m_geom; - }; -}; - -inherit(vgl.lineSource, vgl.source); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global document, vgl, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class utils - * - * Utility class provides helper functions such as functions to create - * shaders, geometry etc. - * - * @returns {vgl.utils} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils = function () { - 'use strict'; - - if (!(this instanceof vgl.utils)) { - return new vgl.utils(); - } - vgl.object.call(this); - - return this; -}; - -inherit(vgl.utils, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * Helper function to compute power of 2 number - * - * @param value - * @param pow - * - * @returns {number} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.computePowerOfTwo = function (value, pow) { - 'use strict'; - pow = pow || 1; - while (pow < value) { - pow *= 2; - } - return pow; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of default vertex shader that uses a texture - * - * Helper function to create default vertex shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createTextureVertexShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, - vertexShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of default fragment shader that uses a texture - * - * Helper function to create default fragment shader with sampler - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createTextureFragmentShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, - fragmentShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create variation of createTextureFragmentShader which uses texture alpha - * - * Helper function to create default fragment shader with sampler - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createRgbaTextureFragmentShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, - fragmentShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of default vertex shader - * - * Helper function to create default vertex shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createVertexShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, - vertexShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of default vertex shader - * - * Helper function to create default vertex shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPointVertexShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, - vertexShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of vertex shader with a solid color - * - * Helper function to create default vertex shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createVertexShaderSolidColor = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, - vertexShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of vertex shader that passes values through - * for color mapping - * - * Helper function to create default vertex shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createVertexShaderColorMap = function (context, min, max) { - 'use strict'; - min = min; /* unused parameter */ - max = max; /* unused parameter */ - 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'); - return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, - vertexShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of default fragment shader - * - * Helper function to create default fragment shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createFragmentShader = function (context) { - 'use strict'; - var fragmentShaderSource = ['varying mediump vec3 iVertexColor;', - 'uniform mediump float opacity;', - 'void main(void) {', - 'gl_FragColor = vec4(iVertexColor, opacity);', - '}'].join('\n'); - return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, - fragmentShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a Phong vertex shader - * - * Helper function to create Phong vertex shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPhongVertexShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, - vertexShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of Phong fragment shader - * - * Helper function to create Phong fragment shader - * - * NOTE: Shader assumes directional light - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPhongFragmentShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, - fragmentShaderSource); -}; - - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of fragment shader with an assigned constant color. - * - * Helper function to create default fragment shader - * - * @param context - * @returns {vgl.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'); - return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, - fragmentShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of fragment shader that maps values into colors bia lookup table - * - * Helper function to create default fragment shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createFragmentShaderColorMap = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, - fragmentShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of vertex shader for point sprites - * - * Helper function to create default point sprites vertex shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPointSpritesVertexShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, - vertexShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of fragment shader for point sprites - * - * Helper function to create default point sprites fragment shader - * - * @param context - * @returns {vgl.shader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPointSpritesFragmentShader = function (context) { - 'use strict'; - 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'); - return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, - fragmentShaderSource); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of texture material - * - * Helper function to create a texture material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createTextureMaterial = function (isRgba, origin) { - 'use strict'; - var 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.0), - modelViewUniform, - projectionUniform = new vgl.projectionUniform('projectionMatrix'), - samplerUniform = new vgl.uniform(vgl.GL.INT, 'sampler2d'), - opacityUniform = null; - if (origin !== undefined) { - modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', - origin); - } else { - modelViewUniform = 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); - - if (isRgba) { - fragmentShader = vgl.utils.createRgbaTextureFragmentShader(vgl.GL); - } else { - fragmentShader = vgl.utils.createTextureFragmentShader(vgl.GL); - } - opacityUniform = new vgl.floatUniform('opacity', 1.0); - prog.addUniform(opacityUniform); - - prog.addShader(fragmentShader); - prog.addShader(vertexShader); - mat.addAttribute(prog); - mat.addAttribute(blend); - - return mat; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of geometry material - * - * Helper function to create geometry material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createGeometryMaterial = function () { - 'use strict'; - var mat = new vgl.material(), - prog = new vgl.shaderProgram(), - pointSize = 5.0, - opacity = 1.0, - 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'); - - 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); - - return mat; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of geometry material - * - * Helper function to create geometry material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPointGeometryMaterial = function (opacity) { - 'use strict'; - opacity = opacity === undefined ? 1.0 : 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'); - - 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); - - return mat; -}; - - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of geometry material with the phong shader - * - * Helper function to create color phong shaded geometry material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -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.0), - modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), - normalUniform = new vgl.normalMatrixUniform('normalMatrix'), - projectionUniform = new vgl.projectionUniform('projectionMatrix'); - - 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(blend); - mat.addAttribute(prog); - - return mat; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of colored geometry material - * - * Helper function to create color geometry material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -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.0), - opacityUniform = new vgl.floatUniform('opacity', 1.0), - modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), - projectionUniform = new vgl.projectionUniform('projectionMatrix'); - - 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); - - return mat; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of geometry material - * - * Helper function to create geometry material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createColorMappedMaterial = function (lut) { - 'use strict'; - if (!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.0), - opacityUniform = new vgl.floatUniform('opacity', 1.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.FLOAT, 'sampler2d'), - lookupTable = lut; - - 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); - - return mat; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Update color mapped material - * - * @param mat - * @param scalarRange - * @param lut - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.updateColorMappedMaterial = function (mat, lut) { - 'use strict'; - if (!mat) { - console.log('[warning] Invalid material. Nothing to update.'); - return; - } - - if (!lut) { - console.log('[warning] Invalid lookup table. Nothing to update.'); - return; - } - - - var lutMin = mat.shaderProgram().uniform('lutMin'), - lutMax = mat.shaderProgram().uniform('lutMax'); - - lutMin.set(lut.range()[0]); - lutMax.set(lut.range()[1]); - - // This will replace the existing lookup table - mat.setAttribute(lut); -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of solid color material - * - * Helper function to create geometry material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createSolidColorMaterial = function (color) { - 'use strict'; - if (!color) { - color = [1.0, 1.0, 1.0]; - } - - 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.0), - opacityUniform = new vgl.floatUniform('opacity', 1.0), - modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), - projectionUniform = new vgl.projectionUniform('projectionMatrix'); - - 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); - - return mat; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of point sprites material - * - * Helper function to create point sprites material - * - * @returns {vgl.material} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPointSpritesMaterial = function (image, lut) { - 'use strict'; - var scalarRange = lut === undefined ? [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.0), - vertexColorWeightUniform = - new vgl.floatUniform('vertexColorWeight', 0.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(); - - samplerUniform.set(0); - scalarsToColors.set(1); - useScalarsToColors.set(0); - useVertexColors.set(0); - pointSize.set([1.0, 1.0]); - - 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); - - if (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); - return mat; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of an actor that contains a plane geometry - * - * Function to create a plane node This method will create a plane actor - * with texture coordinates, eventually normal, and plane material. - * - * @returns {vgl.actor} - */ -////////////////////////////////////////////////////////////////////////////// -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(); - - 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); - - return actor; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of an actor that contains a texture plane geometry - * - * Helper function to create a plane textured node This method will create - * a plane actor with texture coordinates, eventually normal, and plane - * material. - * - * @returns {vgl.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(); - - 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); - - return actor; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of an actor that contains points - * - * Helper function to create a point node This method will create a point - * actor with texture coordinates, eventually normal, and plane material. - * - * @returns {vgl.actor} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPoints = function (positions, size, colors, texcoords, opacity) { - 'use strict'; - if (!positions) { - console.log('[ERROR] Cannot create points without positions'); - return null; - } - - opacity = opacity === undefined ? 1.0 : opacity; - var mapper = new vgl.mapper(), - pointSource = new vgl.pointSource(), - mat = vgl.utils.createPointGeometryMaterial(opacity), - actor = new vgl.actor(); - - pointSource.setPositions(positions); - if (colors) { - pointSource.setColors(colors); - } - - if (texcoords) { - pointSource.setTextureCoordinates(texcoords); - } - - if (size) { - pointSource.setSize(size); - } else { - pointSource.setSize(1.0); - } - - mapper.setGeometryData(pointSource.create()); - actor.setMapper(mapper); - actor.setMaterial(mat); - - return actor; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of an actor that contains point sprites - * - * Helper function to create a point sprites node This method will create - * a point sprites actor with texture coordinates, normals, and a point sprites - * material. - * - * @returns {vgl.actor} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createPointSprites = function (image, positions, colors, - texcoords) { - 'use strict'; - if (!image) { - console.log('[ERROR] Point sprites requires an image'); - return null; - } - - if (!positions) { - console.log('[ERROR] Cannot create points without positions'); - return null; - } - - var mapper = new vgl.mapper(), - pointSource = new vgl.pointSource(), - mat = vgl.utils.createPointSpritesMaterial(image), - actor = new vgl.actor(); - - pointSource.setPositions(positions); - if (colors) { - pointSource.setColors(colors); - } - - if (texcoords) { - pointSource.setTextureCoordinates(texcoords); - } - - mapper.setGeometryData(pointSource.create()); - actor.setMapper(mapper); - actor.setMaterial(mat); - - return actor; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create lines given positions, colors, and desired length - * - * @param positions - * @param colors - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createLines = function (positions, colors) { - 'use strict'; - if (!positions) { - console.log('[ERROR] Cannot create points without positions'); - return null; - } - - var mapper = new vgl.mapper(), - lineSource = new vgl.lineSource(), - mat = vgl.utils.createGeometryMaterial(), - actor = new vgl.actor(); - - lineSource.setPositions(positions); - if (colors) { - lineSource.setColors(colors); - } - - mapper.setGeometryData(lineSource.create()); - actor.setMapper(mapper); - actor.setMaterial(mat); - - return actor; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create color legend - * - * @param lookupTable - * @param width - * @param height - * @param origin - * @param divs - * @returns {Array} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.createColorLegend = function (varname, lookupTable, origin, - width, height, countMajor, - countMinor) { - 'use strict'; - - if (!lookupTable) { - console.log('[error] Invalid lookup table'); - return []; - } - - ////////////////////////////////////////////////////////////////////////////// - /** - * Create labels for the legend - * - * @param ticks - * @param range - * @param divs - */ - ////////////////////////////////////////////////////////////////////////////// - function createLabels(varname, positions, range) { - if (!positions) { - console.log('[error] Create labels requires positions (x,y,z) array'); - return; - } - - if (positions.length % 3 !== 0) { - console.log('[error] Create labels require positions array contain 3d points'); - return; - } - - if (!range) { - console.log('[error] Create labels requires Valid range'); - return; - } - - var actor = null, - size = vgl.utils.computePowerOfTwo(48), - index = 0, - actors = [], - origin = [], - pt1 = [], - pt2 = [], - delta = (positions[6] - positions[0]), - axisLabelOffset = 4, i; - - origin.length = 3; - pt1.length = 3; - pt2.length = 3; - - // For now just create labels for end points - for (i = 0; i < 2; 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], true); - - 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); - } - - // Create axis label - origin[0] = (positions[0] + positions[positions.length - 3] - size) * 0.5; - 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], true); - 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); - - return actors; - } - - ////////////////////////////////////////////////////////////////////////////// - // TODO Currently we assume that the ticks are laid on x-axis - // and this is on a 2D plane (ignoring Z axis. For now lets - // not draw minor ticks. - /** - * Create ticks and labels - * - * @param originX - * @param originY - * @param originZ - * @param pt1X - * @param pt1Y - * @param pt1Z - * @param pt2X - * @param pt2Y - * @param pt2Z - * @param divs - * @param heightMajor - * @param heightMinor - * @returns {Array} Returns array of vgl.actor - */ - ////////////////////////////////////////////////////////////////////////////// - function createTicksAndLabels(varname, lut, - originX, originY, originZ, - pt1X, pt1Y, pt1Z, - pt2X, pt2Y, pt2Z, - countMajor, countMinor, - heightMajor, heightMinor) { - heightMinor = heightMinor; /* unused parameter */ - var width = pt2X - pt1X, - index = null, - delta = width / countMajor, - positions = [], - actors = []; - - for (index = 0; index <= countMajor; 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); - } - - // TODO: Fix this - //actor = vgl.utils.createLines(positions, null); - //actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute); - //actor.material().setBinNumber(vgl.material.RenderBin.Overlay); - //actors.push(actor); - - actors = actors.concat(createLabels(varname, positions, lut.range())); - return actors; - } - - // TODO Currently we create only one type of legend - var pt1X = origin[0] + width, - pt1Y = origin[1], - pt1Z = 0.0, - pt2X = origin[0], - pt2Y = origin[1] + height, - pt2Z = 0.0, - actors = [], - actor = null, - mat = null, - group = vgl.groupNode(); - - actor = vgl.utils.createTexturePlane( - origin[0], origin[1], origin[2], - pt1X, pt1Y, pt1Z, - pt2X, pt2Y, pt2Z, true - ); - - 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)); - - // TODO This needs to change so that we can return a group node - // which should get appended to the scene graph - return actors; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create 2D texture by rendering text using canvas2D context - * - * @param textToWrite - * @param textSize - * @param color - * @returns {vgl.texture} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.utils.create2DTexture = function (textToWrite, textSize, - color, font, alignment, baseline, bold) { - 'use strict'; - - var canvas = document.getElementById('textRendering'), - ctx = null, - texture = vgl.texture(); - - font = font || 'sans-serif'; - alignment = alignment || 'center'; - baseline = baseline || 'bottom'; - - if (typeof bold === 'undefined') { - bold = true; - } - - if (!canvas) { - canvas = document.createElement('canvas'); - } - ctx = canvas.getContext('2d'); - - canvas.setAttribute('id', 'textRendering'); - canvas.style.display = 'none'; - - // Make width and height equal so that we get pretty looking text. - 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); - - // This determines the text colour, it can take a hex value or rgba value - // (e.g. rgba(255,0,0,0.5)) - ctx.fillStyle = 'rgba(200, 85, 10, 1.0)'; - - // This determines the alignment of text, e.g. left, center, right - ctx.textAlign = alignment; - - // This determines the baseline of the text, e.g. top, middle, bottom - ctx.textBaseline = baseline; - - // This determines the size of the text and the font family used - ctx.font = 4 * textSize + 'px ' + font; - if (bold) { - ctx.font = 'bold ' + ctx.font; - } - - ctx.fillText(textToWrite, canvas.width / 2, canvas.height / 2, canvas.width); - - texture.setImage(canvas); - texture.updateDimensions(); - - return texture; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, vec4, inherit*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class picker - * - * @class vgl.picker - * @returns {vgl.picker} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.picker = function () { - 'use strict'; - - if (!(this instanceof vgl.picker)) { - return new vgl.picker(); - } - vgl.object.call(this); - - /** @private */ - var m_actors = []; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get actors intersected - */ - //////////////////////////////////////////////////////////////////////////// - this.getActors = function () { - return m_actors; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Perform pick operation - */ - //////////////////////////////////////////////////////////////////////////// - this.pick = function (selectionX, selectionY, renderer) { - // Check if variables are acceptable - if (selectionX === undefined) { - return 0; - } - if (selectionY === undefined) { - return 0; - } - if (renderer === undefined) { - return 0; - } - - // Clean list of actors intersected previously - m_actors = []; - - // - var camera = renderer.camera(), - width = renderer.width(), - height = renderer.height(), - fpoint = camera.focalPoint(), - focusWorldPt = vec4.fromValues(fpoint[0], fpoint[1], fpoint[2], 1.0), - focusDisplayPt = renderer.worldToDisplay( - focusWorldPt, camera.viewMatrix(), - camera.projectionMatrix(), width, height), - displayPt = vec4.fromValues(selectionX, - selectionY, focusDisplayPt[2], 1.0), - // Convert selection point into world coordinates - worldPt = renderer.displayToWorld(displayPt, camera.viewMatrix(), - camera.projectionMatrix(), width, height), - cameraPos = camera.position(), ray = [], actors, count, i, bb, - tmin, tmax, tymin, tymax, tzmin, tzmax, actor; - - for (i = 0; i < 3; i += 1) { - ray[i] = worldPt[i] - cameraPos[i]; - } - - // Go through all actors and check if intersects - actors = renderer.sceneRoot().children(); - count = 0; - - for (i = 0; i < actors.length; i += 1) { - actor = actors[i]; - if (actor.visible() === true) { - bb = actor.bounds(); - // Ray-aabb intersection - Smits' method - if (ray[0] >= 0) { - tmin = (bb[0] - cameraPos[0]) / ray[0]; - tmax = (bb[1] - cameraPos[0]) / ray[0]; - } else { - tmin = (bb[1] - cameraPos[0]) / ray[0]; - tmax = (bb[0] - cameraPos[0]) / ray[0]; - } - if (ray[1] >= 0) { - tymin = (bb[2] - cameraPos[1]) / ray[1]; - tymax = (bb[3] - cameraPos[1]) / ray[1]; - } else { - tymin = (bb[3] - cameraPos[1]) / ray[1]; - tymax = (bb[2] - cameraPos[1]) / ray[1]; - } - if ((tmin > tymax) || (tymin > tmax)) { - //jscs:disable disallowKeywords - continue; - //jscs:enable disallowKeywords - } - - - if (tymin > tmin) { - tmin = tymin; - } - if (tymax < tmax) { - tmax = tymax; - } - if (ray[2] >= 0) { - tzmin = (bb[4] - cameraPos[2]) / ray[2]; - tzmax = (bb[5] - cameraPos[2]) / ray[2]; - } else { - tzmin = (bb[5] - cameraPos[2]) / ray[2]; - tzmax = (bb[4] - cameraPos[2]) / ray[2]; - } - if ((tmin > tzmax) || (tzmin > tmax)) { - //jscs:disable disallowKeywords - continue; - //jscs:enable disallowKeywords - } - if (tzmin > tmin) { - tmin = tzmin; - } - if (tzmax < tmax) { - tmax = tzmax; - } - - m_actors[count] = actor; - count += 1; - } - } - return count; - }; - - return this; -}; - -inherit(vgl.picker, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, $*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of shapefile reader - * - * This contains code that reads a shapefile and produces vgl geometries - * - * @class - * @returns {vgl.shapefileReader} - */ -////////////////////////////////////////////////////////////////////////////// -vgl.shapefileReader = function () { - 'use strict'; - - if (!(this instanceof vgl.shapefileReader)) { - return new vgl.shapefileReader(); - } - - var m_that = this; - var SHP_NULL = 0; - var SHP_POINT = 1; - var SHP_POLYGON = 5; - var SHP_POLYLINE = 3; - - this.int8 = function (data, offset) { - return data.charCodeAt (offset); - }; - - /*jshint bitwise: false */ - this.bint32 = function (data, offset) { - return ( - ((data.charCodeAt (offset) & 0xff) << 24) + - ((data.charCodeAt (offset + 1) & 0xff) << 16) + - ((data.charCodeAt (offset + 2) & 0xff) << 8) + - (data.charCodeAt (offset + 3) & 0xff) - ); - }; - - this.lint32 = function (data, offset) { - return ( - ((data.charCodeAt (offset + 3) & 0xff) << 24) + - ((data.charCodeAt (offset + 2) & 0xff) << 16) + - ((data.charCodeAt (offset + 1) & 0xff) << 8) + - (data.charCodeAt (offset) & 0xff) - ); - }; - - this.bint16 = function (data, offset) { - return ( - ((data.charCodeAt (offset) & 0xff) << 8) + - (data.charCodeAt (offset + 1) & 0xff) - ); - }; - - this.lint16 = function (data, offset) { - return ( - ((data.charCodeAt (offset + 1) & 0xff) << 8) + - (data.charCodeAt (offset) & 0xff) - ); - }; - - this.ldbl64 = function (data, offset) { - var b0 = data.charCodeAt (offset) & 0xff; - var b1 = data.charCodeAt (offset + 1) & 0xff; - var b2 = data.charCodeAt (offset + 2) & 0xff; - var b3 = data.charCodeAt (offset + 3) & 0xff; - var b4 = data.charCodeAt (offset + 4) & 0xff; - var b5 = data.charCodeAt (offset + 5) & 0xff; - var b6 = data.charCodeAt (offset + 6) & 0xff; - var b7 = data.charCodeAt (offset + 7) & 0xff; - - var sign = 1 - 2 * (b7 >> 7); - var exp = (((b7 & 0x7f) << 4) + ((b6 & 0xf0) >> 4)) - 1023; - //var frac = (b6 & 0x0f) * Math.pow (2, -4) + b5 * Math.pow (2, -12) + b4 * - // Math.pow (2, -20) + b3 * Math.pow (2, -28) + b2 * Math.pow (2, -36) + b1 * - // Math.pow (2, -44) + b0 * Math.pow (2, -52); - - //return sign * (1 + frac) * Math.pow (2, exp); - var frac = (b6 & 0x0f) * 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 = data.charCodeAt (offset) & 0xff; - var b1 = data.charCodeAt (offset + 1) & 0xff; - var b2 = data.charCodeAt (offset + 2) & 0xff; - var b3 = data.charCodeAt (offset + 3) & 0xff; - - var sign = 1 - 2 * (b3 >> 7); - var exp = (((b3 & 0x7f) << 1) + ((b2 & 0xfe) >> 7)) - 127; - var frac = (b2 & 0x7f) * Math.pow (2, 16) + b1 * Math.pow (2, 8) + b0; - - return sign * (1 + frac * Math.pow (2, -23)) * Math.pow (2, exp); - }; - /*jshint bitwise: true */ - - this.str = function (data, offset, length) { - var chars = []; - var index = offset; - while (index < offset + length) { - var c = data[index]; - if (c.charCodeAt (0) !== 0) { - chars.push (c); - } else { - break; - } - index += 1; - } - return chars.join (''); - }; - - this.readHeader = function (data) { - var code = this.bint32(data, 0); - var length = this.bint32(data, 24); - var version = this.lint32(data, 28); - var shapetype = this.lint32(data, 32); - - /* - var xmin = this.ldbl64(data, 36); - var ymin = this.ldbl64(data, 44); - var xmax = this.ldbl64(data, 52); - var ymax = this.ldbl64(data, 60); - */ - return { - code: code, - length: length, - version: version, - shapetype: shapetype - // bounds: new Box (vect (xmin, ymin), vect (xmax, ymax)) - }; - }; - - this.loadShx = function (data) { - var indices = []; - var appendIndex = function (offset) { - indices.push (2 * m_that.bint32(data, offset)); - return offset + 8; - }; - var offset = 100; - while (offset < data.length) { - offset = appendIndex (offset); - } - return indices; - }; - - this.Shapefile = function (options) { - var path = options.path; - $.ajax ({ - url: path + '.shx', - mimeType: 'text/plain; charset=x-user-defined', - success: function (data) { - var indices = this.loadShx(data); - $.ajax ({ - url: path + '.shp', - mimeType: 'text/plain; charset=x-user-defined', - success: function (data) { - $.ajax ({ - url: path + '.dbf', - mimeType: 'text/plain; charset=x-user-defined', - success: function (dbf_data) { - var layer = this.loadShp (data, dbf_data, indices, options); - options.success (layer); - } - }); - } - }); - } - }); - }; - - this.localShapefile = function (options) { - var shxFile = options.shx; - var shpFile = options.shp; - var dbfFile = options.dbf; - var shxReader = new FileReader(); - shxReader.onloadend = function () { - var indices = m_that.loadShx(shxReader.result); - var shpReader = new FileReader(); - - shpReader.onloadend = function () { - var shpData = shpReader.result; - - var dbfReader = new FileReader(); - dbfReader.onloadend = function () { - var dbfData = dbfReader.result; - var layer = m_that.loadShp(shpData, dbfData, indices, options); - options.success(layer); - }; - dbfReader.readAsBinaryString(dbfFile); - }; - shpReader.readAsBinaryString(shpFile); - }; - shxReader.readAsBinaryString(shxFile); - }; - - this.loadDBF = function (data) { - var readHeader = function (offset) { - var name = m_that.str(data, offset, 10); - var type = m_that.str(data, offset + 11, 1); - var length = m_that.int8(data, offset + 16); - return { - name: name, - type: type, - length: length - }; - }; - - // Level of the dBASE file - var level = m_that.int8(data, 0); - if (level === 4) { - throw 'Level 7 dBASE not supported'; - } - - // Date of last update - /* - var year = m_that.int8(data, 1); - var month = m_that.int8(data, 2); - var day = m_that.int8(data, 3); - */ - - var num_entries = m_that.lint32(data, 4); - var header_size = m_that.lint16(data, 8); - var record_size = m_that.lint16(data, 10); - - var FIELDS_START = 32; - var HEADER_LENGTH = 32; - - var header_offset = FIELDS_START; - var headers = []; - while (header_offset < header_size - 1) { - headers.push (readHeader(header_offset)); - header_offset += HEADER_LENGTH; - } - - var records = []; - var record_offset = header_size; - while (record_offset < header_size + num_entries * record_size) { - var declare = m_that.str(data, record_offset, 1); - if (declare === '*') { - // Record size in the header include the size of the delete indicator - record_offset += record_size; - } else { - // Move offset to the start of the actual data - record_offset += 1; - var record = {}; - for (var i = 0; i < headers.length; i += 1) { - var header = headers[i]; - var value; - if (header.type === 'C') { - value = m_that.str(data, record_offset, header.length).trim (); - } else if (header.type === 'N') { - value = parseFloat (m_that.str (data, record_offset, header.length)); - } - record_offset += header.length; - record[header.name] = value; - } - records.push(record); - } - } - return records; - }; - - this.loadShp = function (data, dbf_data, indices, options) { - options = options; /* unused parameter */ - var features = []; - var readRing = function (offset, start, end) { - var ring = []; - for (var i = end - 1; i >= start; i -= 1) { - var x = m_that.ldbl64(data, offset + 16 * i); - var y = m_that.ldbl64(data, offset + 16 * i + 8); - ring.push ([x, y]); - } - //if (ring.length <= 3) - // return []; - return ring; - }; - - var readRecord = function (offset) { - // var index = m_that.bint32(data, offset); - // var record_length = m_that.bint32(data, offset + 4); - var record_offset = offset + 8; - var geom_type = m_that.lint32(data, record_offset); - var num_parts, num_points, parts_start, points_start, i, - start, end, ring, rings; - - if (geom_type === SHP_NULL) { - console.log ('NULL Shape'); - //return offset + 12; - } else if (geom_type === SHP_POINT) { - var x = m_that.ldbl64(data, record_offset + 4); - var y = m_that.ldbl64(data, record_offset + 12); - - features.push ({ - type: 'Point', - attr: {}, - geom: [[x, y]] - }); - } else if (geom_type === SHP_POLYGON) { - 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 = []; - for (i = 0; i < num_parts; i += 1) { - start = m_that.lint32(data, parts_start + i * 4); - if (i + 1 < num_parts) { - end = m_that.lint32(data, parts_start + (i + 1) * 4); - } else { - end = num_points; - } - ring = readRing (points_start, start, end); - rings.push (ring); - } - features.push ({ - type: 'Polygon', - attr: {}, - geom: [rings] - }); - } else if (geom_type === SHP_POLYLINE) { - 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 = []; - for (i = 0; i < num_parts; i += 1) { - start = m_that.lint32(data, parts_start + i * 4); - if (i + 1 < num_parts) { - end = m_that.lint32(data, parts_start + (i + 1) * 4); - } else { - end = num_points; - } - ring = readRing (points_start, start, end); - rings.push (ring); - } - features.push ({ - type: 'Polyline', - attr: {}, - geom: [rings] - }); - } else { - throw 'Not Implemented: ' + geom_type; - } - //return offset + 2 * record_length + SHP_HEADER_LEN; - }; - - var attr = this.loadDBF(dbf_data), i; - - //var offset = 100; - //while (offset < length * 2) { - // offset = readRecord (offset); - //} - for (i = 0; i < indices.length; i += 1) { - var offset = indices[i]; - readRecord (offset); - } - - var layer = []; //new Layer (); - - for (i = 0; i < features.length; i += 1) { - var feature = features[i]; - feature.attr = attr[i]; - layer.push (feature); - } - return layer; - }; - - return this; -}; - -////////////////////////////////////////////////////////////////////////////// -/** - * @module vgl - */ - -/*global vgl, mat4, unescape, Float32Array, Int8Array, Uint16Array*/ -////////////////////////////////////////////////////////////////////////////// - -////////////////////////////////////////////////////////////////////////////// -// -// vbgModule.vtkReader class -// This contains code that unpack a json base64 encoded vtkdataset, -// such as those produced by ParaView's webGL exporter (where much -// of the code originated from) and convert it to VGL representation. -// -////////////////////////////////////////////////////////////////////////////// - -vgl.vtkReader = function () { - 'use strict'; - - if (!(this instanceof vgl.vtkReader)) { - return new vgl.vtkReader(); - } - - var m_base64Chars = - ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', - 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'], - m_reverseBase64Chars = [], - m_vtkRenderedList = {}, - m_vtkObjectCount = 0, - m_vtkScene = null, - m_node = null, - END_OF_INPUT = -1, - m_base64Str = '', - m_base64Count = 0, - m_pos = 0, - m_viewer = null, - i = 0; - - //initialize the array here if not already done. - if (m_reverseBase64Chars.length === 0) { - for (i = 0; i < m_base64Chars.length; i += 1) { - m_reverseBase64Chars[m_base64Chars[i]] = i; - } - } - - - - //////////////////////////////////////////////////////////////////////////// - /** - * ntos - * - * @param n - * @returns unescaped n - */ - //////////////////////////////////////////////////////////////////////////// - this.ntos = function (n) { - var unN; - - unN = n.toString(16); - if (unN.length === 1) { - unN = '0' + unN; - } - unN = '%' + unN; - - return unescape(unN); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * readReverseBase64 - * - * @returns - */ - //////////////////////////////////////////////////////////////////////////// - this.readReverseBase64 = function () { - var nextCharacter; - - if (!m_base64Str) { - return END_OF_INPUT; - } - - while (true) { - if (m_base64Count >= m_base64Str.length) { - return END_OF_INPUT; - } - nextCharacter = m_base64Str.charAt(m_base64Count); - m_base64Count += 1; - - if (m_reverseBase64Chars[nextCharacter]) { - return m_reverseBase64Chars[nextCharacter]; - } - if (nextCharacter === 'A') { - return 0; - } - } - - return END_OF_INPUT; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * decode64 - * - * @param str - * @returns result - */ - //////////////////////////////////////////////////////////////////////////// - this.decode64 = function (str) { - var result = '', - inBuffer = new Array(4), - done = false; - - m_base64Str = str; - m_base64Count = 0; - - while (!done && - (inBuffer[0] = this.readReverseBase64()) !== END_OF_INPUT && - (inBuffer[1] = this.readReverseBase64()) !== END_OF_INPUT) { - inBuffer[2] = this.readReverseBase64(); - inBuffer[3] = this.readReverseBase64(); - /*jshint bitwise: false */ - result += this.ntos((((inBuffer[0] << 2) & 0xff) | inBuffer[1] >> 4)); - if (inBuffer[2] !== END_OF_INPUT) { - result += this.ntos((((inBuffer[1] << 4) & 0xff) | inBuffer[2] >> 2)); - if (inBuffer[3] !== END_OF_INPUT) { - result += this.ntos((((inBuffer[2] << 6) & 0xff) | inBuffer[3])); - } else { - done = true; - } - } else { - done = true; - } - /*jshint bitwise: true */ - } - - return result; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * readNumber - * - * @param ss - * @returns v - */ - //////////////////////////////////////////////////////////////////////////// - this.readNumber = function (ss) { - //jshint plusplus: false, bitwise: false - var v = ((ss[m_pos++]) + - (ss[m_pos++] << 8) + - (ss[m_pos++] << 16) + - (ss[m_pos++] << 24)); - //jshint plusplus: true, bitwise: true - return v; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * readF3Array - * - * @param numberOfPoints - * @param ss - * @returns points - */ - //////////////////////////////////////////////////////////////////////////// - this.readF3Array = function (numberOfPoints, ss) { - var size = numberOfPoints * 4 * 3, test = new Int8Array(size), - points = null, i; - - for (i = 0; i < size; i += 1) { - test[i] = ss[m_pos]; - m_pos += 1; - } - - points = new Float32Array(test.buffer); - - return points; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * readColorArray - * - * @param numberOfPoints - * @param ss - * @param vglcolors - * @returns points - */ - //////////////////////////////////////////////////////////////////////////// - this.readColorArray = function (numberOfPoints, ss, vglcolors) { - var i, idx = 0, tmp = new Array(numberOfPoints * 3); - //jshint plusplus: false - for (i = 0; i < numberOfPoints; i += 1) { - tmp[idx++] = ss[m_pos++] / 255.0; - tmp[idx++] = ss[m_pos++] / 255.0; - tmp[idx++] = ss[m_pos++] / 255.0; - m_pos++; - } - //jshint plusplus: true - vglcolors.insert(tmp); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * parseObject - * - * @param buffer - */ - //////////////////////////////////////////////////////////////////////////// - this.parseObject = function (vtkObject) { - var geom = new vgl.geometryData(), mapper = vgl.mapper(), ss = [], - type = null, data = null, size, matrix = null, material = null, - actor, colorMapData, shaderProg, opacityUniform, lookupTable, - colorTable, windowSize, width, height, position; - - //dehexlify - //data = this.decode64(vtkObject.data); - data = atob(vtkObject.data); - //jshint bitwise: false - for (i = 0; i < data.length; i += 1) { - ss[i] = data.charCodeAt(i) & 0xff; - } - //jshint bitwise: true - - //Determine the Object type - m_pos = 0; - size = this.readNumber(ss); - type = String.fromCharCode(ss[m_pos]); - m_pos += 1; - geom.setName(type); - - // Lines - if (type === 'L') { - matrix = this.parseLineData(geom, ss); - material = vgl.utils.createGeometryMaterial(); - // Mesh - } else if (type === 'M') { - matrix = this.parseMeshData(geom, ss); - material = vgl.utils.createPhongMaterial(); - // Points - } else if (type === 'P') { - matrix = this.parsePointData(geom, ss); - material = vgl.utils.createGeometryMaterial(); - // ColorMap - } else if (type === 'C') { - colorMapData = this.parseColorMapData(geom, ss, size); - colorTable = []; - - for (i = 0; i < colorMapData.colors.length; i += 1) { - colorTable.push(colorMapData.colors[i][1]); - colorTable.push(colorMapData.colors[i][2]); - colorTable.push(colorMapData.colors[i][3]); - colorTable.push(colorMapData.colors[i][0] * 255); - } - - lookupTable = new vgl.lookupTable(); - lookupTable.setColorTable(colorTable); - - windowSize = m_viewer.renderWindow().windowSize(); - width = colorMapData.size[0] * windowSize[0]; - height = colorMapData.size[1] * windowSize[1]; - - position = [colorMapData.position[0] * windowSize[0], - (1 - colorMapData.position[1]) * windowSize[1], 0]; - position[1] = position[1] - height; - - // For now hardcode the height - height = 30; - - return vgl.utils.createColorLegend(colorMapData.title, - lookupTable, position, width, height, 3, 0); - // Unknown - } else { - console.log('Ignoring unrecognized encoded data type ' + type); - } - - mapper.setGeometryData(geom); - - //default opacity === solid. If were transparent, set it lower. - if (vtkObject.hasTransparency) { - shaderProg = material.shaderProgram(); - opacityUniform = shaderProg.uniform('opacity'); - console.log('opacity ', vtkObject.opacity); - opacityUniform.set(vtkObject.opacity); - material.setBinNumber(1000); - } - - actor = vgl.actor(); - actor.setMapper(mapper); - actor.setMaterial(material); - actor.setMatrix(mat4.transpose(mat4.create(), matrix)); - - return [actor]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * parseLineData - * - * @param geom, ss - * @returns matrix - */ - //////////////////////////////////////////////////////////////////////////// - this.parseLineData = function (geom, ss) { - var vglpoints = null, vglcolors = null, vgllines = null, - matrix = mat4.create(), - numberOfIndex, numberOfPoints, points, - temp, index, size, m, i, - p = null, idx = 0; - - numberOfPoints = this.readNumber(ss); - p = new Array(numberOfPoints * 3); - - //Getting Points - vglpoints = new vgl.sourceDataP3fv(); - points = this.readF3Array(numberOfPoints, ss); - - //jshint plusplus: false - for (i = 0; i < numberOfPoints; i += 1) { - p[idx++] = points[i * 3/*+0*/]; - p[idx++] = points[i * 3 + 1]; - p[idx++] = points[i * 3 + 2]; - } - //jshint plusplus: true - vglpoints.insert(p); - geom.addSource(vglpoints); - - //Getting Colors - vglcolors = new vgl.sourceDataC3fv(); - this.readColorArray(numberOfPoints, ss, vglcolors); - geom.addSource(vglcolors); - - //Getting connectivity - vgllines = new vgl.lines(); - geom.addPrimitive(vgllines); - numberOfIndex = this.readNumber(ss); - - temp = new Int8Array(numberOfIndex * 2); - for (i = 0; i < numberOfIndex * 2; i += 1) { - temp[i] = ss[m_pos]; - m_pos += 1; - } - - index = new Uint16Array(temp.buffer); - vgllines.setIndices(index); - vgllines.setPrimitiveType(vgl.GL.LINES); - - //Getting Matrix - size = 16 * 4; - temp = new Int8Array(size); - for (i = 0; i < size; i += 1) { - temp[i] = ss[m_pos]; - m_pos += 1; - } - - m = new Float32Array(temp.buffer); - mat4.copy(matrix, m); - - return matrix; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * parseMeshData - * - * @param geom, ss - * @returns matrix - */ - //////////////////////////////////////////////////////////////////////////// - this.parseMeshData = function (geom, ss) { - var vglpoints = null, vglcolors = null, - normals = null, matrix = mat4.create(), - vgltriangles = null, numberOfIndex, numberOfPoints, - points, temp, index, size, m, i, tcoord, - pn = null, idx = 0; - - numberOfPoints = this.readNumber(ss); - pn = new Array(numberOfPoints * 6); - //Getting Points - vglpoints = new vgl.sourceDataP3N3f(); - points = this.readF3Array(numberOfPoints, ss); - - //Getting Normals - normals = this.readF3Array(numberOfPoints, ss); - //jshint plusplus: false - for (i = 0; i < numberOfPoints; i += 1) { - pn[idx++] = points[i * 3/*+0*/]; - pn[idx++] = points[i * 3 + 1]; - pn[idx++] = points[i * 3 + 2]; - pn[idx++] = normals[i * 3/*+0*/]; - pn[idx++] = normals[i * 3 + 1]; - pn[idx++] = normals[i * 3 + 2]; - } - //jshint plusplus: true - vglpoints.insert(pn); - geom.addSource(vglpoints); - - //Getting Colors - vglcolors = new vgl.sourceDataC3fv(); - this.readColorArray(numberOfPoints, ss, vglcolors); - geom.addSource(vglcolors); - - //Getting connectivity - temp = []; - vgltriangles = new vgl.triangles(); - numberOfIndex = this.readNumber(ss); - - temp = new Int8Array(numberOfIndex * 2); - for (i = 0; i < numberOfIndex * 2; i += 1) { - temp[i] = ss[m_pos]; - m_pos += 1; - } - - index = new Uint16Array(temp.buffer); - vgltriangles.setIndices(index); - geom.addPrimitive(vgltriangles); - - //Getting Matrix - size = 16 * 4; - temp = new Int8Array(size); - for (i = 0; i < size; i += 1) { - temp[i] = ss[m_pos]; - m_pos += 1; - } - - m = new Float32Array(temp.buffer); - mat4.copy(matrix, m); - - //Getting TCoord - //TODO: renderer is not doing anything with this yet - tcoord = null; - - return matrix; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * parsePointData - * - * @param geom, ss - * @returns matrix - */ - //////////////////////////////////////////////////////////////////////////// - this.parsePointData = function (geom, ss) { - var numberOfPoints, points, indices, temp, size, - matrix = mat4.create(), vglpoints = null, - vglcolors = null, vglVertexes = null, m, - p = null, idx = 0; - - numberOfPoints = this.readNumber(ss); - p = new Array(numberOfPoints * 3); - - //Getting Points and creating 1:1 connectivity - vglpoints = new vgl.sourceDataP3fv(); - points = this.readF3Array(numberOfPoints, ss); - - indices = new Uint16Array(numberOfPoints); - - //jshint plusplus: false - for (i = 0; i < numberOfPoints; i += 1) { - indices[i] = i; - p[idx++] = points[i * 3/*+0*/]; - p[idx++] = points[i * 3 + 1]; - p[idx++] = points[i * 3 + 2]; - } - //jshint plusplus: true - vglpoints.insert(p); - geom.addSource(vglpoints); - - //Getting Colors - vglcolors = new vgl.sourceDataC3fv(); - this.readColorArray(numberOfPoints, ss, vglcolors); - geom.addSource(vglcolors); - - //Getting connectivity - vglVertexes = new vgl.points(); - vglVertexes.setIndices(indices); - geom.addPrimitive(vglVertexes); - - //Getting matrix - size = 16 * 4; - temp = new Int8Array(size); - for (i = 0; i < size; i += 1) { - temp[i] = ss[m_pos]; - m_pos += 1; - } - - m = new Float32Array(temp.buffer); - mat4.copy(matrix, m); - - return matrix; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * parseColorMapData - * - * @param geom, ss - * @returns matrix - */ - //////////////////////////////////////////////////////////////////////////// - this.parseColorMapData = function (geom, ss, numColors) { - - var tmpArray, size, xrgb, i, c, obj = {}; - - // Set number of colors - obj.numOfColors = numColors; - - // Getting Position - size = 8; - tmpArray = new Int8Array(size); - for (i = 0; i < size; i += 1) { - tmpArray[i] = ss[m_pos]; - m_pos += 1; - } - obj.position = new Float32Array(tmpArray.buffer); - - // Getting Size - size = 8; - tmpArray = new Int8Array(size); - for (i = 0; i < size; i += 1) { - tmpArray[i] = ss[m_pos]; - m_pos += 1; - } - obj.size = new Float32Array(tmpArray.buffer); - - //Getting Colors - obj.colors = []; - //jshint plusplus: false - for (c = 0; c < obj.numOfColors; c += 1) { - tmpArray = new Int8Array(4); - for (i = 0; i < 4; 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; - } - - obj.orientation = ss[m_pos++]; - obj.numOfLabels = ss[m_pos++]; - obj.title = ''; - while (m_pos < ss.length) { - obj.title += String.fromCharCode(ss[m_pos++]); - } - //jshint plusplus: true - - return obj; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * parseSceneMetadata - * - * @param data - * @returns renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.parseSceneMetadata = function (renderer, layer) { - - var sceneRenderer = m_vtkScene.Renderers[layer], - camera = renderer.camera(), bgc, localWidth, localHeight; - - localWidth = (sceneRenderer.size[0] - sceneRenderer.origin[0]) * m_node.width; - localHeight = (sceneRenderer.size[1] - sceneRenderer.origin[1]) * m_node.height; - renderer.resize(localWidth, localHeight); - - /// We are setting the center to the focal point because of - /// a possible paraview web bug. The center of rotation isn't - /// getting updated correctly on resetCamera. - camera.setCenterOfRotation( - [sceneRenderer.LookAt[1], sceneRenderer.LookAt[2], - sceneRenderer.LookAt[3]]); - camera.setViewAngleDegrees(sceneRenderer.LookAt[0]); - camera.setPosition( - sceneRenderer.LookAt[7], sceneRenderer.LookAt[8], - sceneRenderer.LookAt[9]); - camera.setFocalPoint( - sceneRenderer.LookAt[1], sceneRenderer.LookAt[2], - sceneRenderer.LookAt[3]); - camera.setViewUpDirection( - sceneRenderer.LookAt[4], sceneRenderer.LookAt[5], - sceneRenderer.LookAt[6]); - - if (layer === 0) { - bgc = sceneRenderer.Background1; - renderer.setBackgroundColor(bgc[0], bgc[1], bgc[2], 1); - } else { - renderer.setResizable(false); - } - renderer.setLayer(layer); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * initScene - * - * @returns viewer - */ - //////////////////////////////////////////////////////////////////////////// - this.initScene = function () { - var renderer, layer; - - if (m_vtkScene === null) { - return m_viewer; - } - for (layer = m_vtkScene.Renderers.length - 1; layer >= 0; layer -= 1) { - - renderer = this.getRenderer(layer); - this.parseSceneMetadata(renderer, layer); - } - - return m_viewer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * createViewer - Creates a viewer object. - * - * @param - * - * @returns viewer - */ - //////////////////////////////////////////////////////////////////////////// - this.createViewer = function (node) { - var interactorStyle; - - if (m_viewer === null) { - 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); - } - - return m_viewer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * deleteViewer - Deletes the viewer object associated with the reader. - * - * @returns void - */ - //////////////////////////////////////////////////////////////////////////// - this.deleteViewer = function () { - m_vtkRenderedList = {}; - m_viewer = null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * updateCanvas - - * - * @param - * - * @returns void - */ - //////////////////////////////////////////////////////////////////////////// - this.updateCanvas = function (node) { - m_node = node; - m_viewer.renderWindow().resize(node.width, node.height); - - return m_viewer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * clearVtkObjectData - Clear out the list of VTK geometry data. - * - * @param void - * @returns void - */ - //////////////////////////////////////////////////////////////////////////// - this.numObjects = function () { - return m_vtkObjectCount; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * getRenderer - Gets (or creates) the renderer for a layer. - * - * @param layer - * @returns renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.getRenderer = function (layer) { - var renderer; - - renderer = m_vtkRenderedList[layer]; - if (renderer === null || typeof renderer === 'undefined') { - renderer = new vgl.renderer(); - renderer.setResetScene(false); - renderer.setResetClippingRange(false); - m_viewer.renderWindow().addRenderer(renderer); - - if (layer !== 0) { - renderer.camera().setClearMask(vgl.GL.DepthBufferBit); - } - - m_vtkRenderedList[layer] = renderer; - } - - return renderer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * setVtkScene - Set the VTK scene data for camera initialization. - * - * @param scene - * @returns void - */ - //////////////////////////////////////////////////////////////////////////// - this.setVtkScene = function (scene) { - m_vtkScene = scene; - }; - - return this; -}; - -vgl.DataBuffers = function (initialSize) { - 'use strict'; - if (!(this instanceof vgl.DataBuffers)) { - return new vgl.DataBuffers(initialSize); - } - - var data = {}; - - var size; - if (!initialSize && initialSize !== 0) { - size = 256; - } else { - size = initialSize; - } - - var current = 0; - - var copyArray = function (dst, src, start, count) { - if (!dst) { - console.log ('ack'); - } - if (!start) { - start = 0; - } - if (!count) { - count = src.length; - } - for (var i = 0; i < count; i += 1) { - dst[start + i] = src[i]; - } - }; - - var resize = function (min_expand) { - var new_size = size; - /* If the array would increase substantially, don't just double its - * size. If the array has been increasing gradually, double it as the - * expectation is that it will increase again. */ - if (new_size * 2 < min_expand) { - new_size = min_expand; - } - while (new_size < min_expand) { - new_size *= 2; - } - size = new_size; - for (var name in data) { - if (data.hasOwnProperty(name)) { - var newArray = new Float32Array (new_size * data[name].len); - var oldArray = data[name].array; - copyArray (newArray, oldArray); - data[name].array = newArray; - data[name].dirty = true; - } - } - }; - - this.create = function (name, len) { - if (!len) { - throw 'Length of buffer must be a positive integer'; - } - var array = new Float32Array (size * len); - data[name] = { - array: array, - len: len, - dirty: false - }; - return data[name].array; - }; - - this.alloc = function (num) { - if ((current + num) >= size) { - resize (current + num); - } - var start = current; - current += num; - return 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 = true; - }; - - this.repeat = function (name, elem, start, count) { - for (var i = 0; i < count; i += 1) { - copyArray (data[name].array, elem, - (start + i) * data[name].len, data[name].len); - } - data[name].dirty = true; - }; - - this.count = function () { - return current; - }; - - this.data = function (name) { - return data[name].array; - }; -}; - -return vgl; - -})); - - -(function () { - 'use strict'; - - var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - /** - * Takes a variable number of arguments and returns the first numeric value - * it finds. - * @private - */ - function setNumeric() { - var i; - for (i = 0; i < arguments.length; i += 1) { - if (isFinite(arguments[i])) { - return arguments[i]; - } - } - } - - ////////////////////////////////////////////////////////////////////////////// - /** - * Contains utility classes and methods used by geojs. - * @namespace - */ - ////////////////////////////////////////////////////////////////////////////// - geo.util = { - /** - * Returns true if the given point lies in the given polygon. - * Algorithm description: - * http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html - * @param {geo.screenPosition} point The test point - * @param {geo.screenPosition[]} outer The outer boundary of the polygon - * @param {geo.screenPosition[][]?} inner Inner boundaries (holes) - */ - pointInPolygon: function (point, outer, inner) { - var inside = false, n = outer.length; - - if (n < 3) { - // we need 3 coordinates for this to make sense - return false; - } - - outer.forEach(function (vert, i) { - var j = (n + i - 1) % n; - var intersect = ( - ((outer[i].y > point.y) !== (outer[j].y > point.y)) && - (point.x < (outer[j].x - outer[i].x) * - (point.y - outer[i].y) / - (outer[j].y - outer[i].y) + outer[i].x) - ); - if (intersect) { - inside = !inside; - } - }); - - (inner || []).forEach(function (hole) { - inside = inside && !geo.util.pointInPolygon(point, hole); - }); - - return inside; - }, - - /** - * Returns true if the argument is a function. - */ - isFunction: function (f) { - return typeof f === 'function'; - }, - - /** - * Returns the argument if it is function, otherwise returns a function - * that returns the argument. - */ - ensureFunction: function (f) { - if (geo.util.isFunction(f)) { - return f; - } else { - return function () { return f; }; - } - }, - - /** - * Return a random string of length n || 8. - */ - randomString: function (n) { - var s, i, r; - n = n || 8; - s = ''; - for (i = 0; i < n; i += 1) { - r = Math.floor(Math.random() * chars.length); - s += chars.substring(r, r + 1); - } - return s; - }, - - /** - * Convert a color from hex value or css name to rgb objects - */ - convertColor: function (color) { - if (color.r !== undefined && color.g !== undefined && - color.b !== undefined) { - return color; - } - if (typeof color === 'string') { - if (geo.util.cssColors.hasOwnProperty(color)) { - color = geo.util.cssColors[color]; - } else if (color.charAt(0) === '#') { - color = parseInt(color.slice(1), 16); - } - } - if (isFinite(color)) { - color = { - r: ((color & 0xff0000) >> 16) / 255, - g: ((color & 0xff00) >> 8) / 255, - b: ((color & 0xff)) / 255 - }; - } - return color; - }, - - /** - * Normalize a coordinate object into {x: ..., y: ..., z: ... } form. - * Accepts 2-3d arrays, - * latitude -> lat -> y - * longitude -> lon -> lng -> x - */ - normalizeCoordinates: function (p) { - p = p || {}; - if (Array.isArray(p)) { - return { - x: p[0], - y: p[1], - z: p[2] || 0 - }; - } - return { - 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 - ) - }; - }, - - /** - * 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; - }, - - /** - * Compare two arrays and return if their contents are equal. - * @param {array} a1 first array to compare - * @param {array} a2 second array to compare - * @returns {boolean} true if the contents of the arrays are equal. - */ - compareArrays: function (a1, a2) { - return (a1.length === a2.length && a1.every(function (el, idx) { - return el === a2[idx]; - })); - }, - - /** - * Create a vec3 that is always an array. This should only be used if it - * will not be used in a WebGL context. Plain arrays usually use 64-bit - * float values, whereas vec3 defaults to 32-bit floats. - * - * @returns {Array} zeroed-out vec3 compatible array. - */ - vec3AsArray: function () { - return [0, 0, 0]; - }, - - /** - * Create a mat4 that is always an array. This should only be used if it - * will not be used in a WebGL context. Plain arrays usually use 64-bit - * float values, whereas mat4 defaults to 32-bit floats. - * - * @returns {Array} identity mat4 compatible array. - */ - mat4AsArray: function () { - return [ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ]; - }, - - /** - * Get a buffer for a vgl geometry source. If a buffer already exists and - * is the correct size, return it. Otherwise, allocate a new buffer; any - * data in an old buffer is discarded. - * - * @param geom: the geometry to reference and modify. - * @param srcName: the name of the source. - * @param len: the number of elements for the array. - * @returns {Float32Array} - */ - getGeomBuffer: function (geom, srcName, len) { - var src = geom.sourceByName(srcName), data; - - data = src.data(); - if (data instanceof Float32Array && data.length === len) { - return data; - } - data = new Float32Array(len); - src.setData(data); - return data; - } - }; - - geo.util.cssColors = { - aliceblue: 0xf0f8ff, - antiquewhite: 0xfaebd7, - aqua: 0x00ffff, - aquamarine: 0x7fffd4, - azure: 0xf0ffff, - beige: 0xf5f5dc, - bisque: 0xffe4c4, - black: 0x000000, - blanchedalmond: 0xffebcd, - blue: 0x0000ff, - blueviolet: 0x8a2be2, - brown: 0xa52a2a, - burlywood: 0xdeb887, - cadetblue: 0x5f9ea0, - chartreuse: 0x7fff00, - chocolate: 0xd2691e, - coral: 0xff7f50, - cornflowerblue: 0x6495ed, - cornsilk: 0xfff8dc, - crimson: 0xdc143c, - cyan: 0x00ffff, - darkblue: 0x00008b, - darkcyan: 0x008b8b, - darkgoldenrod: 0xb8860b, - darkgray: 0xa9a9a9, - darkgreen: 0x006400, - darkgrey: 0xa9a9a9, - darkkhaki: 0xbdb76b, - darkmagenta: 0x8b008b, - darkolivegreen: 0x556b2f, - darkorange: 0xff8c00, - darkorchid: 0x9932cc, - darkred: 0x8b0000, - darksalmon: 0xe9967a, - darkseagreen: 0x8fbc8f, - darkslateblue: 0x483d8b, - darkslategray: 0x2f4f4f, - darkslategrey: 0x2f4f4f, - darkturquoise: 0x00ced1, - darkviolet: 0x9400d3, - deeppink: 0xff1493, - deepskyblue: 0x00bfff, - dimgray: 0x696969, - dimgrey: 0x696969, - dodgerblue: 0x1e90ff, - firebrick: 0xb22222, - floralwhite: 0xfffaf0, - forestgreen: 0x228b22, - fuchsia: 0xff00ff, - gainsboro: 0xdcdcdc, - ghostwhite: 0xf8f8ff, - gold: 0xffd700, - goldenrod: 0xdaa520, - gray: 0x808080, - green: 0x008000, - greenyellow: 0xadff2f, - grey: 0x808080, - honeydew: 0xf0fff0, - hotpink: 0xff69b4, - indianred: 0xcd5c5c, - indigo: 0x4b0082, - ivory: 0xfffff0, - khaki: 0xf0e68c, - lavender: 0xe6e6fa, - lavenderblush: 0xfff0f5, - lawngreen: 0x7cfc00, - lemonchiffon: 0xfffacd, - lightblue: 0xadd8e6, - lightcoral: 0xf08080, - lightcyan: 0xe0ffff, - lightgoldenrodyellow: 0xfafad2, - lightgray: 0xd3d3d3, - lightgreen: 0x90ee90, - lightgrey: 0xd3d3d3, - lightpink: 0xffb6c1, - lightsalmon: 0xffa07a, - lightseagreen: 0x20b2aa, - lightskyblue: 0x87cefa, - lightslategray: 0x778899, - lightslategrey: 0x778899, - lightsteelblue: 0xb0c4de, - lightyellow: 0xffffe0, - lime: 0x00ff00, - limegreen: 0x32cd32, - linen: 0xfaf0e6, - magenta: 0xff00ff, - maroon: 0x800000, - mediumaquamarine: 0x66cdaa, - mediumblue: 0x0000cd, - mediumorchid: 0xba55d3, - mediumpurple: 0x9370db, - mediumseagreen: 0x3cb371, - mediumslateblue: 0x7b68ee, - mediumspringgreen: 0x00fa9a, - mediumturquoise: 0x48d1cc, - mediumvioletred: 0xc71585, - midnightblue: 0x191970, - mintcream: 0xf5fffa, - mistyrose: 0xffe4e1, - moccasin: 0xffe4b5, - navajowhite: 0xffdead, - navy: 0x000080, - oldlace: 0xfdf5e6, - olive: 0x808000, - olivedrab: 0x6b8e23, - orange: 0xffa500, - orangered: 0xff4500, - orchid: 0xda70d6, - palegoldenrod: 0xeee8aa, - palegreen: 0x98fb98, - paleturquoise: 0xafeeee, - palevioletred: 0xdb7093, - papayawhip: 0xffefd5, - peachpuff: 0xffdab9, - peru: 0xcd853f, - pink: 0xffc0cb, - plum: 0xdda0dd, - powderblue: 0xb0e0e6, - purple: 0x800080, - red: 0xff0000, - rosybrown: 0xbc8f8f, - royalblue: 0x4169e1, - saddlebrown: 0x8b4513, - salmon: 0xfa8072, - sandybrown: 0xf4a460, - seagreen: 0x2e8b57, - seashell: 0xfff5ee, - sienna: 0xa0522d, - silver: 0xc0c0c0, - skyblue: 0x87ceeb, - slateblue: 0x6a5acd, - slategray: 0x708090, - slategrey: 0x708090, - snow: 0xfffafa, - springgreen: 0x00ff7f, - steelblue: 0x4682b4, - tan: 0xd2b48c, - teal: 0x008080, - thistle: 0xd8bfd8, - tomato: 0xff6347, - turquoise: 0x40e0d0, - violet: 0xee82ee, - wheat: 0xf5deb3, - white: 0xffffff, - whitesmoke: 0xf5f5f5, - yellow: 0xffff00, - yellowgreen: 0x9acd32 - }; -}()); - -////////////////////////////////////////////////////////////////////////////// -/* - * Includes several support classes adapted from wigglemaps. - * - * https://github.com/dotskapes/wigglemaps - * - * Copyright 2013 Preston and Krejci (dotSkapes Virtual Lab) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -////////////////////////////////////////////////////////////////////////////// - -(function () { - 'use strict'; - - var RangeNode = function (elem, start, end, current) { - this.data = elem[current]; - this.left = null; - this.right = null; - if (start !== current) - this.left = new RangeNode(elem, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); - if (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 is populated as needed */ - this.search = rangeNodeSearch; - }; - - var rangeNodeSearch = function (result, box) { - var m_this = this; - - var xrange = function (b) { - return (b.x_in(m_this.elem[m_this.start]) && - b.x_in(m_this.elem[m_this.end])); - }; - - var yrange = function (b, start, end) { - return (b.y_in(m_this.subtree[start]) && - b.y_in(m_this.subtree[end])); - }; - - var subquery = function (result, box, start, end, current) { - if (yrange(box, start, end)) { - for (var i = start; i <= end; i ++) { - result.push(m_this.subtree[i]); - } - return; - } - if (box.y_in(m_this.subtree[current])) - result.push(m_this.subtree[current]); - if (box.y_left(m_this.subtree[current])){ - if (current !== end) - subquery(result, box, current + 1, end, parseInt((end + (current + 1)) / 2, 10)); - } else if (box.x_right(m_this.subtree[current])) { - if (current !== start) - subquery(result, box, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); - } else { - if (current !== end) - subquery(result, box, current + 1, end, parseInt((end + (current + 1)) / 2, 10)); - if (current !== start) - subquery(result, box, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); - } - }; - - if (xrange(box)) { - if (!this.subtree) { - this.subtree = this.elem.slice(this.start, this.end + 1); - this.subtree.sort(function (a, b) { - return a.y - b.y; - }); - } - subquery(result, box, 0, this.subtree.length - 1, parseInt((this.subtree.length - 1) / 2, 10)); - return; - } else { - if (box.contains(this.data)) - result.push(this.data); - if (box.x_left(this.data)) { - if (this.right) - this.right.search(result, box); - } else if (box.x_right(this.data)) { - if (this.left) - this.left.search(result, box); - } else { - if (this.left) - this.left.search(result, box); - if (this.right) - this.right.search(result, box); - } - } - }; - - var RangeTree = function (elem) { - elem.sort(function (a, b) { - return a.x - b.x; - }); - if (elem.length > 0) - this.root = new RangeNode(elem, 0, elem.length - 1, parseInt((elem.length - 1) / 2, 10)); - else - this.root = null; - - this.search = function (_box) { - if (!this.root) - return []; - //var box = new Box (min, max); - var box = _box.clone (); - var result = []; - this.root.search (result, box); - return result; - }; - }; - - var 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; i < 4; i ++) { - for (var j = 0; j < 4; j ++) { - if (vect.intersects (this.vertex (i), this.vertex ((i + 1) % 4), - box.vertex (j), box.vertex ((j + 1) % 4))) - return true; - } - } - if (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))) - return true; - if (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))) - return true; - return false; - }; - - 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); - }; - }; - - // A basic vector type. Supports standard 2D vector operations - var Vector2D = function (x, y) { - this.x = x; - this.y = y; - - this.add = function (v) { - this.x += v.x; - this.y += v.y; - return this; - }; - this.sub = function (v) { - this.x -= v.x; - this.y -= v.y; - return this; - }; - this.scale = function (s) { - this.x *= s; - this.y *= s; - return this; - }; - this.length = function () { - return Math.sqrt (this.x * this.x + this.y * this.y); - }; - this.normalize = function () { - var scale = this.length (); - if (scale === 0) - return this; - this.x /= scale; - this.y /= scale; - return this; - }; - this.div = function (v) { - this.x /= v.x; - this.y /= v.y; - return this; - }; - this.floor = function () { - this.x = Math.floor (this.x); - this.y = Math.floor (this.y); - return this; - }; - this.zero = function (tol) { - tol = tol || 0; - return (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); - var sin = Math.sin (omega); - xp = cos * this.x - sin * this.y; - yp = sin * this.x + cos * this.y; - this.x = xp; - this.y = yp; - return this; - }; - this.clone = function () { - return new Vector2D (this.x, this.y); - }; - - this.array = function () { - return [this.x, this.y]; - }; - }; - - // A shortcut for the vector constructor - function vect (x, y) { - return new Vector2D (x, y); - } - - // Shorthand operations for vectors for operations that make new vectors - - 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) { - if (!tol) - tol = 0; - var v1 = vect.sub (b, a); - var v2 = vect.sub (c, a); - return (vect.cross (v1, v2) >= -tol); - }; - - vect.intersects = function (a, b, c, d, tol) { - if (!tol) - tol = 0; - return (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 (denom === 0) - return Infinity; - - var num_s = a.x * (d.y - c.y) + - c.x * (a.y - d.y) + - d.x * (c.y - a.y); - var s = num_s / denom; - - var num_t = -(a.x * (c.y - b.y) + - b.x * (a.y - c.y) + - c.x * (b.y - a.y)); - var 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 (denom === 0) - return Infinity; - - var num_s = a.x * (d.y - c.y) + - c.x * (a.y - d.y) + - d.x * (c.y - a.y); - var s = num_s / denom; - - /*var num_t = -(a.x * (c.y - b.y) + - b.x * (a.y - c.y) + - c.x * (b.y - a.y)); - var t = num_t / denom;*/ - - var dir = vect.sub (b, a); - dir.scale (s); - return vect.add (a, dir); - }; - - vect.rotate = function (v, omega) { - var cos = Math.cos (omega); - var 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 (); - }; - - // Export to geo.util module - geo.util.RangeTree = RangeTree; - geo.util.Box = Box; - geo.util.vect = vect; -}()); - -/* -markercluster plugin: - -Copyright 2012 David Leaver - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -Leaflet utilities: - -Copyright (c) 2010-2015, Vladimir Agafonkin -Copyright (c) 2010-2011, CloudMade -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are -permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, this list of - conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright notice, this list - of conditions and the following disclaimer in the documentation and/or other - materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, -EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) -HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR -TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -*/ - -/** - * @file - * Code taken from https://github.com/Leaflet/Leaflet.markercluster - * to support faster hierarchical clustering of features. - * @copyright 2012, David Leaver - */ - -(function () { - "use strict"; - - var L = {}; - L.Util = { - // return unique ID of an object - stamp: function (obj) { - obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId; - return 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); - }, - - //Returns true if the object was found - removeObject: 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] || [], - i, len; - - delete this._objectPoint[L.Util.stamp(obj)]; - - for (i = 0, len = cell.length; i < len; i++) { - if (cell[i] === obj) { - - cell.splice(i, 1); - - if (len === 1) { - delete row[x]; - } - - return true; - } - } - - }, - - 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) { - cell = row[j]; - - for (k = 0, len = cell.length; k < len; k++) { - removed = fn.call(context, cell[k]); - if (removed) { - k--; - len--; - } - } - } - } - }, - - getNearObject: function (point) { - var x = this._getCoord(point.x), - y = this._getCoord(point.y), - i, j, k, row, cell, len, obj, dist, - objectPoint = this._objectPoint, - closestDistSq = this._sqCellSize, - closest = null; - - for (i = y - 1; i <= y + 1; i++) { - row = this._grid[i]; - if (row) { - - for (j = x - 1; j <= x + 1; j++) { - cell = row[j]; - if (cell) { - - for (k = 0, len = cell.length; k < len; k++) { - obj = cell[k]; - dist = this._sqDist( - objectPoint[L.Util.stamp(obj)], - point - ); - if (dist < closestDistSq) { - closestDistSq = dist; - closest = obj; - } - } - } - } - } - } - return closest; - }, - - /* return the point coordinates contained in the structure */ - 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; - } - }; -})(); - -/** - * @file - * Using methods adapted from leaflet to cluster an array of positions - * hierarchically given an array of length scales (zoom levels). - */ - -(function () { - 'use strict'; - - /** - * This class manages a group of nearby points that are clustered as a - * single object for display purposes. The class constructor is private - * and only meant to be created by the ClusterGroup object. - * - * This is a tree-like data structure. Each node in the tree is a - * cluster containing child clusters and unclustered points. - * - * @class - * @private - * - * @param {geo.util.ClusterGroup} group The source cluster group - * @param {number} zoom The zoom level of the current node - * @param {object[]} children An array of ClusterTrees or point objects - */ - function ClusterTree(group, zoom, children) { - this._group = group; - this._zoom = zoom; - this._points = []; // Unclustered points - this._clusters = []; // Child clusters - this._count = 0; // Total number of points - this._parent = null; - this._coord = null; // The cached coordinates - var that = this; - - // add the children provided in the constructor call - (children || []).forEach(function (c) { - that._add(c); - }); - } - - /** - * Add a point or cluster as a child to the current cluster. - * @param {object} pt A ClusterTree or point object - * @private - */ - ClusterTree.prototype._add = function (pt) { - var inc = 1; - - if (pt instanceof ClusterTree) { - // add a child cluster - this._clusters.push(pt); - inc = pt._count; - } else { - this._points.push(pt); - } - pt._parent = this; - - // increment the counter - this._increment(inc); - }; - - /** - * Increment the child counter for this and the parent. - * @param {number} inc The value to increment by - * @private - */ - ClusterTree.prototype._increment = function (inc) { - this._coord = null; - this._count += inc; - if (this._parent) { - this._parent._increment(inc); - } - }; - - /** - * Return the total number of child points contained in the cluster. - * @returns {number} Total points contained - */ - ClusterTree.prototype.count = function () { - return this._count; - }; - - /** - * Recursively call a function on all points contained in the cluster. - * Calls the function with `this` as the current ClusterTree object, and - * arguments to arguments the point object and the zoom level: - * func.call(this, point, zoom) - */ - ClusterTree.prototype.each = function (func) { - var i; - for (i = 0; i < this._points.length; i += 1) { - func.call(this, this._points[i], this._zoom); - } - for (i = 0; i < this._clusters.length; i += 1) { - this._clusters[i].each.call( - this._clusters[i], - func - ); - } - }; - - /** - * Get the coordinates of the cluster (the mean position of all the points - * contained). This is lazily calculated and cached. - */ - ClusterTree.prototype.coords = function () { - var i, center = {x: 0, y: 0}; - if (this._coord) { - return this._coord; - } - // first add up the points at the node - for (i = 0; i < this._points.length; i += 1) { - center.x += this._points[i].x; - center.y += this._points[i].y; - } - - // add up the contribution from the clusters - for (i = 0; i < this._clusters.length; i += 1) { - center.x += this._clusters[i].coords().x * this._clusters[i].count(); - center.y += this._clusters[i].coords().y * this._clusters[i].count(); - } - - return { - x: center.x / this.count(), - y: center.y / this.count() - }; - }; - - /** - * This class manages clustering of an array of positions hierarchically. - * The algorithm and code was adapted from the Leaflet marker cluster - * plugin by David Leaver: https://github.com/Leaflet/Leaflet.markercluster - * - * @class geo.util.ClusterGroup - * @param {object} opts An options object - * @param {number} width The width of the window; used for scaling. - * @param {number} height The height of the window; used for scaling. - * @param {number} maxZoom The maximimum zoom level to calculate - * @param {number} radius Proportional to the clustering radius in pixels - */ - function C(opts, width, height) { - - // store the options - this._opts = $.extend({ - maxZoom: 18, - radius: 0.05 - }, opts); - this._opts.width = this._opts.width || width || 256; - this._opts.height = this._opts.height || height || 256; - - // generate the initial datastructures - this._clusters = {}; // clusters at each zoom level - this._points = {}; // unclustered points at each zoom level - - 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); - } - - /** - * Returns a characteristic distance scale at a particular zoom level. This - * scale is used to control the clustering radius. When the renderer supports - * it, this call should be replaced by a calculation involving the view port - * size in point coordinates at a particular zoom level. - * @private - */ - C.prototype._scaleAtLevel = function (zoom, width, height) { - return vgl.zoomToHeight(zoom, width, height) / 2 * this._opts.radius; - }; - - /** - * Add a position to the cluster group. - * @protected - */ - C.prototype.addPoint = function (point) { - var zoom, closest, parent, newCluster, lastParent, z; - - // start at the maximum zoom level and search for nearby - // - // 1. existing clusters - // 2. unclustered points - // - // otherwise add the point as a new unclustered point - - for (zoom = this._opts.maxZoom; zoom >= 0; zoom -= 1) { - - // find near cluster - closest = this._clusters[zoom].getNearObject(point); - if (closest) { - // add the point to the cluster and return - closest._add(point); - return; - } - - // find near point - closest = this._points[zoom].getNearObject(point); - if (closest) { - parent = closest._parent; - if (parent) { - // remove the point from the 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; - } - } - } - - if (!parent) { - $.noop(); - } - // create a new cluster with these two points - newCluster = new ClusterTree(this, zoom, [closest, point]); - this._clusters[zoom].addObject(newCluster, newCluster.coords()); - - // create intermediate parent clusters that don't exist - lastParent = newCluster; - for (z = zoom - 1; z > parent._zoom; z -= 1) { - lastParent = new ClusterTree(this, z, [lastParent]); - this._clusters[z].addObject(lastParent, lastParent.coords()); - } - parent._add(lastParent); - - // remove closest from this zoom level and any above (replace with newCluster) - for (z = zoom; z >= 0; z -= 1) { - if (!this._points[z].removeObject(closest, closest)) { - break; - } - } - - return; - } - - // add an unclustered point - this._points[zoom].addObject(point, point); - } - - // otherwise add to the top - this._topClusterLevel._add(point); - }; - - /** - * Return the unclustered points contained at a given zoom level. - * @param {number} zoom The zoom level - * @return {object[]} The array of unclustered points - */ - C.prototype.points = function (zoom) { - zoom = Math.min(Math.max(Math.floor(zoom), 0), this._opts.maxZoom - 1); - return this._points[Math.floor(zoom)].contents(); - }; - - /** - * Return the clusters contained at a given zoom level. - * @param {number} zoom The zoom level - * @return {ClusterTree[]} The array of clusters - */ - C.prototype.clusters = function (zoom) { - zoom = Math.min(Math.max(Math.floor(zoom), 0), this._opts.maxZoom - 1); - return this._clusters[Math.floor(zoom)].contents(); - }; - - geo.util.ClusterGroup = C; -})(); - -(function () { - 'use strict'; - - geo.util.scale = { - d3: typeof d3 !== 'undefined' ? d3.scale : undefined - }; -})(); - -/** - * @file - * Based on the following jquery throttle / debounce plugin: - * - * jQuery throttle / debounce - v1.1 - 3/7/2010 - * http://benalman.com/projects/jquery-throttle-debounce-plugin/ - * - * @copyright 2010 "Cowboy" Ben Alman - * Dual licensed under the MIT and GPL licenses. - * http://benalman.com/about/license/ - * - * The implementation included here is modified to support a callback - * method that can accumulate values between actual invocations of - * the throttled method. - */ - -(function (window) { - 'use strict'; - - // Internal method reference. - var _throttle; - - /** - * Throttle execution of a function. Especially useful for rate limiting - * execution of handlers on events like resize and scroll. If you want to - * rate-limit execution of a function to a single time see - * {@link geo.util.debounce}. - * - * In this visualization, | is a throttled-function call and X is the actual - * callback execution: - * - * :: - * Throttled with `no_trailing` specified as false or unspecified: - * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| - * X X X X X X X X X X X X - * - * Throttled with `no_trailing` specified as true: - * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| - * X X X X X X X X X X - * - * @function geo.util.throttle - * @param {number} delay A zero-or-greater delay in milliseconds. For event - * callbacks, values around 100 or 250 (or even higher) are most useful. - * @param {boolean} [no_trailing=false] If no_trailing is - * true, callback will only execute every `delay` milliseconds while the - * throttled-function is being called. If no_trailing is false or - * unspecified, callback will be executed one final time after the last - * throttled-function call. (After the throttled-function has not been - * called for `delay` milliseconds, the internal counter is reset) - * @param {function} callback A function to be executed after `delay` - * milliseconds. The `this` context and all arguments are passed through, - * as-is, to `callback` when the throttled-function is executed. - * @param {function} [accumulator] A function to be executed (synchronously) - * during **each** call to the wrapped function. Typically, this - * this method is used to accumulate values that the callback uses - * when it finally executes. - * - * @returns {function} The throttled version of `callback` - * - * @example - * var throttled = geo.util.throttle( delay, [ no_trailing, ] callback ); - * $('selector').bind( 'someevent', throttled ); - * $('selector').unbind( 'someevent', throttled ); - */ - geo.util.throttle = function (delay, no_trailing, - callback, accumulator, debounce_mode) { - // After wrapper has stopped being called, this timeout ensures that - // `callback` is executed at the proper times in `throttle` and `end` - // debounce modes. - var timeout_id, - - // Keep track of the last time `callback` was executed. - last_exec = 0; - - // `no_trailing` defaults to falsy. - if (typeof no_trailing !== 'boolean') { - debounce_mode = accumulator; - accumulator = callback; - callback = no_trailing; - no_trailing = undefined; - } - - // accumulator defaults to no-op - if (typeof accumulator !== 'function') { - debounce_mode = accumulator; - accumulator = function () {}; - } - - // The `wrapper` function encapsulates all of the throttling / debouncing - // functionality and when executed will limit the rate at which `callback` - // is executed. - function wrapper() { - var that = this, - elapsed = +new Date() - last_exec, - args = arguments; - - // Execute `callback` and update the `last_exec` timestamp. - function exec() { - last_exec = +new Date(); - callback.apply(that, args); - } - - // If `debounce_mode` is true (at_begin) this is used to clear the flag - // to allow future `callback` executions. - function clear() { - timeout_id = undefined; - } - - // always call the accumulator first - accumulator.apply(that, args); - - if (debounce_mode && !timeout_id) { - // Since `wrapper` is being called for the first time and - // `debounce_mode` is true (at_begin), execute `callback`. - exec(); - } - - // Clear any existing timeout. - void ( - timeout_id && clearTimeout(timeout_id) - ); - - if (debounce_mode === undefined && elapsed > delay) { - // In throttle mode, if `delay` time has been exceeded, execute - // `callback`. - exec(); - - } else if (no_trailing !== true) { - // In trailing throttle mode, since `delay` time has not been - // exceeded, schedule `callback` to execute `delay` ms after most - // recent execution. - // - // If `debounce_mode` is true (at_begin), schedule `clear` to execute - // after `delay` ms. - // - // If `debounce_mode` is false (at end), schedule `callback` to - // execute after `delay` ms. - timeout_id = setTimeout( - debounce_mode ? - clear : - exec, - debounce_mode === undefined ? - delay - elapsed : - delay - ); - } - } - - // Return the wrapper function. - return wrapper; - }; - - _throttle = geo.util.throttle; - - /** - * Debounce execution of a function. Debouncing, unlike throttling, - * guarantees that a function is only executed a single time, either at the - * very beginning of a series of calls, or at the very end. If you want to - * simply rate-limit execution of a function, see the - * method. - * - * In this visualization, | is a debounced-function call and X is the actual - * callback execution: - * - * :: - * - * Debounced with `at_begin` specified as false or unspecified: - * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| - * X X - * - * Debounced with `at_begin` specified as true: - * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| - * X X - * - * - * @param {number} delay A zero-or-greater delay in milliseconds. For event - * callbacks, values around 100 or 250 (or even higher) are most useful. - * @param {boolean} [at_begin=false] If at_begin is false or - * unspecified, callback will only be executed `delay` milliseconds after - * the last debounced-function call. If at_begin is true, callback will be - * executed only at the first debounced-function call. (After the - * throttled-function has not been called for `delay` milliseconds, the - * internal counter is reset) - * @param {function} callback A function to be executed after delay milliseconds. - * The `this` context and all arguments are passed through, as-is, to - * `callback` when the debounced-function is executed. - * @param {function} [accumulator] A function to be executed (synchronously) - * during **each** call to the wrapped function. Typically, this - * this method is used to accumulate values that the callback uses - * when it finally executes. - * - * @returns {function} A new, debounced, function. - * - * @example - * var debounced = geo.util.debounce( delay, [ at_begin, ] callback ); - * $('selector').bind( 'someevent', debounced ); - * $('selector').unbind( 'someevent', debounced ); - * - */ - - geo.util.debounce = function (delay, at_begin, callback, accumulator) { - if (typeof at_begin !== 'boolean') { - accumulator = callback; - callback = at_begin; - at_begin = false; - } - accumulator = accumulator || function () {}; - return _throttle(delay, false, callback, accumulator, !!at_begin); - }; - -})(this); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class object - * - * @class - * @extends vgl.object - * @returns {geo.object} - */ -////////////////////////////////////////////////////////////////////////////// -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; - - ////////////////////////////////////////////////////////////////////////////// - /** - * 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_promiseCount) { - m_idleHandlers.push(handler); - } else { - handler(); - } - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Add a new promise object preventing idle event handlers from being called - * until it is resolved. - * - * @param {Promise} promise A promise object - */ - ////////////////////////////////////////////////////////////////////////////// - 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; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Bind an event handler to this object - * - * @param {String} event - * An event from {geo.events} - * @param {function} handler - * A function that will be called when ``event`` is triggered. The - * function will be given an event object as a first parameter and - * optionally a second argument provided by the triggerer. - */ - ////////////////////////////////////////////////////////////////////////////// - this.geoOn = function (event, handler) { - if (Array.isArray(event)) { - event.forEach(function (e) { - m_this.geoOn(e, handler); - }); - return m_this; - } - if (!m_eventHandlers.hasOwnProperty(event)) { - m_eventHandlers[event] = []; - } - m_eventHandlers[event].push(handler); - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Trigger an event (or events) on this object and call all handlers - * - * @param {String} event An event from {geo.event} - * @param {Object} args An optional argument to pass to handlers - */ - ////////////////////////////////////////////////////////////////////////////// - this.geoTrigger = function (event, args) { - - // if we have an array of events, recall with single events - if (Array.isArray(event)) { - event.forEach(function (e) { - m_this.geoTrigger(e, args); - }); - 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); - }); - } - - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Remove handlers from an event (or an array of events). If no event is - * provided all hanlders will be removed. - * - * @param {string?} event An event from {geo.events} - * @param {object?} arg A function or array of functions to remove from the events - * or if falsey remove all handlers from the events - */ - ////////////////////////////////////////////////////////////////////////////// - this.geoOff = function (event, arg) { - if (event === undefined) { - m_eventHandlers = {}; - m_idleHandlers = []; - m_promiseCount = 0; - } - if (Array.isArray(event)) { - event.forEach(function (e) { - m_this.geoOff(e, arg); - }); - return m_this; - } - if (!arg) { - m_eventHandlers[event] = []; - } else if (Array.isArray(arg)) { - arg.forEach(function (handler) { - m_this.geoOff(event, handler); - }); - return m_this; - } - // What do we do if the handler is not already bound? - // ignoring for now... - if (m_eventHandlers.hasOwnProperty(event)) { - m_eventHandlers[event] = m_eventHandlers[event].filter(function (f) { - return f !== arg; - } - ); - } - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Free all resources and destroy the object. - */ - ////////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.geoOff(); - }; - - vgl.object.call(this); - - return this; -}; - -inherit(geo.object, vgl.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class sceneObject, which extends the object's - * event handling with a tree-based event propagation. - * - * @class - * @extends geo.object - * @returns {geo.sceneObject} - */ -////////////////////////////////////////////////////////////////////////////// -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; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Override object.addPromise to propagate up the scene tree. - */ - ////////////////////////////////////////////////////////////////////////////// - this.addPromise = function (promise) { - if (m_parent) { - m_parent.addPromise(promise); - } else { - s_addPromise(promise); - } - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Override object.onIdle to propagate up the scene tree. - */ - ////////////////////////////////////////////////////////////////////////////// - this.onIdle = function (handler) { - if (m_parent) { - m_parent.onIdle(handler); - } else { - s_onIdle(handler); - } - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get/set parent of the object - * @param {?geo.sceneObject} parent - */ - ////////////////////////////////////////////////////////////////////////////// - this.parent = function (arg) { - if (arg === undefined) { - return m_parent; - } - m_parent = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Add a child (or an array of children) to the object - */ - ////////////////////////////////////////////////////////////////////////////// - this.addChild = function (child) { - if (Array.isArray(child)) { - child.forEach(m_this.addChild); - return m_this; - } - child.parent(m_this); - m_children.push(child); - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Remove a child (or array of children) from the object - */ - ////////////////////////////////////////////////////////////////////////////// - this.removeChild = function (child) { - if (Array.isArray(child)) { - child.forEach(m_this.removeChild); - return m_this; - } - m_children = m_children.filter(function (c) { return c !== child; }); - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get an array of child objects - */ - ////////////////////////////////////////////////////////////////////////////// - this.children = function () { - return m_children.slice(); - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Force redraw of a scene object, to be implemented by subclasses. - * Base class just calls draw of child objects. - */ - ////////////////////////////////////////////////////////////////////////////// - this.draw = function (arg) { - m_this.children().forEach(function (child) { - child.draw(arg); - }); - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Trigger an event (or events) on this object and call all handlers. - * @param {String} event the event to trigger - * @param {Object} args arbitrary argument to pass to the handler - * @param {Boolean} childrenOnly if true, only propagate down the tree - */ - ////////////////////////////////////////////////////////////////////////////// - this.geoTrigger = function (event, args, childrenOnly) { - - var geoArgs; - - args = args || {}; - geoArgs = args.geo || {}; - args.geo = geoArgs; - - // stop propagation if requested by the handler - if (geoArgs.stopPropagation) { - return m_this; - } - - // If the event was not triggered by the parent, just propagate up the tree - if (!childrenOnly && m_parent && geoArgs._triggeredBy !== m_parent) { - geoArgs._triggeredBy = m_this; - m_parent.geoTrigger(event, args); - return m_this; - } - - // call the object's own handlers - s_trigger.call(m_this, event, args); - - // stop propagation if requested by the handler - if (geoArgs.stopPropagation) { - return m_this; - } - - // trigger the event on the children - m_children.forEach(function (child) { - if (child.geoTrigger) { - geoArgs._triggeredBy = m_this; - child.geoTrigger(event, args); - } - }); - - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Free all resources and destroy the object. - */ - ////////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.children = []; - delete m_this.parent; - s_exit(); - }; - - return this; -}; - -inherit(geo.sceneObject, geo.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class timestamp - * - * @class - * @extends vgl.timestamp - * @returns {geo.timestamp} - */ -////////////////////////////////////////////////////////////////////////////// -geo.timestamp = function () { - 'use strict'; - if (!(this instanceof geo.timestamp)) { - return new geo.timestamp(); - } - vgl.timestamp.call(this); -}; - -inherit(geo.timestamp, vgl.timestamp); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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/ - * - * The default transforms lat/long coordinates into web mercator - * for use with standard tile sets. - * - * 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.transform = function (options) { - 'use strict'; - if (!(this instanceof geo.transform)) { - return new geo.transform(options); - } - - var m_this = this, - m_proj, // The raw proj4js object - m_source, // The source projection - m_target; // The target projection - - /** - * Generate the internal proj4 object. - * @private - */ - function generate_proj4() { - m_proj = new proj4( - m_this.source(), - m_this.target() - ); - } - - /** - * Get/Set the source projection - */ - this.source = function (arg) { - if (arg === undefined) { - return m_source || 'EPSG:4326'; - } - m_source = arg; - generate_proj4(); - return m_this; - }; - - /** - * Get/Set the target projection - */ - this.target = function (arg) { - if (arg === undefined) { - return m_target || 'EPSG:3857'; - } - m_target = arg; - generate_proj4(); - return m_this; - }; - - /** - * 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) - * - * @returns {object} A point object in the target coordinates - */ - this._forward = function (point) { - var pt = m_proj.forward(point); - pt.z = point.z || 0; - return pt; - }; - - /** - * Perform an inverse transformation (target -> source) - * @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) - * - * @returns {object} A point object in the source coordinates - */ - this._inverse = function (point) { - var pt = m_proj.inverse(point); - pt.z = point.z || 0; - return pt; - }; - - /** - * 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) - * - * @returns {object} A point object or array in the target coordinates - */ - this.forward = function (point) { - if (Array.isArray(point)) { - return point.map(m_this._forward); - } - return m_this._forward(point); - }; - - /** - * 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) - * - * @returns {object} A point object in the source coordinates - */ - this.inverse = function (point) { - if (Array.isArray(point)) { - return point.map(m_this._inverse); - } - return m_this._inverse(point); - }; - - // Set defaults given by the constructor - options = options || {}; - try { - this.source(options.source); - } catch (err) { - console.error('Can\'t use transform source: ' + options.source); - this.source('EPSG:4326'); - } - try { - this.target(options.target); - } catch (err) { - console.error('Can\'t use transform target: ' + options.target); - this.target('EPSG:3857'); - } - - geo.object.call(this); - return this; -}; - -/** - * 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 {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.transform.transformCoordinates = function ( - srcPrj, tgtPrj, coordinates, numberOfComponents) { - 'use strict'; - - if (srcPrj === tgtPrj) { - return coordinates; - } - - var i, count, offset, xAcc, yAcc, zAcc, writer, output, projPoint, - trans = geo.transform({source: srcPrj, target: tgtPrj}); - - /// Default Z accessor - zAcc = function () { - return 0.0; - }; - - /// 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; - - 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; - - 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; - - 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'; - } - } - } - - /// 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; - }; - - 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; - }; - - 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'; - } - } - - if (coordinates instanceof Array) { - output = []; - output.length = coordinates.length; - count = coordinates.length; - - if (coordinates[0] instanceof Array || - coordinates[0] instanceof Object) { - offset = 1; - - 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'; - } - } - - 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); - } - return output; -}; - -/** - * 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 {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.transform.affineForward = function (def, coords) { - 'use strict'; - 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; -}; - -/** - * 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. - * - * (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.transform.affineInverse = function (def, coords) { - 'use strict'; - 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); - -(function () { - 'use strict'; - - ////////////////////////////////////////////////////////////////////////////// - /** - * 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. - * - * For the moment, all camera transforms are assumed to be expressible as - * 4x4 matrices. More general cameras may follow that break this assumption. - * - * The interface for the camera is relatively stable for "map-like" views, - * e.g. 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. - * - * 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. There are a few primary methods that are intended 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) - * * viewFromCenterSizeRotation: set the camera view based on a center - * point, boundary size, and rotation angle. - * - * @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 = geo.util.mat4AsArray(); - - /** - * The projection matrix - * @protected - */ - this._proj = geo.util.mat4AsArray(); - - /** - * The projection type (one of `this.constructor.projection`) - * @protected - */ - this._projection = null; - - /** - * The transform matrix (view * proj) - * @protected - */ - this._transform = geo.util.mat4AsArray(); - - /** - * The inverse transform matrix (view * proj)^-1 - * @protected - */ - this._inverse = geo.util.mat4AsArray(); - - /** - * 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( - geo.util.mat4AsArray(), - 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([ - this._viewport.width / viewport.width, - this._viewport.height / viewport.height, - 1 - ]); - - // translate by half the difference to keep the center the same - this._translate([ - (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|Array} 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|Array} 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 ul, ur, ll, lr, bds = {}; - - // get corners - ul = this.displayToWorld({x: 0, y: 0}); - ur = this.displayToWorld({x: this._viewport.width, y: 0}); - ll = this.displayToWorld({x: 0, y: this._viewport.height}); - lr = this.displayToWorld({ - x: this._viewport.width, - y: this._viewport.height - }); - - bds.left = Math.min(ul.x, ur.x, ll.x, lr.x); - bds.bottom = Math.min(ul.y, ur.y, ll.y, lr.y); - bds.right = Math.max(ul.x, ur.x, ll.x, lr.x); - bds.top = Math.max(ul.y, ur.y, ll.y, lr.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 size = { - width: bounds.right - bounds.left, - height: bounds.top - bounds.bottom - }; - var center = { - x: (bounds.left + bounds.right) / 2, - y: (bounds.bottom + bounds.top) / 2 - }; - - this._viewFromCenterSizeRotation(center, size, 0); - return this; - }; - - /** - * Sets the view matrix so that the given world center is centered, at - * least a certain width and height are visible, and a rotation is applied. - * The resulting bounds may be larger in width or height than the values if - * the viewport is a different aspect ratio. - * - * @protected - * @param {object} center - * @param {number} center.x - * @param {number} center.y - * @param {object} size - * @param {number} size.width - * @param {number} size.height - * @param {number} rotation in clockwise radians. Optional - * @return {this} Chainable - */ - this._viewFromCenterSizeRotation = function (center, size, rotation) { - var translate = geo.util.vec3AsArray(), - scale = geo.util.vec3AsArray(), - c_ar, v_ar, w, h; - - // reset view to the identity - this._resetView(); - - w = Math.abs(size.width); - h = Math.abs(size.height); - 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); - - if (rotation) { - this._rotate(rotation); - } - - // translate to the new center. - translate[0] = -center.x; - translate[1] = -center.y; - translate[2] = 0; - - this._translate(translate); - - return this; - }; - - /** - * Public exposure of the viewFromCenterSizeRotation function. - */ - this.viewFromCenterSizeRotation = function (center, size, rotation) { - this._viewFromCenterSizeRotation(center, size, rotation); - this._update(); - 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) { - if (!offset.x && !offset.y && !offset.z) { - return; - } - this._translate([ - 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) { - if (zoom === 1) { - return; - } - mat4.scale(this._view, this._view, [ - zoom, - zoom, - zoom - ]); - this._update(); - }; - - /** - * Rotate the view matrix by the given amount. - * - * @param {number} rotation Counter-clockwise rotation angle in radians. - * @param {object} center Center of rotation in world space coordinates. - * @param {vec3} axis acis of rotation. Defaults to [0, 0, -1] - */ - this._rotate = function (rotation, center, axis) { - if (!rotation) { - return; - } - axis = axis || [0, 0, -1]; - if (!center) { - center = [0, 0, 0]; - } else if (center.x !== undefined) { - center = [center.x || 0, center.y || 0, center.z || 0]; - } - var invcenter = [-center[0], -center[1], -center[2]]; - mat4.translate(this._view, this._view, center); - mat4.rotate(this._view, this._view, rotation, axis); - mat4.translate(this._view, this._view, invcenter); - }; - - /** - * 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 = geo.util.mat4AsArray(); - - // 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(geo.util.mat4AsArray(), 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; - - m_rendererName = geo.checkRenderer(m_rendererName); - - if (!m_map) { - throw new Error('Layers must be initialized on a map.'); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the name of the renderer. - * - * @returns {string} - */ - //////////////////////////////////////////////////////////////////////////// - this.rendererName = function () { - return m_rendererName; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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. - * - * @param {number} [zIndex] The new z-index - * @returns {number|this} - */ - //////////////////////////////////////////////////////////////////////////// - this.zIndex = function (zIndex) { - if (zIndex === undefined) { - return m_zIndex; - } - m_zIndex = zIndex; - m_node.css('z-index', m_zIndex); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bring the layer above the given number of layers. This will rotate the - * current z-indices for this and the next `n` layers. - * - * @param {number} [n=1] The number of positions to move - * @returns {this} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - } - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bring the layer below the given number of layers. This will rotate the - * current z-indices for this and the previous `n` layers. - * - * @param {number} [n=1] The number of positions to move - * @returns {this} - */ - //////////////////////////////////////////////////////////////////////////// - this.moveDown = function (n) { - if (n === undefined) { - n = 1; - } - return m_this.moveUp(-n); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bring the layer to the top of the map layers. - * - * @returns {this} - */ - //////////////////////////////////////////////////////////////////////////// - this.moveToTop = function () { - return m_this.moveUp(m_this.map().children().length - 1); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Bring the layer to the bottom of the map layers. - * - * @returns {this} - */ - //////////////////////////////////////////////////////////////////////////// - this.moveToBottom = function () { - return m_this.moveDown(m_this.map().children().length - 1); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get whether or not the layer is sticky (navigates with the map). - * - * @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. - * - * @returns {Boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.active = function () { - return m_active; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set root node of the layer - * - * @returns {div} - */ - //////////////////////////////////////////////////////////////////////////// - this.node = function () { - return m_node; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set id of the layer - * - * @returns {String} - */ - //////////////////////////////////////////////////////////////////////////// - this.id = function (val) { - if (val === undefined) { - return m_id; - } - m_id = geo.newLayerId(); - m_this.modified(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set name of the layer - * - * @returns {String} - */ - //////////////////////////////////////////////////////////////////////////// - this.name = function (val) { - if (val === undefined) { - return m_name; - } - m_name = val; - m_this.modified(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set map of the layer - */ - //////////////////////////////////////////////////////////////////////////// - this.map = function () { - return m_map; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get renderer for the layer if any - */ - //////////////////////////////////////////////////////////////////////////// - this.renderer = function () { - return m_renderer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get canvas of the layer - * - */ - //////////////////////////////////////////////////////////////////////////// - this.canvas = function () { - return m_canvas; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return last time data got changed - */ - //////////////////////////////////////////////////////////////////////////// - this.dataTime = function () { - return m_dataTime; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the modified time for the last update that did something - */ - //////////////////////////////////////////////////////////////////////////// - this.updateTime = function () { - return m_updateTime; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set if the layer has been initialized - */ - //////////////////////////////////////////////////////////////////////////// - this.initialized = function (val) { - if (val !== undefined) { - m_initialized = val; - return m_this; - } - return m_initialized; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get or set the attribution html content that will displayed with the - * layer. By default, nothing will be displayed. Note, this content - * is **not** html escaped, so care should be taken when renderering - * user provided content. - * @param {string?} arg An html fragment - * @returns {string|this} Chainable as a setter - */ - //////////////////////////////////////////////////////////////////////////// - this.attribution = function (arg) { - if (arg !== undefined) { - m_attribution = arg; - m_this.map().updateAttribution(); - return m_this; - } - return m_attribution; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Init layer - * - * @param {boolean} noEvents if a subclass of this intends to bind the - * resize, pan, and zoom events itself, set this flag to true to avoid - * binding them here. - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (noEvents) { - if (m_initialized) { - return m_this; - } - - m_map.node().append(m_node); - - /* Pass along the arguments, but not the map reference */ - var options = $.extend({}, arg); - delete options.map; - - 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 { - m_renderer = geo.createRenderer(m_rendererName, m_this, undefined, - options); - m_canvas = m_renderer.canvas(); - } - - if (!m_this.active()) { - m_node.css('pointerEvents', 'none'); - } - - m_initialized = true; - - if (!noEvents) { - /// 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.rotate, function (event) { - m_this._update({event: event}); - }); - - m_this.geoOn(geo.event.zoom, function (event) { - m_this._update({event: event}); - }); - } - - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Clean up resouces - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.geoOff(); - if (m_renderer) { - m_renderer._exit(); - } - m_node.off(); - m_node.remove(); - arg = {}; - m_canvas = null; - m_renderer = null; - s_exit(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update layer - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the width of the layer in pixels. - * **DEPRECIATED: use map.size instead. - */ - //////////////////////////////////////////////////////////////////////////// - this.width = function () { - return m_this.map().size().width; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the height of the layer in pixels - * **DEPRECIATED: use map.size instead. - */ - //////////////////////////////////////////////////////////////////////////// - this.height = function () { - return m_this.map().size().height; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get or set the current layer opacity. - */ - //////////////////////////////////////////////////////////////////////////// - this.opacity = function (opac) { - if (opac !== undefined) { - m_opacity = opac; - m_node.css('opacity', m_opacity); - return m_this; - } - return m_opacity; - }; - - 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; -}; - -/** - * Gets a new id number for a layer. - * @protected - * @instance - * @returns {number} - */ -geo.layer.newLayerId = (function () { - 'use strict'; - var currentId = 1; - return function () { - var id = currentId; - currentId += 1; - return id; - }; -}() -); - -/** - * General object specification for feature types. - * @typedef geo.layer.spec - * @type {object} - * @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 {geo.feature.spec[]} [features=[]] Features - * to add to the layer - */ - -/** - * Create a layer from an object. Any errors in the creation - * of the layer will result in returning null. - * @param {geo.map} map The map to add the layer to - * @param {geo.layer.spec} spec The object specification - * @returns {geo.layer|null} - */ -geo.layer.create = function (map, spec) { - 'use strict'; - - spec = spec || {}; - - // add osmLayer later - spec.type = 'feature'; - if (spec.type !== 'feature') { - console.warn('Unsupported layer type'); - return null; - } - - spec.renderer = spec.renderer || 'vgl'; - spec.renderer = geo.checkRenderer(spec.renderer); - - if (!spec.renderer) { - console.warn('Invalid renderer'); - return null; - } - - var layer = map.createLayer(spec.type, spec); - if (!layer) { - console.warn('Unable to create a layer'); - return null; - } - - // probably move this down to featureLayer eventually - spec.features.forEach(function (f) { - f.data = f.data || spec.data; - f.feature = geo.feature.create(layer, f); - }); - - return layer; -}; - -inherit(geo.layer, geo.sceneObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * Layer to draw points, lines, and polygons on the map The polydata layer - * provide mechanisms to create and draw geometrical shapes such as points, - * lines, and polygons. - * @class - * @extends geo.layer - * @returns {geo.featureLayer} - */ -////////////////////////////////////////////////////////////////////////////// -geo.featureLayer = function (arg) { - 'use strict'; - if (!(this instanceof geo.featureLayer)) { - return new geo.featureLayer(arg); - } - geo.layer.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - m_features = [], - s_init = this._init, - s_exit = this._exit, - s_update = this._update, - s_draw = this.draw; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create feature give a name - * - * @returns {geo.Feature} Will return a new feature - */ - //////////////////////////////////////////////////////////////////////////// - this.createFeature = function (featureName, arg) { - - var newFeature = geo.createFeature( - featureName, m_this, m_this.renderer(), arg); - - m_this.addChild(newFeature); - m_features.push(newFeature); - m_this.features(m_features); - m_this.modified(); - return newFeature; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Delete feature - * - */ - //////////////////////////////////////////////////////////////////////////// - this.deleteFeature = function (feature) { - var i; - - for (i = 0; i < m_features.length; i += 1) { - if (m_features[i] === feature) { - m_features[i]._exit(); - m_this.dataTime().modified(); - m_this.modified(); - m_features.splice(i, 1); - } - } - m_this.removeChild(feature); - - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set drawables - * - * @returns {Array} - */ - //////////////////////////////////////////////////////////////////////////// - this.features = function (val) { - if (val === undefined) { - return m_features; - } else { - m_features = val.slice(0); - m_this.dataTime().modified(); - m_this.modified(); - return m_this; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function () { - if (m_this.initialized()) { - return m_this; - } - - /// Call super class init - s_init.call(m_this, true); - - /// Bind events to handlers - m_this.geoOn(geo.event.resize, function (event) { - if (m_this.renderer()) { - m_this.renderer()._resize(event.x, event.y, event.width, event.height); - m_this._update({event: event}); - m_this.renderer()._render(); - } else { - m_this._update({event: event}); - } - }); - - m_this.geoOn(geo.event.pan, function (event) { - m_this._update({event: event}); - if (m_this.renderer()) { - m_this.renderer()._render(); - } - }); - - m_this.geoOn(geo.event.rotate, function (event) { - m_this._update({event: event}); - if (m_this.renderer()) { - m_this.renderer()._render(); - } - }); - - m_this.geoOn(geo.event.zoom, function (event) { - m_this._update({event: event}); - if (m_this.renderer()) { - m_this.renderer()._render(); - } - }); - - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update layer - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function (request) { - var i; - - if (!m_features.length) { - return m_this; - } - - /// Call base class update - s_update.call(m_this, request); - - if (m_features && m_features.length === 0) { - console.log('[info] No valid data source found.'); - return; - } - - if (m_this.dataTime().getMTime() > m_this.updateTime().getMTime()) { - for (i = 0; i < m_features.length; i += 1) { - m_features[i].renderer(m_this.renderer()); - } - } - - for (i = 0; i < m_features.length; i += 1) { - m_features[i]._update(); - } - - m_this.updateTime().modified(); - - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Free all resources - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.clear(); - s_exit(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Draw - */ - //////////////////////////////////////////////////////////////////////////// - this.draw = function () { - // Call sceneObject.draw, which calls draw on all child objects. - s_draw(); - - // 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. - if (m_this.renderer()) { - m_this.renderer()._render(); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Clear all features in layer - */ - //////////////////////////////////////////////////////////////////////////// - this.clear = function () { - var i; - - if (!m_features.length) { - return m_this; - } - - for (i = 0; i < m_features.length; i += 1) { - m_features[i]._exit(); - m_this.removeChild(m_features[i]); - } - - m_this.dataTime().modified(); - m_this.modified(); - m_features = []; - - return m_this; - }; - - return m_this; -}; - -inherit(geo.featureLayer, geo.layer); - -// Now register it -geo.registerLayer('feature', geo.featureLayer); - -////////////////////////////////////////////////////////////////////////////// -/** - * Common object containing all event types that are provided by the GeoJS - * API. Each property contained here is a valid target for event handling - * via {@link geo.object#geoOn}. The event object provided to handlers is - * different for each event type. Each handler will generally be called - * with a the this context being the class that caused the event.
- *
- * The following properties are common to all event objects: - * - * @namespace - * @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) { - * // event is an object with type: {@link geo.event.layerAdd} - * }); - * - */ -////////////////////////////////////////////////////////////////////////////// -geo.event = {}; - -////////////////////////////////////////////////////////////////////////////// -/* - * Event types - */ -////////////////////////////////////////////////////////////////////////////// - -// 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.layerSelect = 'geo_layerSelect'; -// geo.event.layerUnselect = 'geo_layerUnselect'; -// geo.event.query = 'geo_query'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered when a layer is added to the map. - * - * @property {geo.map} target The current map - * @property {geo.layer} layer The new layer - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.layerAdd = 'geo_layerAdd'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered when a layer is removed from the map. - * - * @property {geo.map} target The current map - * @property {geo.layer} layer The old layer - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.layerRemove = 'geo_layerRemove'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered when the map's zoom level is changed. Note that zoom is never - * triggered on the map itself. Instead it is triggered individually on - * layers, starting with the base layer. - * - * @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 current map center (pointing - * downward so that positive angles are clockwise rotations). - * - * @property {number} angle The angle of the rotation in radians - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.rotate = 'geo_rotate'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered when the map is panned either by user interaction or map - * transition. - * - * @property {object} screenDelta The number of pixels to pan the map by - * @property {object} center The new map center - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.pan = 'geo_pan'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered when the map's canvas is resized. - * - * @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.worldChanged = 'geo_worldChanged'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered on every call to {@link geo.map#draw} before the map is rendered. - * - * @property {geo.map} target The current map - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.draw = 'geo_draw'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered on every call to {@link geo.map#draw} after the map is rendered. - * - * @property {geo.map} target The current map - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.drawEnd = 'geo_drawEnd'; - -////////////////////////////////////////////////////////////////////////////// -/** - * 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'; - -////////////////////////////////////////////////////////////////////////////// -/** - * 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'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered on every 'mousemove' during a brushing selection. - * The event object extends {@link geo.brushSelection}. - * @mixes geo.brushSelection - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.brush = 'geo_brush'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered after a brush selection ends. - * The event object extends {@link geo.brushSelection}. - * @mixes geo.brushSelection - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.brushend = 'geo_brushend'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered when a brush selection starts. - * The event object extends {@link geo.brushSelection}. - * @mixes geo.brushSelection - */ -////////////////////////////////////////////////////////////////////////////// -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 after a general view matrix change (any change in the visible - * bounds). This is equivalent to the union of pan and zoom. - * - * @property {geo.camera} camera The camera instance - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.camera.view = 'geo_camera_view'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered after a pan in the x/y plane (no zoom level change). - * - * @property {geo.camera} camera The camera instance - * @property {object} delta The translation delta in world coordinates. - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.camera.pan = 'geo_camera_pan'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered after a view matrix change that is not a simple pan. This - * includes, but is not limited to, pure zooms. - * - * @property {geo.camera} camera The camera instance - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.camera.zoom = 'geo_camera_zoom'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered after a projection change. - * - * @property {geo.camera} camera The camera instance - * @property {string} type The projection type ('perspective'|'parallel') - */ -////////////////////////////////////////////////////////////////////////////// -geo.event.camera.projection = 'geo_camera_projection'; - -////////////////////////////////////////////////////////////////////////////// -/** - * Triggered after a viewport change. - * - * @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.camera.viewport = 'geo_camera_viewport'; - -////////////////////////////////////////////////////////////////////////////// -/** - * 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 - * @returns {geo.mapInteractor} - */ -////////////////////////////////////////////////////////////////////////////// -geo.mapInteractor = function (args) { - 'use strict'; - if (!(this instanceof geo.mapInteractor)) { - return new geo.mapInteractor(args); - } - geo.object.call(this); - - var m_options = args || {}, - m_this = this, - m_mouse, - m_keyboard, - m_state, - m_queue, - $node, - m_selectionLayer = null, - m_selectionPlane = null, - m_paused = false, - m_clickMaybe = false, - m_callZoom = function () {}; - - // Helper method to decide if the current button/modifiers match a set of - // conditions. - // button: 'left' | 'right' | 'middle' - // modifiers: [ 'alt' | 'meta' | 'ctrl' | 'shift' ] - function eventMatch(button, modifiers) { - return (button === 'wheel' || m_mouse.buttons[button]) && - (!!m_mouse.modifiers.alt) === (!!modifiers.alt) && - (!!m_mouse.modifiers.meta) === (!!modifiers.meta) && - (!!m_mouse.modifiers.shift) === (!!modifiers.shift) && - (!!m_mouse.modifiers.ctrl) === (!!modifiers.ctrl); - } - - // Helper method to calculate the speed from a velocity - function calcSpeed(v) { - var x = v.x, y = v.y; - return Math.sqrt(x * x + y * y); - } - - // copy the options object with defaults - m_options = $.extend( - true, - {}, - { - throttle: 30, - discreteZoom: false, - panMoveButton: 'left', - panMoveModifiers: {}, - zoomMoveButton: 'right', - zoomMoveModifiers: {}, - rotateMoveButton: 'left', - rotateMoveModifiers: {'ctrl': true}, - panWheelEnabled: false, - panWheelModifiers: {}, - zoomWheelEnabled: true, - zoomWheelModifiers: {}, - rotateWheelEnabled: true, - rotateWheelModifiers: {'ctrl': true}, - wheelScaleX: 1, - wheelScaleY: 1, - zoomScale: 1, - rotateWheelScale: 6 * Math.PI / 180, - selectionButton: 'left', - selectionModifiers: {'shift': true}, - momentum: { - enabled: true, - maxSpeed: 2.5, - minSpeed: 0.01, - drag: 0.01 - }, - spring: { - enabled: false, - springConstant: 0.00005 - }, - click: { - enabled: true, - buttons: {left: true, right: true, middle: true}, - duration: 0, - cancelOnMove: true - } - }, - m_options - ); - - // options supported: - // { - // // throttle mouse events to at most this many milliseconds each (default 30) - // throttle: number - // - // // Clamp zoom events to discrete (integer) zoom levels. If a number is - // // provided then zoom events will be debounced (and accumulated) - // // with the given delay. The default debounce interval is 400 ms. - // discreteZoom: boolean | number > 0 - // - // // button that must be pressed to initiate a pan on mousedown - // panMoveButton: 'right' | 'left' | 'middle' - // - // // modifier keys that must be pressed to initiate a pan on mousemove - // panMoveModifiers: { 'ctrl' | 'alt' | 'meta' | 'shift' } - // - // // button that must be pressed to initiate a zoom on mousedown - // zoomMoveButton: 'right' | 'left' | 'middle' - // - // // modifier keys that must be pressed to initiate a zoom on mousemove - // zoomMoveModifiers: { 'ctrl' | 'alt' | 'meta' | 'shift' } - // - // // button that must be pressed to initiate a rotate on mousedown - // rotateMoveButton: 'right' | 'left' | 'middle' - // - // // modifier keys that must be pressed to initiate a rotate on mousemove - // rotateMoveModifiers: { 'ctrl' | 'alt' | 'meta' | 'shift' } - // - // // enable or disable panning with the mouse wheel - // panWheelEnabled: true | false - // - // // modifier keys that must be pressed to trigger a pan on wheel - // panWheelModifiers: {...} - // - // // enable or disable zooming with the mouse wheel - // zoomWheelEnabled: true | false - // - // // modifier keys that must be pressed to trigger a zoom on wheel - // zoomWheelModifiers: {...} - // - // // enable or disable rotation with the mouse wheel - // rotateWheelEnabled: true | false - // - // // modifier keys that must be pressed to trigger a rotate on wheel - // rotateWheelModifiers: {...} - // - // // wheel scale factor to change the magnitude of wheel interactions - // wheelScaleX: 1 - // wheelScaleY: 1 - // - // // zoom scale factor to change the magnitude of zoom move interactions - // zoomScale: 1 - // - // // scale factor to change the magnitude of wheel rotation interactions - // rotateWheelScale: 1 - // - // // button that must be pressed to enable drag selection - // selectionButton: 'right' | 'left' | 'middle' - // - // // keyboard modifiers that must be pressed to initiate a selection - // selectionModifiers: {...} - // - // // enable momentum when panning - // momentum: { - // enabled: true | false, - // drag: number, // drag coefficient - // maxSpeed: number, // don't allow animation to pan faster than this - // minSpeed: number // stop animations if the speed is less than this - // } - // - // // enable spring clamping to screen edges to enforce clamping - // spring: { - // 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: - /** - * General representation of rectangular bounds in world coordinates - * @typedef geo.geoBounds - * @type {object} - * @property {geo.geoPosition} upperLeft Upper left corner - * @property {geo.geoPosition} upperRight Upper right corner - * @property {geo.geoPosition} lowerLeft Lower left corner - * @property {geo.geoPosition} lowerRight Lower right corner - */ - - /** - * General representation of rectangular bounds in pixel coordinates - * @typedef geo.screenBounds - * @type {object} - * @property {geo.screenPosition} upperLeft Upper left corner - * @property {geo.screenPosition} upperRight Upper right corner - * @property {geo.screenPosition} lowerLeft Lower left corner - * @property {geo.screenPosition} lowerRight Lower right corner - */ - - /** - * General representation of a point on the screen. - * @typedef geo.screenPosition - * @type {object} - * @property {Number} x Horizontal coordinate in pixels - * @property {Number} y Vertical coordinate in pixels - */ - - /** - * General represention of a point on the earth. - * @typedef geo.geoPosition - * @type {object} - * @property {Number} x Horizontal coordinate in degrees longitude - * @property {Number} y Vertical coordinate in degrees latitude - */ - - /** - * The status of all mouse buttons. - * @typedef geo.mouseButtons - * @type {object} - * @property {true|false} left The left mouse button - * @property {true|false} right The right mouse button - * @property {true|false} middle The middle mouse button - */ - - /** - * The status of all modifier keys these are copied from the - * standard DOM events. - * @typedef geo.modifierKeys - * @type {object} - * @property {true|false} alt Event.alt - * @property {true|false} ctrl Event.ctrl - * @property {true|false} shift Event.shift - * @property {true|false} meta Event.meta - */ - - /** - * Provides information about the state of the mouse - * @typedef geo.mouseState - * @type {object} - * @property {geo.screenPosition} page Mouse location in pixel space - * @property {geo.geoPosition} map Mouse location in world space - * @property {geo.mouseButtons} buttons The current state of the mouse buttons - * @property {geo.modifierKeys} modifiers The current state of all modifier keys - * @property {Date} time The timestamp the event took place - * @property {Number} deltaTime The time in milliseconds since the last mouse event - * @property {geo.screenPosition} velocity The velocity of the mouse pointer - * in pixels per second - */ - - /** - * @typedef geo.brushSelection - * @type {object} - * @property {geo.screenBounds} display The selection bounds in pixel space - * @property {geo.geoBounds} gcs The selection bounds in world space - * @property {geo.mouseState} mouse The current mouse state - * @property {geo.mouseState} origin The mouse state at the start of the - * brush action - */ - - // default mouse object - m_mouse = { - page: { // mouse position relative to the page - x: 0, - y: 0 - }, - map: { // mouse position relative to the map - x: 0, - y: 0 - }, - // mouse button status - buttons: { - left: false, - right: false, - middle: false - }, - // keyboard modifier status - modifiers: { - alt: false, - ctrl: false, - shift: false, - meta: false - }, - // time the event was captured - time: new Date(), - // time elapsed since the last mouse event - deltaTime: 1, - // pixels/ms - velocity: { - x: 0, - y: 0 - } - }; - - // default keyboard object - // (keyboard events not implemented yet) - m_keyboard = { - }; - - // The interactor state determines what actions are taken in response to - // core browser events. - // - // i.e. - // { - // 'action': 'pan', // an ongoing pan event - // 'origin': {...}, // mouse object at the start of the action - // 'delta': {x: *, y: *} // mouse movement since action start - // // not including the current event - // } - // - // { - // 'action': 'zoom', // an ongoing zoom event - // ... - // } - // - // { - // 'action': 'rotate', // an ongoing rotate event - // 'origin': {...}, // mouse object at the start of the action - // 'delta': {x: *, y: *} // mouse movement since action start - // // not including the current event - // } - // - // { - // 'acton': 'select', - // 'origin': {...}, - // 'delta': {x: *, y: *} - // } - // - // { - // 'action': 'momentum', - // 'origin': {...}, - // 'handler': function () { }, // called in animation loop - // 'timer': animate loop timer - // } - m_state = {}; - - /** - * Store queued map navigation commands (due to throttling) here - * { - * kind: 'move' | 'wheel', // what kind of mouse action triggered this - * method: function () {}, // the throttled method - * scroll: {x: ..., y: ...} // accumulated scroll wheel deltas - * } - */ - m_queue = {}; - - //////////////////////////////////////////////////////////////////////////// - /** - * Connects events to a map. If the map is not set, then this does nothing. - * @returns {geo.mapInteractor} - */ - //////////////////////////////////////////////////////////////////////////// - this._connectEvents = function () { - if (!m_options.map) { - return m_this; - } - - // prevent double binding to dom elements - m_this._disconnectEvents(); - - // store the connected element - $node = $(m_options.map.node()); - - // set methods related to asyncronous event handling - m_this._handleMouseWheel = throttled_wheel(); - m_callZoom = debounced_zoom(); - - // add event handlers - $node.on('wheel.geojs', m_this._handleMouseWheel); - $node.on('mousemove.geojs', m_this._handleMouseMove); - $node.on('mousedown.geojs', m_this._handleMouseDown); - $node.on('mouseup.geojs', m_this._handleMouseUp); - // Disable dragging images and such - $node.on('dragstart', function () { return false; }); - if (m_options.panMoveButton === 'right' || - m_options.zoomMoveButton === 'right' || - m_options.rotateMoveButton === 'right' || - m_options.selectionButton === 'right') { - $node.on('contextmenu.geojs', function () { return false; }); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Disonnects events to a map. If the map is not set, then this does nothing. - * @returns {geo.mapInteractor} - */ - //////////////////////////////////////////////////////////////////////////// - this._disconnectEvents = function () { - if ($node) { - $node.off('.geojs'); - $node = null; - } - m_this._handleMouseWheel = function () {}; - m_callZoom = function () {}; - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Sets or gets map for this interactor, adds draw region layer if needed - * - * @param {geo.map} newMap optional - * @returns {geo.interactorStyle|geo.map} - */ - //////////////////////////////////////////////////////////////////////////// - this.map = function (val) { - if (val !== undefined) { - m_options.map = val; - m_this._connectEvents(); - return m_this; - } - return m_options.map; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Gets/sets the options object for the interactor. - * - * @param {Object} opts optional - * @returns {geo.interactorStyle|Object} - */ - //////////////////////////////////////////////////////////////////////////// - this.options = function (opts) { - if (opts === undefined) { - return $.extend({}, m_options); - } - $.extend(m_options, opts); - - // reset event handlers for new options - this._connectEvents(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Stores the current mouse position from an event - */ - //////////////////////////////////////////////////////////////////////////// - this._getMousePosition = function (evt) { - var offset = $node.offset(), dt, t; - - 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) { - // catch georeferencing problems and move on - // needed for handling the map before the base layer - // is attached - m_mouse.geo = null; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Stores the current mouse button - */ - //////////////////////////////////////////////////////////////////////////// - this._getMouseButton = function (evt) { - if (evt.which === 1) { - m_mouse.buttons.left = evt.type !== 'mouseup'; - } else if (evt.which === 3) { - m_mouse.buttons.right = evt.type !== 'mouseup'; - } else if (evt.which === 2) { - m_mouse.buttons.middle = evt.type !== 'mouseup'; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Stores the current keyboard modifiers - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Compute a selection information object. - * @private - * @returns {Object} - */ - //////////////////////////////////////////////////////////////////////////// - this._getSelection = function () { - var origin = m_state.origin, - mouse = m_this.mouse(), - map = m_this.map(), - display = {}, gcs = {}; - - // TODO: clamp to map bounds - // Get the display coordinates - 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 - }; - - // Get the gcs coordinates - 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(); - - return { - display: display, - gcs: gcs, - mouse: mouse, - origin: $.extend({}, m_state.origin) - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Immediately cancel an ongoing action. - * - * @param {string?} action The action type, if null cancel any action - * @returns {bool} If an action was canceled - */ - //////////////////////////////////////////////////////////////////////////// - this.cancel = function (action) { - var out; - if (!action) { - out = !!m_state.action; - } else { - out = m_state.action === action; - } - if (out) { - // cancel any queued interaction events - m_queue = {}; - m_state = {}; - } - return out; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle event when a mouse button is pressed - */ - //////////////////////////////////////////////////////////////////////////// - this._handleMouseDown = function (evt) { - var action = null; - - if (m_paused) { - return; - } - - // cancel momentum on click - m_this.cancel('momentum'); - - m_this._getMousePosition(evt); - 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)) { - action = 'zoom'; - } else if (eventMatch(m_options.rotateMoveButton, m_options.rotateMoveModifiers)) { - action = 'rotate'; - } else if (eventMatch(m_options.selectionButton, m_options.selectionModifiers)) { - action = 'select'; - } - - m_mouse.velocity = { - x: 0, - y: 0 - }; - - if (action) { - // cancel any ongoing interaction queue - m_queue = { - kind: 'move' - }; - - // store the state object - m_state = { - action: action, - origin: $.extend(true, {}, m_mouse), - delta: {x: 0, y: 0} - }; - - if (action === 'select') { - // Make sure the old selection layer is gone. - if (m_selectionLayer) { - m_selectionLayer.clear(); - m_this.map().deleteLayer(m_selectionLayer); - m_selectionLayer = null; - } - // Create a feature layer and plane feature to show the selection bounds - m_selectionLayer = m_this.map().createLayer('feature', {renderer: 'd3'}); - m_selectionPlane = m_selectionLayer.createFeature('plane'); - m_selectionPlane.style({ - screenCoordinates: true, - fillOpacity: function () { return 0.25; } - }); - m_this.map().geoTrigger(geo.event.brushstart, m_this._getSelection()); - } - - // bind temporary handlers to document - if (m_options.throttle > 0) { - $(document).on( - 'mousemove.geojs', - geo.util.throttle( - m_options.throttle, - m_this._handleMouseMoveDocument - ) - ); - } else { - $(document).on('mousemove.geojs', m_this._handleMouseMoveDocument); - } - $(document).on('mouseup.geojs', m_this._handleMouseUpDocument); - } - - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event - */ - //////////////////////////////////////////////////////////////////////////// - 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; - } - - m_this._getMousePosition(evt); - m_this._getMouseButton(evt); - m_this._getMouseModifiers(evt); - - if (m_clickMaybe) { - return; - } - - m_this.map().geoTrigger(geo.event.mousemove, m_this.mouse()); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse move event on the document (temporary bindings) - */ - //////////////////////////////////////////////////////////////////////////// - this._handleMouseMoveDocument = function (evt) { - var dx, dy, selectionObj; - - if (m_paused || m_queue.kind !== 'move') { - 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.'); - return; - } - - // calculate the delta from the origin point to avoid - // accumulation of floating point errors - 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; - - if (m_state.action === 'pan') { - m_this.map().pan({x: dx, y: dy}); - } else if (m_state.action === 'zoom') { - m_callZoom(-dy * m_options.zoomScale / 120, m_state); - } else if (m_state.action === 'rotate') { - var cx, cy; - if (m_state.origin.rotation === undefined) { - cx = m_state.origin.map.x - m_this.map().size().width / 2; - cy = m_state.origin.map.y - m_this.map().size().height / 2; - m_state.origin.rotation = m_this.map().rotation() - Math.atan2(cy, cx); - } - cx = m_mouse.map.x - m_this.map().size().width / 2; - cy = m_mouse.map.y - m_this.map().size().height / 2; - m_this.map().rotation(m_state.origin.rotation + Math.atan2(cy, cx)); - } else if (m_state.action === 'select') { - // Get the bounds of the current selection - selectionObj = m_this._getSelection(); - m_this.map().geoTrigger(geo.event.brush, selectionObj); - } - - // Prevent default to stop text selection in particular - evt.preventDefault(); - }; - - /** - * Use interactor options to modify the mouse velocity by momentum - * or spring equations depending on the current map state. - * @private - * @param {object} v Current velocity in pixels / ms - * @param {number} deltaT The time delta - * @returns {object} New velocity - */ - function modifyVelocity(v, deltaT) { - deltaT = deltaT <= 0 ? 30 : deltaT; - var sf = springForce(); - var speed = calcSpeed(v); - var vx = v.x / speed; - var vy = v.y / speed; - - speed = speed * Math.exp(-m_options.momentum.drag * deltaT); - - // |force| + |velocity| < c <- stopping condition - if (calcSpeed(sf) * deltaT + speed < m_options.momentum.minSpeed) { - return null; - } - - if (speed > 0) { - vx = vx * speed; - vy = vy * speed; - } else { - vx = 0; - vy = 0; - } - - return { - x: vx - sf.x * deltaT, - y: vy - sf.y * deltaT - }; - } - - /** - * Get the spring force for the current map bounds - * (This method might need to move elsewhere to deal - * with different projections) - * @private - * @returns {object} The spring force - */ - function springForce() { - var xplus, // force to the right - xminus, // force to the left - yplus, // force to the top - yminus; // force to the bottom - - if (!m_options.spring.enabled) { - return {x: 0, y: 0}; - } - // get screen coordinates of corners - var ul = m_this.map().gcsToDisplay({ - x: -180, - y: 82 - }); - var lr = m_this.map().gcsToDisplay({ - x: 180, - y: -82 - }); - - var c = m_options.spring.springConstant; - // Arg... map needs to expose the canvas size - var width = m_this.map().node().width(); - var height = m_this.map().node().height(); - - 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; - - return { - x: xplus - xminus, - y: yplus - yminus - }; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle event when a mouse button is unpressed on the document. - * Removes temporary bindings. - */ - //////////////////////////////////////////////////////////////////////////// - this._handleMouseUpDocument = function (evt) { - var selectionObj, oldAction; - - if (m_paused) { - return; - } - - // cancel queued interactions - m_queue = {}; - - m_clickMaybe = false; - m_this._getMouseButton(evt); - m_this._getMouseModifiers(evt); - - // unbind temporary handlers on document - $(document).off('.geojs'); - - if (m_mouse.buttons.right) { - evt.preventDefault(); - } - - if (m_state.action === 'select') { - 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); - } - - // reset the interactor state - oldAction = m_state.action; - m_state = {}; - - // if momentum is enabled, start the action here - if (m_options.momentum.enabled && oldAction === 'pan') { - m_this.springBack(true); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle event when a mouse button is unpressed - */ - //////////////////////////////////////////////////////////////////////////// - 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); - - // 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()); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Private wrapper around the map zoom method that is debounced to support - * discrete zoom interactions. - * @param {number} deltaZ The zoom increment - */ - //////////////////////////////////////////////////////////////////////////// - function debounced_zoom() { - var deltaZ = 0, delay = 400, direction; - - function accum(dz, dir) { - var map = m_this.map(), zoom; - - direction = dir; - deltaZ += dz; - - // Respond to debounced events when they add up to a change in the - // discrete zoom level. - if (map && Math.abs(deltaZ) >= 1 && m_options.discreteZoom) { - - zoom = Math.round(deltaZ + map.zoom()); - - // delta is what is left over from the zoom delta after the new zoom value - deltaZ = deltaZ + map.zoom() - zoom; - - map.zoom(zoom, direction); - } - } - - function apply() { - var map = m_this.map(), zoom; - if (map) { - - zoom = deltaZ + map.zoom(); - - if (m_options.discreteZoom) { - // round off the zoom to an integer and throw away the rest - zoom = Math.round(zoom); - } - map.zoom(zoom, direction); - } - - deltaZ = 0; - } - - if (m_options.discreteZoom !== true && m_options.discreteZoom > 0) { - delay = m_options.discreteZoom; - } - if (m_options.discreteZoom === true || m_options.discreteZoom > 0) { - return geo.util.debounce(delay, false, apply, accum); - } else { - return function (dz, dir) { - accum(dz, dir); - apply(dz, dir); - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Attaches wrapped methods for accumulating fast mouse wheel events and - * throttling map interactions. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - function throttled_wheel() { - var my_queue = {}; - - function accum(evt) { - var dx, dy; - - if (m_paused) { - return; - } - - if (my_queue !== m_queue) { - my_queue = { - kind: 'wheel', - scroll: {x: 0, y: 0} - }; - m_queue = my_queue; - } - - evt.preventDefault(); - - // try to normalize deltas using the wheel event standard: - // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent - evt.deltaFactor = 1; - if (evt.originalEvent.deltaMode === 1) { - // DOM_DELTA_LINE -- estimate line height - evt.deltaFactor = 40; - } else if (evt.originalEvent.deltaMode === 2) { - // DOM_DELTA_PAGE -- get window height - evt.deltaFactor = $(window).height(); - } - - // prevent NaN's on legacy browsers - dx = evt.originalEvent.deltaX || 0; - dy = evt.originalEvent.deltaY || 0; - - // scale according to the options - dx = dx * m_options.wheelScaleX * evt.deltaFactor / 120; - dy = dy * m_options.wheelScaleY * evt.deltaFactor / 120; - - my_queue.scroll.x += dx; - my_queue.scroll.y += dy; - } - - function wheel(evt) { - var zoomFactor; - - // If the current queue doesn't match the queue passed in as an argument, - // assume it was cancelled and do nothing. - if (my_queue !== m_queue) { - return; - } - - // perform the map navigation event - m_this._getMouseModifiers(evt); - if (m_options.panWheelEnabled && - eventMatch('wheel', m_options.panWheelModifiers)) { - - m_this.map().pan({ - x: m_queue.scroll.x, - y: m_queue.scroll.y - }); - - } else if (m_options.zoomWheelEnabled && - eventMatch('wheel', m_options.zoomWheelModifiers)) { - - zoomFactor = -m_queue.scroll.y; - - m_callZoom(zoomFactor, m_mouse); - } else if (m_options.rotateWheelEnabled && - eventMatch('wheel', m_options.rotateWheelModifiers)) { - m_this.map().rotation( - m_this.map().rotation() + - m_queue.scroll.y * m_options.rotateWheelScale, - m_mouse); - } - - // reset the queue - m_queue = {}; - } - - if (m_options.throttle > 0) { - return geo.util.throttle(m_options.throttle, false, wheel, accum); - } else { - return function (evt) { - accum(evt); - wheel(evt); - }; - } - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle mouse wheel event. (Defined inside _connectEvents). - */ - //////////////////////////////////////////////////////////////////////////// - this._handleMouseWheel = function () {}; - - //////////////////////////////////////////////////////////////////////////// - /** - * Start up a spring back action when the map bounds are out of range. - * Not to be user callable. - * @todo Move this and momentum handling to the map class - * @protected - * - */ - //////////////////////////////////////////////////////////////////////////// - this.springBack = function (initialVelocity) { - if (m_state.action === 'momentum') { - return; - } - if (!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; - - // Not sure the correct way to do this. We need the delta t for the - // next time step... Maybe use a better interpolator and the time - // parameter from requestAnimationFrame. - dt = Math.min(m_mouse.deltaTime, 30); - if (m_state.action !== 'momentum' || - !m_this.map() || - m_this.map().transition()) { - // cancel if a new action was performed - return; - } - - last = m_state.start.valueOf(); - m_state.start = new Date(); - - v = modifyVelocity(m_mouse.velocity, m_state.start - last); - - // stop panning when the speed is below the threshold - if (!v) { - m_state = {}; - return; - } - - s = calcSpeed(v); - if (s > m_options.momentum.maxSpeed) { - s = m_options.momentum.maxSpeed / s; - v.x = v.x * s; - v.y = v.y * s; - } - - if (!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 - }); - - if (m_state.handler) { - window.requestAnimationFrame(m_state.handler); - } - }; - if (m_state.handler) { - window.requestAnimationFrame(m_state.handler); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle double click event - */ - //////////////////////////////////////////////////////////////////////////// - this._handleDoubleClick = function () { - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Public method that unbinds all events - */ - //////////////////////////////////////////////////////////////////////////// - this.destroy = function () { - m_this._disconnectEvents(); - m_this.map(null); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get current mouse information - */ - //////////////////////////////////////////////////////////////////////////// - this.mouse = function () { - return $.extend(true, {}, m_mouse); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get current keyboard information - */ - //////////////////////////////////////////////////////////////////////////// - this.keyboard = function () { - return $.extend(true, {}, m_keyboard); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the current interactor state - */ - //////////////////////////////////////////////////////////////////////////// - this.state = function () { - 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. - * - * The options for creating the events are as follows, not all - * options are required for every event type. :: - * - * options = { - * page: {x, y} // position on the page - * map: {x, y} // position on the map (overrides page) - * button: 'left' | 'right' | 'middle' - * modifiers: [ 'alt' | 'ctrl' | 'meta' | 'shift' ] - * wheelDelta: {x, y} - * } - * - * @param {string} type Event type 'mousemove', 'mousedown', 'mouseup', ... - * @param {object} options - * @returns {mapInteractor} - */ - //////////////////////////////////////////////////////////////////////////// - this.simulateEvent = function (type, options) { - var evt, page, offset, which; - - if (!m_this.map()) { - return m_this; - } - - page = options.page || {}; - - if (options.map) { - offset = $node.offset(); - page.x = options.map.x + offset.left; - page.y = options.map.y + offset.top; - } - - if (options.button === 'left') { - which = 1; - } else if (options.button === 'right') { - which = 3; - } else if (options.button === 'middle') { - 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 - } - } - ); - $node.trigger(evt); - }; - this._connectEvents(); - return this; -}; - -inherit(geo.mapInteractor, geo.object); - -////////////////////////////////////////////////////////////////////////////// -/** - * Stores the current time for a map, triggers time keeping events, and - * handles the animation state and interaction. - * - * @class geo.clock - * @extends geo.object - * @returns {geo.clock} - */ -////////////////////////////////////////////////////////////////////////////// -geo.clock = function (opts) { - 'use strict'; - - if (!(this instanceof geo.clock)) { - return new geo.clock(opts); - } - opts = opts || {}; - geo.object.call(this, opts); - - ////////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - ////////////////////////////////////////////////////////////////////////////// - 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; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the geo.object to trigger events on. - */ - ////////////////////////////////////////////////////////////////////////////// - this.object = function (arg) { - if (arg === undefined) { - return m_object; - } - m_object = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Returns true if attached to a valid geo.object. - * @private - */ - ////////////////////////////////////////////////////////////////////////////// - this._attached = function () { - return (m_object instanceof geo.object); - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the current time. - */ - ////////////////////////////////////////////////////////////////////////////// - this.now = function (arg) { - var previous = m_now; - if (arg === undefined) { - return m_now; - } - m_now = arg; - - if (m_now !== previous && - m_this._attached()) { - m_this.object().geoTrigger(geo.event.clock.change, { - previous: previous, - current: m_now, - clock: m_this - }); - } - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the animation start time. - */ - ////////////////////////////////////////////////////////////////////////////// - this.start = function (arg) { - if (arg === undefined) { - return m_start; - } - m_start = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the animation end time. - */ - ////////////////////////////////////////////////////////////////////////////// - this.end = function (arg) { - if (arg === undefined) { - return m_end; - } - m_end = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the animation time step. - */ - ////////////////////////////////////////////////////////////////////////////// - this.step = function (arg) { - if (arg === undefined) { - return m_step; - } - m_step = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set looping control of the clock. This controls how many times the - * animation will repeat before stopping. Default - * ``Number.POSITIVE_INFINITY``, the animation repeats forever. - */ - ////////////////////////////////////////////////////////////////////////////// - this.loop = function (arg) { - if (arg === undefined) { - return m_loop; - } - m_loop = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the animation state. Valid values are: - * - * * 'stop' - * * 'play' - * * 'pause' - * - * This will also trigger relevant events, but they may be fired - * asynchronously. - */ - ////////////////////////////////////////////////////////////////////////////// - this.state = function (arg, step) { - - if (arg === undefined) { - return m_state; - } - if (['stop', 'play', 'pause'].indexOf(arg) < 0) { - console.log('WARNING: Ignored invalid state: ' + arg); - return m_this; - } - - if (arg === 'play' && m_state === 'stop') { - // reset animation parameters - m_currentLoop = 0; - m_this.now(m_this.start()); - } - - if (arg === 'play' && m_state !== 'play') { - // Start a new animation. - m_state = arg; - m_this._animate(step || 1); - } - - m_state = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the animation frame rate. This is approximately the number - * of frames displayed per second. A null value will use the browser's - * native requestAnimationFrame to draw new frames. - */ - ////////////////////////////////////////////////////////////////////////////// - this.framerate = function (arg) { - if (arg === undefined) { - return m_rate; - } - m_rate = arg; - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Step to the next frame in the animation. Pauses the animation if it is - * playing. - */ - ////////////////////////////////////////////////////////////////////////////// - this.stepForward = function () { - m_this.state('pause'); - m_this._setNextFrame(1); - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Step to the previous frame in the animation. Pauses the animation if it is - * playing. - */ - ////////////////////////////////////////////////////////////////////////////// - this.stepBackward = function () { - m_this.state('pause'); - m_this._setNextFrame(-1); - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * 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; - this._queue = spec.queue || null; - - /** - * 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).then(function () { - this._fetched = true; - }.bind(this)).promise(this); - } - return this; - }; - - /** - * Return whether this tile has been fetched already. - * - * @returns {boolean} True if the tile has been fetched. - */ - this.fetched = function () { - return this._fetched; - }; - - /** - * 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) { - // both fetch and _queueAdd can replace the current then method - if (!this.fetched() && this._queue && this._queue.add && (!this.state || - this.state() === 'pending')) { - this._queue.add(this, this.fetch); - } else { - this.fetch(); - } - // 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] - */ - ////////////////////////////////////////////////////////////////////////////// - geo.imageTile = function (spec) { - if (!(this instanceof geo.imageTile)) { - return new geo.imageTile(spec); - } - - 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 = defer.resolve; - this._image.onerror = defer.reject; - this._image.src = this._url; - - // attach a promise interface to `this` - defer.then(function () { - this._fetched = true; - }.bind(this)).promise(this); - } - 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'; - - ////////////////////////////////////////////////////////////////////////////// - /** - * 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 - */ - ////////////////////////////////////////////////////////////////////////////// - geo.tileCache = function (options) { - if (!(this instanceof geo.tileCache)) { - return new geo.tileCache(options); - } - options = options || {}; - this._size = options.size || 64; - - /** - * 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; - } - }); - - /** - * 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; - } - - // Remove the tile from the access queue - this._atime.splice(this._access(hash), 1); - - // Remove the tile from the cache - delete this._cache[hash]; - return true; - }; - - /** - * 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; - }; - - /** - * 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 - * @param {boolean} noMove if true, don't move the tile to the front of the - * access queue. - * @returns {geo.tile|null} - */ - this.get = function (hash, noMove) { - hash = typeof hash === 'string' ? hash : hash.toString(); - if (!(hash in this._cache)) { - return null; - } - - if (!noMove) { - this._atime.splice(this._access(hash), 1); - this._atime.unshift(hash); - } - return this._cache[hash]; - }; - - /** - * Add a tile to the cache. - * @param {geo.tile} tile - * @param {function} removeFunc if specified and tiles must be purged from - * the cache, call this function on each tile before purging. - * @param {boolean} noPurge if true, don't purge tiles. - */ - this.add = function (tile, removeFunc, noPurge) { - // 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); - - if (!noPurge) { - this.purge(removeFunc); - } - }; - - /** - * Purge tiles from the cache if it is full. - * @param {function} removeFunc if specified and tiles must be purged from - * the cache, call this function on each tile before purging. - */ - this.purge = function (removeFunc) { - var hash; - while (this._atime.length > this.size) { - hash = this._atime.pop(); - var tile = this._cache[hash]; - if (removeFunc) { - removeFunc(tile); - } - delete this._cache[hash]; - } - }; - - this.clear(); - return this; - }; -})(); - -(function () { - 'use strict'; - - /** - * Standard modulo operator where the output is in [0, b) for all inputs. - * @private - */ - function modulo(a, b) { - return ((a % b) + b) % b; - } - - /** - * 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. - */ - function m_getTileSubdomain(x, y, subdomains) { - return subdomains[modulo(x + y, subdomains.length)]; - } - - /** - * 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); - }; - } - - ////////////////////////////////////////////////////////////////////////////// - /** - * 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 {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} - */ - ////////////////////////////////////////////////////////////////////////////// - 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 ? 600 : 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); - } - - var s_init = this._init, - s_exit = this._exit, - m_lastTileSet = [], - m_exited; - - // copy the options into a private variable - this._options = $.extend(true, {}, options); - - // set the layer attribution text - this.attribution(options.attribution); - - // initialize the object that keeps track of actively drawn tiles - this._activeTiles = {}; - - // 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}); - - // initialize the tile fetch queue - this._queue = geo.fetchQueue({ - // this should probably be 6 * subdomains.length if subdomains are used - size: 6, - // if track is the same as the cache size, then neither processing time - // nor memory will be wasted. Larger values will use more memory, - // smaller values will do needless computations. - track: options.cacheSize, - needed: function (tile) { - return tile === this.cache.get(tile.toString(), true); - }.bind(this) - }); - - var m_tileOffsetValues = {}; - - /** - * 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; - }}); - - /** - * 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 - }}); - - /** - * 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; - }; - - /** - * 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); - if (isNaN(point.x)) { point.x = 0; } - if (isNaN(point.y)) { point.y = 0; } - var to = this._tileOffset(level); - if (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; - }; - - /** - * 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._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 - }; - }; - - /** - * 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}, - queue: this._queue, - 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 - * @param {boolean} delayPurge If true, don't purge tiles from the cache - * @returns {geo.tile} - */ - this._getTileCached = function (index, source, delayPurge) { - var tile = this.cache.get(this._tileHash(index)); - if (tile === null) { - tile = this._getTile(index, source); - this.cache.add(tile, this.remove.bind(this), delayPurge); - } - 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) { - var corners = [ - this.tileAtPoint({x: bounds.left, y: bounds.top}, level), - this.tileAtPoint({x: bounds.right, y: bounds.top}, level), - this.tileAtPoint({x: bounds.left, y: bounds.bottom}, level), - this.tileAtPoint({x: bounds.right, y: bounds.bottom}, level) - ]; - return { - start: { - x: Math.min(corners[0].x, corners[1].x, corners[2].x, corners[3].x), - y: Math.min(corners[0].y, corners[1].y, corners[2].y, corners[3].y) - }, - end: { - x: Math.max(corners[0].x, corners[1].x, corners[2].x, corners[3].x), - y: Math.max(corners[0].y, corners[1].y, corners[2].y, corners[3].y) - } - }; - }; - - /** - * 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 - * @param {boolean} onlyIfChanged If the set of tiles have not changed - * (even if their desired order has), return undefined instead of an - * array of tiles. - * @returns {geo.tile[]} An array of tile objects - */ - this._getTiles = function (maxLevel, bounds, sorted, onlyIfChanged) { - var i, j, tiles = [], index, nTilesLevel, - start, end, indexRange, source, center, changed = false, old, level, - minLevel = (this._options.keepLower ? this._options.minLevel : - maxLevel); - if (maxLevel < minLevel) { - maxLevel = minLevel; - } - - /* Generate a list of the tiles that we want to create. This is done - * before sorting, because we want to actually generate the tiles in - * the sort order. */ - 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); - - if (!this._options.wrapX) { - start.x = Math.min(Math.max(start.x, 0), nTilesLevel.x - 1); - end.x = Math.min(Math.max(end.x, 0), nTilesLevel.x - 1); - if (level === minLevel) { - start.x = 0; - end.x = nTilesLevel.x - 1; - } - } - if (!this._options.wrapY) { - start.y = Math.min(Math.max(start.y, 0), nTilesLevel.y - 1); - end.y = Math.min(Math.max(end.y, 0), nTilesLevel.y - 1); - if (level === minLevel) { - start.y = 0; - end.y = nTilesLevel.y - 1; - } - } - /* If we are reprojecting tiles, we need a check to not use all levels - * if the number of tiles is excessive. */ - if (this._options.gcs && this._options.gcs !== this.map().gcs() && - level !== minLevel && - (end.x + 1 - start.x) * (end.y + 1 - start.y) > - (this.map().size().width * this.map().size().height / - this._options.tileWidth / this._options.tileHeight) * 16) { - break; - } - - // loop over the tile range - for (i = start.x; i <= end.x; i += 1) { - for (j = start.y; j <= end.y; j += 1) { - index = {level: level, x: i, y: j}; - source = {level: level, x: i, y: j}; - if (this._options.wrapX) { - source.x = modulo(source.x, nTilesLevel.x); - } - if (this._options.wrapY) { - source.y = modulo(source.y, nTilesLevel.y); - } - if (this.isValid(source)) { - if (onlyIfChanged && tiles.length < m_lastTileSet.length) { - old = m_lastTileSet[tiles.length]; - changed = changed || (index.level !== old.level || - index.x !== old.x || index.y !== old.y); - } - tiles.push({index: index, source: source}); - } - } - } - } - - if (onlyIfChanged) { - if (!changed && tiles.length === m_lastTileSet.length) { - return; - } - m_lastTileSet.splice(0, m_lastTileSet.length); - $.each(tiles, function (idx, tile) { - m_lastTileSet.push(tile.index); - }); - } - - if (sorted) { - center = { - x: (start.x + end.x) / 2, - y: (start.y + end.y) / 2, - level: maxLevel, - bottomLevel: maxLevel - }; - var numTiles = Math.max(end.x - start.x, end.y - start.y) + 1; - for (; numTiles >= 1; numTiles /= 2) { - center.bottomLevel -= 1; - } - tiles.sort(this._loadMetric(center)); - /* If we are using a fetch queue, start a new batch */ - if (this._queue) { - this._queue.batch(true); - } - } - if (this.cache.size < tiles.length) { - console.log('Increasing cache size to ' + tiles.length); - this.cache.size = tiles.length; - } - /* Actually get the tiles. */ - for (i = 0; i < tiles.length; i += 1) { - tiles[i] = this._getTileCached(tiles[i].index, tiles[i].source, true); - } - this.cache.purge(this.remove.bind(this)); - return tiles; - }; - - /** - * 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 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; - - a = a.index || a; - b = b.index || b; - // shortcut if zoom level differs - if (a.level !== b.level) { - if (center.bottomLevel && ((a.level >= center.bottomLevel) !== - (b.level >= center.bottomLevel))) { - return a.level >= center.bottomLevel ? -1 : 1; - } - return a.level - b.level; - } - - /* compute the center coordinates relative to a.level. Since we really - * care about the center of the tiles, use an offset */ - scale = Math.pow(2, a.level - center.level); - cx = (center.x + 0.5) * scale - 0.5; - cy = (center.y + 0.5) * scale - 0.5; - - // calculate distances to the center squared - 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; - - // return negative if a < b, or positive if a > b - return a0 - b0; - }; - }; - - /** - * 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: coord.x * s, - y: coord.y * s - }; - }; - - /** - * 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 - }; - }; - - /** - * Draw the given tile on the active canvas. - * @param {geo.tile} tile The tile to draw - */ - this.drawTile = function (tile) { - var hash = tile.toString(); - - 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); - } - - // add the tile to the active cache - this._activeTiles[hash] = tile; - }; - - /** - * 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.'); - } - - // 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 - parseInt(div.attr('offsetx') || 0, 10)) + 'px', - top: (bounds.top - parseInt(div.attr('offsety') || 0, 10)) + 'px' - }); - - // 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 { - /* 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); - } - $(tile.image).remove(); - } - }; - - /** - * 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); - }; - - /** - * 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}), - ur = this.displayToLevel({x: size.width, y: 0}), - ll = this.displayToLevel({x: 0, y: size.height}), - lr = this.displayToLevel({x: size.width, y: size.height}); - return { - level: zoom, - scale: scale, - left: Math.min(ul.x, ur.x, ll.x, lr.x), - right: Math.max(ul.x, ur.x, ll.x, lr.x), - top: Math.min(ul.y, ur.y, ll.y, lr.y), - bottom: Math.max(ul.y, ur.y, ll.y, lr.y) - }; - }; - - /** - * 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. - * @param {object} bounds view bounds. If not specified, this is - * obtained from _getViewBounds(). - */ - this._purge = function (zoom, doneLoading, bounds) { - var tile, hash; - - // Don't purge tiles in an active update - if (this._updating) { - return; - } - - // get the view bounds - if (!bounds) { - bounds = this._getViewBounds(); - } - - for (hash in this._activeTiles) { - - tile = this._activeTiles[hash]; - if (this._canPurge(tile, bounds, zoom, doneLoading)) { - this.remove(tile); - } - } - return this; - }; - - /** - * Remove all active tiles from the canvas. - * @returns {geo.tile[]} The array of tiles removed - */ - this.clear = function () { - var tiles = [], tile; - - // ignoring the warning here because this is a privately - // controlled object with simple keys - for (tile in this._activeTiles) { - tiles.push(this.remove(tile)); - } - - // clear out the tile coverage tree - this._tileTree = {}; - - m_lastTileSet = []; - - return tiles; - }; - - /** - * 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(); - }; - - /** - * 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 - }; - }; - - /** - * 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) { - //DWM:: these need to alawys use the *layer* unitsPerPixel, or possibly - //DWM:: convert tile space using a transform - var map = this.map(), - unit = map.unitsPerPixel(zoom === undefined ? map.zoom() : zoom); - return { - x: pt.x * unit, - y: this._topDown() * pt.y * unit - }; - }; - - /** - * Return a factor for invertin the y units as appropriate. - * @return {number} - */ - this._topDown = function () { - return this._options.topDown ? 1 : -1; - }; - - /** - * 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) { - if (!this.canvas()) { - return; - } - var node = this.canvas() - .find('div[data-tile-layer=' + level.toFixed() + ']').get(0); - if (!node) { - node = $( - '
' - ).css({ - 'transform-origin': '0px 0px', - 'line-height': 0, - 'font-size': 0 - }).get(0); - this.canvas().append(node); - } - return node; - }; - - /** - * Set sublayer transforms to align them with the given zoom level. - * @param {number} level The target zoom level - * @param {object} view The view bounds. The top and left are used to - * adjust the offset of tile layers. - * @return {object} the x and y offsets for the current level. - */ - this._updateSubLayers = function (level, view) { - var canvas = this.canvas(), - lastlevel = parseInt(canvas.attr('lastlevel'), 10), - lastx = parseInt(canvas.attr('lastoffsetx') || 0, 10), - lasty = parseInt(canvas.attr('lastoffsety') || 0, 10); - if (lastlevel === level && Math.abs(lastx - view.left) < 65536 && - Math.abs(lasty - view.top) < 65536) { - return {x: lastx, y: lasty}; - } - var map = this.map(), - to = this._tileOffset(level), - x = parseInt((view.left + view.right - map.size().width) / 2 + to.x, 10), - y = parseInt((view.top + view.bottom - map.size().height) / 2 + to.y, 10); - canvas.find('.geo-tile-layer').each(function (idx, el) { - var $el = $(el), - layer = parseInt($el.data('tileLayer'), 10); - $el.css( - 'transform', - 'scale(' + Math.pow(2, level - layer) + ')' - ); - var layerx = parseInt(x / Math.pow(2, level - layer), 10), - layery = parseInt(y / Math.pow(2, level - layer), 10), - dx = layerx - parseInt($el.attr('offsetx') || 0, 10), - dy = layery - parseInt($el.attr('offsety') || 0, 10); - $el.attr({offsetx: layerx, offsety: layery}); - $el.find('.geo-tile-container').each(function (tileidx, tileel) { - $(tileel).css({ - left: (parseInt($(tileel).css('left'), 10) - dx) + 'px', - top: (parseInt($(tileel).css('top'), 10) - dy) + 'px' - }); - }); - }); - canvas.attr({lastoffsetx: x, lastoffsety: y, lastlevel: level}); - return {x: x, y: y}; - }; - - /** - * Update the view according to the map/camera. - * @returns {this} Chainable - */ - this._update = function (evt) { - /* Ignore zoom and rotate events, as they are ALWAYS followed by a pan - * event */ - if (evt && evt.event && (evt.event.event === geo.event.zoom || - evt.event.event === geo.event.rotate)) { - return; - } - var map = this.map(), - bounds = map.bounds(undefined, null), - tiles; - - if (this._updateSubLayers) { - var mapZoom = map.zoom(), - zoom = this._options.tileRounding(mapZoom), - view = this._getViewBounds(); - // Update the transform for the local layer coordinates - var offset = this._updateSubLayers(zoom, view) || {x: 0, y: 0}; - - var to = this._tileOffset(zoom); - if (this.renderer() === null) { - var scale = Math.pow(2, mapZoom - zoom), - rotation = map.rotation(), - rx = -to.x + -(view.left + view.right) / 2 + offset.x, - ry = -to.y + -(view.bottom + view.top) / 2 + offset.y, - dx = (rx + map.size().width / 2) * scale, - dy = (ry + map.size().height / 2) * scale; - - this.canvas().css({ - 'transform-origin': '' + - -rx + 'px ' + - -ry + 'px' - }); - var transform = 'translate(' + dx + 'px' + ',' + dy + 'px' + ')' + - 'scale(' + scale + ')'; - if (rotation) { - transform += 'rotate(' + (rotation * 180 / Math.PI) + 'deg)'; - } - this.canvas().css('transform', transform); - } - /* 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, - offsetx: offset.x, - offsety: offset.y, - rotation: map.rotation() - }); - } - - tiles = this._getTiles( - zoom, bounds, true, true - ); - - if (tiles === undefined) { - return; - } - - // reset the tile coverage tree - this._tileTree = {}; - - tiles.forEach(function (tile) { - if (tile.fetched()) { - /* if we have already fetched the tile, we know we can just draw it, - * as the bounds won't have changed since the call to _getTiles. */ - this.drawTile(tile); - - // mark the tile as covered - this._setTileTree(tile); - } else { - if (!tile._queued) { - tile.then(function () { - if (m_exited) { - /* If we have disconnected the renderer, do nothing. This - * happens when the layer is being deleted. */ - return; - } - 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), - view = this._getViewBounds(); - if (this._canPurge(tile, view, zoom)) { - this.remove(tile); - return; - } - - this.drawTile(tile); - - // mark the tile as covered - this._setTileTree(tile); - }.bind(this)); - - this.addPromise(tile); - tile._queued = true; - } else { - /* If we are using a fetch queue, tell the queue so this tile can - * be reprioritized. */ - var pos = this._queue ? this._queue.get(tile) : -1; - if (pos >= 0) { - this._queue.add(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 () { - var map = this.map(), - mapZoom = map.zoom(), - zoom = this._options.tileRounding(mapZoom); - 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) { - if (this._options.keepLower) { - return; - } - 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]; - } - - // 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._tileOffset(tile.index.level); - var scale = 1; - if (tile.index.level !== bounds.level) { - scale = Math.pow(2, (bounds.level || 0) - (tile.index.level || 0)); - } - 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 && - tile.index.level !== this._options.minLevel) { - return true; - } - if (tile.index.level === this._options.minLevel && - !this._options.wrapX && !this._options.wrapY) { - return false; - } - } else { - /* 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; - }; - - /** - * 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, this._options.gcs || 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; - }; - - /** - * Get or set the subdomains used for templating. - * - * @param {string|list} [subdomains] A comma-separated list, a string of - * single character subdomains, or a list. - * @returns {string|list|this} - */ - this.subdomains = function (subdomains) { - if (subdomains === undefined) { - return this._options.subdomains; - } - if (subdomains) { - if ($.type(subdomains) === 'string') { - if (subdomains.indexOf(',') >= 0) { - subdomains = subdomains.split(','); - } else { - subdomains = subdomains.split(''); - } - } - this._options.subdomains = subdomains; - this.reset(); - this.map().draw(); - } - return this; - }; - - /** - * Return a value from the tileOffset function, caching it for different - * levels. - * - * @param {Number} level the level to pass to the tileOffset function. - * @returns {Object} a tile offset object with x and y properties. - */ - this._tileOffset = function (level) { - if (m_tileOffsetValues[level] === undefined) { - m_tileOffsetValues[level] = this._options.tileOffset(level); - } - return m_tileOffsetValues[level]; - }; - - /** - * 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; - }; - - /** - * Clean up the layer. - */ - this._exit = function () { - this.reset(); - // call super method - s_exit.apply(this, arguments); - m_exited = true; - return this; - }; - - geo.adjustLayerForRenderer('tile', this); - - return 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', - 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); -})(); - -(function () { - 'use strict'; - - ////////////////////////////////////////////////////////////////////////////// - /** - * This class implements a queue for Deferred objects. Whenever one of the - * objects in the queue completes (resolved or rejected), another item in the - * queue is processed. The number of concurrently processing items can be - * adjusted. At this time (2015-12-29) most major browsers support 6 - * concurrent requests from any given server, so, when using the queue for - * tile images, thie number of concurrent requests should be 6 * (number of - * subdomains serving tiles). - * - * @class - * - * @param {Object?} [options] A configuration object for the queue - * @param {Number} [options.size=6] The maximum number of concurrent deferred - * objects. - * @param {Number} [options.track=600] The number of objects that are tracked - * that trigger checking if any of them have been abandoned. The fetch - * queue can grow to the greater of this size and the number of items that - * are still needed. Setting this to a low number will increase - * processing time, to a high number can increase memory. Ideally, it - * should reflect the number of items that are kept in memory elsewhere. - * If needed is null, this is ignored. - * @param {function} [options.needed=null] If set, this function is passed a - * Deferred object and must return a truthy value if the object is still - * needed. - */ - ////////////////////////////////////////////////////////////////////////////// - geo.fetchQueue = function (options) { - if (!(this instanceof geo.fetchQueue)) { - return new geo.fetchQueue(options); - } - options = options || {}; - this._size = options.size || 6; - this._track = options.track || 600; - this._needed = options.needed || null; - this._batch = false; - - var m_this = this, - m_next_batch = 1; - - /** - * Get/set the maximum concurrent deferred object size. - */ - Object.defineProperty(this, 'size', { - get: function () { return this._size; }, - set: function (n) { - this._size = n; - this.next_item(); - } - }); - - /** - * Get the current queue size. - */ - Object.defineProperty(this, 'length', { - get: function () { return this._queue.length; } - }); - - /** - * Get the current number of processing items. - */ - Object.defineProperty(this, 'processing', { - get: function () { return this._processing; } - }); - - /** - * Remove all items from the queue. - */ - this.clear = function () { - this._queue = []; - this._processing = 0; - return this; - }; - - /** - * Add a Deferred object to the queue. - * @param {Deferred} defer Deferred object to add to the queue. - * @param {function} callback a function to call when the item's turn is - * granted. - * @param {boolean} atEnd if false, add the item to the front of the queue - * if batching is turned off or at the end of the current batch if it is - * turned on. If true, always add the item to the end of the queue. - */ - this.add = function (defer, callback, atEnd) { - if (defer.__fetchQueue) { - var pos = $.inArray(defer, this._queue); - if (pos >= 0) { - this._queue.splice(pos, 1); - this._addToQueue(defer, atEnd); - return defer; - } - } - var wait = new $.Deferred(); - var process = new $.Deferred(); - wait.then(function () { - $.when(callback.call(defer)).always(process.resolve); - }, process.resolve); - defer.__fetchQueue = wait; - this._addToQueue(defer, atEnd); - $.when(wait, process).always(function () { - if (m_this._processing > 0) { - m_this._processing -= 1; - } - m_this.next_item(); - }).promise(defer); - m_this.next_item(); - return defer; - }; - - /** - * Add an item to the queue. If batches are being used, add it at after - * other items in the same batch. - * @param {Deferred} defer Deferred object to add to the queue. - * @param {boolean} atEnd if false, add the item to the front of the queue - * if batching is turned off or at the end of the current batch if it is - * turned on. If true, always add the item to the end of the queue. - */ - this._addToQueue = function (defer, atEnd) { - defer.__fetchQueue._batch = this._batch; - if (atEnd) { - this._queue.push(defer); - } else if (!this._batch) { - this._queue.unshift(defer); - } else { - for (var i = 0; i < this._queue.length; i += 1) { - if (this._queue[i].__fetchQueue._batch !== this._batch) { - break; - } - } - this._queue.splice(i, 0, defer); - } - }; - - /** - * Get the position of a deferred object in the queue. - * @param {Deferred} defer Deferred object to get the position of. - * @returns {number} -1 if not in the queue, or the position in the queue. - */ - this.get = function (defer) { - return $.inArray(defer, this._queue); - }; - - /** - * Remove a Deferred object from the queue. - * @param {Deferred} defer Deferred object to add to the queue. - * @returns {bool} true if the object was removed - */ - this.remove = function (defer) { - var pos = $.inArray(defer, this._queue); - if (pos >= 0) { - this._queue.splice(pos, 1); - return true; - } - return false; - }; - - /** - * Start a new batch or clear using batches. - * @param {boolean} start true to start a new batch, false to turn off - * using batches. Undefined to return the current - * state of batches. - * @return {Number|boolean|Object} the current batch state or this object. - */ - this.batch = function (start) { - if (start === undefined) { - return this._batch; - } - if (!start) { - this._batch = false; - } else { - this._batch = m_next_batch; - m_next_batch += 1; - } - return this; - }; - - /** - * Check if any items are queued and if there if there are not too many - * deferred objects being processed. If so, process more items. - */ - this.next_item = function () { - if (m_this._innextitem) { - return; - } - m_this._innextitem = true; - /* if the queue is greater than the track size, check each item to see - * if it is still needed. */ - if (m_this._queue.length > m_this._track && this._needed) { - for (var i = m_this._queue.length - 1; i >= 0; i -= 1) { - if (!m_this._needed(m_this._queue[i])) { - var discard = m_this._queue.splice(i, 1)[0]; - m_this._processing += 1; - discard.__fetchQueue.reject(); - delete discard.__fetchQueue; - } - } - } - while (m_this._processing < m_this._size && m_this._queue.length) { - var defer = m_this._queue.shift(); - if (defer.__fetchQueue) { - m_this._processing += 1; - var needed = m_this._needed ? m_this._needed(defer) : true; - if (needed) { - defer.__fetchQueue.resolve(); - } else { - defer.__fetchQueue.reject(); - } - delete defer.__fetchQueue; - } - } - m_this._innextitem = false; - }; - - this.clear(); - return this; - }; -})(); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - arg = arg || {}; - - if (!(arg.layer instanceof geo.featureLayer)) { - throw 'fileReader must be given a feature layer'; - } - - var m_layer = arg.layer; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the feature layer attached to the reader - */ - //////////////////////////////////////////////////////////////////////////// - this.layer = function () { - return m_layer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Tells the caller if it can handle the given file by returning a boolean. - */ - //////////////////////////////////////////////////////////////////////////// - this.canRead = function () { - return false; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.read = function (file, done) { - done(false); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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); - }; - 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._getString = function (file, done, progress) { - var reader = newFileReader(done, progress); - reader.readAsText(file); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Like _getString, but returns an ArrayBuffer object. - */ - //////////////////////////////////////////////////////////////////////////// - this._getArrayBuffer = function (file, done, progress) { - var reader = newFileReader(done, progress); - reader.readAsText(file); - }; - - return this; -}; - -inherit(geo.fileReader, geo.object); - -/*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); - } - - 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 (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; - }; - - 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); - }); - } - } - } - - if (file instanceof File) { - m_this._getString(file, onDone, progress); - } else if (typeof file === 'string') { - onDone(file); - } else { - done(file); - } - }; - - 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; - }; - - 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._getCoordinates = function (spec) { - var geometry = spec.geometry || {}, - coordinates = geometry.coordinates || [], elv; - - if ((coordinates.length === 2 || coordinates.length === 3) && - (isFinite(coordinates[0]) && isFinite(coordinates[1]))) { - - // Do we have a elevation component - if (isFinite(coordinates[2])) { - elv = coordinates[2]; - } - - // special handling for single point coordinates - return [{x: coordinates[0], y: coordinates[1], z: elv}]; - } - - // 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 an array of points for LineString, MultiPoint, etc... - return 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); - 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); - } - }); - - if (done) { - done(allFeatures); - } - } - - m_this._readObject(file, _done, progress); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 - }; - }); - }; - - 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; - }); - } - return feature; - }; - -}; - -inherit(geo.jsonReader, geo.fileReader); - -geo.registerFileReader('jsonReader', geo.jsonReader); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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} [rotation=0] Clockwise rotation in radians - * @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. - * @param {boolean} [allowRotation=true] False prevents rotation, true allows - * any rotation. If a function, the function is called with a rotation - * (angle in radians) and returns a valid rotation (this can be used to - * constrain the rotation to a range or specific values). - * - * *** 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 || {}; - - if (arg.node === undefined || arg.node === null) { - console.warn('map creation requires a node'); - return this; - } - - geo.sceneObject.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * 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_rotation = 0, - 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_allowRotation = (typeof arg.allowRotation === 'function' ? - arg.allowRotation : (arg.allowRotation === undefined ? - true : !!arg.allowRotation)), - 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 and ignored 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. */ - var mcx = ((m_maxBounds.left || 0) + (m_maxBounds.right || 0)) / 2, - mcy = ((m_maxBounds.bottom || 0) + (m_maxBounds.top || 0)) / 2; - m_maxBounds.left = geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ - x: m_maxBounds.left !== undefined ? m_maxBounds.left : -180, y: mcy - }])[0].x; - m_maxBounds.right = geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ - x: m_maxBounds.right !== undefined ? m_maxBounds.right : 180, y: mcy - }])[0].x; - m_maxBounds.top = (m_maxBounds.top !== undefined ? - geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ - x: mcx, y: m_maxBounds.top}])[0].y : m_maxBounds.right); - m_maxBounds.bottom = (m_maxBounds.bottom !== undefined ? - geo.transform.transformCoordinates(m_ingcs, m_gcs, [{ - x: mcx, 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/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.unitsPerPixel = function (zoom, unit) { - zoom = zoom || 0; - if (unit) { - // get the units at level 0 - m_unitsPerPixel = Math.pow(2, zoom) * unit; - - // redraw all the things - m_this.draw(); - return m_this; - } - return Math.pow(2, -zoom) * m_unitsPerPixel; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/set the clampBoundsX setting. If changed, adjust the bounds of the - * map as needed. - * - * @param {boolean?} clamp The new clamp value. - * @returns {boolean|this} - */ - //////////////////////////////////////////////////////////////////////////// - this.clampBoundsX = function (clamp) { - if (clamp === undefined) { - return m_clampBoundsX; - } - if (clamp !== m_clampBoundsX) { - m_clampBoundsX = !!clamp; - m_this.pan({x: 0, y: 0}); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/set the clampBoundsY setting. If changed, adjust the bounds of the - * map as needed. - * - * @param {boolean?} clamp The new clamp value. - * @returns {boolean|this} - */ - //////////////////////////////////////////////////////////////////////////// - this.clampBoundsY = function (clamp) { - if (clamp === undefined) { - return m_clampBoundsY; - } - if (clamp !== m_clampBoundsY) { - m_clampBoundsY = !!clamp; - m_this.pan({x: 0, y: 0}); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/set the clampZoom setting. If changed, adjust the bounds of the map - * as needed. - * - * @param {boolean?} clamp The new clamp value. - * @returns {boolean|this} - */ - //////////////////////////////////////////////////////////////////////////// - this.clampZoom = function (clamp) { - if (clamp === undefined) { - return m_clampZoom; - } - if (clamp !== m_clampZoom) { - m_clampZoom = !!clamp; - reset_minimum_zoom(); - m_this.zoom(m_zoom); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/set the allowRotation setting. If changed, adjust the map as needed. - * - * @param {boolean|function} allowRotation the new allowRotation value. - * @returns {boolean|function|this} - */ - //////////////////////////////////////////////////////////////////////////// - this.allowRotation = function (allowRotation) { - if (allowRotation === undefined) { - return m_allowRotation; - } - if (typeof allowRotation !== 'function') { - allowRotation = !!allowRotation; - } - if (allowRotation !== m_allowRotation) { - m_allowRotation = allowRotation; - m_this.rotation(m_rotation); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the map's world coordinate origin in gcs coordinates - * - * @returns {object} - */ - //////////////////////////////////////////////////////////////////////////// - this.origin = function () { - return $.extend({}, m_origin); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the map's world coordinate scaling relative gcs units - * - * @returns {object} - */ - //////////////////////////////////////////////////////////////////////////// - this.scale = function () { - return $.extend({}, m_scale); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the camera - * - * @returns {geo.camera} - */ - //////////////////////////////////////////////////////////////////////////// - this.camera = function () { - return m_camera; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get map gcs - * - * @returns {string} - */ - //////////////////////////////////////////////////////////////////////////// - this.gcs = function (arg) { - if (arg === undefined) { - return m_gcs; - } - if (arg !== m_gcs) { - var oldCenter = m_this.center(undefined, undefined); - m_gcs = arg; - reset_minimum_zoom(); - var newZoom = fix_zoom(m_zoom); - if (newZoom !== m_zoom) { - m_this.zoom(newZoom); - } - m_this.center(oldCenter, undefined); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get map interface gcs - * - * @returns {string} - */ - //////////////////////////////////////////////////////////////////////////// - this.ingcs = function (arg) { - if (arg === undefined) { - return m_ingcs; - } - m_ingcs = arg; - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get root node of the map - * - * @returns {object} - */ - //////////////////////////////////////////////////////////////////////////// - this.node = function () { - return m_node; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set zoom level of the map - * - * @returns {Number|geo.map} - */ - //////////////////////////////////////////////////////////////////////////// - this.zoom = function (val, origin, ignoreDiscreteZoom) { - var evt, bounds; - if (val === undefined) { - return 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; - } - - m_zoom = val; - - bounds = m_this.boundsFromZoomAndCenter(val, m_center, m_rotation, null); - m_this.modified(); - - camera_bounds(bounds, m_rotation); - evt = { - geo: {}, - zoomLevel: m_zoom, - screenPosition: origin ? origin.map : undefined - }; - m_this.geoTrigger(geo.event.zoom, evt); - - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 - }; - - unit = m_this.unitsPerPixel(m_zoom); - - var sinr = Math.sin(m_rotation), cosr = Math.cos(m_rotation); - m_camera.pan({ - x: (delta.x * cosr - (-delta.y) * sinr) * unit, - y: (delta.x * sinr + (-delta.y) * cosr) * unit - }); - /* If m_clampBounds* is true, clamp the pan */ - var bounds = fix_bounds(m_camera.bounds, m_rotation); - if (bounds !== m_camera.bounds) { - var panPos = m_this.gcsToDisplay({ - x: m_camera.bounds.left, y: m_camera.bounds.top}, null); - bounds = m_this.boundsFromZoomAndCenter(m_zoom, { - x: (bounds.left + bounds.right) / 2, - y: (bounds.top + bounds.bottom) / 2 - }, m_rotation, null); - camera_bounds(bounds, m_rotation); - 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 map rotation. The rotation is performed around the current - * view center. - * - * @param {Object} rotation angle in radians (positive is clockwise) - * @param {Object} origin is specified, rotate about this origin - * @param {boolean} ignoreRotationFunc if true, don't constrain the rotation. - * @returns {geo.map} - */ - //////////////////////////////////////////////////////////////////////////// - this.rotation = function (rotation, origin, ignoreRotationFunc) { - if (rotation === undefined) { - return m_rotation; - } - rotation = fix_rotation(rotation, ignoreRotationFunc); - if (rotation === m_rotation) { - return m_this; - } - m_rotation = rotation; - - var bounds = m_this.boundsFromZoomAndCenter( - m_zoom, m_center, m_rotation, null); - m_this.modified(); - - camera_bounds(bounds, m_rotation); - - var evt = { - geo: {}, - rotation: m_rotation, - screenPosition: origin ? origin.map : undefined - }; - - m_this.geoTrigger(geo.event.rotate, evt); - - 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}); - } - /* Changing the rotation can change our minimum zoom */ - reset_minimum_zoom(); - m_this.zoom(m_zoom); - return m_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 - * @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.center = function (coordinates, gcs) { - var center; - if (coordinates === undefined) { - center = $.extend({}, m_this.worldToGcs(m_center, gcs)); - return center; - } - - // 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, m_rotation, null), m_rotation); - m_this.modified(); - // trigger a pan event - m_this.geoTrigger( - geo.event.pan, - { - geo: coordinates, - screenDelta: null - } - ); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Add layer to the map - * - * @param {geo.layer} layer to be added to the map - * @return {geom.map} - */ - //////////////////////////////////////////////////////////////////////////// - this.createLayer = function (layerName, arg) { - arg = arg || {}; - var newLayer = geo.createLayer( - layerName, m_this, arg); - - if (newLayer) { - m_this.addChild(newLayer); - m_this.children().forEach(function (c) { - if (c instanceof geo.gui.uiLayer) { - c.moveToTop(); - } - }); - newLayer._update(); - m_this.modified(); - - m_this.geoTrigger(geo.event.layerAdd, { - type: geo.event.layerAdd, - target: m_this, - layer: newLayer - }); - } - - return newLayer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Remove layer from the map - * - * @param {geo.layer} layer that should be removed from the map - * @return {geo.map} - */ - //////////////////////////////////////////////////////////////////////////// - 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 - }); - } - - /// 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the rotated size of the map. This is the width and height of the - * non-rotated area necessary to enclose the rotated area in pixels. - * - * @returns {Object} An object containing width and height as keys - */ - //////////////////////////////////////////////////////////////////////////// - this.rotatedSize = function () { - if (!this.rotation()) { - return { - width: m_width, - height: m_height - }; - } - var bds = rotate_bounds_center( - {x: 0, y: 0}, {width: m_width, height: m_height}, this.rotation()); - return { - width: Math.abs(bds.right - bds.left), - height: Math.abs(bds.top - bds.bottom) - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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) { - - // 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; - - reset_minimum_zoom(); - var newZoom = fix_zoom(m_zoom); - if (newZoom !== m_zoom) { - m_this.zoom(newZoom); - } - m_this.camera().viewport = {width: w, height: h}; - m_this.center(oldCenter); - - m_this.geoTrigger(geo.event.resize, { - type: geo.event.resize, - target: m_this, - x: m_x, - y: m_y, - width: w, - height: h - }); - - m_this.modified(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 - */ - //////////////////////////////////////////////////////////////////////////// - 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]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.worldToDisplay = function (c) { - return m_camera.worldToDisplay(c); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from display to gcs coordinates - * - * displayToWorld | worldToGcs - * - * @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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.displayToWorld = function (c) { - return m_camera.displayToWorld(c); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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(); - } - - m_this.geoTrigger(geo.event.drawEnd, { - type: geo.event.drawEnd, - target: m_this - } - ); - - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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(); - }; - - 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; - - function done() { - m_this.draw(); - } - - if (reader) { - evt.stopPropagation(); - evt.preventDefault(); - - 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 - } - } - } - }); - - //////////////////////////////////////////////////////////////////////////// - /** - * Get or set the map interactor - */ - //////////////////////////////////////////////////////////////////////////// - this.interactor = function (arg) { - if (arg === undefined) { - return m_interactor; - } - m_interactor = arg; - - // 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get or set the map clock - */ - //////////////////////////////////////////////////////////////////////////// - this.clock = function (arg) { - if (arg === undefined) { - return m_clock; - } - m_clock = arg; - - if (m_clock) { - m_clock.object(m_this); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get or set the min/max zoom range. - * - * @param {Object} arg {min: minimumzoom, max: maximumzom} - * @param {boolean} noRefresh if true, don't update the map if the zoom level - * has changed. - * @returns {Object|geo.map} - */ - //////////////////////////////////////////////////////////////////////////// - this.zoomRange = function (arg, noRefresh) { - if (arg === undefined) { - return $.extend({}, m_validZoomRange); - } - if (arg.max !== undefined) { - m_validZoomRange.max = arg.max; - } - if (arg.min !== undefined) { - m_validZoomRange.min = m_validZoomRange.origMin = arg.min; - } - reset_minimum_zoom(); - if (!noRefresh) { - m_this.zoom(m_zoom); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Start an animated zoom/pan/rotate. If a second transition is requested - * while a transition is already in progress, a new transition is created - * that is functionally from whereever the map has moved to (possibly partway - * through the first transition) going to the end point of the new - * transition. - * - * Options: - *
-   *   opts = {
-   *     center: { x: ... , y: ... } // the new center
-   *     zoom: ... // the new zoom level
-   *     rotation: ... // the new rotation angle
-   *     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.transition = function (opts, gcs) { - - 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) { - var result = []; - $.each(p0, function (idx) { - result.push(interp1(p0[idx], p1[idx], t)); - }); - return result; - }; - } - - 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(), - rotation: m_this.rotation(), - 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 = $.extend(true, {}, opts); - opts.center = geo.util.normalizeCoordinates(opts.center); - if (gcs !== m_gcs) { - opts.center = geo.transform.transformCoordinates(gcs, m_gcs, [ - opts.center])[0]; - } - } - opts = $.extend(true, {}, defaultOpts, opts); - - m_transition = { - start: { - center: m_this.center(undefined, null), - zoom: m_this.zoom(), - rotation: m_this.rotation() - }, - end: { - center: opts.center, - zoom: fix_zoom(opts.zoom), - rotation: fix_rotation(opts.rotation, undefined, true) - }, - ease: opts.ease, - zCoord: opts.zCoord, - done: opts.done, - duration: opts.duration - }; - - if (opts.zCoord) { - m_transition.interp = opts.interp( - [ - m_transition.start.center.x, - m_transition.start.center.y, - zoom2z(m_transition.start.zoom), - m_transition.start.rotation - ], - [ - m_transition.end.center.x, - m_transition.end.center.y, - zoom2z(m_transition.end.zoom), - m_transition.end.rotation - ] - ); - } else { - m_transition.interp = opts.interp( - [ - m_transition.start.center.x, - m_transition.start.center.y, - m_transition.start.zoom, - m_transition.start.rotation - ], - [ - m_transition.end.center.x, - m_transition.end.center.y, - m_transition.end.zoom, - m_transition.end.rotation - ] - ); - } - - 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 + opts.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); - m_this.rotation(fix_rotation(m_transition.end.rotation)); - } - - m_transition = null; - - m_this.geoTrigger(geo.event.transitionend, opts); - - if (done) { - done(); - } - - if (next) { - m_queuedTransition = null; - m_this.transition(next); - } - - return; - } - - var z = m_transition.ease( - (time - m_transition.start.time) / opts.duration - ); - - var p = m_transition.interp(z); - if (m_transition.zCoord) { - p[2] = z2zoom(p[2]); - } - if (fix_zoom(p[2], true) === m_zoom) { - m_this.center({ - x: p[0], - y: p[1] - }, null); - } else { - m_center = m_this.gcsToWorld({x: p[0], y: p[1]}, null); - m_this.zoom(p[2], undefined, true); - } - m_this.rotation(p[3], undefined, true); - - window.requestAnimationFrame(anim); - } - - m_this.geoTrigger(geo.event.transitionstart, opts); - - if (geo.event.cancelNavigation) { - m_transition = null; - m_this.geoTrigger(geo.event.transitionend, opts); - return m_this; - } else if (geo.event.cancelAnimation) { - // run the navigation synchronously - opts.duration = 0; - anim(0); - } else { - window.requestAnimationFrame(anim); - } - 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 left, - * top, right, bottom declaring the desired new map bounds. The new bounds - * will contain at least the min/max lat/lngs provided modified by clamp - * settings. 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.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, m_rotation); - nav = m_this.zoomAndCenterFromBounds(bds, m_rotation, null); - - // This might have consequences 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, m_rotation, gcs); - }; - - this.maxBounds = function (bounds, gcs) { - gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); - if (bounds === undefined) { - return { - left: geo.transform.transformCoordinates(m_gcs, gcs, [{ - x: m_maxBounds.left, y: 0}])[0].x, - right: geo.transform.transformCoordinates(m_gcs, gcs, [{ - x: m_maxBounds.right, y: 0}])[0].x, - bottom: geo.transform.transformCoordinates(m_gcs, gcs, [{ - x: 0, y: m_maxBounds.bottom}])[0].y, - top: geo.transform.transformCoordinates(m_gcs, gcs, [{ - x: 0, y: m_maxBounds.top}])[0].y - }; - } - var cx = ((bounds.left || 0) + (bounds.right || 0)) / 2, - cy = ((bounds.bottom || 0) + (bounds.top || 0)) / 2; - if (bounds.left !== undefined) { - m_maxBounds.left = geo.transform.transformCoordinates(gcs, m_gcs, [{ - x: bounds.left, y: cy}])[0].x; - } - if (bounds.right !== undefined) { - m_maxBounds.right = geo.transform.transformCoordinates(gcs, m_gcs, [{ - x: bounds.right, y: cy}])[0].x; - } - if (bounds.bottom !== undefined) { - m_maxBounds.bottom = geo.transform.transformCoordinates(gcs, m_gcs, [{ - x: cx, y: bounds.bottom}])[0].y; - } - if (bounds.top !== undefined) { - m_maxBounds.top = geo.transform.transformCoordinates(gcs, m_gcs, [{ - x: cx, y: bounds.top}])[0].y; - } - reset_minimum_zoom(); - m_this.zoom(m_zoom); - m_this.pan({x: 0, y: 0}); - return this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the center zoom level necessary to display the given lat/lon bounds. - * - * @param {geo.geoBounds} [bds] The requested map bounds - * @param {number} rotation Rotation in clockwise radians. - * @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.zoomAndCenterFromBounds = function (bounds, rotation, 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'); - } - - // calculate the zoom to fit the bounds - zoom = fix_zoom(calculate_zoom(bounds, rotation)); - - // clamp bounds if necessary - bounds = fix_bounds(bounds, rotation); - - /* 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 - }; - if (gcs !== m_gcs) { - center = geo.transform.transformCoordinates(m_gcs, gcs, [center])[0]; - } - return { - zoom: zoom, - center: center - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the bounds that will be displayed with the given zoom and center. - * - * 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 {number} rotation The requested rotation - * @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.boundsFromZoomAndCenter = function (zoom, center, rotation, gcs) { - var width, height, halfw, halfh, 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; - height = m_height * units; - halfw = width / 2; - halfh = height / 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. - - if (rotation) { - center.x += m_origin.x; - center.y += m_origin.y; - bounds = rotate_bounds_center( - center, {width: width, height: height}, rotation); - // correct the bounds when clamping is enabled - bounds.width = width; - bounds.height = height; - bounds = fix_bounds(bounds, rotation); - } else { - bounds = { - left: center.x - halfw + m_origin.x, - right: center.x + halfw + m_origin.x, - bottom: center.y - halfh + m_origin.y, - top: center.y + halfh + m_origin.y - }; - // correct the bounds when clamping is enabled - bounds = fix_bounds(bounds, 0); - } - if (gcs !== m_gcs) { - var bds = geo.transform.transformCoordinates( - m_gcs, gcs, - [[bounds.left, bounds.top], [bounds.right, bounds.bottom]]); - bounds = { - left: bds[0][0], top: bds[0][1], right: bds[1][0], bottom: bds[1][1] - }; - } - /* Add the original width and height of the viewport before rotation. */ - bounds.width = width; - bounds.height = height; - return bounds; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/set the discrete zoom flag. - * - * @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.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())); - } - m_this.interactor().options({discreteZoom: m_discreteZoom}); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the layers contained in the map. - * Alias of {@linkcode geo.sceneObject.children}. - */ - //////////////////////////////////////////////////////////////////////////// - this.layers = this.children; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 - * - * 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.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. - // - //////////////////////////////////////////////////////////////////////////// - /** - * 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 - */ - 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; - - if (ar_bds > ar_vp) { - // fit left and right - sclx = 1; - - // grow top and bottom - scly = ar_bds / ar_vp; - } else { - // fit top and bottom - scly = 1; - - // grow left and right - sclx = ar_vp / ar_bds; - } - return {x: sclx, y: scly}; - } - - /** - * Adjust a set of bounds based on a rotation. - * @private. - */ - function rotate_bounds(bounds, rotation) { - if (rotation) { - var center = { - x: (bounds.left + bounds.right) / 2, - y: (bounds.top + bounds.bottom) / 2 - }; - var size = { - width: Math.abs(bounds.left - bounds.right), - height: Math.abs(bounds.top - bounds.bottom) - }; - bounds = rotate_bounds_center(center, size, rotation); - } - return bounds; - } - - /** - * Generate a set of bounds based on a center point, a width and height, and - * a rotation. - * @private. - */ - function rotate_bounds_center(center, size, rotation) { - // calculate the half width and height - var width = size.width / 2, height = size.height / 2; - var sinr = Math.sin(rotation), cosr = Math.cos(rotation); - var ul = {}, ur = {}, ll = {}, lr = {}; - ul.x = center.x + (-width) * cosr - (-height) * sinr; - ul.y = center.y + (-width) * sinr + (-height) * cosr; - ur.x = center.x + width * cosr - (-height) * sinr; - ur.y = center.y + width * sinr + (-height) * cosr; - ll.x = center.x + (-width) * cosr - height * sinr; - ll.y = center.y + (-width) * sinr + height * cosr; - lr.x = center.x + width * cosr - height * sinr; - lr.y = center.y + width * sinr + height * cosr; - return { - left: Math.min(ul.x, ur.x, ll.x, lr.x), - right: Math.max(ul.x, ur.x, ll.x, lr.x), - bottom: Math.min(ul.y, ur.y, ll.y, lr.y), - top: Math.max(ul.y, ur.y, ll.y, lr.y) - }; - } - - /** - * 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, rotation) { - if (rotation === undefined) { - rotation = m_rotation; - } - bounds = rotate_bounds(bounds, rotation); - // 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; - } - - /** - * 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; - } - } - - /** - * Return the nearest valid zoom level to the requested zoom. - * @private - */ - 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; - } - - /** - * Return a valid rotation angle. - * @private - */ - function fix_rotation(rotation, ignoreRotationFunc, noRangeLimit) { - if (!m_allowRotation) { - return 0; - } - if (!ignoreRotationFunc && typeof m_allowRotation === 'function') { - rotation = m_allowRotation(rotation); - } - /* Ensure that the rotation is in the range [0, 2pi) */ - if (!noRangeLimit) { - var range = Math.PI * 2; - rotation = (rotation % range) + (rotation >= 0 ? 0 : range); - if (Math.min(Math.abs(rotation), Math.abs(rotation - range)) < 0.00001) { - rotation = 0; - } - } - return rotation; - } - - /** - * Return the nearest valid bounds maintaining the - * width and height. Does nothing if m_clampBounds* is - * false. - * @private - */ - function fix_bounds(bounds, rotation) { - if (!m_clampBoundsX && !m_clampBoundsY) { - return bounds; - } - var dx, dy, maxBounds = m_maxBounds; - if (rotation) { - maxBounds = $.extend({}, m_maxBounds); - /* When rotated, expand the maximum bounds so that they will allow the - * corners to be visible. We know the rotated bounding box, plus the - * original maximum bounds. To fit the corners of the maximum bounds, we - * can expand the total bounds by the same factor that the rotated - * bounding box is expanded from the non-rotated bounding box (for a - * small rotation, this is sin(rotation) * (original bounding box height) - * in the width). This feels like appropriate behaviour with one of the - * two bounds clamped. With both, it seems mildly peculiar. */ - var bw = Math.abs(bounds.right - bounds.left), - bh = Math.abs(bounds.top - bounds.bottom), - absinr = Math.abs(Math.sin(rotation)), - abcosr = Math.abs(Math.cos(rotation)), - ow, oh; - if (bounds.width && bounds.height) { - ow = bounds.width; - oh = bounds.height; - } else if (Math.abs(absinr - abcosr) < 0.0005) { - /* If we are close to a 45 degree rotation, it is ill-determined to - * compute the original (pre-rotation) bounds width and height. In - * this case, assume that we are using the map's aspect ratio. */ - if (m_width && m_height) { - var aspect = Math.abs(m_width / m_height); - var fac = Math.pow(1 + Math.pow(aspect, 2), 0.5); - ow = Math.max(bw, bh) / fac; - oh = ow * aspect; - } else { - /* Fallback if we don't have width or height */ - ow = bw * abcosr; - oh = bh * absinr; - } - } else { - /* Compute the pre-rotation (original) bounds width and height */ - ow = (abcosr * bw - absinr * bh) / (abcosr * abcosr - absinr * absinr); - oh = (abcosr * bh - absinr * bw) / (abcosr * abcosr - absinr * absinr); - } - /* Our maximum bounds are expanded based on the projected length of a - * tilted side of the original bounding box in the rotated bounding box. - * To handle all rotations, take the minimum difference in width or - * height. */ - var bdx = bw - Math.max(abcosr * ow, absinr * oh), - bdy = bh - Math.max(abcosr * oh, absinr * ow); - maxBounds.left -= bdx; - maxBounds.right += bdx; - maxBounds.top += bdy; - maxBounds.bottom -= bdy; - } - if (m_clampBoundsX) { - if (bounds.right - bounds.left > maxBounds.right - maxBounds.left) { - dx = maxBounds.left - ((bounds.right - bounds.left - ( - maxBounds.right - maxBounds.left)) / 2) - bounds.left; - } else if (bounds.left < maxBounds.left) { - dx = maxBounds.left - bounds.left; - } else if (bounds.right > maxBounds.right) { - dx = maxBounds.right - bounds.right; - } - if (dx) { - bounds = { - left: bounds.left += dx, - right: bounds.right += dx, - top: bounds.top, - bottom: bounds.bottom - }; - } - } - if (m_clampBoundsY) { - if (bounds.top - bounds.bottom > maxBounds.top - maxBounds.bottom) { - dy = maxBounds.bottom - ((bounds.top - bounds.bottom - ( - maxBounds.top - maxBounds.bottom)) / 2) - bounds.bottom; - } else if (bounds.top > maxBounds.top) { - dy = maxBounds.top - bounds.top; - } else if (bounds.bottom < maxBounds.bottom) { - dy = maxBounds.bottom - bounds.bottom; - } - if (dy) { - bounds = { - top: bounds.top += dy, - bottom: bounds.bottom += dy, - left: bounds.left, - right: bounds.right - }; - } - } - return bounds; - } - - /** - * Call the camera bounds method with the given bounds, but - * correct for the viewport aspect ratio. - * @private - */ - function camera_bounds(bounds, rotation) { - m_camera.rotation = rotation || 0; - /* When dealing with rotation, use the original width and height of the - * bounds, as the rotation will have expanded them. */ - if (bounds.width && bounds.height && rotation) { - var cx = (bounds.left + bounds.right) / 2, - cy = (bounds.top + bounds.bottom) / 2; - m_camera.viewFromCenterSizeRotation({x: cx, y: cy}, bounds, rotation); - } else { - m_camera.bounds = bounds; - } - } - - //////////////////////////////////////////////////////////////////////////// - // - // All the methods are now defined. From here, we are initializing all - // internal variables and event handlers. - // - //////////////////////////////////////////////////////////////////////////// - - // Set the world origin - m_origin = {x: 0, y: 0}; - - // Fix the zoom level (minimum and initial) - this.zoomRange(arg, true); - m_zoom = fix_zoom(m_zoom); - m_rotation = fix_rotation(m_rotation); - // Now update to the correct center and zoom level - this.center($.extend({}, arg.center || m_center), undefined); - - this.interactor(arg.interactor || geo.mapInteractor({discreteZoom: m_discreteZoom})); - this.clock(arg.clock || geo.clock()); - - function resizeSelf() { - m_this.resize(0, 0, m_node.width(), m_node.height()); - } - - 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 - */ - -/** - * 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'; - - var map = geo.map(spec); - - /* If the spec is bad, we still end up with an object, but it won't have a - * zoom function */ - if (!map || !map.zoom) { - console.warn('Could not create map.'); - return null; - } - - 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); - }); - - return map; -}; - -inherit(geo.map, geo.sceneObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - 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, - 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 = []; - - //////////////////////////////////////////////////////////////////////////// - /** - * Private method to bind mouse handlers on the map element. - */ - //////////////////////////////////////////////////////////////////////////// - this._bindMouseHandlers = function () { - - // Don't bind handlers for improved performance on features that don't - // require it. - if (!m_selectionAPI) { - return; - } - - // 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Private method to unbind mouse handlers on the map element. - */ - //////////////////////////////////////////////////////////////////////////// - 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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. - * }); - */ - //////////////////////////////////////////////////////////////////////////// - - //////////////////////////////////////////////////////////////////////////// - /** - * 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} - */ - //////////////////////////////////////////////////////////////////////////// - this.pointSearch = function () { - // base class method does nothing - return { - index: [], - found: [] - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Private mousemove handler - */ - //////////////////////////////////////////////////////////////////////////// - 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; - - // Get the index of the element that was previously on top - if (m_selectedFeatures.length) { - lastTop = m_selectedFeatures[m_selectedFeatures.length - 1]; - } - - // 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; - }); - - 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); - }); - - 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.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); - }); - - // Replace the selected features array - m_selectedFeatures = over.index; - - // Get the index of the element that is now on top - if (m_selectedFeatures.length) { - top = m_selectedFeatures[m_selectedFeatures.length - 1]; - } - - 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 (top !== -1) { - m_this.geoTrigger(geo.event.feature.mouseon, { - data: data[top], - index: top, - mouse: mouse - }, true); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 brush handler. - */ - //////////////////////////////////////////////////////////////////////////// - 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); - }); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Private brushend handler. - */ - //////////////////////////////////////////////////////////////////////////// - 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 style used by the feature - */ - //////////////////////////////////////////////////////////////////////////// - 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(); - 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; - } - 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])); - } - } else { - out = geo.util.ensureFunction(m_style[key]); - } - return out; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get layer referenced by the feature - */ - //////////////////////////////////////////////////////////////////////////// - this.layer = function () { - return m_layer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get renderer used by the feature - */ - //////////////////////////////////////////////////////////////////////////// - this.renderer = function () { - return m_renderer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set projection of the feature - */ - //////////////////////////////////////////////////////////////////////////// - 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; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from the renderer's input gcs coordinates to display coordinates. - * - * @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.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); - } - return c; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set visibility of the feature - */ - //////////////////////////////////////////////////////////////////////////// - this.visible = function (val) { - if (val === undefined) { - return m_visible; - } else { - m_visible = val; - m_this.modified(); - - // bind or unbind mouse handlers on visibility change - if (m_visible) { - m_this._bindMouseHandlers(); - } else { - m_this._unbindMouseHandlers(); - } - - return m_this; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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(); - return m_this; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set timestamp of last time update happened - */ - //////////////////////////////////////////////////////////////////////////// - this.updateTime = function (val) { - if (val === undefined) { - return m_updateTime; - } else { - m_updateTime = val; - m_this.modified(); - return m_this; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set the data array for the feature. - * - * @returns {Array|this} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Query if the selection API is enabled for this feature. - * @returns {bool} - */ - //////////////////////////////////////////////////////////////////////////// - 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(); - }; - - 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. - */ - -/** - * 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'; - - 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.feature, geo.sceneObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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); - } - arg = arg || {}; - geo.feature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @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 - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set clustering option - * - * @returns {geo.pointFeature|boolean} - */ - //////////////////////////////////////////////////////////////////////////// - this.clustering = function (val) { - if (val === undefined) { - 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(); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Generate the clustering tree from positions. This might be async in the - * future. - */ - //////////////////////////////////////////////////////////////////////////// - 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; - - // 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()); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update the current range tree object. Should be called whenever the - * data changes. - */ - //////////////////////////////////////////////////////////////////////////// - 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(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.pointSearch = function (p) { - var min, max, data, idx = [], box, found = [], ifound = [], map, pt, - corners, - stroke = m_this.style.get('stroke'), - strokeWidth = m_this.style.get('strokeWidth'), - radius = m_this.style.get('radius'); - - if (!m_this.selectionAPI()) { - return []; - } - - data = m_this.data(); - if (!data || !data.length) { - return { - found: [], - index: [] - }; - } - - map = m_this.layer().map(); - pt = map.gcsToDisplay(p); - // check all corners to make sure we handle rotations - corners = [ - map.displayToGcs({x: pt.x - m_maxRadius, y: pt.y - m_maxRadius}), - map.displayToGcs({x: pt.x + m_maxRadius, y: pt.y - m_maxRadius}), - map.displayToGcs({x: pt.x - m_maxRadius, y: pt.y + m_maxRadius}), - map.displayToGcs({x: pt.x + m_maxRadius, y: pt.y + m_maxRadius}) - ]; - min = { - x: Math.min(corners[0].x, corners[1].x, corners[2].x, corners[3].x), - y: Math.min(corners[0].y, corners[1].y, corners[2].y, corners[3].y) - }; - max = { - x: Math.max(corners[0].x, corners[1].x, corners[2].x, corners[3].x), - y: Math.max(corners[0].y, corners[1].y, corners[2].y, corners[3].y) - }; - - // Find 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, rad2; - - rad = radius(data[i], i); - rad += stroke(data[i], i) ? strokeWidth(data[i], i) : 0; - rad2 = rad * rad; - p = map.gcsToDisplay(p); - dx = p.x - pt.x; - dy = p.y - pt.y; - if (dx * dx + dy * dy <= rad2) { - found.push(d); - ifound.push(i); - } - }); - - return { - data: found, - index: ifound - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Returns an array of datum indices that are contained in the given box. - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Overloaded data method that updates the internal range tree on write. - */ - //////////////////////////////////////////////////////////////////////////// - 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: :: - * - * { - * min: { - * x: value, - * y: value - * }, - * max: { - * x: value, - * y: value - * } - * } - * - * @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); - - return { - min: { - x: pt.x - radius, - y: pt.y - radius - }, - max: { - x: pt.x + radius, - y: pt.y + radius - } - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - 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 - ); - - 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; -}; - -geo.event.pointFeature = $.extend({}, geo.event.feature); - -/** - * 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} - */ - -/** - * 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'; - - spec.type = 'point'; - return geo.feature.create(layer, spec); -}; - -inherit(geo.pointFeature, geo.feature); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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 - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set line accessor - * - * @returns {geo.pointFeature} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set position accessor - * - * @returns {geo.pointFeature} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.pointSearch = function (p) { - var data, pt, line, width, indices = [], found = [], pos; - data = m_this.data(); - if (!data || !data.length) { - return { - found: [], - index: [] - }; - } - - line = m_this.line(); - width = m_this.style.get('strokeWidth'); - pos = m_this.position(); - pt = m_this.featureGcsToDisplay(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) - } - ); - } - - // 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 { - data: found, - index: indices - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Returns an array of line indices that are contained in the given box. - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - 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._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 - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - m_position = arg.position === undefined ? [] : arg.position, - s_init = this._init; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set positions - * - * @returns {geo.pathFeature} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - 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 - ); - - m_this.style(defaultStyle); - - if (m_position) { - m_this.dataTime().modified(); - } - }; - - 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 - */ - //////////////////////////////////////////////////////////////////////////// - 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; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Override the parent data method to keep track of changes to the - * internal coordinates. - */ - //////////////////////////////////////////////////////////////////////////// - this.data = function (arg) { - var ret = s_data(arg); - if (arg !== undefined) { - getCoordinates(); - } - 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 - */ - //////////////////////////////////////////////////////////////////////////// - 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 - }; - }); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set polygon accessor - * - * @returns {geo.pointFeature} - */ - //////////////////////////////////////////////////////////////////////////// - this.polygon = function (val) { - if (val === undefined) { - return m_polygon; - } else { - m_polygon = val; - m_this.dataTime().modified(); - m_this.modified(); - getCoordinates(); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set position accessor - * - * @returns {geo.pointFeature} - */ - //////////////////////////////////////////////////////////////////////////// - this.position = function (val) { - if (val === undefined) { - return m_position; - } else { - m_position = val; - m_this.dataTime().modified(); - m_this.modified(); - getCoordinates(); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 { - index: indices, - found: found - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - - var defaultStyle = $.extend( - {}, - { - 'fillColor': { r: 0.0, g: 0.5, b: 0.5 }, - 'fillOpacity': 1.0 - }, - arg.style === undefined ? {} : arg.style - ); - - m_this.style(defaultStyle); - - if (m_position) { - m_this.dataTime().modified(); - } - }; - - this._init(arg); - return this; -}; - -inherit(geo.polygonFeature, geo.feature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class planeFeature - * - * @class - * @extends geo.polygonFeature - * @returns {geo.planeFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.planeFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.planeFeature)) { - return new geo.planeFeature(arg); - } - arg = 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); - - 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; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 { - m_drawOnAsyncResourceLoad = val; - return m_this; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - } - m_this.style(style); - }; - - this._init(arg); - return this; -}; - -inherit(geo.planeFeature, geo.polygonFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class quadFeature - * - * @class - * @param {Object} arg Options object - * @extends geo.feature - * @param {Object|string|Function} [color] Color for quads without images. - * Default is white ({r: 1, g: 1, b: 1}). - * @param {number|Function} [opacity=1] Opacity for quad - * @param {number|Function} [depth=0] Default z-coordinate for positions that - * don't explicitly specify one. - * @param {boolean|Function} [drawOnAsyncResourceLoaded=true] Redraw quads - * when images are loaded after initial render. - * @param {Image|string|Function} [image] Image for each data item. If - * undefined or null, the quad is a solid color. Default is (data).image. - * @param {Object|string|Function} [previewColor=null] If specified, a color to - * show on image quads while waiting for the image to load. - * @param {Image|string|Function} [previewImage=null] If specified, an image to - * show on image quads while waiting for the quad-specific image to load. - * This will only be shown if it is already loaded. - * @param {Object|Function} [position] Position of the quad. Default is - * (data). The position is an Object which specifies the corners of the - * quad: ll, lr, ur, ul. At least two opposite corners must be specified. - * The corners do not have to physically correspond to the order specified, - * but rather correspond to that part of an image (if there is one). If a - * corner is unspecified, it will use the x coordinate from one adjacent - * corner, the y coordinate from the other adjacent corner, and the average - * z value of those two corners. For instance, if ul is unspecified, it is - * {x: ll.x, y: ur.y}. Note that each quad is rendered as a pair of - * triangles: (ll, lr, ul) and (ur, ul, lr). Nothing special is done for - * quads that are not convex or quads that have substantially different - * transformations for those two triangles. - * @param {boolean} [cacheQuads=true] If true, a set of internal information is - * stored on each data item in the _cachedQuad attribute. If this is false, - * the data item is not altered. If the data (positions, opacity, etc,) of - * individual quads will change, set this to false or delete the _cachedQuad - * attribute of the data item. - * @returns {geo.quadFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.quadFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.quadFeature)) { - return new geo.quadFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - m_cacheQuads, - m_images = [], - m_quads; - - /** - * Track a list of object->object mappings. The mappings are kept in a list. - * This marks all known mappings as unused. If they are not marked used - * before _objectListEnd is called, that function will remove them. - * - * @param {array} list the list of mappings. - */ - this._objectListStart = function (list) { - $.each(list, function (idx, item) { - item.used = false; - }); - }; - - /** - * Get the value from a list of object->object mappings. If the key object - * is not present, return undefined. If found, the entry is marked as being - * in use. - * - * @param {array} list the list of mappings. - * @param {object} entry the key to search for. - * @returns {object} the associated object or undefined. - */ - this._objectListGet = function (list, entry) { - for (var i = 0; i < list.length; i += 1) { - if (list[i].entry === entry) { - list[i].used = true; - return list[i].value; - } - } - return undefined; - }; - - /** - * Add a new object to a list of object->object mappings. The key object - * should not exist, or this will create a duplicated. The new entry is - * marked as being in use. - * - * @param {array} list the list of mappings. - * @param {object} entry the key to add. - * @param {object} value the value to store with the entry. - */ - this._objectListAdd = function (list, entry, value) { - list.push({entry: entry, value: value, used: true}); - }; - - /** - * Remove all unused entries from a list of object->object mappings. - * - * @param {array} list the list of mappings. - */ - this._objectListEnd = function (list) { - for (var i = list.length - 1; i >= 0; i -= 1) { - if (!list[i].used) { - list.splice(i, 1); - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Point search 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(), i, - poly1 = [{}, {}, {}, {}], poly2 = [{}, {}, {}, {}], - map = m_this.layer().map(), - order1 = [0, 1, 2, 0], order2 = [1, 2, 3, 1]; - coordinate = geo.transform.transformCoordinates( - map.ingcs(), map.gcs(), [coordinate])[0]; - if (!m_quads) { - this._generateQuads(); - } - $.each([m_quads.clrQuads, m_quads.imgQuads], function (idx, quadList) { - quadList.forEach(function (quad, idx) { - for (i = 0; i < order1.length; i += 1) { - poly1[i].x = quad.pos[order1[i] * 3]; - poly1[i].y = quad.pos[order1[i] * 3 + 1]; - poly1[i].z = quad.pos[order1[i] * 3 + 2]; - poly2[i].x = quad.pos[order2[i] * 3]; - poly2[i].y = quad.pos[order2[i] * 3 + 1]; - poly2[i].z = quad.pos[order2[i] * 3 + 2]; - } - if (geo.util.pointInPolygon(coordinate, poly1) || - geo.util.pointInPolygon(coordinate, poly2)) { - indices.push(quad.idx); - found.push(data[quad.idx]); - } - }); - }); - return { - index: indices, - found: found - }; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set position - * - * @returns {geo.quadFeature} - */ - //////////////////////////////////////////////////////////////////////////// - this.position = function (val) { - if (val === undefined) { - return m_this.style('position'); - } else { - m_this.style('position', geo.util.ensureFunction(val)); - m_this.dataTime().modified(); - m_this.modified(); - } - return m_this; - }; - - /** - * Given a data item and its index, fetch its position and ensure we have - * compelte information for the quad. This generates missing corners and z - * values. - * - * @param {function} posFunc a function to call to get the position of a data - * item. It is passed (d, i). - * @param {function} depthFunc a function to call to get the z-value of a - * data item. It is passed (d, i). - * @param d a data item. Used to fetch position and possibly depth. - * @param i the index within the data. Used to fetch position and possibly - * depth. - * @returns {Object|undefined} either an object with all four corners, or - * undefined if no such object can be generated. The coordinates have been - * converted to map coordinates. - */ - this._positionToQuad = function (posFunc, depthFunc, d, i) { - var initPos = posFunc.call(m_this, d, i); - if ((!initPos.ll || !initPos.ur) && (!initPos.ul || !initPos.lr)) { - return; - } - var gcs = m_this.gcs(), - map_gcs = m_this.layer().map().gcs(), - pos = {}; - $.each(['ll', 'lr', 'ul', 'ur'], function (idx, key) { - if (initPos[key] !== undefined) { - pos[key] = {}; - if (initPos[key].x === undefined) { - pos[key] = [initPos[key][0], initPos[key][1], initPos[key][2]]; - } else { - pos[key] = [initPos[key].x, initPos[key].y, initPos[key].z]; - } - if (pos[key][2] === undefined) { - pos[key][2] = depthFunc.call(m_this, d, i); - } - if (gcs !== map_gcs) { - pos[key] = geo.transform.transformCoordinates( - gcs, map_gcs, pos[key]); - } - } - }); - pos.ll = pos.ll || [pos.ul[0], pos.lr[1], (pos.ul[2] + pos.lr[2]) / 2]; - pos.lr = pos.lr || [pos.ur[0], pos.ll[1], (pos.ur[2] + pos.ll[2]) / 2]; - pos.ur = pos.ur || [pos.lr[0], pos.ul[1], (pos.lr[2] + pos.ul[2]) / 2]; - pos.ul = pos.ul || [pos.ll[0], pos.ur[1], (pos.ll[2] + pos.ur[2]) / 2]; - return pos; - }; - - /** - * Convert the current data set to a pair of arrays, one of quads that are - * solid color and one of qudas that have an image. All quads are objects - * with pos (a 12 value array containing 4 three-dimensional position - * coordinates), and opacity. Color quads also have a color. Image quads - * may have an image element, if the image is loaded. If it isn't, this - * element will be missing. For preview images, the image quad will have a - * reference to the preview element that may later be removed. If a preview - * color is used, the quad will be in both lists, but may be removed from the - * color quad list once the image is loaded. - * - * The value for origin is one of an ll corner from one of the quads with the - * smallest sum of diagonals. The assumption is that, if using the origin to - * improve precision, the smallest quads are the ones most in need of this - * benefit. - * - * @returns {Object} An object with clrQuads and imgQuads, each of which is - * an array, and origin, which is a triplet that is guaranteed to be one of - * the quads corners for a quad with the smallest sum of diagonal lengths. - */ - this._generateQuads = function () { - var posFunc = m_this.position(), - imgFunc = geo.util.ensureFunction(m_this.style('image')), - colorFunc = geo.util.ensureFunction(m_this.style('color')), - depthFunc = geo.util.ensureFunction(m_this.style('depth')), - opacityFunc = geo.util.ensureFunction(m_this.style('opacity')), - loadedFunc = geo.util.ensureFunction(m_this.style( - 'drawOnAsyncResourceLoaded')), - previewColorFunc = geo.util.ensureFunction(m_this.style( - 'previewColor')), - previewImageFunc = geo.util.ensureFunction(m_this.style( - 'previewImage')), - data = m_this.data(), - clrQuads = [], imgQuads = [], - origin = [0, 0, 0], origindiag2, diag2; - /* Keep track of images that we are using. This prevents creating - * additional Image elemnts for repeated urls. */ - m_this._objectListStart(m_images); - $.each(data, function (i, d) { - if (d._cachedQuad) { - diag2 = d._cachedQuad.diag2; - if (origindiag2 === undefined || (d._cachedQuad.diag2 && - d._cachedQuad.diag2 < origindiag2)) { - origin = d._cachedQuad.ll; - origindiag2 = d._cachedQuad.diag2; - } - if (d._cachedQuad.clrquad) { - clrQuads.push(d._cachedQuad.clrquad); - } - if (d._cachedQuad.imgquad) { - imgQuads.push(d._cachedQuad.imgquad); - } - return; - } - var quad, reload, image, prev_onload, - pos, img, opacity, previewColor, previewImage, quadinfo = {}; - - pos = m_this._positionToQuad(posFunc, depthFunc, d, i); - opacity = opacityFunc.call(m_this, d, i); - if (pos === undefined || !opacity) { - return; - } - diag2 = Math.pow(pos.ll[0] - pos.ur[0], 2) + Math.pow(pos.ll[1] - - pos.ur[1], 2) + Math.pow(pos.ll[2] - pos.ur[0], 2) + Math.pow( - pos.lr[0] - pos.ur[0], 2) + Math.pow(pos.lr[1] - pos.ur[1], 2) + - Math.pow(pos.lr[2] - pos.ur[0], 2); - quadinfo.diag2 = diag2; - quadinfo.ll = pos.ll; - if (origindiag2 === undefined || (diag2 && diag2 < origindiag2)) { - origin = pos.ll; - origindiag2 = diag2; - } - pos = [pos.ll[0], pos.ll[1], pos.ll[2], - pos.lr[0], pos.lr[1], pos.lr[2], - pos.ul[0], pos.ul[1], pos.ul[2], - pos.ur[0], pos.ur[1], pos.ur[2]]; - img = imgFunc.call(m_this, d, i); - if (!img) { - quad = { - idx: i, - pos: pos, - opacity: opacity, - color: geo.util.convertColor(colorFunc.call(m_this, d, i)) - }; - clrQuads.push(quad); - quadinfo.clrquad = quad; - } else { - image = m_this._objectListGet(m_images, img); - if (image === undefined) { - if (img instanceof Image) { - image = img; - } else { - image = new Image(); - image.src = img; - } - m_this._objectListAdd(m_images, img, image); - } - quad = { - idx: i, - pos: pos, - opacity: opacity - }; - if (image.complete && image.naturalWidth && image.naturalHeight) { - quad.image = image; - } else { - previewColor = undefined; - previewImage = previewImageFunc.call(m_this, d, i); - if (previewImage && previewImage instanceof Image && - previewImage.complete && previewImage.naturalWidth && - previewImage.naturalHeight) { - quad.image = previewImage; - } else { - previewColor = previewColorFunc.call(m_this, d, i); - if (previewColor === null) { - previewColor = undefined; - } - if (previewColor !== undefined) { - quad.color = geo.util.convertColor(previewColor); - clrQuads.push(quad); - quadinfo.keep = false; - } - } - reload = loadedFunc.call(m_this, d, i); - if (reload) { - prev_onload = image.onload; - image.onload = function () { - if (previewColor !== undefined) { - if ($.inArray(quad, clrQuads) >= 0) { - clrQuads.splice($.inArray(quad, clrQuads), 1); - } - } - quad.image = image; - m_this.dataTime().modified(); - m_this.modified(); - m_this._update(); - m_this.layer().draw(); - if (prev_onload) { - return prev_onload.apply(this, arguments); - } - }; - } else if (previewColor === undefined && !quad.image) { - return; - } - } - imgQuads.push(quad); - quadinfo.imgquad = quad; - } - if (m_cacheQuads !== false && quadinfo.keep !== false) { - d._cachedQuad = quadinfo; - } - }); - m_this._objectListEnd(m_images); - m_quads = {clrQuads: clrQuads, imgQuads: imgQuads, origin: origin}; - return m_quads; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - arg = arg || {}; - s_init.call(m_this, arg); - - m_cacheQuads = (arg.cacheQuads !== false); - - var style = $.extend( - {}, - { - color: { r: 1.0, g: 1, b: 1 }, - opacity: 1, - depth: 0, - drawOnAsyncResourceLoaded: true, - previewColor: null, - previewImage: null, - image: function (d) { return d.image; }, - position: function (d) { return d; } - }, - arg.style === undefined ? {} : arg.style - ); - - if (arg.position !== undefined) { - style.position = geo.util.ensureFunction(arg.position); - } - m_this.style(style); - m_this.dataTime().modified(); - }; - - return m_this; -}; - -geo.event.quadFeature = $.extend({}, geo.event.feature); - -/** - * Object specification for a quad feature. - * - * @extends geo.feature.spec // need to make a jsdoc plugin for this to work - * @typedef geo.quadFeature.spec - * @type {object} - */ - -/** - * Create a quadFeature from an object. - * @see {@link geo.feature.create} - * @param {geo.layer} layer The layer to add the feature to - * @param {geo.quadFeature.spec} spec The object specification - * @returns {geo.quadFeature|null} - */ -geo.quadFeature.create = function (layer, spec) { - 'use strict'; - - spec = spec || {}; - spec.type = 'quad'; - return geo.feature.create(layer, spec); -}; - -inherit(geo.quadFeature, geo.feature); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - s_style = this.style; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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 { - s_style('delta', val); - m_this.dataTime().modified(); - m_this.modified(); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - * @protected - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - - 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 (arg.origin !== undefined) { - defaultStyle.origin = arg.origin; - } - - m_this.style(defaultStyle); - m_this.dataTime().modified(); - }; -}; - -inherit(geo.vectorFeature, geo.feature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class geomFeature - * - * @class - * @extends geo.feature - * @returns {geo.geomFeature} - */ -////////////////////////////////////////////////////////////////////////////// -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); - - return this; -}; - -inherit(geo.geomFeature, geo.feature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class graphFeature - * - * @class - * @extends geo.feature - * @returns {geo.graphFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.graphFeature = function (arg) { - 'use strict'; - - if (!(this instanceof geo.graphFeature)) { - return new geo.graphFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @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; }); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Call child _build methods - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - m_this.children().forEach(function (child) { - child._build(); - }); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Call child _update methods - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - m_this.children().forEach(function (child) { - child._update(); - }); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set links accessor. - */ - //////////////////////////////////////////////////////////////////////////// - this.links = function (arg) { - if (arg === undefined) { - return m_children; - } - - m_children = geo.util.ensureFunction(arg); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set nodes - */ - //////////////////////////////////////////////////////////////////////////// - this.nodes = function (val) { - if (val === undefined) { - return m_nodes; - } - m_nodes = val; - m_this.modified(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get internal node feature - */ - //////////////////////////////////////////////////////////////////////////// - this.nodeFeature = function () { - return m_points; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get internal link features - */ - //////////////////////////////////////////////////////////////////////////// - this.linkFeatures = function () { - return m_links; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build the feature for drawing - */ - //////////////////////////////////////////////////////////////////////////// - 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); - - // 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]); - }); - }); - - m_links.splice(nLinks, m_links.length - nLinks).forEach(function (l) { - l._exit(); - m_this.removeChild(l); - }); - - s_draw(); - return m_this; - }; - - 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 contourFeature - * - * @class - * @extends geo.feature - * @returns {geo.contourFeature} - * - */ -////////////////////////////////////////////////////////////////////////////// -geo.contourFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.contourFeature)) { - return new geo.contourFeature(arg); - } - arg = arg || {}; - geo.feature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - 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; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Override the parent data method to keep track of changes to the - * internal coordinates. - */ - //////////////////////////////////////////////////////////////////////////// - this.data = function (arg) { - var ret = s_data(arg); - return ret; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set contour accessor - * - * @returns {geo.pointFeature} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.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/Set position accessor - * - * @returns {geo.pointFeature} - */ - //////////////////////////////////////////////////////////////////////////// - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - } - } - return result; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, 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; - } - }, - arg.style === undefined ? {} : arg.style - ); - - m_this.style(defaultStyle); - - if (m_contour) { - m_this.dataTime().modified(); - } - }; - - this._init(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. - */ - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class renderer - * - * @class - * @extends geo.object - * @returns {geo.renderer} - */ -////////////////////////////////////////////////////////////////////////////// -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 = arg.layer === undefined ? null : arg.layer, - m_canvas = arg.canvas === undefined ? null : arg.canvas, - m_initialized = false; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get layer of the renderer - * - * @returns {*} - */ - //////////////////////////////////////////////////////////////////////////// - this.layer = function () { - return m_layer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get canvas for the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.canvas = function (val) { - if (val === undefined) { - return m_canvas; - } else { - m_canvas = val; - m_this.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get map that this renderer belongs to - */ - //////////////////////////////////////////////////////////////////////////// - this.map = function () { - if (m_layer) { - return m_layer.map(); - } else { - return null; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get base layer that belongs to this renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.baseLayer = function () { - if (m_this.map()) { - return m_this.map().baseLayer(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set if renderer has been initialized - */ - //////////////////////////////////////////////////////////////////////////// - this.initialized = function (val) { - if (val === undefined) { - return m_initialized; - } else { - m_initialized = val; - return m_this; - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function () { - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle resize event - */ - //////////////////////////////////////////////////////////////////////////// - this._resize = function () { - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render - */ - //////////////////////////////////////////////////////////////////////////// - this._render = function () { - }; - - return this; -}; - -inherit(geo.renderer, geo.object); - -(function () { - 'use strict'; - - ////////////////////////////////////////////////////////////////////////////// - /** - * 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) { - - 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); - - /* mapOpacity is just another name for the layer opacity. */ - this.mapOpacity = this.opacity; - - /** - * 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}, - queue: this._queue, - url: this._options.url(urlParams.x, urlParams.y, urlParams.level || 0, - this._options.subdomains) - }); - }.bind(this); - }; - - /** - * This object contains the default options used to initialize the osmLayer. - */ - geo.osmLayer.defaults = $.extend({}, geo.tileLayer.defaults, { - 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' - }); - - 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; - - this.api = function () { - return 'dom'; - }; - - 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]); - } - }; - - this._init(arg); - return this; -}; - -inherit(geo.domRenderer, geo.renderer); - -geo.registerRenderer('dom', geo.domRenderer); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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); - - //////////////////////////////////////////////////////////////////////////// - /** - * @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 (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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set choropleth accessor - * - * @returns {geo.feature.choropleth} - */ - //////////////////////////////////////////////////////////////////////////// - this.choropleth = function (arg1, arg2) { - var choropleth; - - if (arg1 === undefined) { - return m_choropleth; - } - if (typeof arg1 === 'string' && arg2 === undefined) { - return m_choropleth[arg1]; - } - 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.modified(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.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); - } - } - return all; - } - return geo.util.ensureFunction(m_choropleth[key]); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * A method that adds polygons from a given feature to the current layer. - * - * @param {} geoJsonFeature - * @param geo.color - * @return [{geo.feature}] - */ - //////////////////////////////////////////////////////////////////////////// - this._featureToPolygons = function (feature, fillValue) { - return m_this - ._addPolygonFeature(feature, fillValue); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * A method that sets a choropleth scale's domain and range. - * - * @param {undefined | function({})} valueAccessor - * @return {geo.feature.choropleth} - */ - //////////////////////////////////////////////////////////////////////////// - this._generateScale = function (valueAccessor) { - var extent = - d3.extent(m_this.scalar(), valueAccessor || undefined); - - m_this.choropleth() - .scale - .domain(extent) - .range(m_this.choropleth().colorRange); - - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /**sr - * Generate scale for choropleth.data(), make polygons from features. - * @returns: [ [geo.feature.polygon, ...] , ... ] - */ - //////////////////////////////////////////////////////////////////////////// - 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); - }); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - - if (m_choropleth) { - m_this.dataTime().modified(); - } - }; - - this._init(arg); - return this; -}; - -inherit(geo.choroplethFeature, geo.feature); - -/* Example: - */ - -/** - * @namespace - */ -geo.gl = {}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of lineFeature - * - * @class - * @extends geo.lineFeature - * @returns {geo.gl.lineFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.lineFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.gl.lineFeature)) { - return new geo.gl.lineFeature(arg); - } - arg = arg || {}; - geo.lineFeature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_exit = this._exit, - m_actor = null, - m_mapper = null, - m_material = null, - m_pixelWidthUnif = null, - m_aspectUniform = null, - m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, - s_init = this._init, - s_update = this._update; - - 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 any vertex has been deliberately set to a negative opacity, - * skip doing computations on it. */ - ' 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); - shader.setShaderSource(vertexShaderSource); - return 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); - shader.setShaderSource(fragmentShaderSource); - return shader; - } - - function createGLLines() { - var data = m_this.data(), - i, j, k, v, - numSegments = 0, len, - lineItem, lineItemData, - vert = [{}, {}], vertTemp, - pos, posIdx3, - 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(), - posBuf, nextBuf, prevBuf, offsetBuf, indicesBuf, - strokeWidthBuf, strokeColorBuf, strokeOpacityBuf, - dest, dest3, - geom = m_mapper.geometryData(); - - for (i = 0; i < data.length; i += 1) { - lineItem = m_this.line()(data[i], i); - numSegments += lineItem.length - 1; - for (j = 0; j < lineItem.length; j += 1) { - pos = posFunc(lineItem[j], j, lineItem, i); - position.push(pos.x); - position.push(pos.y); - position.push(pos.z || 0.0); - } - } - - position = geo.transform.transformCoordinates( - m_this.gcs(), m_this.layer().map().gcs(), - position, 3); - - len = numSegments * order.length; - posBuf = geo.util.getGeomBuffer(geom, 'pos', len * 3); - nextBuf = geo.util.getGeomBuffer(geom, 'next', len * 3); - prevBuf = geo.util.getGeomBuffer(geom, 'prev', len * 3); - offsetBuf = geo.util.getGeomBuffer(geom, 'offset', len); - strokeWidthBuf = geo.util.getGeomBuffer(geom, 'strokeWidth', len); - strokeColorBuf = geo.util.getGeomBuffer(geom, 'strokeColor', len * 3); - strokeOpacityBuf = geo.util.getGeomBuffer(geom, 'strokeOpacity', len); - indicesBuf = geom.primitive(0).indices(); - if (!(indicesBuf instanceof Uint16Array) || indicesBuf.length !== len) { - indicesBuf = new Uint16Array(len); - geom.primitive(0).setIndices(indicesBuf); - } - - for (i = posIdx3 = dest = dest3 = 0; i < data.length; i += 1) { - lineItem = m_this.line()(data[i], i); - for (j = 0; j < lineItem.length; j += 1, posIdx3 += 3) { - lineItemData = lineItem[j]; - /* swap entries in vert so that vert[0] is the first vertex, and - * vert[1] will be reused for the second vertex */ - if (j) { - vertTemp = vert[0]; - vert[0] = vert[1]; - vert[1] = vertTemp; - } - vert[1].pos = posIdx3; - vert[1].prev = posIdx3 - (j ? 3 : 0); - vert[1].next = posIdx3 + (j + 1 < lineItem.length ? 3 : 0); - vert[1].strokeWidth = strkWidthFunc(lineItemData, j, lineItem, i); - vert[1].strokeColor = strkColorFunc(lineItemData, j, lineItem, i); - vert[1].strokeOpacity = strkOpacityFunc(lineItemData, j, lineItem, i); - if (j) { - for (k = 0; k < order.length; k += 1, dest += 1, dest3 += 3) { - v = vert[order[k][0]]; - posBuf[dest3] = position[v.pos]; - posBuf[dest3 + 1] = position[v.pos + 1]; - posBuf[dest3 + 2] = position[v.pos + 2]; - prevBuf[dest3] = position[v.prev]; - prevBuf[dest3 + 1] = position[v.prev + 1]; - prevBuf[dest3 + 2] = position[v.prev + 2]; - nextBuf[dest3] = position[v.next]; - nextBuf[dest3 + 1] = position[v.next + 1]; - nextBuf[dest3 + 2] = position[v.next + 2]; - offsetBuf[dest] = order[k][1]; - /* We can ignore the indicies (they will all be zero) */ - strokeWidthBuf[dest] = v.strokeWidth; - strokeColorBuf[dest3] = v.strokeColor.r; - strokeColorBuf[dest3 + 1] = v.strokeColor.g; - strokeColorBuf[dest3 + 2] = v.strokeColor.b; - strokeOpacityBuf[dest] = v.strokeOpacity; - } - } - } - } - - geom.boundsDirty(true); - m_mapper.modified(); - m_mapper.boundsDirtyTimestamp().modified(); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the arrangement of vertices used for each line segment. - * - * @returns {Number} - */ - //////////////////////////////////////////////////////////////////////////// - this.featureVertices = function () { - return [[0, 1], [1, -1], [0, -1], [0, 1], [1, 1], [1, -1]]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the number of vertices used for each line segment. - * - * @returns {Number} - */ - //////////////////////////////////////////////////////////////////////////// - this.verticesPerFeature = function () { - return m_this.featureVertices().length; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - var prog = vgl.shaderProgram(), - vs = createVertexShader(), - fs = createFragmentShader(), - // Vertex attributes - posAttr = vgl.vertexAttribute('pos'), - prvAttr = vgl.vertexAttribute('prev'), - nxtAttr = vgl.vertexAttribute('next'), - offAttr = vgl.vertexAttribute('offset'), - strkWidthAttr = vgl.vertexAttribute('strokeWidth'), - strkColorAttr = vgl.vertexAttribute('strokeColor'), - strkOpacityAttr = vgl.vertexAttribute('strokeOpacity'), - // Shader uniforms - mviUnif = new vgl.modelViewUniform('modelViewMatrix'), - prjUnif = new vgl.projectionUniform('projectionMatrix'), - geom = vgl.geometryData(), - // Sources - posData = vgl.sourceDataP3fv({'name': 'pos'}), - prvPosData = vgl.sourceDataAnyfv( - 3, vgl.vertexAttributeKeysIndexed.Four, {'name': 'prev'}), - nxtPosData = vgl.sourceDataAnyfv( - 3, vgl.vertexAttributeKeysIndexed.Five, {'name': 'next'}), - offPosData = vgl.sourceDataAnyfv( - 1, vgl.vertexAttributeKeysIndexed.Six, {'name': 'offset'}), - strkWidthData = vgl.sourceDataAnyfv( - 1, vgl.vertexAttributeKeysIndexed.One, {'name': 'strokeWidth'}), - strkColorData = vgl.sourceDataAnyfv( - 3, vgl.vertexAttributeKeysIndexed.Two, {'name': 'strokeColor'}), - strkOpacityData = vgl.sourceDataAnyfv( - 1, vgl.vertexAttributeKeysIndexed.Three, - {'name': 'strokeOpacity'}), - // Primitive indices - triangles = vgl.triangles(); - - m_pixelWidthUnif = new vgl.floatUniform('pixelWidth', - 1.0 / 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_material = vgl.material(); - m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); - - prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); - prog.addVertexAttribute(strkWidthAttr, vgl.vertexAttributeKeysIndexed.One); - prog.addVertexAttribute(strkColorAttr, vgl.vertexAttributeKeysIndexed.Two); - prog.addVertexAttribute(strkOpacityAttr, vgl.vertexAttributeKeysIndexed.Three); - prog.addVertexAttribute(prvAttr, vgl.vertexAttributeKeysIndexed.Four); - prog.addVertexAttribute(nxtAttr, vgl.vertexAttributeKeysIndexed.Five); - prog.addVertexAttribute(offAttr, vgl.vertexAttributeKeysIndexed.Six); - - prog.addUniform(mviUnif); - prog.addUniform(prjUnif); - prog.addUniform(m_pixelWidthUnif); - prog.addUniform(m_aspectUniform); - - prog.addShader(fs); - prog.addShader(vs); - - m_material.addAttribute(prog); - m_material.addAttribute(vgl.blend()); - - m_actor = vgl.actor(); - m_actor.setMaterial(m_material); - m_actor.setMapper(m_mapper); - - geom.addSource(posData); - geom.addSource(prvPosData); - geom.addSource(nxtPosData); - geom.addSource(strkWidthData); - geom.addSource(strkColorData); - geom.addSource(strkOpacityData); - geom.addSource(offPosData); - geom.addPrimitive(triangles); - m_mapper.setGeometryData(geom); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return list of actors - * - * @returns {vgl.actor[]} - */ - //////////////////////////////////////////////////////////////////////////// - this.actors = function () { - if (!m_actor) { - return []; - } - return [m_actor]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - if (m_actor) { - m_this.renderer().contextRenderer().removeActor(m_actor); - } - - createGLLines(); - - m_this.renderer().contextRenderer().addActor(m_actor); - m_this.buildTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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._build(); - } - - m_pixelWidthUnif.set(1.0 / 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(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.renderer().contextRenderer().removeActor(m_actor); - s_exit(); - }; - - this._init(arg); - return this; -}; - -inherit(geo.gl.lineFeature, geo.lineFeature); - -// Now register it -geo.registerFeature('vgl', 'line', geo.gl.lineFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of pointFeature - * - * @class - * @extends geo.pointFeature - * @returns {geo.gl.pointFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.pointFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.gl.pointFeature)) { - return new geo.gl.pointFeature(arg); - } - arg = arg || {}; - geo.pointFeature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_exit = this._exit, - m_actor = null, - m_mapper = null, - m_pixelWidthUniform = null, - m_aspectUniform = null, - m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, - m_primitiveShape = 'sprite', // arg can change this, below - s_init = this._init, - s_update = this._update, - vertexShaderSource = null, - fragmentShaderSource = null; - - if (arg.primitiveShape === 'triangle' || - arg.primitiveShape === 'square' || - arg.primitiveShape === 'sprite') { - 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;' - ]; - - if (m_primitiveShape !== 'sprite') { - 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 the point has no visible pixels, skip doing computations on it. */ - ' 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;' - ]); - - if (m_primitiveShape === 'sprite') { - vertexShaderSource.push.apply(vertexShaderSource, [ - ' gl_Position = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw;', - ' gl_PointSize = 2.0 * (rad + strokeWidthVar); ', - '}' - ]); - } else { - 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;' - ]; - - if (m_primitiveShape !== 'sprite') { - 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;' - ]); - - if (m_primitiveShape === 'sprite') { - fragmentShaderSource.push( - ' float rad = 2.0 * length (gl_PointCoord - vec2(0.5));'); - } else { - 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'); - - function createVertexShader() { - var shader = new vgl.shader(vgl.GL.VERTEX_SHADER); - shader.setShaderSource(vertexShaderSource); - return shader; - } - - function createFragmentShader() { - var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); - shader.setShaderSource(fragmentShaderSource); - return shader; - } - - function pointPolygon(x, y, w, h) { - var verts; - switch (m_primitiveShape) { - case 'triangle': - /* Use an equilateral triangle. While this has 30% more area than a - * square, the reduction in vertices should help more than the - * processing the additional fragments. */ - verts = [ - x, y - h * 2, - x - w * Math.sqrt(3.0), y + h, - x + w * Math.sqrt(3.0), y + h - ]; - break; - case 'sprite': - /* Point sprite uses only one vertex per point. */ - verts = [x, y]; - break; - default: // "square" - /* Use a surrounding square split diagonally into two triangles. */ - verts = [ - x - w, y + h, - x - w, y - h, - x + w, y + h, - x - w, y - h, - x + w, y - h, - x + w, y + h - ]; - break; - } - return verts; - } - - function createGLPoints() { - // unit and associated data is not used when drawing sprite - var i, j, numPts = m_this.data().length, - unit = pointPolygon(0, 0, 1, 1), - position = new Array(numPts * 3), posBuf, posVal, posFunc, - unitBuf, indices, - radius, radiusVal, radFunc, - stroke, strokeVal, strokeFunc, - strokeWidth, strokeWidthVal, strokeWidthFunc, - strokeOpacity, strokeOpacityVal, strokeOpacityFunc, - strokeColor, strokeColorVal, strokeColorFunc, - fill, fillVal, fillFunc, - fillOpacity, fillOpacityVal, fillOpacityFunc, - fillColor, fillColorVal, fillColorFunc, - vpf = m_this.verticesPerFeature(), - data = m_this.data(), - item, ivpf, ivpf3, iunit, i3, - geom = m_mapper.geometryData(), nonzeroZ; - - posFunc = m_this.position(); - radFunc = m_this.style.get('radius'); - strokeFunc = m_this.style.get('stroke'); - strokeWidthFunc = m_this.style.get('strokeWidth'); - strokeOpacityFunc = 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'); - - /* It is more efficient to do a transform on a single array rather than on - * an array of arrays or an array of objects. */ - for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { - posVal = posFunc(data[i]); - position[i3] = posVal.x; - position[i3 + 1] = posVal.y; - position[i3 + 2] = posVal.z || 0; - nonzeroZ = nonzeroZ || position[i3 + 2]; - } - position = geo.transform.transformCoordinates( - m_this.gcs(), m_this.layer().map().gcs(), - position, 3); - /* Some transforms modify the z-coordinate. If we started with all zero z - * coordinates, don't modify them. This could be changed if the - * z-coordinate space of the gl cube is scaled appropriately. */ - if (!nonzeroZ && m_this.gcs() !== m_this.layer().map().gcs()) { - for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { - position[i3 + 2] = 0; - } - } - - posBuf = geo.util.getGeomBuffer(geom, 'pos', vpf * numPts * 3); - - if (m_primitiveShape !== 'sprite') { - unitBuf = geo.util.getGeomBuffer(geom, 'unit', vpf * numPts * 2); - } - - radius = geo.util.getGeomBuffer(geom, 'rad', vpf * numPts); - stroke = geo.util.getGeomBuffer(geom, 'stroke', vpf * numPts); - strokeWidth = geo.util.getGeomBuffer(geom, 'strokeWidth', vpf * numPts); - strokeOpacity = geo.util.getGeomBuffer(geom, 'strokeOpacity', vpf * numPts); - strokeColor = geo.util.getGeomBuffer(geom, 'strokeColor', vpf * numPts * 3); - fill = geo.util.getGeomBuffer(geom, 'fill', vpf * numPts); - fillOpacity = geo.util.getGeomBuffer(geom, 'fillOpacity', vpf * numPts); - fillColor = geo.util.getGeomBuffer(geom, 'fillColor', vpf * numPts * 3); - indices = geom.primitive(0).indices(); - if (!(indices instanceof Uint16Array) || indices.length !== vpf * numPts) { - indices = new Uint16Array(vpf * numPts); - geom.primitive(0).setIndices(indices); - } - - for (i = ivpf = ivpf3 = iunit = i3 = 0; i < numPts; i += 1, i3 += 3) { - item = data[i]; - if (m_primitiveShape !== 'sprite') { - for (j = 0; j < unit.length; j += 1, iunit += 1) { - unitBuf[iunit] = unit[j]; - } - } - /* We can ignore the indicies (they will all be zero) */ - radiusVal = radFunc(item); - strokeVal = strokeFunc(item) ? 1.0 : 0.0; - strokeWidthVal = strokeWidthFunc(item); - strokeOpacityVal = strokeOpacityFunc(item); - strokeColorVal = strokeColorFunc(item); - fillVal = fillFunc(item) ? 1.0 : 0.0; - fillOpacityVal = fillOpacityFunc(item); - fillColorVal = fillColorFunc(item); - for (j = 0; j < vpf; 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(true); - m_mapper.modified(); - m_mapper.boundsDirtyTimestamp().modified(); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Return list of actors - * - * @returns {vgl.actor[]} - */ - //////////////////////////////////////////////////////////////////////////// - this.actors = function () { - if (!m_actor) { - return []; - } - return [m_actor]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the number of vertices used for each point. - * - * @returns {Number} - */ - //////////////////////////////////////////////////////////////////////////// - this.verticesPerFeature = function () { - var unit = pointPolygon(0, 0, 1, 1); - return unit.length / 2; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - 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(); - - if (m_primitiveShape === 'sprite') { - primitive = new vgl.points(); - } - - m_pixelWidthUniform = new vgl.floatUniform('pixelWidth', - 2.0 / 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}); - - // TODO: Right now this is ugly but we will fix it. - prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); - if (m_primitiveShape !== 'sprite') { - 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - - if (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(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - - s_update.call(m_this); - - // For now build if the data or style changes. In the future we may - // we able to partially update the data using dynamic gl buffers. - if (m_this.dataTime().getMTime() >= m_this.buildTime().getMTime() || - m_this.updateTime().getMTime() < m_this.getMTime()) { - m_this._build(); - } - - // Update uniforms - m_pixelWidthUniform.set(2.0 / 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(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.renderer().contextRenderer().removeActor(m_actor); - s_exit(); - }; - - m_this._init(); - return this; -}; - -inherit(geo.gl.pointFeature, geo.pointFeature); - -// Now register it -geo.registerFeature('vgl', 'point', geo.gl.pointFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of geomFeature - * - * @class - * @extends geo.geomFeature - * @param {vgl.geometryData} arg - * @returns {geo.gl.geomFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.geomFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.gl.geomFeature)) { - return new geo.gl.geomFeature(arg); - } - arg = arg || {}; - geo.geomFeature.call(this, arg); - - // Initialize - var m_this = this, - m_geom = arg.geom || null, - m_actor = vgl.actor(), - m_mapper = vgl.mapper(), - m_material = null, - m_scalar = null, - m_color = arg.color || [1.0, 1.0, 1.0], - m_buildTime = null, - m_noOfPrimitives = 0; - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - var style = m_this.style(); - - // Vertex color gets the preference - if (m_geom !== null) { - m_scalar = m_geom.sourceData(vgl.vertexAttributeKeys.Scalar); - m_color = m_geom.sourceData(vgl.vertexAttributeKeys.Color); - m_mapper.setGeometryData(m_geom); - } - - m_this.setMapper(m_mapper); - - if (style.point_sprites !== undefined && style.point_sprites && - style.point_sprites_image !== undefined && - style.point_sprites_image !== null && - m_noOfPrimitives === 1 && - m_geom.primitive(0).primitiveType() === gl.POINTS) { - m_material = vgl.utils.createPointSpritesMaterial( - style.point_sprites_image); - } else if (m_scalar) { - if (m_color instanceof vgl.lookupTable) { - m_color.updateRange(m_scalar.scalarRange()); - m_material = vgl.utils.createColorMappedMaterial(m_color); - } else { - m_color = vgl.lookupTable(); - m_color.updateRange(m_scalar.scalarRange()); - m_material = vgl.utils.createColorMappedMaterial(m_color); - } - } else if (m_color) { - m_material = vgl.utils.createColorMaterial(); - } else { - m_material = vgl.utils.createSolidColorMaterial(); - } - m_actor.setMaterial(m_material); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update - * - * @private - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - if (m_buildTime && - m_buildTime.getMTime() < m_this.getMTime()) { - if (m_color instanceof vgl.lookupTable) { - vgl.utils.updateColorMappedMaterial(m_this.material(), - m_this.style.color); - }/* else { - // TODO - }*/ - } else { - m_buildTime = vgl.timestamp(); - m_buildTime.modified(); - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get/Set geometry - * - * @returns {geo.gl.geomFeature} - */ - //////////////////////////////////////////////////////////////////////////// - this.geometry = function (val) { - if (val === undefined) { - return m_geom; - } else { - m_geom = val; - m_this.modified(); - return m_this; - } - }; - - return this; -}; - -inherit(geo.gl.geomFeature, geo.geomFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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.gl.planeFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.planeFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.gl.planeFeature)) { - return new geo.gl.planeFeature(arg); - } - geo.planeFeature.call(this, arg); - - var m_this = this, - s_exit = this._exit, - m_actor = null, - m_onloadCallback = arg.onload === undefined ? null : arg.onload; - - //////////////////////////////////////////////////////////////////////////// - /** - * Gets the coordinates for this plane - * - * @returns {Array} [[origin], [upper left] [lower right]] - */ - //////////////////////////////////////////////////////////////////////////// - this.coords = function () { - return [m_this.origin(), m_this.upperLeft(), m_this.lowerRight()]; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build this feature - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - var or = m_this.origin(), - ul = m_this.upperLeft(), - lr = m_this.lowerRight(), - /// img could be a source or an Image - img = m_this.style().image, - image = null, - texture = null, - gcs = m_this.gcs(), - map_gcs = m_this.layer().map().gcs(); - - if (gcs !== map_gcs) { - or = geo.transform.transformCoordinates(gcs, map_gcs, or); - ul = geo.transform.transformCoordinates(gcs, map_gcs, ul); - lr = geo.transform.transformCoordinates(gcs, map_gcs, lr); - } - - m_this.buildTime().modified(); - - if (m_actor) { - m_this.renderer().contextRenderer().removeActor(m_actor); - } - - if (img && img instanceof Image) { - image = img; - } else if (img) { - image = new Image(); - image.src = img; - } - - if (!image) { - m_actor = vgl.utils.createPlane(or[0], or[1], or[2], - ul[0], ul[1], ul[2], - lr[0], lr[1], lr[2]); - - m_actor.material().shaderProgram().uniform('opacity').set( - m_this.style().opacity !== undefined ? m_this.style().opacity : 1); - - m_this.renderer().contextRenderer().addActor(m_actor); - - } else { - m_actor = vgl.utils.createTexturePlane(or[0], or[1], or[2], - lr[0], lr[1], lr[2], - ul[0], ul[1], ul[2], true); - - m_actor.material().shaderProgram().uniform('opacity').set( - m_this.style().opacity !== undefined ? m_this.style().opacity : 1); - - texture = vgl.texture(); - m_this.visible(false); - - m_this.renderer().contextRenderer().addActor(m_actor); - - /* An image is already loaded if .complete is true and .naturalWidth - * and .naturalHeight are defined and non-zero (not falsy seems to be - * sufficient). */ - if (image.complete && image.naturalWidth && image.naturalHeight) { - texture.setImage(image); - m_actor.material().addAttribute(texture); - /// NOTE Currently we assume that we want to show the feature as - /// soon as the image gets loaded. However, there might be a case - /// where we want to lock down the visibility. We will deal with that - /// later. - m_this.visible(true); - - if (m_onloadCallback) { - m_onloadCallback.call(m_this); - } - } else { - image.onload = function () { - texture.setImage(image); - m_actor.material().addAttribute(texture); - /// NOTE Currently we assume that we want to show the feature as - /// soon as the image gets loaded. However, there might be a case - /// where we want to lock down the visibility. We will deal with that - /// later. - m_this.visible(true); - - if (m_onloadCallback) { - m_onloadCallback.call(m_this); - } - - if (m_this.drawOnAsyncResourceLoad()) { - m_this._update(); - m_this.layer().draw(); - } - }; - } - } - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime()) { - m_this._build(); - } - - if (m_this.updateTime().getMTime() <= m_this.getMTime()) { - m_actor.setVisible(m_this.visible()); - m_actor.material().setBinNumber(m_this.bin()); - m_actor.material().shaderProgram().uniform('opacity').set( - m_this.style().opacity !== undefined ? m_this.style().opacity : 1); - } - - m_this.updateTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.renderer().contextRenderer().removeActor(m_actor); - s_exit(); - }; - - return this; -}; - -inherit(geo.gl.planeFeature, geo.planeFeature); - -// Now register it -geo.registerFeature('vgl', 'plane', geo.gl.planeFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class quadFeature - * - * @class - * @param {Object} arg Options object - * @extends geo.quadFeature - * @returns {geo.gl.quadFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.quadFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.gl.quadFeature)) { - return new geo.gl.quadFeature(arg); - } - geo.quadFeature.call(this, arg); - - var m_this = this, - s_exit = this._exit, - s_init = this._init, - s_update = this._update, - m_modelViewUniform, - m_actor_image, m_actor_color, m_glBuffers = {}, m_imgposbuf, - m_clrposbuf, m_clrModelViewUniform, - m_glCompileTimestamp = vgl.timestamp(), - m_glColorCompileTimestamp = vgl.timestamp(), - m_quads; - var vertexShaderImageSource = [ - 'attribute vec3 vertexPosition;', - 'attribute vec3 textureCoord;', - 'uniform mat4 modelViewMatrix;', - 'uniform mat4 projectionMatrix;', - 'varying highp vec3 iTextureCoord;', - 'void main(void) {', - ' gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);', - ' iTextureCoord = textureCoord;', - '}'].join('\n'); - var vertexShaderColorSource = [ - 'attribute vec3 vertexPosition;', - 'uniform vec3 vertexColor;', - 'uniform mat4 modelViewMatrix;', - 'uniform mat4 projectionMatrix;', - 'varying mediump vec3 iVertexColor;', - 'varying highp vec3 iTextureCoord;', - 'void main(void) {', - ' gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);', - ' iVertexColor = vertexColor;', - '}'].join('\n'); - - /** - * Allocate buffers that we need to control for image quads. This mimics - * the actions from vgl.mapper to some degree. - * - * @private - */ - function setupDrawObjects(renderState) { - var context = renderState.m_context, - newbuf = false; - - if (m_quads.imgQuads.length) { - if (!m_imgposbuf || m_imgposbuf.length < m_quads.imgQuads.length * 12 || - !m_glBuffers.imgQuadsPosition) { - if (m_glBuffers.imgQuadsPosition) { - context.deleteBuffer(m_glBuffers.imgQuadsPosition); - } - m_glBuffers.imgQuadsPosition = context.createBuffer(); - m_imgposbuf = new Float32Array(Math.max( - 128, m_quads.imgQuads.length * 2) * 12); - newbuf = true; - } - $.each(m_quads.imgQuads, function (idx, quad) { - for (var i = 0; i < 12; i += 1) { - m_imgposbuf[idx * 12 + i] = quad.pos[i] - m_quads.origin[i % 3]; - } - }); - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); - if (newbuf) { - context.bufferData(vgl.GL.ARRAY_BUFFER, m_imgposbuf, vgl.GL.DYNAMIC_DRAW); - } else { - context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, m_imgposbuf); - } - } - m_glCompileTimestamp.modified(); - } - - /** - * Allocate buffers that we need to control for color quads. This mimics - * the actions from vgl.mapper to some degree. - * - * @private - */ - function setupColorDrawObjects(renderState) { - var context = renderState.m_context, - newbuf = false; - - if (m_quads.clrQuads.length) { - if (!m_clrposbuf || m_clrposbuf.length < m_quads.clrQuads.length * 12 || - !m_glBuffers.clrQuadsPosition) { - if (m_glBuffers.imgQuadsPosition) { - context.deleteBuffer(m_glBuffers.clrQuadsPosition); - } - m_glBuffers.clrQuadsPosition = context.createBuffer(); - m_clrposbuf = new Float32Array(Math.max( - 128, m_quads.clrQuads.length * 2) * 12); - newbuf = true; - } - $.each(m_quads.clrQuads, function (idx, quad) { - for (var i = 0; i < 12; i += 1) { - m_clrposbuf[idx * 12 + i] = quad.pos[i] - m_quads.origin[i % 3]; - } - }); - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition); - if (newbuf) { - context.bufferData(vgl.GL.ARRAY_BUFFER, m_clrposbuf, vgl.GL.DYNAMIC_DRAW); - } else { - context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, m_clrposbuf); - } - } - m_glColorCompileTimestamp.modified(); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Build this feature - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - var mapper, mat, prog, srctex, geom; - - if (!m_this.position()) { - return; - } - m_quads = this._generateQuads(); - /* Create an actor to render image quads */ - if (m_quads.imgQuads.length && !m_actor_image) { - m_this.visible(false); - mapper = new vgl.mapper({dynamicDraw: true}); - m_actor_image = new vgl.actor(); - /* This is similar to vgl.utils.createTextureMaterial */ - m_actor_image.setMapper(mapper); - mat = new vgl.material(); - prog = new vgl.shaderProgram(); - prog.addVertexAttribute(new vgl.vertexAttribute('vertexPosition'), - vgl.vertexAttributeKeys.Position); - prog.addVertexAttribute(new vgl.vertexAttribute('textureCoord'), - vgl.vertexAttributeKeys.TextureCoordinate); - m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', - m_quads.origin); - prog.addUniform(m_modelViewUniform); - prog.addUniform(new vgl.projectionUniform('projectionMatrix')); - prog.addUniform(new vgl.floatUniform('opacity', 1.0)); - prog.addShader(vgl.getCachedShader( - vgl.GL.VERTEX_SHADER, vgl.GL, vertexShaderImageSource)); - prog.addShader(vgl.utils.createRgbaTextureFragmentShader(vgl.GL)); - mat.addAttribute(prog); - mat.addAttribute(new vgl.blend()); - /* This is similar to vgl.planeSource */ - geom = new vgl.geometryData(); - m_imgposbuf = undefined; - srctex = new vgl.sourceDataT2fv(); - srctex.pushBack([0, 0, 1, 0, 0, 1, 1, 1]); - geom.addSource(srctex); - /* We deliberately do not add a primitive to our geometry -- we take care - * of that ourselves. */ - - mapper.setGeometryData(geom); - m_actor_image.setMaterial(mat); - - mapper.s_render = mapper.render; - mapper.render = m_this._renderImageQuads; - m_this.renderer().contextRenderer().addActor(m_actor_image); - m_this.visible(true); - } - /* Create an actor to render color quads */ - if (m_quads.clrQuads.length && !m_actor_color) { - m_this.visible(false); - mapper = new vgl.mapper({dynamicDraw: true}); - m_actor_color = new vgl.actor(); - /* This is similar to vgl.utils.createTextureMaterial */ - m_actor_color.setMapper(mapper); - mat = new vgl.material(); - prog = new vgl.shaderProgram(); - prog.addVertexAttribute(new vgl.vertexAttribute('vertexPosition'), - vgl.vertexAttributeKeys.Position); - m_clrModelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', - m_quads.origin); - prog.addUniform(m_clrModelViewUniform); - prog.addUniform(new vgl.projectionUniform('projectionMatrix')); - prog.addUniform(new vgl.floatUniform('opacity', 1.0)); - prog.addUniform(new vgl.uniform(vgl.GL.FLOAT_VEC3, 'vertexColor')); - prog.addShader(vgl.getCachedShader( - vgl.GL.VERTEX_SHADER, vgl.GL, vertexShaderColorSource)); - prog.addShader(vgl.utils.createFragmentShader(vgl.GL)); - mat.addAttribute(prog); - mat.addAttribute(new vgl.blend()); - /* This is similar to vgl.planeSource */ - geom = new vgl.geometryData(); - m_clrposbuf = undefined; - /* We deliberately do not add a primitive to our geometry -- we take care - * of that ourselves. */ - - mapper.setGeometryData(geom); - m_actor_color.setMaterial(mat); - - mapper.s_render = mapper.render; - mapper.render = m_this._renderColorQuads; - m_this.renderer().contextRenderer().addActor(m_actor_color); - m_this.visible(true); - } - if (m_modelViewUniform) { - m_modelViewUniform.setOrigin(m_quads.origin); - } - if (m_clrModelViewUniform) { - m_clrModelViewUniform.setOrigin(m_quads.origin); - } - m_this.buildTime().modified(); - }; - - /** - * Check all of the image quads. If any do not have the correct texture, - * update them. */ - this._updateTextures = function () { - var texture; - - $.each(m_quads.imgQuads, function (idx, quad) { - if (!quad.image) { - return; - } - if (quad.image._texture) { - quad.texture = quad.image._texture; - } else { - texture = new vgl.texture(); - texture.setImage(quad.image); - quad.texture = quad.image._texture = texture; - } - }); - }; - - /** - * Render all of the color quads using a single mapper. - * - * @param renderState: the render state used for the render. - */ - this._renderColorQuads = function (renderState) { - if (!m_quads.clrQuads.length) { - return; - } - var mapper = this; - if (mapper.getMTime() > m_glColorCompileTimestamp.getMTime() || - m_this.dataTime().getMTime() > m_glColorCompileTimestamp.getMTime() || - renderState.m_contextChanged || !m_clrposbuf || - m_quads.clrQuads.length * 12 > m_clrposbuf.length) { - setupColorDrawObjects(renderState); - } - mapper.s_render(renderState); - - var context = renderState.m_context, opacity = 1, color; - - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition); - $.each(m_quads.clrQuads, function (idx, quad) { - if (quad.opacity !== opacity) { - opacity = quad.opacity; - context.uniform1fv(renderState.m_material.shaderProgram( - ).uniformLocation('opacity'), new Float32Array([opacity])); - } - if (!color || color.r !== quad.color.r || color.g !== quad.color.g || - color.b !== quad.color.b) { - color = quad.color; - context.uniform3fv(renderState.m_material.shaderProgram( - ).uniformLocation('vertexColor'), new Float32Array([ - color.r, color.g, color.b])); - } - - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition); - context.vertexAttribPointer(vgl.vertexAttributeKeys.Position, 3, - vgl.GL.FLOAT, false, 12, idx * 12 * 4); - context.enableVertexAttribArray(vgl.vertexAttributeKeys.Position); - - context.drawArrays(vgl.GL.TRIANGLE_STRIP, 0, 4); - }); - context.bindBuffer(vgl.GL.ARRAY_BUFFER, null); - }; - - /** - * Render all of the image quads using a single mapper. - * - * @param renderState: the render state used for the render. - */ - this._renderImageQuads = function (renderState) { - if (!m_quads.imgQuads.length) { - return; - } - var mapper = this; - if (mapper.getMTime() > m_glCompileTimestamp.getMTime() || - m_this.dataTime().getMTime() > m_glCompileTimestamp.getMTime() || - renderState.m_contextChanged || !m_imgposbuf || - m_quads.imgQuads.length * 12 > m_imgposbuf.length) { - setupDrawObjects(renderState); - } - mapper.s_render(renderState); - - var context = renderState.m_context, opacity = 1; - - m_this._updateTextures(); - - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); - $.each(m_quads.imgQuads, function (idx, quad) { - if (!quad.image) { - return; - } - quad.texture.bind(renderState); - - if (quad.opacity !== opacity) { - opacity = quad.opacity; - context.uniform1fv(renderState.m_material.shaderProgram( - ).uniformLocation('opacity'), new Float32Array([opacity])); - } - - context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); - context.vertexAttribPointer(vgl.vertexAttributeKeys.Position, 3, - vgl.GL.FLOAT, false, 12, idx * 12 * 4); - context.enableVertexAttribArray(vgl.vertexAttributeKeys.Position); - - context.drawArrays(vgl.GL.TRIANGLE_STRIP, 0, 4); - quad.texture.undoBind(renderState); - }); - context.bindBuffer(vgl.GL.ARRAY_BUFFER, null); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - s_update.call(m_this); - if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime() || - m_this.updateTime().getMTime() < m_this.getMTime()) { - m_this._build(); - } - if (m_actor_color) { - m_actor_color.setVisible(m_this.visible()); - m_actor_color.material().setBinNumber(m_this.bin()); - } - if (m_actor_image) { - m_actor_image.setVisible(m_this.visible()); - m_actor_image.material().setBinNumber(m_this.bin()); - } - m_this.updateTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function () { - s_init.call(m_this, arg); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - if (m_actor_image) { - m_this.renderer().contextRenderer().removeActor(m_actor_image); - m_actor_image = null; - } - if (m_actor_color) { - m_this.renderer().contextRenderer().removeActor(m_actor_color); - m_actor_color = null; - } - s_exit.call(m_this); - }; - - m_this._init(arg); - return this; -}; - -inherit(geo.gl.quadFeature, geo.quadFeature); - -// Now register it -geo.registerFeature('vgl', 'quad', geo.gl.quadFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of polygonFeature - * - * @class - * @extends geo.polygonFeature - * @returns {geo.gl.polygonFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.polygonFeature = function (arg) { - 'use strict'; - if (!(this instanceof geo.gl.polygonFeature)) { - return new geo.gl.polygonFeature(arg); - } - arg = arg || {}; - geo.polygonFeature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - 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; - - function createVertexShader() { - var vertexShaderSource = [ - 'attribute vec3 pos;', - 'attribute vec3 fillColor;', - 'attribute float fillOpacity;', - 'uniform mat4 modelViewMatrix;', - 'uniform mat4 projectionMatrix;', - 'uniform float pixelWidth;', - 'varying vec3 fillColorVar;', - 'varying float fillOpacityVar;', - - 'void main(void)', - '{', - ' vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(pos.xyz, 1);', - ' if (clipPos.w != 0.0) {', - ' clipPos = clipPos/clipPos.w;', - ' }', - ' fillColorVar = fillColor;', - ' fillOpacityVar = fillOpacity;', - ' gl_Position = clipPos;', - '}' - ].join('\n'), - shader = new vgl.shader(vgl.GL.VERTEX_SHADER); - shader.setShaderSource(vertexShaderSource); - return shader; - } - - function createFragmentShader() { - var fragmentShaderSource = [ - '#ifdef GL_ES', - ' precision highp float;', - '#endif', - 'varying vec3 fillColorVar;', - 'varying float fillOpacityVar;', - 'void main () {', - ' gl_FragColor = vec4 (fillColorVar, fillOpacityVar);', - '}' - ].join('\n'), - shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); - shader.setShaderSource(fragmentShaderSource); - return shader; - } - - function createGLPolygons() { - var i = null, - numPts = null, - start = null, - itemIndex = 0, - polygonItemCoordIndex = 0, - position = [], - fillColor = [], - fillOpacity = [], - fillColorNew = [], - fillOpacityNew = [], - posFunc = null, - fillColorFunc = null, - polygonItem = null, - fillOpacityFunc = null, - buffers = vgl.DataBuffers(1024), - sourcePositions = vgl.sourceDataP3fv(), - sourceFillColor = - vgl.sourceDataAnyfv(3, vgl.vertexAttributeKeysIndexed.Two), - sourceFillOpacity = - vgl.sourceDataAnyfv(1, vgl.vertexAttributeKeysIndexed.Three), - trianglePrimitive = vgl.triangles(), - geom = vgl.geometryData(), - polygon = null, - holes = null, - extRing = null, - extIndex = 0, - extLength = null, - intIndex = 0, - posInstance = null, - triangulator = new PNLTRI.Triangulator(), - triangList = null, - newTriangList = null, - fillColorInstance = null; - - posFunc = m_this.position(); - fillColorFunc = m_this.style.get('fillColor'); - fillOpacityFunc = m_this.style.get('fillOpacity'); - - m_this.data().forEach(function (item) { - polygon = m_this.polygon()(item, itemIndex); - polygonItem = polygon.outer || []; - holes = polygon.inner || []; - polygonItemCoordIndex = 0; - extRing = []; - extIndex = 0; - extLength = polygonItem.length - 1; - extRing[0] = []; - intIndex = 0; - - polygonItem.forEach(function (extRingCoords) { - if (extIndex !== extLength) { - //extRing = extRing.concat(extRingCoords); - posInstance = posFunc(extRingCoords, - polygonItemCoordIndex, - item, itemIndex); - extRing[0].push({ - x: posInstance.x, y: posInstance.y, i: fillColor.length - }); - - fillColorInstance = fillColorFunc(extRingCoords, - polygonItemCoordIndex, - item, itemIndex); - fillColor.push([fillColorInstance.r, - fillColorInstance.g, - fillColorInstance.b]); - fillOpacity.push(fillOpacityFunc(extRingCoords, - polygonItemCoordIndex, - item, - itemIndex)); - polygonItemCoordIndex += 1; - } - extIndex += 1; - }); - - polygonItemCoordIndex = 0; - holes.forEach(function (hole) { - extRing[intIndex + 1] = []; - hole.forEach(function (intRingCoords) { - posInstance = posFunc(intRingCoords, polygonItemCoordIndex, - item, itemIndex); - extRing[intIndex + 1].push({ - x: posInstance.x, y: posInstance.y, i: fillColor.length - }); - fillColorInstance = fillColorFunc(intRingCoords, - polygonItemCoordIndex, - item, itemIndex); - fillColor.push([fillColorInstance.r, - fillColorInstance.g, - fillColorInstance.b]); - fillOpacity.push(fillOpacityFunc(intRingCoords, - polygonItemCoordIndex, - item, itemIndex)); - polygonItemCoordIndex += 1; - }); - intIndex += 1; - }); - - //console.log("extRing ", extRing); - //console.log("result", PolyK.Triangulate(extRing)); - triangList = triangulator.triangulate_polygon(extRing); - newTriangList = []; - - triangList.forEach(function (newIndices) { - Array.prototype.push.apply(newTriangList, newIndices); - }); - - for (i = 1; i < extRing.length; i += 1) { - extRing[0] = extRing[0].concat(extRing[i]); - } - - newTriangList.forEach(function (polygonIndex) { - var polygonItemCoords = extRing[0][polygonIndex]; - position.push([polygonItemCoords.x, - polygonItemCoords.y, - polygonItemCoords.z || 0.0]); - fillColorNew.push(fillColor[polygonItemCoords.i]); - fillOpacityNew.push(fillOpacity[polygonItemCoords.i]); - }); - - itemIndex += 1; - }); - - position = geo.transform.transformCoordinates( - m_this.gcs(), m_this.layer().map().gcs(), - position, 3); - - buffers.create('pos', 3); - buffers.create('indices', 1); - buffers.create('fillColor', 3); - buffers.create('fillOpacity', 1); - - numPts = position.length; - - start = buffers.alloc(numPts); - - //console.log("numPts ", numPts); - for (i = 0; i < numPts; 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); - } - - //console.log(buffers.get('fillColor')); - sourcePositions.pushBack(buffers.get('pos')); - geom.addSource(sourcePositions); - - sourceFillColor.pushBack(buffers.get('fillColor')); - geom.addSource(sourceFillColor); - - sourceFillOpacity.pushBack(buffers.get('fillOpacity')); - geom.addSource(sourceFillOpacity); - - //console.log(buffers.get('indices')); - trianglePrimitive.setIndices(buffers.get('indices')); - geom.addPrimitive(trianglePrimitive); - - m_mapper.setGeometryData(geom); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - if (m_actor) { - m_this.renderer().contextRenderer().removeActor(m_actor); - } - - createGLPolygons(); - - m_this.renderer().contextRenderer().addActor(m_actor); - m_this.buildTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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._build(); - } - - m_actor.setVisible(m_this.visible()); - m_actor.material().setBinNumber(m_this.bin()); - m_this.updateTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.renderer().contextRenderer().removeActor(m_actor); - s_exit(); - }; - - this._init(arg); - return this; -}; - -inherit(geo.gl.polygonFeature, geo.polygonFeature); - -// Now register it -geo.registerFeature('vgl', 'polygon', geo.gl.polygonFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of contourFeature - * - * @class - * @extends geo.contourFeature - * @returns {geo.gl.contourFeature} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gl.contourFeature = function (arg) { - 'use strict'; - - if (!(this instanceof geo.gl.contourFeature)) { - return new geo.gl.contourFeature(arg); - } - arg = arg || {}; - geo.contourFeature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - 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 = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, - s_init = this._init, - s_update = this._update; - - 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)', - '{', - /* Don't use z values; something is rotten in one of our matrices */ - ' 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); - shader.setShaderSource(vertexShaderSource); - return 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); - shader.setShaderSource(fragmentShaderSource); - return shader; - } - - /* Create the contours. This calls the base class to generate the geometry, - * color map, and other parameters. The generated geoemtry is then loaded - * into the various gl uniforms and buffers. - */ - function createGLContours() { - var contour = m_this.createContours(), - numPts = contour.elements.length, - colorTable = [], - i, i3, j, j3, - posBuf, opacityBuf, valueBuf, indicesBuf, - geom = m_mapper.geometryData(); - - 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); - for (i = 0; i < contour.colorMap.length; i += 1) { - colorTable.push(contour.colorMap[i].r * 255); - colorTable.push(contour.colorMap[i].g * 255); - colorTable.push(contour.colorMap[i].b * 255); - colorTable.push(contour.colorMap[i].a * 255); - } - m_texture.setColorTable(colorTable); - contour.pos = geo.transform.transformCoordinates( - m_this.gcs(), m_this.layer().map().gcs(), contour.pos, 3); - posBuf = geo.util.getGeomBuffer(geom, 'pos', numPts * 3); - opacityBuf = geo.util.getGeomBuffer(geom, 'opacity', numPts); - valueBuf = geo.util.getGeomBuffer(geom, 'value', numPts); - for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { - j = contour.elements[i]; - j3 = j * 3; - 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(); - if (!(indicesBuf instanceof Uint16Array) || indicesBuf.length !== numPts) { - indicesBuf = new Uint16Array(numPts); - geom.primitive(0).setIndices(indicesBuf); - } - geom.boundsDirty(true); - m_mapper.modified(); - m_mapper.boundsDirtyTimestamp().modified(); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - 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); - /* steps is always an integer, but it is more efficient if we use a float - */ - 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - if (m_actor) { - m_this.renderer().contextRenderer().removeActor(m_actor); - } - - createGLContours(); - - m_this.renderer().contextRenderer().addActor(m_actor); - m_this.buildTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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._build(); - } - - m_actor.setVisible(m_this.visible()); - m_actor.material().setBinNumber(m_this.bin()); - m_this.updateTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.renderer().contextRenderer().removeActor(m_actor); - s_exit(); - }; - - this._init(arg); - return this; -}; - -inherit(geo.gl.contourFeature, geo.contourFeature); - -// Now register it -geo.registerFeature('vgl', 'contour', geo.gl.contourFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class vglRenderer - * - * @class - * @extends geo.renderer - * @param canvas - * @returns {geo.gl.vglRenderer} - */ -////////////////////////////////////////////////////////////////////////////// -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, - s_exit = this._exit; - - /// TODO: Move this API to the base class - //////////////////////////////////////////////////////////////////////////// - /** - * Return width of the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.width = function () { - return m_width; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return height of the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.height = function () { - return m_height; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get context specific renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.contextRenderer = function () { - return m_contextRenderer; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get API used by the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.api = function () { - return 'vgl'; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - 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(false); - - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle resize event - */ - //////////////////////////////////////////////////////////////////////////// - this._resize = function (x, y, w, h) { - 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(); - - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Render - */ - //////////////////////////////////////////////////////////////////////////// - this._render = function () { - if (m_renderAnimFrameRef === null) { - m_renderAnimFrameRef = window.requestAnimationFrame(function () { - m_renderAnimFrameRef = null; - m_viewer.render(); - }); - } - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Exit - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.canvas().remove(); - m_viewer.exit(); - s_exit(); - }; - - this._updateRendererCamera = function () { - var renderWindow = m_viewer.renderWindow(), - map = m_this.layer().map(), - camera = map.camera(), - rotation = map.rotation() || 0, - 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(geo.util.mat4AsArray(), 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(geo.util.mat4AsArray(), proj, - [0, 0, camera.constructor.bounds.far]); - /* Check if the rotation is a multiple of 90 */ - var basis = Math.PI / 2, - angle = rotation % basis, // move to range (-pi/2, pi/2) - ortho = (Math.min(Math.abs(angle), Math.abs(angle - basis)) < 0.00001); - renderWindow.renderers().forEach(function (renderer) { - var cam = renderer.camera(); - if (geo.util.compareArrays(view, cam.viewMatrix()) && - geo.util.compareArrays(proj, cam.projectionMatrix())) { - return; - } - cam.setViewMatrix(view, true); - 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 || !ortho || - (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 rotation event - m_this.layer().geoOn(geo.event.rotate, 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(); - - 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(); - } - }); - - return this; -}; - -inherit(geo.gl.vglRenderer, geo.renderer); - -geo.registerRenderer('vgl', geo.gl.vglRenderer); - -(function () { - 'use strict'; - - var checkedWebGL; - - /** - * Report if the vgl renderer is supported. This is just a check if webGL is - * supported and available. - * - * @returns {boolean} true if available. - */ - geo.gl.vglRenderer.supported = function () { - if (checkedWebGL === undefined) { - /* This is extracted from what Modernizr uses. */ - var canvas, ctx, exts; // eslint-disable-line no-unused-vars - try { - canvas = document.createElement('canvas'); - ctx = (canvas.getContext('webgl') || - canvas.getContext('experimental-webgl')); - exts = ctx.getSupportedExtensions(); - checkedWebGL = true; - } catch (e) { - console.error('No webGL support'); - checkedWebGL = false; - } - canvas = undefined; - ctx = undefined; - exts = undefined; - } - return checkedWebGL; - }; - - /** - * If the vgl renderer is not supported, supply the name of a renderer that - * should be used instead. This asks for the null renderer. - * - * @returns null for the null renderer. - */ - geo.gl.vglRenderer.fallback = function () { - return null; - }; -})(); - -geo.gl.tileLayer = function () { - 'use strict'; - var m_this = this, - s_init = this._init, - s_exit = this._exit, - m_quadFeature, - m_nextTileId = 0, - m_tiles = []; - - /* Add a tile to the list of quads */ - this._drawTile = function (tile) { - if (!m_quadFeature) { - return; - } - var bounds = this._tileBounds(tile), - level = tile.index.level || 0, - to = this._tileOffset(level), - quad = {}; - quad.ul = this.fromLocal(this.fromLevel({ - x: bounds.left - to.x, y: bounds.top - to.y - }, level), 0); - quad.ll = this.fromLocal(this.fromLevel({ - x: bounds.left - to.x, y: bounds.bottom - to.y - }, level), 0); - quad.ur = this.fromLocal(this.fromLevel({ - x: bounds.right - to.x, y: bounds.top - to.y - }, level), 0); - quad.lr = this.fromLocal(this.fromLevel({ - x: bounds.right - to.x, y: bounds.bottom - to.y - }, level), 0); - quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; - m_nextTileId += 1; - quad.id = m_nextTileId; - tile.quadId = quad.id; - quad.image = tile.image; - m_tiles.push(quad); - m_quadFeature.data(m_tiles); - m_quadFeature._update(); - m_this.draw(); - }; - - /* Remove the tile feature. */ - this._remove = function (tile) { - if (tile.quadId !== undefined && m_quadFeature) { - for (var i = 0; i < m_tiles.length; i += 1) { - if (m_tiles[i].id === tile.quadId) { - m_tiles.splice(i, 1); - break; - } - } - m_quadFeature.data(m_tiles); - m_quadFeature._update(); - m_this.draw(); - } - }; - - /** - * Clean up the layer. - */ - this._exit = function () { - m_this.deleteFeature(m_quadFeature); - m_quadFeature = null; - m_tiles = []; - s_exit.apply(m_this, arguments); - }; - - /** - * Initialize after the layer is added to the map. - */ - this._init = function () { - s_init.apply(m_this, arguments); - m_quadFeature = this.createFeature('quad', { - previewColor: m_this._options.previewColor, - previewImage: m_this._options.previewImage - }); - m_quadFeature.geoTrigger = undefined; - m_quadFeature.gcs(m_this._options.gcs || m_this.map().gcs()); - m_quadFeature.data(m_tiles); - m_quadFeature._update(); - }; - - /* These functions don't need to do anything. */ - this._getSubLayer = function () {}; - this._updateSubLayer = undefined; -}; - -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(); - } - m_this.updateTime().modified(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy Polygon Sub-Features - */ - //////////////////////////////////////////////////////////////////////////// - this._wipePolygons = function () { - if (m_gl_polygons) { - m_gl_polygons.map(function (polygon) { - return polygon._exit(); - }); - } - m_gl_polygons = null; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Destroy - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this._wipePolygons(); - s_exit(); - }; - - this._init(arg); - return this; -}; - -inherit(geo.gl.choroplethFeature, geo.choroplethFeature); - -// Now register it -geo.registerFeature('vgl', 'choropleth', geo.gl.choroplethFeature); - -/** @namespace */ -geo.d3 = {}; - -(function () { - 'use strict'; - - var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz', - strLength = 8; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get a random string to use as a div ID - * @returns {string} - */ - ////////////////////////////////////////////////////////////////////////////// - geo.d3.uniqueID = function () { - var strArray = [], - i; - strArray.length = strLength; - for (i = 0; i < strLength; i += 1) { - strArray[i] = chars.charAt(Math.floor(Math.random() * chars.length)); - } - return strArray.join(''); - }; - - // event propagated when the d3 renderer rescales its group element - geo.event.d3Rescale = 'geo_d3_rescale'; -}()); - -////////////////////////////////////////////////////////////////////////////// -/** - * D3 specific subclass of object which adds an id property for d3 selections - * on groups of objects by class id. - * @class - * @extends geo.sceneObject - */ -////////////////////////////////////////////////////////////////////////////// - -geo.d3.object = function (arg) { - 'use strict'; - // this is used to extend other geojs classes, so only generate - // a new object when that is not the case... like if this === window - 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; - - this._d3id = function () { - return m_id; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Returns a d3 selection for the feature elements - */ - //////////////////////////////////////////////////////////////////////////// - this.select = function () { - return m_this.renderer().select(m_this._d3id()); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Redraw the object. - */ - //////////////////////////////////////////////////////////////////////////// - this.draw = function () { - m_this._update(); - s_draw(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Removes the element from the svg and the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_this.renderer()._removeFeature(m_this._d3id()); - s_exit(); - }; - - return this; -}; - -inherit(geo.d3.object, geo.sceneObject); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class d3Renderer - * - * @class - * @extends geo.renderer - * @returns {geo.d3.d3Renderer} - */ -////////////////////////////////////////////////////////////////////////////// -geo.d3.d3Renderer = function (arg) { - 'use strict'; - - 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_diagonal = null, - m_scale = 1, - m_transform = {dx: 0, dy: 0, rx: 0, ry: 0, rotation: 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) { - 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] && - !styles.hasOwnProperty('strokeColor')) { - 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 = map.size().width, - height = map.size().height; - - m_width = width; - m_height = height; - if (!m_width || !m_height) { - throw 'Map layer has size 0'; - } - m_diagonal = Math.pow(width * width + height * height, 0.5); - m_corners = { - upperLeft: map.displayToGcs({'x': 0, 'y': 0}, null), - lowerRight: map.displayToGcs({'x': width, 'y': height}, null), - center: map.displayToGcs({'x': width / 2, 'y': height / 2}, null) - }; - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Set the translation, scale, and zoom for the current view. - * @note rotation not yet supported - * @private - */ - //////////////////////////////////////////////////////////////////////////// - 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), - center = map.gcsToDisplay(m_corners.center, null), - group = getGroup(), - canvas = m_this.canvas(), - dx, dy, scale, rotation, rx, ry; - - if (canvas.attr('scale') !== null) { - scale = parseFloat(canvas.attr('scale') || 1); - rx = (parseFloat(canvas.attr('dx') || 0) + - parseFloat(canvas.attr('offsetx') || 0)); - ry = (parseFloat(canvas.attr('dy') || 0) + - parseFloat(canvas.attr('offsety') || 0)); - rotation = parseFloat(canvas.attr('rotation') || 0); - dx = scale * rx + map.size().width / 2; - dy = scale * ry + map.size().height / 2; - } else { - scale = Math.sqrt( - Math.pow(lowerRight.y - upperLeft.y, 2) + - Math.pow(lowerRight.x - upperLeft.x, 2)) / m_diagonal; - // calculate the translation - rotation = map.rotation(); - rx = -m_width / 2; - ry = -m_height / 2; - dx = scale * rx + center.x; - dy = scale * ry + center.y; - } - - // set the group transform property - var transform = 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'; - if (rotation) { - transform += ' rotate(' + [ - rotation * 180 / Math.PI, -rx, -ry].join() + ')'; - } - group.attr('transform', transform); - - // set internal variables - m_scale = scale; - m_transform.dx = dx; - m_transform.dy = dy; - m_transform.rx = rx; - m_transform.ry = ry; - m_transform.rotation = rotation; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from screen pixel coordinates to the local coordinate system - * in the SVG group element taking into account the transform. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - this.baseToLocal = function (pt) { - pt = { - x: (pt.x - m_transform.dx) / m_scale, - y: (pt.y - m_transform.dy) / m_scale - }; - if (m_transform.rotation) { - var sinr = Math.sin(-m_transform.rotation), - cosr = Math.cos(-m_transform.rotation); - var x = pt.x + m_transform.rx, y = pt.y + m_transform.ry; - pt = { - x: x * cosr - y * sinr - m_transform.rx, - y: x * sinr + y * cosr - m_transform.ry - }; - } - return pt; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Convert from the local coordinate system in the SVG group element - * to screen pixel coordinates. - * @private - */ - //////////////////////////////////////////////////////////////////////////// - this.localToBase = function (pt) { - if (m_transform.rotation) { - var sinr = Math.sin(m_transform.rotation), - cosr = Math.cos(m_transform.rotation); - var x = pt.x + m_transform.rx, y = pt.y + m_transform.ry; - pt = { - x: x * cosr - y * sinr - m_transform.rx, - y: x * sinr + y * cosr - m_transform.ry - }; - } - pt = { - x: pt.x * m_scale + m_transform.dx, - y: pt.y * m_scale + m_transform.dy - }; - return pt; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - if (!m_this.canvas()) { - var canvas; - arg.widget = arg.widget || false; - - 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'); - } - - // 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'); - - if (!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()); - - if (!arg.widget) { - canvas.attr('class', 'group-' + m_this._d3id()); - - m_this.canvas(canvas); - } else { - m_this.canvas(m_svg); - } - } - m_this._setTransform(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get API used by the renderer - */ - //////////////////////////////////////////////////////////////////////////// - this.api = function () { - return 'd3'; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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.scaleFactor = function () { - return m_scale; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Handle resize event - */ - //////////////////////////////////////////////////////////////////////////// - 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 noop for geo.d3.object api. - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Exit - */ - //////////////////////////////////////////////////////////////////////////// - this._exit = function () { - m_features = {}; - m_this.canvas().remove(); - m_svg.remove(); - m_svg = undefined; - m_defs.remove(); - m_defs = undefined; - s_exit(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Get the definitions dom element for the layer - * @protected - */ - //////////////////////////////////////////////////////////////////////////// - this._definitions = function () { - return m_defs; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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. - * parentId: If set, the group ID of the parent element. - * } - */ - //////////////////////////////////////////////////////////////////////////// - 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); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Returns a d3 selection for the given feature id. - */ - //////////////////////////////////////////////////////////////////////////// - this.select = function (id, parentId) { - return getGroup(parentId).selectAll('.' + id); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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. - */ - //////////////////////////////////////////////////////////////////////////// - this.draw = function () { - }; - - // connect to pan event - this.layer().geoOn(geo.event.pan, m_this._setTransform); - - // connect to rotate event - this.layer().geoOn(geo.event.rotate, m_this._setTransform); - - // 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.d3Renderer, geo.renderer); - -geo.registerRenderer('d3', geo.d3.d3Renderer); - -(function () { - 'use strict'; - - /** - * Report if the d3 renderer is supported. This is just a check if d3 is - * available. - * - * @returns {boolean} true if available. - */ - geo.d3.d3Renderer.supported = function () { - return (typeof d3 !== 'undefined'); - }; - - /** - * If the d3 renderer is not supported, supply the name of a renderer that - * should be used instead. This asks for the null renderer. - * - * @returns null for the null renderer. - */ - geo.d3.d3Renderer.fallback = function () { - return null; - }; -})(); - -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), - offsetx = parseInt(parentNode.attr('offsetx') || 0, 10), - offsety = parseInt(parentNode.attr('offsety') || 0, 10); - tile.feature = m_this.createFeature( - 'plane', {drawOnAsyncResourceLoad: true}) - .origin([bounds.left - offsetx, bounds.top - offsety]) - .upperLeft([bounds.left - offsetx, bounds.top - offsety]) - .lowerRight([bounds.right - offsetx, bounds.bottom - offsety]) - .style({ - image: tile._url, - opacity: 1, - reference: tile.toString(), - parentId: parentNode.attr('data-tile-layer-id') - }); - /* Don't respond to geo events */ - tile.feature.geoTrigger = undefined; - tile.feature._update(); - m_this.draw(); - }; - - /** - * 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 = 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; - }; - - /** - * Set sublayer transforms to align them with the given zoom level. - * @param {number} level The target zoom level - * @param {object} view The view bounds. The top and left are used to - * adjust the offset of tile layers. - * @return {object} the x and y offsets for the current level. - */ - this._updateSubLayers = function (level, view) { - var canvas = m_this.canvas(), - lastlevel = parseInt(canvas.attr('lastlevel'), 10), - lastx = parseInt(canvas.attr('lastoffsetx') || 0, 10), - lasty = parseInt(canvas.attr('lastoffsety') || 0, 10); - if (lastlevel === level && Math.abs(lastx - view.left) < 65536 && - Math.abs(lasty - view.top) < 65536) { - return {x: lastx, y: lasty}; - } - var to = this._tileOffset(level), - x = parseInt(view.left, 10) + to.x, - y = parseInt(view.top, 10) + to.y; - var tileCache = m_this.cache._cache; - $.each(canvas.selectAll('.geo-tile-layer')[0], function (idx, el) { - var layer = parseInt($(el).attr('data-tile-layer'), 10), - scale = Math.pow(2, level - layer); - el = m_this._getSubLayer(layer); - el.attr('transform', 'matrix(' + [scale, 0, 0, scale, 0, 0].join() + ')'); - /* x and y are the upper left of our view. This is the zero-point for - * offsets at the current level. Other tile layers' offsets are scaled - * by appropriate factors of 2. We need to shift the tiles of each - * layer by the appropriate amount (computed as dx and dy). */ - var layerx = parseInt(x / Math.pow(2, level - layer), 10), - layery = parseInt(y / Math.pow(2, level - layer), 10), - dx = layerx - parseInt(el.attr('offsetx') || 0, 10), - dy = layery - parseInt(el.attr('offsety') || 0, 10); - el.attr({offsetx: layerx, offsety: layery}); - /* We have to update the values stored in the tile features, too, - * otherwise when d3 regenerates these features, the offsets will be - * wrong. */ - $.each(tileCache, function (idx, tile) { - if (tile._index.level === layer && tile.feature) { - var f = tile.feature, - o = f.origin(), ul = f.upperLeft(), lr = f.lowerRight(); - f.origin([o[0] - dx, o[1] - dy, o[2]]); - f.upperLeft([ul[0] - dx, ul[1] - dy, ul[2]]); - f.lowerRight([lr[0] - dx, lr[1] - dy, lr[2]]); - f._update(); - } - }); - }); - canvas.attr({lastoffsetx: x, lastoffsety: y, lastlevel: level}); - return {x: x, y: y}; - }; - - /* 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; - - s_init.apply(m_this, arguments); - for (sublayer = 0; sublayer <= m_this._options.maxLevel; sublayer += 1) { - m_this._getSubLayer(sublayer); - } - }; - - /* 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 new instance of pointFeature - * - * @class - * @extends geo.pointFeature - * @extends geo.d3.object - * @returns {geo.d3.pointFeature} - */ -////////////////////////////////////////////////////////////////////////////// -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); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - s_update = this._update, - m_buildTime = geo.timestamp(), - m_style = {}; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - 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_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']; - - // 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 - */ - //////////////////////////////////////////////////////////////////////////// - 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.pointFeature, geo.pointFeature); - -// Now register it -geo.registerFeature('d3', 'point', geo.d3.pointFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - m_buildTime = geo.timestamp(), - s_update = this._update; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - 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; }); - - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - 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 - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - m_buildTime = geo.timestamp(), - s_update = this._update, - m_style = {}; - - m_style.style = {}; - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._build = function () { - var data = m_this.data() || [], - s_style = m_this.style(), - 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_this.featureGcsToDisplay(src), - target: m_this.featureGcsToDisplay(trg) - }); - } - }); - m_style.data = tmp; - m_style.attributes = { - d: diag - }; - - 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); - - m_this.renderer()._drawFeatures(m_style); - - m_buildTime.modified(); - m_this.updateTime().modified(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update - * - * @override - */ - //////////////////////////////////////////////////////////////////////////// - this._update = function () { - s_update.call(m_this); - - if (m_this.dataTime().getMTime() >= m_buildTime.getMTime()) { - m_this._build(); - } - - return m_this; - }; - - this._init(arg); - return this; -}; - -inherit(geo.d3.pathFeature, geo.pathFeature); - -geo.registerFeature('d3', 'path', geo.d3.pathFeature); - -/** - * @class - * @extends geo.graphFeature - */ -geo.d3.graphFeature = function (arg) { - 'use strict'; - - var m_this = this; - - if (!(this instanceof geo.d3.graphFeature)) { - return new geo.d3.graphFeature(arg); - } - geo.graphFeature.call(this, arg); - - //////////////////////////////////////////////////////////////////////////// - /** - * 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()); - }); - return selection; - }; - - return this; -}; - -inherit(geo.d3.graphFeature, geo.graphFeature); - -geo.registerFeature('d3', 'graph', geo.d3.graphFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * 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); - - var m_this = this, - m_style = {}, - s_update = this._update, - s_init = this._init, - m_buildTime = geo.timestamp(); - - ////////////////////////////////////////////////////////////////////////////// - /** - * Normalize a coordinate as an object {x: ..., y: ...} - * - * @private - * @returns {Object} - */ - ////////////////////////////////////////////////////////////////////////////// - function normalize(pt) { - if (Array.isArray(pt)) { - return { - x: pt[0], - y: pt[1] - }; - } - return pt; - } - - ////////////////////////////////////////////////////////////////////////////// - /** - * Build the feature object and pass to the renderer for drawing. - * - * @private - * @returns {geo.d3.planeFeature} - */ - ////////////////////////////////////////////////////////////////////////////// - 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); - } - */ - 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 { - m_style.append = 'rect'; - } - 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; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Redraw the plane feature if necessary. - * - * @private - * @returns {geo.d3.planeFeature} - */ - ////////////////////////////////////////////////////////////////////////////// - this._update = function () { - s_update.call(m_this); - - if (m_this.dataTime().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(); - 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); - - //////////////////////////////////////////////////////////////////////////// - /** - * @private - */ - //////////////////////////////////////////////////////////////////////////// - var m_this = this, - s_init = this._init, - s_exit = this._exit, - s_update = this._update, - m_buildTime = geo.timestamp(), - m_style = {}; - - //////////////////////////////////////////////////////////////////////////// - /** - * 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); - } - - //////////////////////////////////////////////////////////////////////////// - /** - * Initialize - * @protected - */ - //////////////////////////////////////////////////////////////////////////// - this._init = function (arg) { - s_init.call(m_this, arg); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build - * @protected - */ - //////////////////////////////////////////////////////////////////////////// - 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; - - // 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; - } - - 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; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Update - * @protected - */ - //////////////////////////////////////////////////////////////////////////// - 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 - ); - } - - 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); - -// Now register it -geo.registerFeature('d3', 'vector', geo.d3.vectorFeature); - -////////////////////////////////////////////////////////////////////////////// -/** - * @namespace - */ -////////////////////////////////////////////////////////////////////////////// -geo.gui = {}; - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class uiLayer - * - * @class - * @extends {geo.layer} - * @returns {geo.gui.uiLayer} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gui.uiLayer = function (arg) { - 'use strict'; - - // The widget stays fixed on the screen. - arg.renderer = 'dom'; - arg.sticky = false; - - if (!(this instanceof geo.gui.uiLayer)) { - return new geo.gui.uiLayer(arg); - } - geo.layer.call(this, arg); - - var m_this = this, - s_exit = this._exit; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create a new ui control - * - * @returns {geo.gui.Widget} Will return a new control widget - */ - //////////////////////////////////////////////////////////////////////////// - this.createWidget = function (widgetName, arg) { - var newWidget = geo.createWidget(widgetName, m_this, arg); - - // We only want top level widgets to be a child of the uiLayer - if (!(arg && 'parent' in arg)) { - m_this.addChild(newWidget); - } - - newWidget._init(arg); - m_this.modified(); - return newWidget; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Delete a ui control - */ - //////////////////////////////////////////////////////////////////////////// - this.deleteWidget = function (widget) { - widget._exit(); - m_this.removeChild(widget); - m_this.modified(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Free memory and destroy the layer. - */ - //////////////////////////////////////////////////////////////////////////// - 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); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class widget - * - * @class - * @extends {geo.sceneObject} - * @returns {geo.gui.widget} - */ -////////////////////////////////////////////////////////////////////////////// -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; - - 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(); - }; - - 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(); - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Create feature give a name - * - * @returns {geo.Feature} Will return a new feature - */ - //////////////////////////////////////////////////////////////////////////// - this._createFeature = function (featureName, arg) { - - var newFeature = geo.createFeature( - featureName, m_this, m_this.renderer(), arg); - - m_this.addChild(newFeature); - m_this.modified(); - return newFeature; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Delete feature - */ - //////////////////////////////////////////////////////////////////////////// - this._deleteFeature = function (feature) { - m_this.removeChild(feature); - feature._exit(); - return m_this; - }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Return the layer associated with this widget. - */ - //////////////////////////////////////////////////////////////////////////// - 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 (pos) { - if (pos !== undefined) { - arg.position = pos; - this.reposition(); - return this; - } - 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.svgWidget} - * @returns {geo.gui.sliderWidget} - */ -////////////////////////////////////////////////////////////////////////////// -geo.gui.sliderWidget = function (arg) { - 'use strict'; - if (!(this instanceof geo.gui.sliderWidget)) { - return new geo.gui.sliderWidget(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, - m_minus, - m_nub, - m_width = 20, // Approximate size of the widget in pixels - m_height = 100, - m_nubSize = 10, - m_plusIcon, - m_minusIcon, - m_group, - m_lowContrast, - m_highlightDur = 100; - - /* http://icomoon.io */ - /* CC BY 3.0 http://creativecommons.org/licenses/by/3.0/ */ - 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'; - - // Define off-white gray colors for low contrast ui (unselected). - m_lowContrast = { - white: '#f4f4f4', - 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 - */ - ////////////////////////////////////////////////////////////////////////////// - function put_icon(icon, base, cx, cy, size) { - var g = base.append('g'); - - // the scale factor - var s = size / 1024; - - g.append('g') - .append('g') - .attr( - 'transform', - 'translate(' + cx + ',' + cy + ') scale(' + s + ') translate(-512,-512)' - ) - .append('path') - .attr('d', icon) - .attr('class', 'geo-glyphicon'); - - return g; - } - - ////////////////////////////////////////////////////////////////////////////// - /** - * Initialize the slider widget in the map. - * - * @function - * @returns {geo.gui.sliderWidget} - * @private - */ - ////////////////////////////////////////////////////////////////////////////// - this._init = function () { - 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(); - - // create d3 scales for positioning - // TODO: make customizable and responsive - 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]); - - // Create the main group element - svg = svg.append('g').classed('geo-ui-slider', true); - m_group = svg; - - // Create + zoom button - m_plus = svg.append('g'); - m_plus.append('circle') - .datum({ - fill: 'white', - stroke: null - }) - .classed('geo-zoom-in', true) - .attr('cx', m_xscale(0)) - .attr('cy', m_yscale(0.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 - }); - - // Create the - zoom button - m_minus = svg.append('g'); - m_minus.append('circle') - .datum({ - fill: 'white', - stroke: null - }) - .classed('geo-zoom-out', true) - .attr('cx', m_xscale(0)) - .attr('cy', m_yscale(1.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_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 - }); - - // Respond to a mouse event on the widget - 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; - if (trans) { - map.transition({ - zoom: z, - ease: d3.ease('cubic-in-out'), - duration: 500, - done: m_this._update() - }); - } else { - map.zoom(z); - m_this._update(); - } - evt.stopPropagation(); - } - - // Create the track - svg.append('rect') - .datum({ - fill: 'white', - stroke: 'black' - }) - .classed('geo-zoom-track', true) - .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, true); - }); - - // Create the nub - m_nub = svg.append('rect') - .datum({ - fill: 'black', - stroke: null - }) - .classed('geo-zoom-nub', true) - .attr('x', m_xscale(-4)) - .attr('y', m_yscale(0.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; - }); - - }; - - var 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); - - // Update the nub position on zoom - m_this.layer().geoOn(geo.event.zoom, function () { - m_this._update(); - }); - - mouseOut(); - m_this._update(); - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * 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 - */ - ////////////////////////////////////////////////////////////////////////////// - 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(true); - - 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); - -////////////////////////////////////////////////////////////////////////////// -/** - * Create a new instance of class legendWidget - * - * @class - * @extends geo.gui.svgWidget - * @returns {geo.gui.legendWidget} - */ -////////////////////////////////////////////////////////////////////////////// -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); - - /** @private */ - var m_this = this, - m_categories = [], - m_top = null, - m_group = null, - m_border = null, - m_spacing = 20, // distance in pixels between lines - m_padding = 12, // padding in pixels inside the border - s_createCanvas = this._createCanvas, - s_appendChild = this._appendChild; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get or set the category array associated with - * the legend. Each element of this array is - * an object: :: - * { - * name: string, - * style: object, - * type: 'point' | 'line' | ... - * } - * - * The style property can contain the following feature styles: - * * fill: bool - * * fillColor: object | string - * * fillOpacity: number - * * stroke: bool - * * strokeColor: object | string - * * strokeWidth: number - * * strokeOpacity: number - * - * The type controls how the element is displayed, point as a circle, - * line as a line segment. Any other value will display as a rounded - * rectangle. - * - * @param {object[]?} categories The categories to display - */ - ////////////////////////////////////////////////////////////////////////////// - this.categories = function (arg) { - if (arg === undefined) { - return m_categories.slice(); - } - m_categories = arg.slice().map(function (d) { - if (d.type === 'line') { - d.style.fill = false; - d.style.stroke = true; - } - return d; - }); - m_this.draw(); - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get the widget's size - * @return {{width: number, height: number}} The size in pixels - */ - ////////////////////////////////////////////////////////////////////////////// - this.size = function () { - var width = 1, height; - var test = d3.select(m_this.canvas()).append('text') - .style('opacity', 1e-6); - - 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); - return { - width: width + 50, - height: height - }; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Redraw the legend - */ - ////////////////////////////////////////////////////////////////////////////// - this.draw = function () { - - 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'; - } - }) - .style('fill-opacity', function (d) { - if (d.style.fillOpacity === undefined) { - return 1; - } - return d.style.fillOpacity; - }) - .style('stroke', function (d) { - if (d.style.stroke || d.style.stroke === undefined) { - return d.style.strokeColor; - } else { - return 'none'; - } - }) - .style('stroke-opacity', function (d) { - if (d.style.strokeOpacity === undefined) { - return 1; - } - return d.style.strokeOpacity; - }) - .style('stroke-width', function (d) { - if (d.style.strokeWidth === undefined) { - return 1.5; - } - return d.style.strokeWidth; - }); - } - - m_border.attr('height', m_this.size().height + 2 * m_padding) - .style('display', null); - - var scale = m_this._scale(); - - var labels = m_group.selectAll('g.geo-label') - .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) + ')'; - }); - - 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) - ); - - applyColor(g.filter(function (d) { - 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) - ); - - g.append('text') - .attr('x', '50px') - .attr('y', 0) - .attr('dy', '0.3em') - .text(function (d) { - return d.name; - }); - - m_this.reposition(); - - return m_this; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Get scales for the x and y axis for the current size. - * @private - */ - ////////////////////////////////////////////////////////////////////////////// - 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]) - }; - }; - - ////////////////////////////////////////////////////////////////////////////// - /** - * Private initialization. Creates the widget's DOM container and internal - * variables. - * @private - */ - ////////////////////////////////////////////////////////////////////////////// - this._init = function () { - // 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(); - } - - 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': 0.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', 0.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 () { - - // This requires jquery ui, which we don't want to make a - // hard requirement, so bail out here if the widget factory - // is not available and throw a helpful message when the - // tries to use it. - if (!$.widget) { - $.fn.geojsMap = function () { - throw new Error( - 'The geojs jquery plugin requires jquery ui to be available.' - ); - }; - return; - } - - /** - * Takes an option key and returns true if it should - * return a color accessor. - * @private - */ - function isColorKey(key) { - return key.slice(key.length - 5, key.length) - .toLowerCase() === 'color'; - } - - /** - * Take an array of data and an accessor for a color property - * and return a wrapped accessor mapping to actual color - * values. This allows users to pass arbitrary strings - * or numbers as any color property and this will wrap - * a categorical scale or linear scale. - * - * Requires d3 - * @private - * @param {Object[]} data A data array - * @param {(string|number|function)} acc A color accessor - * @return {function} - */ - function makeColorScale(data, acc) { - if (!d3) { - console.warn('d3 is unavailable, cannot apply color scales.'); - return acc; - } - var domain; - var cannotHandle = false; - var doNotHandle = true; - var categorical = false; - var min = Number.POSITIVE_INFINITY; - var max = Number.NEGATIVE_INFINITY; - - function wrap(func) { - if (geo.util.isFunction(func)) { - return function () { - return func(acc.apply(this, arguments)); - }; - } else { - return func(acc); - } - } - - if (geo.util.isFunction(acc)) { - domain = d3.set(data.map(acc)).values(); - } else { - domain = [acc]; - } - domain.forEach(function (v) { - if (!(typeof v === 'string' && - typeof geo.util.convertColor(v) === 'object')) { - // This is to handle cases when values are css names or - // hex strings. We don't want to apply a categorical - // scale. - doNotHandle = false; - } - if (typeof v === 'string') { - categorical = true; - } else if (!isFinite(v)) { - cannotHandle = true; - } else if (+v > max) { - max = +v; - } else if (+v < min) { - min = +v; - } - }); - if (cannotHandle) { - // At least one value is not a string or a numeric value. - // Pass the bare accessor back to geojs to handle it. - return acc; - } - if (doNotHandle) { - return acc; - } - if (categorical) { - if (domain.length <= 10) { - return wrap(d3.scale.category10().domain(domain)); - } else if (domain.length <= 20) { - return wrap(d3.scale.category20().domain(domain)); - } else { - // TODO: sort domain by most used and make an "other" category - return wrap(d3.scale.category20().domain(domain)); - } - } - // http://colorbrewer2.org/?type=diverging&scheme=RdYlBu&n=3 - return wrap(d3.scale.linear() - .range([ - 'rgb(252,141,89)', - 'rgb(255,255,191)', - 'rgb(145,191,219)' - ]) - .domain([ - min, - (min + max) / 2, - max - ])); - } - - /** - * @class geojsMap - * @memberOf jQuery.fn - * - * @description Generates a geojs map inside an element. - * - * - * Due to current limitations in geojs, only a single map can be instantiated - * on a page. Trying to create a second map will throw an error - * (see issue - * #154). - * - * @example Create a map with the default options. - * $("#map").geojsMap(); - * @example Create a map with a given initial center and zoom - * $("#map").geojsMap({ - * longitude: -125, - * latitude: 35, - * zoom: 5 - * }); - * @example Create a map with points - * $("#map").geojsMap({ - * data: [...], - * layers: [{ - * renderer: 'vgl', - * features: [{ - * type: 'point', - * radius: 5, - * position: function (d) { return {x: d.geometry.x, y: d.geometry.y} }, - * fillColor: function (d, i) { return i < 5 ? 'red' : 'blue' }, - * stroke: false - * }] - * }] - * }; - * @example Create a map with points, lines and multiple layers - * $("#map").geojsMap({ - * center: { x: -130, y: 40 }, - * zoom: 3, - * layers: [{ - * renderer: 'vgl', - * features: [{ - * data: [...], - * type: 'point', - * radius: 5, - * position: function (d) { return {x: d.geometry.x, y: d.geometry.y} }, - * fillColor: function (d, i) { return i < 5 ? 'red' : 'blue' }, - * stroke: false - * }] - * }, - * { - * renderer: 'd3', - * features[{ - * data: [...], - * type: 'line', - * position: function (d) { return { x: d[0], y: d[1] } }, - * line: function (d) { return d.coordinates; }, - * strokeWidth: 3, - * strokeColor: 'black', - * strokeOpacity: 0.5 - * }] - * }] - * }; - */ - $.widget('geojs.geojsMap', /** @lends jQuery.fn.geojsMap */{ - /** - * A coordinate object as accepted by geojs to express positions in an - * arbitrary coordinate system (geographic, screen, etc). Coordinates returned by - * geojs methods are always expressed with "x" and "y" properties, but - * it will accept any of the aliased properties. - * @typedef coordinate - * @type {object} - * @property {number} longitude Alias: "x", "lng", or "lon" - * @property {number} latitude Alias: "y" or "lat" - * @property {number} [elevation=0] Alias: "z", "elev", or "height" - */ - - /** - * Colors can be expressed in multiple ways: - *
    - *
  • css name ("steelblue")
  • - *
  • 24 bit hex value (0xff0051)
  • - *
  • 25 bit hex string ("#ff0051")
  • - *
  • rgb object (values from 0-1, {r: 1, g: 0.5, b: 0})
  • - *
- * @typedef color - * @type {*} - */ - - /** - * Point feature options object. All styles can be - * given as accessor functions or constants. Accessor - * functions are called with the following signature: - *
-       *     function func(d, i) {
-       *         // d    - data object
-       *         // i    - index of d in the data array
-       *         // this - geo.pointFeature
-       *     }
-       * 
- * Pass null to remove saved options from previous calls. - * @typedef pointOptions - * @type {Object} - * @property {Object[]} data Data array - * @property {coordinate} position Location of the point center - * @property {number} radius - * Radius of the circle in pixels (ignored when size - * is present) - * @property {function} size - * A function returning a numerical value - * @property {boolean} fill Presence or absence of the fill - * @property {color} fillColor Interior color - * @property {float} fillOpacity Opacity of the interior [0,1] - * @property {boolean} stroke Presence or absence of the stroke - * @property {color} strokeColor Stroke color - * @property {float} strokeOpacity Opacity of the stroke [0,1] - */ - - /** - * @instance - * @description - * Map options (not fully implemented). - * @example Get the current map center - * var center=$("#map").geojsMap("center"); - * @example Pan the map to a new center - * $("#map").geojsMap("center", {lat: 10, lng: -100}); - * @property {object[]} [data=[]] The default data array used for - * features/layers not already containing data. - * @property {coordinate} [center={lat: 0, lng: 0}] The map center - * @property {number} [zoom=0] The zoom level (floating point >= 0) - * @property {(number|null)} [width=null] - * The width of the map in pixels or null for 100% - * @property {(number|null)} [height=null] - * The height of the map in pixels or null for 100% - * @property {geo.layer.spec[]} [layers=[]] - * Describes layers added to the map - * @property {boolean} [autoresize=true] - * Resize the map on window.resize (initialization only) - * @property {string} [url] - * The open street map tile server spec default: - * http://tile.openstreetmap.org/<zoom>/<x>/<y>.png - */ - options: { - center: {latitude: 0, longitude: 0}, - zoom: 0, - width: null, - height: null, - layers: [], - data: [], - url: 'http://tile.openstreetmap.org/{z}/{x}/{y}.png', - attribution: undefined, - - // These options are for future use, but shouldn't - // be changed at the moment, so they aren't documented. - baseLayer: 'osm', - baseRenderer: 'vgl' - }, - - /** - * Internal constructor - * @instance - * @protected - */ - _create: function () { - if (this._map || !this.element.length) { - // when called multiple times on a single element, do nothing - return; - } - - // create the map - 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) - }); - - // create the base layer - this._baseLayer = this._map.createLayer( - this.options.baseLayer, - { - renderer: this.options.baseRenderer, - url: this.options.url, - attribution: this.options.attribution - } - ); - - // Trigger a resize to a valid size before adding - // the feature layer to handle some of the bugs that - // occur when initializing onto a node of size 0. - this._resize({width: 800, height: 600}); - - this._layers = []; - this.update(); - }, - - /** - * Update the layers and features using a new array of - * {@link geo.layer.spec} objects. All existing layers - * and features are deleted. If only the data has changed, - * you can usually just call {@link jQuery.fn.geojsMap#redraw redraw}. - * @instance - * @param {geo.layer.spec[]} [layers] New map layers - * @example Delete and recreate all existing layers - * $("#map").geojsMap("update"); - * @example Remove all existing feature layers. - * $("#map").geojsMap("update", []); - */ - update: function (layers) { - var m_this = this; - this.options.layers = layers || this.options.layers || []; - - // delete existing layers - this._layers.forEach(function (layer) { - layer.clear(); - m_this._map.deleteLayer(layer); - }); - - // create new layers - this._layers = this.options.layers.map(function (layer) { - layer.data = layer.data || m_this.options.data; - - // Until auto color scaling gets moved into geojs core, we will - // mutate the spec and replace the color and radius options. - (layer.features || []).forEach(function (feature) { - var data = feature.data || layer.data || []; - var scl; - if (feature.type === 'point') { - if (feature.size) { - feature._size = geo.util.ensureFunction(feature.size); - } else if (feature.size === null) { - delete feature._size; - } - - if (data.length && feature._size) { - scl = d3.scale.linear() - .domain( - d3.extent(data, feature._size) - ) - .range([5, 20]); - feature.radius = function () { - // TODO: wrong `this` (wait for style refactor) - return scl(feature._size.apply(this, arguments)); - }; - } - delete feature.size; - } - - var key; - for (key in feature) { - if (feature.hasOwnProperty(key) && - isColorKey(key)) { - feature[key] = makeColorScale(data, feature[key]); - } - } - }); - return geo.layer.create(m_this._map, layer); - }); - - // trigger an initial draw - this.redraw(); - - return this; - }, - - /** - * Return the geojs map object. - * @instance - * @returns {geo.map} - */ - map: function () { - return this._map; - }, - - /** - * Set the tile server URL. - * @instance - * @param {string} url The url format string of an OSM tile server. - */ - url: function (url) { - this._baseLayer.url(url); - return this; - }, - - /** - * Resize the map canvas. - * @instance - * @protected - * @param {object?} size Explicit size or use this.options. - */ - _resize: function (size) { - var width = this.options.width, - height = this.options.height; - if (size) { - width = size.width; - height = size.height; - } - if (!width) { - width = this.element.width(); - } - if (!height) { - height = this.element.height(); - } - this._map.resize(0, 0, width, height); - }, - - /** - * Do a full redraw of the map. In general, users shouldn't need to - * call this method, but it could be useful when accessing lower - * level features of the mapping api. - * @todo This function may need to go through each feature and call - * {@link geo.feature#modified} to properly update. - * @instance - */ - redraw: function () { - this._resize(); - return this; - } - }); - - // Some argument type definitions used only by this plugin: - /** - * A geojs renderer is one of the following: - *
    - *
  • "vgl": Uses webGL
  • - *
  • "d3": Uses svg
  • - *
- * @typedef renderer - * @type {string} - */ - - }; - - /* Provide a method to reload the plugin in case jquery-ui is loaded after - * the plugin. */ - geo.jqueryPlugin = {reload: load}; - - $(load); -})(typeof $ !== 'undefined' ? $ : window.$, - typeof geo !== 'undefined' ? geo : window.geo, - typeof d3 !== 'undefined' ? d3 : window.d3); +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(require("jQuery"), require("d3")); + else if(typeof define === 'function' && define.amd) + define(["jQuery", "d3"], factory); + else if(typeof exports === 'object') + exports["geo"] = factory(require("jQuery"), require("d3")); + else + root["geo"] = factory(root["jQuery"], root["d3"]); +})(this, function(__WEBPACK_EXTERNAL_MODULE_1__, __WEBPACK_EXTERNAL_MODULE_132__) { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; + +/******/ // The require function +/******/ function __webpack_require__(moduleId) { + +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) +/******/ return installedModules[moduleId].exports; + +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = true; + +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } + + +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; + +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "dist/built"; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ function(module, exports, __webpack_require__) { + + // License headers that will be preserved in distributed bundles. + /** + * GeoJS + * @copyright 2013-2016, Kitware, Inc. + * @license Apache-2.0 + * + * Bundled with the following libraries: + * + * vgl + * @copyright 2014-2016, Kitware, Inc. + * @license Apache-2.0 + * + * Proj4js + * @copyright 2014, Mike Adair, Richard Greenwood, Didier Richard, Stephen Irons, Olivier Terral and Calvin Metcalf + * @license MIT + * + * gl-matrix + * @copyright 2015, Brandon Jones, Colin MacKenzie IV + * @license MIT + * + * pnltri + * @copyright 2014, Jurgen Ahting + * @license MIT + */ + + var $ = __webpack_require__(1); + __webpack_require__(2); + + module.exports = $.extend({ + camera: __webpack_require__(3), + choroplethFeature: __webpack_require__(126), + clock: __webpack_require__(133), + contourFeature: __webpack_require__(134), + domRenderer: __webpack_require__(135), + event: __webpack_require__(125), + feature: __webpack_require__(127), + featureLayer: __webpack_require__(137), + fetchQueue: __webpack_require__(138), + fileReader: __webpack_require__(139), + geomFeature: __webpack_require__(140), + graphFeature: __webpack_require__(141), + imageTile: __webpack_require__(142), + jsonReader: __webpack_require__(144), + layer: __webpack_require__(130), + lineFeature: __webpack_require__(145), + map: __webpack_require__(146), + mapInteractor: __webpack_require__(217), + object: __webpack_require__(5), + osmLayer: __webpack_require__(219), + pathFeature: __webpack_require__(222), + planeFeature: __webpack_require__(223), + pointFeature: __webpack_require__(225), + polygonFeature: __webpack_require__(224), + quadFeature: __webpack_require__(227), + renderer: __webpack_require__(136), + sceneObject: __webpack_require__(128), + tile: __webpack_require__(143), + tileCache: __webpack_require__(221), + tileLayer: __webpack_require__(220), + timestamp: __webpack_require__(129), + transform: __webpack_require__(147), + vectorFeature: __webpack_require__(228), + inherit: __webpack_require__(4), + version: __webpack_require__(229), + + util: __webpack_require__(120), + d3: __webpack_require__(230), + gl: __webpack_require__(242), + canvas: __webpack_require__(255), + gui: __webpack_require__(259) + }, __webpack_require__(131)); + + +/***/ }, +/* 1 */ +/***/ function(module, exports) { + + module.exports = __WEBPACK_EXTERNAL_MODULE_1__; + +/***/ }, +/* 2 */ +/***/ function(module, exports) { + + // Add a polyfill for window.requestAnimationFrame. + if (!window.requestAnimationFrame) { + var _animationFrameFunc = []; + if (!window.performance) { + window.performance = {now: function () { return new Date().getTime(); }}; + } + window.requestAnimationFrame = function (func) { + 'use strict'; + + if (!_animationFrameFunc.length) { + var time = window.performance.now(); + window.setTimeout(function () { + var funcs = _animationFrameFunc; + _animationFrameFunc = []; + var curtime = window.performance.now(); + for (var i = 0; i < funcs.length; i += 1) { + funcs[i].call(window, curtime); + } + }, 15 - (parseInt(time, 10) % 15)); + } + _animationFrameFunc.push(func); + }; + } + + // Add a polyfill for Math.log2 + if (!Math.log2) { + Math.log2 = function () { + return Math.log.apply(Math, arguments) / Math.LN2; + }; + } + + // Add a polyfill for Math.sinh + Math.sinh = Math.sinh || function (x) { + var y = Math.exp(x); + return (y - 1 / y) / 2; + }; + + + +/***/ }, +/* 3 */ +/***/ function(module, exports, __webpack_require__) { + + (function () { + 'use strict'; + + var inherit = __webpack_require__(4); + var object = __webpack_require__(5); + var util = __webpack_require__(120); + var mat4 = __webpack_require__(8); + var vec4 = __webpack_require__(32); + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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. + * + * For the moment, all camera transforms are assumed to be expressible as + * 4x4 matrices. More general cameras may follow that break this assumption. + * + * The interface for the camera is relatively stable for "map-like" views, + * e.g. 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. + * + * 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. There are a few primary methods that are intended 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) + * * viewFromCenterSizeRotation: set the camera view based on a center + * point, boundary size, and rotation angle. + * + * @class geo.camera + * @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} + */ + ////////////////////////////////////////////////////////////////////////////// + var camera = function (spec) { + if (!(this instanceof camera)) { + return new camera(spec); + } + + var geo_event = __webpack_require__(125); + + spec = spec || {}; + object.call(this, spec); + + /** + * The view matrix + * @protected + */ + this._view = util.mat4AsArray(); + + /** + * The projection matrix + * @protected + */ + this._proj = util.mat4AsArray(); + + /** + * The projection type (one of `this.constructor.projection`) + * @protected + */ + this._projection = null; + + /** + * The transform matrix (view * proj) + * @protected + */ + this._transform = util.mat4AsArray(); + + /** + * The inverse transform matrix (view * proj)^-1 + * @protected + */ + this._inverse = util.mat4AsArray(); + + /** + * 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 = 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 = 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 = 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( + util.mat4AsArray(), + 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([ + this._viewport.width / viewport.width, + this._viewport.height / viewport.height, + 1 + ]); + + // translate by half the difference to keep the center the same + this._translate([ + (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|Array} 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|Array} 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 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 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 ul, ur, ll, lr, bds = {}; + + // get corners + ul = this.displayToWorld({x: 0, y: 0}); + ur = this.displayToWorld({x: this._viewport.width, y: 0}); + ll = this.displayToWorld({x: 0, y: this._viewport.height}); + lr = this.displayToWorld({ + x: this._viewport.width, + y: this._viewport.height + }); + + bds.left = Math.min(ul.x, ur.x, ll.x, lr.x); + bds.bottom = Math.min(ul.y, ur.y, ll.y, lr.y); + bds.right = Math.max(ul.x, ur.x, ll.x, lr.x); + bds.top = Math.max(ul.y, ur.y, ll.y, lr.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 size = { + width: bounds.right - bounds.left, + height: bounds.top - bounds.bottom + }; + var center = { + x: (bounds.left + bounds.right) / 2, + y: (bounds.bottom + bounds.top) / 2 + }; + + this._viewFromCenterSizeRotation(center, size, 0); + return this; + }; + + /** + * Sets the view matrix so that the given world center is centered, at + * least a certain width and height are visible, and a rotation is applied. + * The resulting bounds may be larger in width or height than the values if + * the viewport is a different aspect ratio. + * + * @protected + * @param {object} center + * @param {number} center.x + * @param {number} center.y + * @param {object} size + * @param {number} size.width + * @param {number} size.height + * @param {number} rotation in clockwise radians. Optional + * @return {this} Chainable + */ + this._viewFromCenterSizeRotation = function (center, size, rotation) { + var translate = util.vec3AsArray(), + scale = util.vec3AsArray(), + c_ar, v_ar, w, h; + + // reset view to the identity + this._resetView(); + + w = Math.abs(size.width); + h = Math.abs(size.height); + 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); + + if (rotation) { + this._rotate(rotation); + } + + // translate to the new center. + translate[0] = -center.x; + translate[1] = -center.y; + translate[2] = 0; + + this._translate(translate); + + return this; + }; + + /** + * Public exposure of the viewFromCenterSizeRotation function. + */ + this.viewFromCenterSizeRotation = function (center, size, rotation) { + this._viewFromCenterSizeRotation(center, size, rotation); + this._update(); + 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) { + if (!offset.x && !offset.y && !offset.z) { + return; + } + this._translate([ + 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) { + if (zoom === 1) { + return; + } + mat4.scale(this._view, this._view, [ + zoom, + zoom, + zoom + ]); + this._update(); + }; + + /** + * Rotate the view matrix by the given amount. + * + * @param {number} rotation Counter-clockwise rotation angle in radians. + * @param {object} center Center of rotation in world space coordinates. + * @param {vec3} axis acis of rotation. Defaults to [0, 0, -1] + */ + this._rotate = function (rotation, center, axis) { + if (!rotation) { + return; + } + axis = axis || [0, 0, -1]; + if (!center) { + center = [0, 0, 0]; + } else if (center.x !== undefined) { + center = [center.x || 0, center.y || 0, center.z || 0]; + } + var invcenter = [-center[0], -center[1], -center[2]]; + mat4.translate(this._view, this._view, center); + mat4.rotate(this._view, this._view, rotation, axis); + mat4.translate(this._view, this._view, invcenter); + }; + + /** + * 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 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. + */ + camera.projection = { + perspective: true, + parallel: true + }; + + /** + * Camera clipping bounds, probably shouldn't be modified. + */ + 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 + */ + 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 + */ + camera.affine = function (pre, scale, post) { + var mat = util.mat4AsArray(); + + // 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} + */ + 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 + */ + camera.combine = function (A, B) { + return mat4.multiply(util.mat4AsArray(), A, B); + }; + + inherit(camera, object); + module.exports = camera; + })(); + + +/***/ }, +/* 4 */ +/***/ function(module, exports) { + + function newfunc() { + return function () {}; + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * Convenient function to define JS inheritance + */ + ////////////////////////////////////////////////////////////////////////////// + module.exports = function (C, P) { + var F = newfunc(); + F.prototype = P.prototype; + C.prototype = new F(); + C.prototype.constructor = C; + }; + + +/***/ }, +/* 5 */ +/***/ function(module, exports, __webpack_require__) { + + var vgl = __webpack_require__(6); + var inherit = __webpack_require__(4); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class object + * + * @class geo.object + * @extends vgl.object + * @returns {geo.object} + */ + ////////////////////////////////////////////////////////////////////////////// + var object = function () { + 'use strict'; + if (!(this instanceof object)) { + return new object(); + } + + var m_this = this, + m_eventHandlers = {}, + m_idleHandlers = [], + m_promiseCount = 0; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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_promiseCount) { + m_idleHandlers.push(handler); + } else { + handler(); + } + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Add a new promise object preventing idle event handlers from being called + * until it is resolved. + * + * @param {Promise} promise A promise object + */ + ////////////////////////////////////////////////////////////////////////////// + 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; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Bind an event handler to this object + * + * @param {String} event + * An event from {geo.events} + * @param {function} handler + * A function that will be called when ``event`` is triggered. The + * function will be given an event object as a first parameter and + * optionally a second argument provided by the triggerer. + */ + ////////////////////////////////////////////////////////////////////////////// + this.geoOn = function (event, handler) { + if (Array.isArray(event)) { + event.forEach(function (e) { + m_this.geoOn(e, handler); + }); + return m_this; + } + if (!m_eventHandlers.hasOwnProperty(event)) { + m_eventHandlers[event] = []; + } + m_eventHandlers[event].push(handler); + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Trigger an event (or events) on this object and call all handlers + * + * @param {String} event An event from {geo.event} + * @param {Object} args An optional argument to pass to handlers + */ + ////////////////////////////////////////////////////////////////////////////// + this.geoTrigger = function (event, args) { + + // if we have an array of events, recall with single events + if (Array.isArray(event)) { + event.forEach(function (e) { + m_this.geoTrigger(e, args); + }); + 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); + }); + } + + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Remove handlers from an event (or an array of events). If no event is + * provided all hanlders will be removed. + * + * @param {string?} event An event from {geo.events} + * @param {object?} arg A function or array of functions to remove from the events + * or if falsey remove all handlers from the events + */ + ////////////////////////////////////////////////////////////////////////////// + this.geoOff = function (event, arg) { + if (event === undefined) { + m_eventHandlers = {}; + m_idleHandlers = []; + m_promiseCount = 0; + } + if (Array.isArray(event)) { + event.forEach(function (e) { + m_this.geoOff(e, arg); + }); + return m_this; + } + if (!arg) { + m_eventHandlers[event] = []; + } else if (Array.isArray(arg)) { + arg.forEach(function (handler) { + m_this.geoOff(event, handler); + }); + return m_this; + } + // What do we do if the handler is not already bound? + // ignoring for now... + if (m_eventHandlers.hasOwnProperty(event)) { + m_eventHandlers[event] = m_eventHandlers[event].filter(function (f) { + return f !== arg; + } + ); + } + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Free all resources and destroy the object. + */ + ////////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.geoOff(); + }; + + vgl.object.call(this); + + return this; + }; + + inherit(object, vgl.object); + module.exports = object; + + +/***/ }, +/* 6 */ +/***/ function(module, exports, __webpack_require__) { + + /* WEBPACK VAR INJECTION */(function(global) {module.exports = global["vgl"] = __webpack_require__(7); + /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) + +/***/ }, +/* 7 */ +/***/ function(module, exports, __webpack_require__) { + + var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*** IMPORTS FROM imports-loader ***/ + var mat4 = __webpack_require__(8); + var vec4 = __webpack_require__(32); + var vec3 = __webpack_require__(58); + var vec2 = __webpack_require__(91); + var $ = __webpack_require__(1); + + (function (root, factory) { + if (true) { + // AMD. Register as an anonymous module unless amdModuleId is set + !(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_RESULT__ = function () { + return (root['vgl'] = factory()); + }.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + root['vgl'] = factory(); + } + }(this, function () { + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl: true, ogs: true, inherit*/ + /*exported vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + if (typeof ogs === 'undefined') { + var ogs = {}; + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create namespace for the given name + * + * @param ns_string + * @returns {*|{}} + */ + ////////////////////////////////////////////////////////////////////////////// + ogs.namespace = function (ns_string) { + 'use strict'; + + var parts = ns_string.split('.'), parent = ogs, i; + + // strip redundant leading global + if (parts[0] === 'ogs') { + parts = parts.slice(1); + } + for (i = 0; i < parts.length; i += 1) { + // create a property if it doesn't exist + if (typeof parent[parts[i]] === 'undefined') { + parent[parts[i]] = {}; + } + parent = parent[parts[i]]; + } + return parent; + }; + + /** vgl namespace */ + var vgl = ogs.namespace('gl'); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Convenient function to define JS inheritance + * + * @param C + * @param P + */ + ////////////////////////////////////////////////////////////////////////////// + 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; + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * Convenient function to get size of an object + * + * @param obj + * @returns {number} * + */ + ////////////////////////////////////////////////////////////////////////////// + Object.size = function (obj) { + 'use strict'; + + var size = 0, key = null; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + size += 1; + } + } + return size; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Wrap GL enums. Currently to get values of the enums we need to create + * or access the context. + * + * Using enums from here: + * https://github.com/toji/dart-gl-enums/blob/master/lib/gl_enums.dart + * + * @class + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.GL = { + ACTIVE_ATTRIBUTES : 0x8B89, + ACTIVE_TEXTURE : 0x84E0, + ACTIVE_UNIFORMS : 0x8B86, + ALIASED_LINE_WIDTH_RANGE : 0x846E, + ALIASED_POINT_SIZE_RANGE : 0x846D, + ALPHA : 0x1906, + ALPHA_BITS : 0x0D55, + ALWAYS : 0x0207, + ARRAY_BUFFER : 0x8892, + ARRAY_BUFFER_BINDING : 0x8894, + ATTACHED_SHADERS : 0x8B85, + BACK : 0x0405, + BLEND : 0x0BE2, + BLEND_COLOR : 0x8005, + BLEND_DST_ALPHA : 0x80CA, + BLEND_DST_RGB : 0x80C8, + BLEND_EQUATION : 0x8009, + BLEND_EQUATION_ALPHA : 0x883D, + BLEND_EQUATION_RGB : 0x8009, + BLEND_SRC_ALPHA : 0x80CB, + BLEND_SRC_RGB : 0x80C9, + BLUE_BITS : 0x0D54, + BOOL : 0x8B56, + BOOL_VEC2 : 0x8B57, + BOOL_VEC3 : 0x8B58, + BOOL_VEC4 : 0x8B59, + BROWSER_DEFAULT_WEBGL : 0x9244, + BUFFER_SIZE : 0x8764, + BUFFER_USAGE : 0x8765, + BYTE : 0x1400, + CCW : 0x0901, + CLAMP_TO_EDGE : 0x812F, + COLOR_ATTACHMENT0 : 0x8CE0, + COLOR_BUFFER_BIT : 0x00004000, + COLOR_CLEAR_VALUE : 0x0C22, + COLOR_WRITEMASK : 0x0C23, + COMPILE_STATUS : 0x8B81, + COMPRESSED_TEXTURE_FORMATS : 0x86A3, + CONSTANT_ALPHA : 0x8003, + CONSTANT_COLOR : 0x8001, + CONTEXT_LOST_WEBGL : 0x9242, + CULL_FACE : 0x0B44, + CULL_FACE_MODE : 0x0B45, + CURRENT_PROGRAM : 0x8B8D, + CURRENT_VERTEX_ATTRIB : 0x8626, + CW : 0x0900, + DECR : 0x1E03, + DECR_WRAP : 0x8508, + DELETE_STATUS : 0x8B80, + DEPTH_ATTACHMENT : 0x8D00, + DEPTH_BITS : 0x0D56, + DEPTH_BUFFER_BIT : 0x00000100, + DEPTH_CLEAR_VALUE : 0x0B73, + DEPTH_COMPONENT : 0x1902, + DEPTH_COMPONENT16 : 0x81A5, + DEPTH_FUNC : 0x0B74, + DEPTH_RANGE : 0x0B70, + DEPTH_STENCIL : 0x84F9, + DEPTH_STENCIL_ATTACHMENT : 0x821A, + DEPTH_TEST : 0x0B71, + DEPTH_WRITEMASK : 0x0B72, + DITHER : 0x0BD0, + DONT_CARE : 0x1100, + DST_ALPHA : 0x0304, + DST_COLOR : 0x0306, + DYNAMIC_DRAW : 0x88E8, + ELEMENT_ARRAY_BUFFER : 0x8893, + ELEMENT_ARRAY_BUFFER_BINDING : 0x8895, + EQUAL : 0x0202, + FASTEST : 0x1101, + FLOAT : 0x1406, + FLOAT_MAT2 : 0x8B5A, + FLOAT_MAT3 : 0x8B5B, + FLOAT_MAT4 : 0x8B5C, + FLOAT_VEC2 : 0x8B50, + FLOAT_VEC3 : 0x8B51, + FLOAT_VEC4 : 0x8B52, + FRAGMENT_SHADER : 0x8B30, + FRAMEBUFFER : 0x8D40, + FRAMEBUFFER_ATTACHMENT_OBJECT_NAME : 0x8CD1, + FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE : 0x8CD0, + FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE : 0x8CD3, + FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL : 0x8CD2, + FRAMEBUFFER_BINDING : 0x8CA6, + FRAMEBUFFER_COMPLETE : 0x8CD5, + FRAMEBUFFER_INCOMPLETE_ATTACHMENT : 0x8CD6, + FRAMEBUFFER_INCOMPLETE_DIMENSIONS : 0x8CD9, + FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT : 0x8CD7, + FRAMEBUFFER_UNSUPPORTED : 0x8CDD, + FRONT : 0x0404, + FRONT_AND_BACK : 0x0408, + FRONT_FACE : 0x0B46, + FUNC_ADD : 0x8006, + FUNC_REVERSE_SUBTRACT : 0x800B, + FUNC_SUBTRACT : 0x800A, + GENERATE_MIPMAP_HINT : 0x8192, + GEQUAL : 0x0206, + GREATER : 0x0204, + GREEN_BITS : 0x0D53, + HIGH_FLOAT : 0x8DF2, + HIGH_INT : 0x8DF5, + INCR : 0x1E02, + INCR_WRAP : 0x8507, + INT : 0x1404, + INT_VEC2 : 0x8B53, + INT_VEC3 : 0x8B54, + INT_VEC4 : 0x8B55, + INVALID_ENUM : 0x0500, + INVALID_FRAMEBUFFER_OPERATION : 0x0506, + INVALID_OPERATION : 0x0502, + INVALID_VALUE : 0x0501, + INVERT : 0x150A, + KEEP : 0x1E00, + LEQUAL : 0x0203, + LESS : 0x0201, + LINEAR : 0x2601, + LINEAR_MIPMAP_LINEAR : 0x2703, + LINEAR_MIPMAP_NEAREST : 0x2701, + LINES : 0x0001, + LINE_LOOP : 0x0002, + LINE_STRIP : 0x0003, + LINE_WIDTH : 0x0B21, + LINK_STATUS : 0x8B82, + LOW_FLOAT : 0x8DF0, + LOW_INT : 0x8DF3, + LUMINANCE : 0x1909, + LUMINANCE_ALPHA : 0x190A, + MAX_COMBINED_TEXTURE_IMAGE_UNITS : 0x8B4D, + MAX_CUBE_MAP_TEXTURE_SIZE : 0x851C, + MAX_FRAGMENT_UNIFORM_VECTORS : 0x8DFD, + MAX_RENDERBUFFER_SIZE : 0x84E8, + MAX_TEXTURE_IMAGE_UNITS : 0x8872, + MAX_TEXTURE_SIZE : 0x0D33, + MAX_VARYING_VECTORS : 0x8DFC, + MAX_VERTEX_ATTRIBS : 0x8869, + MAX_VERTEX_TEXTURE_IMAGE_UNITS : 0x8B4C, + MAX_VERTEX_UNIFORM_VECTORS : 0x8DFB, + MAX_VIEWPORT_DIMS : 0x0D3A, + MEDIUM_FLOAT : 0x8DF1, + MEDIUM_INT : 0x8DF4, + MIRRORED_REPEAT : 0x8370, + NEAREST : 0x2600, + NEAREST_MIPMAP_LINEAR : 0x2702, + NEAREST_MIPMAP_NEAREST : 0x2700, + NEVER : 0x0200, + NICEST : 0x1102, + NONE : 0, + NOTEQUAL : 0x0205, + NO_ERROR : 0, + ONE : 1, + ONE_MINUS_CONSTANT_ALPHA : 0x8004, + ONE_MINUS_CONSTANT_COLOR : 0x8002, + ONE_MINUS_DST_ALPHA : 0x0305, + ONE_MINUS_DST_COLOR : 0x0307, + ONE_MINUS_SRC_ALPHA : 0x0303, + ONE_MINUS_SRC_COLOR : 0x0301, + OUT_OF_MEMORY : 0x0505, + PACK_ALIGNMENT : 0x0D05, + POINTS : 0x0000, + POLYGON_OFFSET_FACTOR : 0x8038, + POLYGON_OFFSET_FILL : 0x8037, + POLYGON_OFFSET_UNITS : 0x2A00, + RED_BITS : 0x0D52, + RENDERBUFFER : 0x8D41, + RENDERBUFFER_ALPHA_SIZE : 0x8D53, + RENDERBUFFER_BINDING : 0x8CA7, + RENDERBUFFER_BLUE_SIZE : 0x8D52, + RENDERBUFFER_DEPTH_SIZE : 0x8D54, + RENDERBUFFER_GREEN_SIZE : 0x8D51, + RENDERBUFFER_HEIGHT : 0x8D43, + RENDERBUFFER_INTERNAL_FORMAT : 0x8D44, + RENDERBUFFER_RED_SIZE : 0x8D50, + RENDERBUFFER_STENCIL_SIZE : 0x8D55, + RENDERBUFFER_WIDTH : 0x8D42, + RENDERER : 0x1F01, + REPEAT : 0x2901, + REPLACE : 0x1E01, + RGB : 0x1907, + RGB565 : 0x8D62, + RGB5_A1 : 0x8057, + RGBA : 0x1908, + RGBA4 : 0x8056, + SAMPLER_2D : 0x8B5E, + SAMPLER_CUBE : 0x8B60, + SAMPLES : 0x80A9, + SAMPLE_ALPHA_TO_COVERAGE : 0x809E, + SAMPLE_BUFFERS : 0x80A8, + SAMPLE_COVERAGE : 0x80A0, + SAMPLE_COVERAGE_INVERT : 0x80AB, + SAMPLE_COVERAGE_VALUE : 0x80AA, + SCISSOR_BOX : 0x0C10, + SCISSOR_TEST : 0x0C11, + SHADER_TYPE : 0x8B4F, + SHADING_LANGUAGE_VERSION : 0x8B8C, + SHORT : 0x1402, + SRC_ALPHA : 0x0302, + SRC_ALPHA_SATURATE : 0x0308, + SRC_COLOR : 0x0300, + STATIC_DRAW : 0x88E4, + STENCIL_ATTACHMENT : 0x8D20, + STENCIL_BACK_FAIL : 0x8801, + STENCIL_BACK_FUNC : 0x8800, + STENCIL_BACK_PASS_DEPTH_FAIL : 0x8802, + STENCIL_BACK_PASS_DEPTH_PASS : 0x8803, + STENCIL_BACK_REF : 0x8CA3, + STENCIL_BACK_VALUE_MASK : 0x8CA4, + STENCIL_BACK_WRITEMASK : 0x8CA5, + STENCIL_BITS : 0x0D57, + STENCIL_BUFFER_BIT : 0x00000400, + STENCIL_CLEAR_VALUE : 0x0B91, + STENCIL_FAIL : 0x0B94, + STENCIL_FUNC : 0x0B92, + STENCIL_INDEX : 0x1901, + STENCIL_INDEX8 : 0x8D48, + STENCIL_PASS_DEPTH_FAIL : 0x0B95, + STENCIL_PASS_DEPTH_PASS : 0x0B96, + STENCIL_REF : 0x0B97, + STENCIL_TEST : 0x0B90, + STENCIL_VALUE_MASK : 0x0B93, + STENCIL_WRITEMASK : 0x0B98, + STREAM_DRAW : 0x88E0, + SUBPIXEL_BITS : 0x0D50, + TEXTURE : 0x1702, + TEXTURE0 : 0x84C0, + TEXTURE1 : 0x84C1, + TEXTURE10 : 0x84CA, + TEXTURE11 : 0x84CB, + TEXTURE12 : 0x84CC, + TEXTURE13 : 0x84CD, + TEXTURE14 : 0x84CE, + TEXTURE15 : 0x84CF, + TEXTURE16 : 0x84D0, + TEXTURE17 : 0x84D1, + TEXTURE18 : 0x84D2, + TEXTURE19 : 0x84D3, + TEXTURE2 : 0x84C2, + TEXTURE20 : 0x84D4, + TEXTURE21 : 0x84D5, + TEXTURE22 : 0x84D6, + TEXTURE23 : 0x84D7, + TEXTURE24 : 0x84D8, + TEXTURE25 : 0x84D9, + TEXTURE26 : 0x84DA, + TEXTURE27 : 0x84DB, + TEXTURE28 : 0x84DC, + TEXTURE29 : 0x84DD, + TEXTURE3 : 0x84C3, + TEXTURE30 : 0x84DE, + TEXTURE31 : 0x84DF, + TEXTURE4 : 0x84C4, + TEXTURE5 : 0x84C5, + TEXTURE6 : 0x84C6, + TEXTURE7 : 0x84C7, + TEXTURE8 : 0x84C8, + TEXTURE9 : 0x84C9, + TEXTURE_2D : 0x0DE1, + TEXTURE_BINDING_2D : 0x8069, + TEXTURE_BINDING_CUBE_MAP : 0x8514, + TEXTURE_CUBE_MAP : 0x8513, + TEXTURE_CUBE_MAP_NEGATIVE_X : 0x8516, + TEXTURE_CUBE_MAP_NEGATIVE_Y : 0x8518, + TEXTURE_CUBE_MAP_NEGATIVE_Z : 0x851A, + TEXTURE_CUBE_MAP_POSITIVE_X : 0x8515, + TEXTURE_CUBE_MAP_POSITIVE_Y : 0x8517, + TEXTURE_CUBE_MAP_POSITIVE_Z : 0x8519, + TEXTURE_MAG_FILTER : 0x2800, + TEXTURE_MIN_FILTER : 0x2801, + TEXTURE_WRAP_S : 0x2802, + TEXTURE_WRAP_T : 0x2803, + TRIANGLES : 0x0004, + TRIANGLE_FAN : 0x0006, + TRIANGLE_STRIP : 0x0005, + UNPACK_ALIGNMENT : 0x0CF5, + UNPACK_COLORSPACE_CONVERSION_WEBGL : 0x9243, + UNPACK_FLIP_Y_WEBGL : 0x9240, + UNPACK_PREMULTIPLY_ALPHA_WEBGL : 0x9241, + UNSIGNED_BYTE : 0x1401, + UNSIGNED_INT : 0x1405, + UNSIGNED_SHORT : 0x1403, + UNSIGNED_SHORT_4_4_4_4 : 0x8033, + UNSIGNED_SHORT_5_5_5_1 : 0x8034, + UNSIGNED_SHORT_5_6_5 : 0x8363, + VALIDATE_STATUS : 0x8B83, + VENDOR : 0x1F00, + VERSION : 0x1F02, + VERTEX_ATTRIB_ARRAY_BUFFER_BINDING : 0x889F, + VERTEX_ATTRIB_ARRAY_ENABLED : 0x8622, + VERTEX_ATTRIB_ARRAY_NORMALIZED : 0x886A, + VERTEX_ATTRIB_ARRAY_POINTER : 0x8645, + VERTEX_ATTRIB_ARRAY_SIZE : 0x8623, + VERTEX_ATTRIB_ARRAY_STRIDE : 0x8624, + VERTEX_ATTRIB_ARRAY_TYPE : 0x8625, + VERTEX_SHADER : 0x8B31, + VIEWPORT : 0x0BA2, + ZERO : 0 + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class timestamp + * + * @class + * @returns {vgl.timestamp} + */ + ////////////////////////////////////////////////////////////////////////////// + var m_globalModifiedTime = 0; + + vgl.timestamp = function () { + 'use strict'; + + if (!(this instanceof vgl.timestamp)) { + return new vgl.timestamp(); + } + + var m_modifiedTime = 0; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Update modified time + */ + ///////////////////////////////////////////////////////////////////////////// + this.modified = function () { + m_globalModifiedTime += 1; + m_modifiedTime = m_globalModifiedTime; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get modified time + * + * @returns {number} + */ + ///////////////////////////////////////////////////////////////////////////// + this.getMTime = function () { + return m_modifiedTime; + }; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class object + * + * @class + * @returns {vgl.object} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.object = function () { + 'use strict'; + + if (!(this instanceof vgl.object)) { + return new vgl.object(); + } + + /** @private */ + var m_modifiedTime = vgl.timestamp(); + m_modifiedTime.modified(); + + //////////////////////////////////////////////////////////////////////////// + /** + * Mark the object modified + */ + //////////////////////////////////////////////////////////////////////////// + this.modified = function () { + m_modifiedTime.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return modified time of the object + * + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.getMTime = function () { + return m_modifiedTime.getMTime(); + }; + + return this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class event + * + * @class event + * @returns {vgl.event} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.event = function () { + 'use strict'; + + if (!(this instanceof vgl.event)) { + return new vgl.event(); + } + vgl.object.call(this); + + return this; + }; + + inherit(vgl.event, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * types + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.event.keyPress = 'vgl.event.keyPress'; + vgl.event.mousePress = 'vgl.event.mousePress'; + vgl.event.mouseRelease = 'vgl.event.mouseRelease'; + vgl.event.contextMenu = 'vgl.event.contextMenu'; + vgl.event.configure = 'vgl.event.configure'; + vgl.event.enable = 'vgl.event.enable'; + vgl.event.mouseWheel = 'vgl.event.mouseWheel'; + vgl.event.keyRelease = 'vgl.event.keyRelease'; + vgl.event.middleButtonPress = 'vgl.event.middleButtonPress'; + vgl.event.startInteraction = 'vgl.event.startInteraction'; + vgl.event.enter = 'vgl.event.enter'; + vgl.event.rightButtonPress = 'vgl.event.rightButtonPress'; + vgl.event.middleButtonRelease = 'vgl.event.middleButtonRelease'; + vgl.event.char = 'vgl.event.char'; + vgl.event.disable = 'vgl.event.disable'; + vgl.event.endInteraction = 'vgl.event.endInteraction'; + vgl.event.mouseMove = 'vgl.event.mouseMove'; + vgl.event.mouseOut = 'vgl.event.mouseOut'; + vgl.event.expose = 'vgl.event.expose'; + vgl.event.timer = 'vgl.event.timer'; + vgl.event.leftButtonPress = 'vgl.event.leftButtonPress'; + vgl.event.leave = 'vgl.event.leave'; + vgl.event.rightButtonRelease = 'vgl.event.rightButtonRelease'; + vgl.event.leftButtonRelease = 'vgl.event.leftButtonRelease'; + vgl.event.click = 'vgl.event.click'; + vgl.event.dblClick = 'vgl.event.dblClick'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class boundingObject + * + * @class + * @return {vgl.boundingObject} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.boundingObject = function () { + 'use strict'; + + if (!(this instanceof vgl.boundingObject)) { + return new vgl.boundingObject(); + } + vgl.object.call(this); + + /** @private */ + var m_bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + m_computeBoundsTimestamp = vgl.timestamp(), + m_boundsDirtyTimestamp = vgl.timestamp(); + + m_computeBoundsTimestamp.modified(); + m_boundsDirtyTimestamp.modified(); + + //////////////////////////////////////////////////////////////////////////// + /** + * Get current bounds of the object + */ + //////////////////////////////////////////////////////////////////////////// + this.bounds = function () { + return m_bounds; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if bounds are valid + */ + //////////////////////////////////////////////////////////////////////////// + this.hasValidBounds = function (bounds) { + if (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) { + return false; + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set current bounds of the object + */ + //////////////////////////////////////////////////////////////////////////// + this.setBounds = function (minX, maxX, minY, maxY, minZ, maxZ) { + if (!this.hasValidBounds([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; + + this.modified(); + m_computeBoundsTimestamp.modified(); + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Reset bounds to default values + */ + //////////////////////////////////////////////////////////////////////////// + this.resetBounds = function () { + m_bounds[0] = Number.MAX_VALUE; + m_bounds[1] = -Number.MAX_VALUE; + m_bounds[2] = Number.MAX_VALUE; + m_bounds[3] = -Number.MAX_VALUE; + m_bounds[4] = Number.MAX_VALUE; + m_bounds[5] = -Number.MAX_VALUE; + + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute bounds of the object + * + * Should be implemented by the concrete class + */ + //////////////////////////////////////////////////////////////////////////// + this.computeBounds = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return bounds computation modification time + * + * @returns {vgl.timestamp} + */ + //////////////////////////////////////////////////////////////////////////// + this.computeBoundsTimestamp = function () { + return m_computeBoundsTimestamp; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return bounds dirty timestamp + * + * @returns {vgl.timestamp} + */ + //////////////////////////////////////////////////////////////////////////// + this.boundsDirtyTimestamp = function () { + return m_boundsDirtyTimestamp; + }; + + this.resetBounds(); + + return this; + }; + + vgl.boundingObject.ReferenceFrame = { + 'Relative' : 0, + 'Absolute' : 1 + }; + + inherit(vgl.boundingObject, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class node + * + * @class + * @returns {vgl.node} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.node = function () { + 'use strict'; + + if (!(this instanceof vgl.node)) { + return new vgl.node(); + } + vgl.boundingObject.call(this); + + /** @private */ + var m_parent = null, + m_material = null, + m_visible = true, + m_overlay = false; + + //////////////////////////////////////////////////////////////////////////// + /** + * Accept visitor for scene traversal + */ + //////////////////////////////////////////////////////////////////////////// + this.accept = function (visitor) { + visitor.visit(this); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return active material used by the node + */ + //////////////////////////////////////////////////////////////////////////// + this.material = function () { + return m_material; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set material to be used the node + * + * @param material + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setMaterial = function (material) { + if (material !== m_material) { + m_material = material; + this.modified(); + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if the node is visible or node + * + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.visible = function () { + return m_visible; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Turn ON/OFF visibility of the node + * + * @param flag + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setVisible = function (flag) { + if (flag !== m_visible) { + m_visible = flag; + this.modified(); + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return current parent of the node + * + * @returns {null} + */ + //////////////////////////////////////////////////////////////////////////// + this.parent = function () { + return m_parent; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set parent of the node + * + * @param parent + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setParent = function (parent) { + if (parent !== m_parent) { + if (m_parent !== null) { + m_parent.removeChild(this); + } + m_parent = parent; + this.modified(); + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if the node is an overlay node + * + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.overlay = function () { + return m_overlay; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set if the node is an overlay node or not + * + * @param flag + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setOverlay = function (flag) { + if (m_overlay !== flag) { + m_overlay = flag; + this.modified(); + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /* + * Traverse parent and their parent and so on + */ + //////////////////////////////////////////////////////////////////////////// + this.ascend = function (visitor) { + visitor = visitor; /* unused parameter */ + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Traverse children + */ + //////////////////////////////////////////////////////////////////////////// + this.traverse = function (visitor) { + visitor = visitor; /* unused parameter */ + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Mark that the bounds are modified + * + */ + //////////////////////////////////////////////////////////////////////////// + this.boundsModified = function () { + // @todo Implement this + this.boundsDirtyTimestamp().modified(); + + if (m_parent !== null) { + m_parent.boundsModified(); + } + }; + + return this; + }; + + inherit(vgl.node, vgl.boundingObject); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class groupNode + * + * @class + * @returns {vgl.groupNode} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.groupNode = function () { + 'use strict'; + + if (!(this instanceof vgl.groupNode)) { + return new vgl.groupNode(); + } + vgl.node.call(this); + + var m_children = []; + + // Reference to base class methods + this.b_setVisible = this.setVisible; + + //////////////////////////////////////////////////////////////////////////// + /** + * Turn on / off visibility + * + * @param flag + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setVisible = function (flag) { + var i; + + if (this.b_setVisible(flag) !== true) { + return false; + } + + for (i = 0; i < m_children.length; i += 1) { + m_children[i].setVisible(flag); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Make the incoming node as child of the group node + * + * @param childNode + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.addChild = function (childNode) { + if (childNode instanceof vgl.node) { + if (m_children.indexOf(childNode) === -1) { + childNode.setParent(this); + m_children.push(childNode); + this.boundsDirtyTimestamp().modified(); + return true; + } + return false; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove parent-child relationship between the group and incoming node + * + * @param childNode + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.removeChild = function (childNode) { + if (childNode.parent() === this) { + var index = m_children.indexOf(childNode); + m_children.splice(index, 1); + this.boundsDirtyTimestamp().modified(); + return true; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove parent-child relationship between child nodes and the group node + */ + //////////////////////////////////////////////////////////////////////////// + this.removeChildren = function () { + var i; + for (i = 0; i < m_children.length; i += 1) { + this.removeChild(m_children[i]); + } + + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return children of this group node + * + * @returns {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.children = function () { + return m_children; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return true if this group node has node as a child, false otherwise. + * + * @param node + * @returns {bool} + */ + //////////////////////////////////////////////////////////////////////////// + this.hasChild = function (node) { + var i = 0, child = false; + + for (i = 0; i < m_children.length; i += 1) { + if (m_children[i] === node) { + child = true; + break; + } + } + + return child; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Accept a visitor and traverse the scene tree + * + * @param visitor + */ + //////////////////////////////////////////////////////////////////////////// + this.accept = function (visitor) { + visitor.visit(this); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Traverse the scene + * + * @param visitor + */ + //////////////////////////////////////////////////////////////////////////// + this.traverse = function (visitor) { + switch (visitor.type()) { + case visitor.UpdateVisitor: + this.traverseChildrenAndUpdateBounds(visitor); + break; + case visitor.CullVisitor: + this.traverseChildren(visitor); + break; + default: + break; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Traverse all of the children and update the bounds for each + * + * @param visitor + */ + //////////////////////////////////////////////////////////////////////////// + this.traverseChildrenAndUpdateBounds = function (visitor) { + var i; + + if (this.m_parent && this.boundsDirtyTimestamp().getMTime() > + this.computeBoundsTimestamp().getMTime()) { + // Flag parents bounds dirty. + this.m_parent.boundsDirtyTimestamp.modified(); + } + + this.computeBounds(); + + if (visitor.mode() === visitor.TraverseAllChildren) { + for (i = 0; i < m_children.length(); i += 1) { + m_children[i].accept(visitor); + this.updateBounds(m_children[i]); + } + } + + this.computeBoundsTimestamp().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Traverse children of the group node + * + * @param visitor + */ + //////////////////////////////////////////////////////////////////////////// + this.traverseChildren = function (visitor) { + var i; + + if (visitor.mode() === vgl.vesVisitor.TraverseAllChildren) { + for (i = 0; i < m_children.length(); i += 1) { + m_children[i].accept(visitor); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute bounds for the group node + */ + //////////////////////////////////////////////////////////////////////////// + this.computeBounds = function () { + var i = 0; + + if (this.computeBoundsTimestamp().getMTime() > + this.boundsDirtyTimestamp().getMTime()) { + return; + } + + for (i = 0; i < m_children.length; i += 1) { + this.updateBounds(m_children[i]); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update bounds for the group node + * + * This method is used internally to update bounds of the group node by + * traversing each of its child. + * + * @param child + */ + //////////////////////////////////////////////////////////////////////////// + this.updateBounds = function (child) { + // FIXME: This check should not be required and possibly is incorrect + if (child.overlay()) { + return; + } + + // Make sure that child bounds are upto date + child.computeBounds(); + + var bounds = this.bounds(), + childBounds = child.bounds(), + istep = 0, + jstep = 0, + i; + + for (i = 0; i < 3; i += 1) { + istep = i * 2; + jstep = i * 2 + 1; + if (childBounds[istep] < bounds[istep]) { + bounds[istep] = childBounds[istep]; + } + if (childBounds[jstep] > bounds[jstep]) { + bounds[jstep] = childBounds[jstep]; + } + } + + this.setBounds(bounds[0], bounds[1], bounds[2], bounds[3], + bounds[4], bounds[5]); + }; + + return this; + }; + + inherit(vgl.groupNode, vgl.node); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, vec3, mat4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class actor + * + * @class + * @returns {vgl.actor} + */ + //////////////////////////////////////////////////////////////////////////// + vgl.actor = function () { + 'use strict'; + + if (!(this instanceof vgl.actor)) { + return new vgl.actor(); + } + vgl.node.call(this); + + /** @private */ + var m_this = this, + m_transformMatrix = mat4.create(), + m_referenceFrame = vgl.boundingObject.ReferenceFrame.Relative, + m_mapper = null; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get transformation matrix used by the actor + * + * @returns {mat4} + */ + //////////////////////////////////////////////////////////////////////////// + this.matrix = function () { + return m_transformMatrix; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set transformation matrix for the actor + * + * @param {mat4} 4X4 transformation matrix + */ + //////////////////////////////////////////////////////////////////////////// + this.setMatrix = function (tmatrix) { + if (tmatrix !== m_transformMatrix) { + m_transformMatrix = tmatrix; + m_this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get reference frame for the transformations + * + * @returns {String} Possible values are Absolute or Relative + */ + //////////////////////////////////////////////////////////////////////////// + this.referenceFrame = function () { + return m_referenceFrame; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set reference frame for the transformations + * + * @param {vgl.boundingObject.ReferenceFrame} + * referenceFrame Possible values are (Absolute | Relative) + */ + //////////////////////////////////////////////////////////////////////////// + this.setReferenceFrame = function (referenceFrame) { + if (referenceFrame !== m_referenceFrame) { + m_referenceFrame = referenceFrame; + m_this.modified(); + return true; + } + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return mapper where actor gets it behavior and data + * + * @returns {vgl.mapper} + */ + //////////////////////////////////////////////////////////////////////////// + this.mapper = function () { + return m_mapper; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Connect an actor to its data source + * + * @param {vgl.mapper} + */ + //////////////////////////////////////////////////////////////////////////// + this.setMapper = function (mapper) { + if (mapper !== m_mapper) { + m_mapper = mapper; + m_this.boundsModified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * @todo + */ + //////////////////////////////////////////////////////////////////////////// + this.accept = function (visitor) { + visitor = visitor; /* ignore this parameter */ + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * @todo + */ + //////////////////////////////////////////////////////////////////////////// + this.ascend = function (visitor) { + visitor = visitor; /* ignore this parameter */ + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute object space to world space matrix + * @todo + */ + //////////////////////////////////////////////////////////////////////////// + this.computeLocalToWorldMatrix = function (matrix, visitor) { + matrix = matrix; /* ignore this parameter */ + visitor = visitor; /* ignore this parameter */ + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute world space to object space matrix + * @todo + */ + //////////////////////////////////////////////////////////////////////////// + this.computeWorldToLocalMatrix = function (matrix, visitor) { + matrix = matrix; /* ignore this parameter */ + visitor = visitor; /* ignore this parameter */ + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute actor bounds + */ + //////////////////////////////////////////////////////////////////////////// + this.computeBounds = function () { + if (m_mapper === null || m_mapper === undefined) { + m_this.resetBounds(); + return; + } + + var computeBoundsTimestamp = m_this.computeBoundsTimestamp(), + mapperBounds, minPt, maxPt, newBounds; + + if (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(); + } + }; + + return m_this; + }; + + inherit(vgl.actor, vgl.node); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Freeze javascript object + * + * @param obj + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.freezeObject = function (obj) { + 'use strict'; + + /** + * Freezes an object, using Object.freeze if available, otherwise returns + * the object unchanged. This function should be used in setup code to prevent + * errors from completely halting JavaScript execution in legacy browsers. + * + * @exports freezeObject + */ + var freezedObject = Object.freeze(obj); + if (typeof freezedObject === 'undefined') { + freezedObject = function (o) { + return o; + }; + } + + return freezedObject; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Returns the first parameter if not undefined, + * otherwise the second parameter. + * + * @class + * @returns {vgl.defaultValue} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.defaultValue = function (a, b) { + 'use strict'; + + if (typeof a !== 'undefined') { + return a; + } + return b; + }; + + vgl.defaultValue.EMPTY_OBJECT = vgl.freezeObject({}); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class graphicsObject + * + * @class + * @param type + * @returns {vgl.graphicsObject} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.graphicsObject = function (type) { + 'use strict'; + + type = type; /* unused parameter */ + if (!(this instanceof vgl.graphicsObject)) { + return new vgl.graphicsObject(); + } + vgl.object.call(this); + + var m_this = this; + + //////////////////////////////////////////////////////////////////////////// + /** + * Setup (initialize) the object + * + * @param renderState + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this._setup = function (renderState) { + renderState = renderState; /* unused parameter */ + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove any resources acquired before deletion + * + * @param renderState + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this._cleanup = function (renderState) { + renderState = renderState; /* unused parameter */ + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bind and activate + * + * @param renderState + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.bind = function (renderState) { + renderState = renderState; /* unused parameter */ + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Undo bind and deactivate + * + * @param renderState + * @returns {boolean} + * + * TODO: Change it to unbind (simple) + */ + //////////////////////////////////////////////////////////////////////////// + this.undoBind = function (renderState) { + renderState = renderState; /* unused parameter */ + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render the object + */ + //////////////////////////////////////////////////////////////////////////// + this.render = function (renderState) { + renderState = renderState; /* unused parameter */ + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove the object and release its graphics resources + */ + //////////////////////////////////////////////////////////////////////////// + this.remove = function (renderState) { + m_this._cleanup(renderState); + }; + + return m_this; + }; + + inherit(vgl.graphicsObject, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, Uint16Array*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of geojson reader + * + * This contains code that reads a geoJSON file and produces rendering + * primitives from it. + * + * @class + * @returns {vgl.geojsonReader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.geojsonReader = function () { + 'use strict'; + + if (!(this instanceof vgl.geojsonReader)) { + return new vgl.geojsonReader(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Read scalars + * + * @param coordinates + * @param geom + * @param size_estimate + * @param idx + */ + //////////////////////////////////////////////////////////////////////////// + this.readScalars = function (coordinates, geom, size_estimate, idx) { + var array = null, + s = null, + r = null, + g = null, + b = null; + + if (this.m_scalarFormat === 'values' && coordinates.length === 4) { + s = coordinates[3]; + array = geom.sourceData(vgl.vertexAttributeKeys.Scalar); + + if (!array) { + array = new vgl.sourceDataSf(); + if (this.m_scalarRange) { + array.setScalarRange(this.m_scalarRange[0], this.m_scalarRange[1]); + } + if (size_estimate !== undefined) { + //array.length = size_estimate; //no, slow on Safari + array.data().length = size_estimate; + } + geom.addSource(array); + } + if (size_estimate === undefined) { + array.pushBack(s); + } else { + array.insertAt(idx, s); + } + } else if (this.m_scalarFormat === 'rgb' && coordinates.length === 6) { + array = geom.sourceData(vgl.vertexAttributeKeys.Color); + if (!array) { + array = new vgl.sourceDataC3fv(); + if (size_estimate !== undefined) { + array.length = size_estimate * 3; + } + geom.addSource(array); + } + r = coordinates[3]; + g = coordinates[4]; + b = coordinates[5]; + if (size_estimate === undefined) { + array.pushBack([r, g, b]); + } else { + array.insertAt(idx, [r, g, b]); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Read point data + * + * @param coordinates + * @returns {vgl.geometryData} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + + geom.addSource(vglcoords); + for (i = 0; i < 1; i += 1) { + indices[i] = i; + + x = coordinates[0]; + y = coordinates[1]; + z = 0.0; + if (coordinates.length > 2) { + z = coordinates[2]; + } + + //console.log('read ' + x + ',' + y + ',' + z); + vglcoords.pushBack([x, y, z]); + + //attributes + this.readScalars(coordinates, geom); + } + + vglpoints.setIndices(indices); + geom.addPrimitive(vglpoints); + geom.setName('aPoint'); + return geom; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Read multipoint data + * + * @param coordinates + * @returns {vgl.geometryData} + */ + //////////////////////////////////////////////////////////////////////////// + this.readMultiPoint = function (coordinates) { + var 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, + i; + + //preallocate with size estimate + vglcoords.data().length = estpntcnt * 3; //x,y,z + + for (i = 0; i < coordinates.length; i += 1) { + indices[i] = i; + x = coordinates[i][0]; + y = coordinates[i][1]; + z = 0.0; + if (coordinates[i].length > 2) { + z = coordinates[i][2]; + } + + //console.log('read ' + x + ',' + y + ',' + z); + vglcoords.insertAt(pntcnt, [x, y, z]); + + //attributes + this.readScalars(coordinates[i], geom, estpntcnt, pntcnt); + + pntcnt += 1; + } + + vglpoints.setIndices(indices); + geom.addPrimitive(vglpoints); + geom.addSource(vglcoords); + geom.setName('manyPoints'); + return geom; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Read line string data + * + * @param coordinates + * @returns {vgl.geometryData} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + + vglline.setIndicesPerPrimitive(coordinates.length); + + for (i = 0; i < coordinates.length; i += 1) { + indices.push(i); + x = coordinates[i][0]; + y = coordinates[i][1]; + z = 0.0; + if (coordinates[i].length > 2) { + z = coordinates[i][2]; + } + + //console.log('read ' + x + ',' + y + ',' + z); + vglcoords.pushBack([x, y, z]); + + //attributes + this.readScalars(coordinates[i], geom); + } + + vglline.setIndices(indices); + geom.addPrimitive(vglline); + geom.addSource(vglcoords); + geom.setName('aLineString'); + return geom; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Read multi line string + * + * @param coordinates + * @returns {vgl.geometryData} + */ + //////////////////////////////////////////////////////////////////////////// + this.readMultiLineString = function (coordinates) { + var geom = new vgl.geometryData(), + vglcoords = new vgl.sourceDataP3fv(), + pntcnt = 0, + //lines should be at least 2 verts long, underest OK + estpntcnt = coordinates.length * 2, + i = null, + j = null, + x = null, + y = null, + z = null, + indices = null, + vglline = null, + thisLineLength = null; + + // Preallocate with size estimate + vglcoords.data().length = estpntcnt * 3; //x,y,z + + for (j = 0; j < coordinates.length; j += 1) { + indices = []; + //console.log('getting line ' + j); + vglline = new vgl.lineStrip(); + thisLineLength = coordinates[j].length; + vglline.setIndicesPerPrimitive(thisLineLength); + for (i = 0; i < thisLineLength; i += 1) { + indices.push(pntcnt); + x = coordinates[j][i][0]; + y = coordinates[j][i][1]; + z = 0.0; + if (coordinates[j][i].length > 2) { + z = coordinates[j][i][2]; + } + + //console.log('read ' + x + ',' + y + ',' + z); + vglcoords.insertAt(pntcnt, [x, y, z]); + + //attributes + this.readScalars(coordinates[j][i], geom, estpntcnt * 2, pntcnt); + + pntcnt += 1; + } + + vglline.setIndices(indices); + geom.addPrimitive(vglline); + } + + geom.setName('aMultiLineString'); + geom.addSource(vglcoords); + return geom; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Read polygon data + * + * @param coordinates + * @returns {vgl.geometryData} + */ + //////////////////////////////////////////////////////////////////////////// + this.readPolygon = function (coordinates) { + //TODO: ignoring holes given in coordinates[1...] + //TODO: ignoring convex + //TODO: implement ear clipping in VGL instead of this to handle both + 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; i < thisPolyLength; i += 1) { + x = coordinates[0][i][0]; + y = coordinates[0][i][1]; + z = 0.0; + if (coordinates[0][i].length > 2) { + z = coordinates[0][i][2]; + } + + //console.log('read ' + x + ',' + y + ',' + z); + vglcoords.pushBack([x, y, z]); + + //attributes + this.readScalars(coordinates[0][i], geom); + + if (i > 1) { + //console.log('Cutting new triangle 0,'+ vl+ ','+ i); + indices = new Uint16Array([0, vl, i]); + vgltriangle = new vgl.triangles(); + vgltriangle.setIndices(indices); + geom.addPrimitive(vgltriangle); + vl = i; + } + } + + geom.setName('POLY'); + geom.addSource(vglcoords); + return geom; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Read multi polygon data + * + * @param coordinates + * @returns {vgl.geometryData} + */ + //////////////////////////////////////////////////////////////////////////// + this.readMultiPolygon = function (coordinates) { + var geom = new vgl.geometryData(), + vglcoords = new vgl.sourceDataP3fv(), + ccount = 0, + numPolys = coordinates.length, + pntcnt = 0, + estpntcnt = numPolys * 3, // assume triangles, underest is fine + vgltriangle = new vgl.triangles(), + indexes = [], + i = null, + j = null, + x = null, + y = null, + z = null, + thisPolyLength = null, + vf = null, + vl = null, + flip = null, + flipped = false, + tcount = 0; + + + //var time1 = new Date().getTime() + //var a = 0; + //var b = 0; + //var c = 0; + //var d = 0; + + //preallocate with size estimate + vglcoords.data().length = numPolys * 3; //x,y,z + for (j = 0; j < numPolys; j += 1) { + //console.log('getting poly ' + j); + + thisPolyLength = coordinates[j][0].length; + vf = ccount; + vl = ccount + 1; + flip = [false, false, false]; + for (i = 0; i < thisPolyLength; i += 1) { + //var timea = new Date().getTime() + + x = coordinates[j][0][i][0]; + y = coordinates[j][0][i][1]; + z = 0.0; + if (coordinates[j][0][i].length > 2) { + z = coordinates[j][0][i][2]; + } + flipped = false; + if (x > 180) { + flipped = true; + x = x - 360; + } + if (i === 0) { + flip[0] = flipped; + } else { + flip[1 + (i - 1) % 2] = flipped; + } + //var timeb = new Date().getTime(); + //console.log('read ' + x + ',' + y + ',' + z); + + vglcoords.insertAt(pntcnt, [x, y, z]); + //var timec = new Date().getTime(); + + //attributes + this.readScalars(coordinates[j][0][i], geom, estpntcnt, pntcnt); + pntcnt += 1; + //var timed = new Date().getTime() + + if (i > 1) { + //if (vl < 50) { + //console.log('Cutting new triangle ' + tcount + ':' + vf + ',' + + // vl + ',' + ccount); + //console.log(indexes); + //} + if (flip[0] === flip[1] && flip[1] === flip[2]) { + //indexes = indexes.concat([vf,vl,ccount]); //no, very slow in Safari + indexes[tcount * 3 + 0] = vf; + indexes[tcount * 3 + 1] = vl; + indexes[tcount * 3 + 2] = ccount; + tcount += 1; + } + //else { + // //TODO: duplicate triangles that straddle boundary on either side + //} + + vl = ccount; + } + ccount += 1; + //var timee = new Date().getTime() + //a = a + (timeb-timea) + //b = b + (timec-timeb) + //c = c + (timed-timec) + //d = d + (timee-timed) + } + } + vgltriangle.setIndices(indexes); + geom.addPrimitive(vgltriangle); + + //console.log('NUMPOLYS ' + pntcnt); + //console.log('RMP: ', a, ',', b, ',', c, ',', d) + //var time2 = new Date().getTime() + + geom.setName('aMultiPoly'); + geom.addSource(vglcoords); + //var time3 = new Date().getTime() + //console.log('RMP: ', time2-time1, ',', time3-time2) + + return geom; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * @param object + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.readGJObjectInt = function (object) { + if (!object.hasOwnProperty('type')) { + //console.log('uh oh, not a geojson object'); + return null; + } + + //look for properties type annotation + if (object.properties && + object.properties.ScalarFormat && + object.properties.ScalarFormat === 'values') { + this.m_scalarFormat = 'values'; + if (object.properties.ScalarRange) { + this.m_scalarRange = object.properties.ScalarRange; + } + } + if (object.properties && + object.properties.ScalarFormat && + object.properties.ScalarFormat === 'rgb') { + this.m_scalarFormat = 'rgb'; + } + + //TODO: ignoring 'crs' and 'bbox' and misc meta data on all of these, + //best to handle as references into original probably + var ret, + type = object.type, + next = null, + nextset = null, + i = null; + + switch (type) { + case 'Point': + //console.log('parsed Point'); + ret = this.readPoint(object.coordinates); + break; + case 'MultiPoint': + //console.log('parsed MultiPoint'); + ret = this.readMultiPoint(object.coordinates); + break; + case 'LineString': + //console.log('parsed LineString'); + ret = this.readLineString(object.coordinates); + break; + case 'MultiLineString': + //console.log('parsed MultiLineString'); + ret = this.readMultiLineString(object.coordinates); + break; + case 'Polygon': + //console.log('parsed Polygon'); + ret = this.readPolygon(object.coordinates); + break; + case 'MultiPolygon': + //console.log('parsed MultiPolygon'); + ret = this.readMultiPolygon(object.coordinates); + break; + case 'GeometryCollection': + //console.log('parsed GeometryCollection'); + nextset = []; + for (i = 0; i < object.geometries.length; i += 1) { + next = this.readGJObject(object.geometries[i]); + nextset.push(next); + } + ret = nextset; + break; + case 'Feature': + //console.log('parsed Feature'); + next = this.readGJObject(object.geometry); + ret = next; + break; + case 'FeatureCollection': + //console.log('parsed FeatureCollection'); + nextset = []; + for (i = 0; i < object.features.length; i += 1) { + next = this.readGJObject(object.features[i]); + nextset.push(next); + } + ret = nextset; + break; + default: + console.log('Don\'t understand type ' + type); + ret = null; + break; + } + return ret; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * @param object + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.readGJObject = function (object) { + //var time1, time2; + var ret; + //time1 = new Date().getTime() + ret = this.readGJObjectInt(object); + //time2 = new Date().getTime() + //console.log('ELAPSED: ', time2-time1) + return ret; + }; + + /** + * Linearize geometries + * + * @param geoms + * @param geom + */ + this.linearizeGeoms = function (geoms, geom) { + var i = null; + + if (Object.prototype.toString.call(geom) === '[object Array]') { + for (i = 0; i < geom.length; i += 1) { + this.linearizeGeoms(geoms, geom[i]); + } + } else { + geoms.push(geom); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Read geometries from geojson object + * + * @param object + * @returns {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.readGeomObject = function (object) { + var geom, + geoms = []; + + geom = this.readGJObject(object); + this.linearizeGeoms(geoms, geom); + return geoms; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Given a buffer get rendering primitives + * + * @param buffer + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.getPrimitives = function (buffer) { + //console.log('Parsing geoJSON'); + if (!buffer) { + return []; + } + + var obj = JSON.parse(buffer), + geom = this.readGJObject(obj), + geoms = []; + + this.m_scalarFormat = 'none'; + this.m_scalarRange = null; + + this.linearizeGeoms(geoms, geom); + + return { 'geoms': geoms, + 'scalarFormat': this.m_scalarFormat, + 'scalarRange': this.m_scalarRange }; + }; + + return this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl*/ + ////////////////////////////////////////////////////////////////////////////// + + vgl.data = function () { + 'use strict'; + + if (!(this instanceof vgl.data)) { + return new vgl.data(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Return data type. Should be implemented by the derived class + */ + //////////////////////////////////////////////////////////////////////////// + this.type = function () { + }; + }; + + vgl.data.raster = 0; + vgl.data.point = 1; + vgl.data.lineString = 2; + vgl.data.polygon = 3; + vgl.data.geometry = 10; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, Uint16Array, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class primitive + * + * @class + * @return {vgl.primitive} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.primitive = function () { + 'use strict'; + + if (!(this instanceof vgl.primitive)) { + return new vgl.primitive(); + } + + /** @private */ + var m_indicesPerPrimitive = 0, + m_primitiveType = 0, + m_indicesValueType = 0, + m_indices = null; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get indices of the primitive + * + * @returns {null} + */ + //////////////////////////////////////////////////////////////////////////// + this.indices = function () { + return m_indices; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create indices array for the primitive + * @param type + */ + //////////////////////////////////////////////////////////////////////////// + this.createIndices = function (type) { + type = type; /* unused parameters */ + // TODO Check for the type + m_indices = new Uint16Array(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the number of indices + */ + //////////////////////////////////////////////////////////////////////////// + this.numberOfIndices = function () { + return m_indices.length; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return size of indices in bytes + */ + //////////////////////////////////////////////////////////////////////////// + this.sizeInBytes = function () { + return m_indices.length * Uint16Array.BYTES_PER_ELEMENT; + }; + + //////////////////////////////////////////////////////////////////////////// + /* + * Return primitive type g + */ + //////////////////////////////////////////////////////////////////////////// + this.primitiveType = function () { + return m_primitiveType; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set primitive type + */ + //////////////////////////////////////////////////////////////////////////// + this.setPrimitiveType = function (type) { + m_primitiveType = type; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return count of indices that form a primitives + */ + //////////////////////////////////////////////////////////////////////////// + this.indicesPerPrimitive = function () { + return m_indicesPerPrimitive; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set count of indices that form a primitive + */ + //////////////////////////////////////////////////////////////////////////// + this.setIndicesPerPrimitive = function (count) { + m_indicesPerPrimitive = count; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return indices value type + */ + //////////////////////////////////////////////////////////////////////////// + this.indicesValueType = function () { + return m_indicesValueType; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set indices value type + */ + //////////////////////////////////////////////////////////////////////////// + this.setIndicesValueType = function (type) { + m_indicesValueType = type; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set indices from a array + */ + //////////////////////////////////////////////////////////////////////////// + this.setIndices = function (indicesArray) { + // TODO Check for the type + m_indices = new Uint16Array(indicesArray); + }; + + return this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class triangleStrip + * + * @returns {vgl.triangleStrip} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.triangleStrip = function () { + 'use strict'; + + if (!(this instanceof vgl.triangleStrip)) { + return new vgl.triangleStrip(); + } + + vgl.primitive.call(this); + + this.setPrimitiveType(vgl.GL.TRIANGLE_STRIP); + this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); + this.setIndicesPerPrimitive(3); + + return this; + }; + + inherit(vgl.triangleStrip, vgl.primitive); + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class triangles + * + * @returns {vgl.triangles} + */ + //////////////////////////////////////////////////////////////////////////// + vgl.triangles = function () { + 'use strict'; + + if (!(this instanceof vgl.triangles)) { + return new vgl.triangles(); + } + vgl.primitive.call(this); + + this.setPrimitiveType(vgl.GL.TRIANGLES); + this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); + this.setIndicesPerPrimitive(3); + + return this; + }; + + inherit(vgl.triangles, vgl.primitive); + + ////////////////////////////////////////////////////////////////////////////// + /** + * create a instance of lines primitive type + * + * @returns {vgl.lines} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.lines = function () { + 'use strict'; + + if (!(this instanceof vgl.lines)) { + return new vgl.lines(); + } + vgl.primitive.call(this); + + this.setPrimitiveType(vgl.GL.LINES); + this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); + this.setIndicesPerPrimitive(2); + + return this; + }; + inherit(vgl.lines, vgl.primitive); + + ////////////////////////////////////////////////////////////////////////////// + /** + * create a instance of line strip primitive type + * + * @returns {vgl.lineStrip} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.lineStrip = function () { + 'use strict'; + + if (!(this instanceof vgl.lineStrip)) { + return new vgl.lineStrip(); + } + vgl.primitive.call(this); + + this.setPrimitiveType(vgl.GL.LINE_STRIP); + this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); + this.setIndicesPerPrimitive(2); + + return this; + }; + inherit(vgl.lineStrip, vgl.primitive); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class points + * + * @returns {vgl.points} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.points = function () { + 'use strict'; + + if (!(this instanceof vgl.points)) { + return new vgl.points(); + } + vgl.primitive.call(this); + + this.setPrimitiveType(vgl.GL.POINTS); + this.setIndicesValueType(vgl.GL.UNSIGNED_SHORT); + this.setIndicesPerPrimitive(1); + + return this; + }; + + inherit(vgl.points, vgl.primitive); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class vertexDataP3f + * + * @returns {vgl.vertexDataP3f} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.vertexDataP3f = function () { + 'use strict'; + + if (!(this instanceof vgl.vertexDataP3f)) { + return new vgl.vertexDataP3f(); + } + + /** @private */ + this.m_position = []; + + return this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class vertexDataP3N3f + * + * @class + * @returns {vgl.vertexDataP3N3f} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.vertexDataP3N3f = function () { + 'use strict'; + + if (!(this instanceof vgl.vertexDataP3N3f)) { + return new vgl.vertexDataP3N3f(); + } + + this.m_position = []; + this.m_normal = []; + + return this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class vertexDataP3T3f + * + * @class + * @returns {vgl.vertexDataP3T3f} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.vertexDataP3T3f = function () { + 'use strict'; + + if (!(this instanceof vgl.vertexDataP3T3f)) { + return new vgl.vertexDataP3T3f(); + } + + this.m_position = []; + this.m_texCoordinate = []; + + return this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceData + * @class + * @returns {vgl.sourceData} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceData = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceData)) { + return new vgl.sourceData(arg); + } + + arg = arg || {}; + var m_attributesMap = {}, + m_data = [], + m_name = arg.name || 'Source ' + new Date().toISOString(), + + //////////////////////////////////////////////////////////////////////////// + /** + * Attribute data for the source + */ + //////////////////////////////////////////////////////////////////////////// + vglAttributeData = function () { + // Number of components per group + // Type of data type (GL_FLOAT etc) + this.m_numberOfComponents = 0; + // Size of data type + this.m_dataType = 0; + this.m_dataTypeSize = 0; + // Specifies whether fixed-point data values should be normalized + // (true) or converted directly as fixed-point values (false) + // when they are accessed. + this.m_normalized = false; + // Strides for each attribute. + this.m_stride = 0; + // Offset + this.m_offset = 0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return raw data for this source + * + * @returns {Array or Float32Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.data = function () { + return m_data; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return raw data for this source + * + * @returns {Array or Float32Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.getData = function () { + return this.data(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * If the raw data is not a Float32Array, convert it to one. Then, return + * raw data for this source + * + * @returns {Float32Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.dataToFloat32Array = function () { + if (!(m_data instanceof Float32Array)) { + m_data = new Float32Array(m_data); + } + return m_data; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set data for this source + * + */ + //////////////////////////////////////////////////////////////////////////// + this.setData = function (data) { + if (!(data instanceof Array) && !(data instanceof Float32Array)) { + console.log('[error] Requires array'); + return; + } + if (data instanceof Float32Array) { + m_data = data; + } else { + m_data = data.slice(0); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add new attribute data to the source + */ + //////////////////////////////////////////////////////////////////////////// + this.addAttribute = function (key, dataType, sizeOfDataType, offset, stride, + noOfComponents, normalized) { + + if (!m_attributesMap.hasOwnProperty(key)) { + /* jshint newcap: false */ + //jscs:disable requireCapitalizedConstructors + var newAttr = new vglAttributeData(); + //jscs:enable requireCapitalizedConstructors + /* jshint newcap: true */ + newAttr.m_dataType = dataType; + newAttr.m_dataTypeSize = sizeOfDataType; + newAttr.m_offset = offset; + newAttr.m_stride = stride; + newAttr.m_numberOfComponents = noOfComponents; + newAttr.m_normalized = normalized; + m_attributesMap[key] = newAttr; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return size of the source data + */ + //////////////////////////////////////////////////////////////////////////// + this.sizeOfArray = function () { + return Object.size(m_data); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return length of array + */ + //////////////////////////////////////////////////////////////////////////// + this.lengthOfArray = function () { + return m_data.length; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return size of the source data in bytes + */ + //////////////////////////////////////////////////////////////////////////// + /* + * TODO: code below is probably wrong. + * Example: + * format P3N3f + * m_data = [ 1, 2, 3, 4, 5, 6 ]; // contains one vertex, + * // one normal, m_data.length == 6 + * + * The inner loop computes: + * sizeInBytes += 3 * 4; // for position + * sizeInBytes += 3 * 4; // for normal + * + * Then sizeInBytes *= 6; // m_data.length == 6 + * which gives sizeInBytes == 144 bytes when it should have been 4*6 = 24 + */ + this.sizeInBytes = function () { + var sizeInBytes = 0, + keys = this.keys(), i; + + for (i = 0; i < keys.length(); i += 1) { + sizeInBytes += this.numberOfComponents(keys[i]) * + this.sizeOfAttributeDataType(keys[i]); + } + + sizeInBytes *= this.sizeOfArray(); + + return sizeInBytes; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if there is attribute exists of a given key type + */ + //////////////////////////////////////////////////////////////////////////// + this.hasKey = function (key) { + return m_attributesMap.hasOwnProperty(key); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return keys of all attributes + */ + //////////////////////////////////////////////////////////////////////////// + this.keys = function () { + return Object.keys(m_attributesMap); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return number of attributes of source data + */ + //////////////////////////////////////////////////////////////////////////// + this.numberOfAttributes = function () { + return Object.size(m_attributesMap); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return number of components of the attribute data + */ + //////////////////////////////////////////////////////////////////////////// + this.attributeNumberOfComponents = function (key) { + if (m_attributesMap.hasOwnProperty(key)) { + return m_attributesMap[key].m_numberOfComponents; + } + + return 0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return if the attribute data is normalized + */ + //////////////////////////////////////////////////////////////////////////// + this.normalized = function (key) { + if (m_attributesMap.hasOwnProperty(key)) { + return m_attributesMap[key].m_normalized; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return size of the attribute data type + */ + //////////////////////////////////////////////////////////////////////////// + this.sizeOfAttributeDataType = function (key) { + if (m_attributesMap.hasOwnProperty(key)) { + return m_attributesMap[key].m_dataTypeSize; + } + + return 0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return attribute data type + */ + //////////////////////////////////////////////////////////////////////////// + this.attributeDataType = function (key) { + if (m_attributesMap.hasOwnProperty(key)) { + return m_attributesMap[key].m_dataType; + } + + return undefined; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return attribute offset + */ + //////////////////////////////////////////////////////////////////////////// + this.attributeOffset = function (key) { + if (m_attributesMap.hasOwnProperty(key)) { + return m_attributesMap[key].m_offset; + } + + return 0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return attribute stride + */ + //////////////////////////////////////////////////////////////////////////// + this.attributeStride = function (key) { + if (m_attributesMap.hasOwnProperty(key)) { + return m_attributesMap[key].m_stride; + } + + return 0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Virtual function to insert new vertex data at the end + */ + //////////////////////////////////////////////////////////////////////////// + this.pushBack = function (vertexData) { + vertexData = vertexData; /* unused parameter */ + // Should be implemented by the base class + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Insert new data block to the raw data + */ + //////////////////////////////////////////////////////////////////////////// + this.insert = function (data) { + var i; + + //m_data = m_data.concat(data); //no, slow on Safari + /* If we will are given a Float32Array and don't have any other data, use + * it directly. */ + if (!m_data.length && data.length && data instanceof Float32Array) { + m_data = data; + return; + } + /* If our internal array is immutable and we will need to change it, create + * a regular mutable array from it. */ + if (!m_data.slice && (m_data.length || !data.slice)) { + m_data = Array.prototype.slice.call(m_data); + } + if (!data.length) { + /* data is a singular value, so append it to our array */ + m_data[m_data.length] = data; + } else { + /* We don't have any data currently, so it is faster to copy the data + * using slice. */ + if (!m_data.length && data.slice) { + m_data = data.slice(0); + } else { + for (i = 0; i < data.length; i += 1) { + m_data[m_data.length] = data[i]; + } + } + } + }; + + this.insertAt = function (index, data) { + var i; + + if (!data.length) { + m_data[index] = data; + } else { + for (i = 0; i < data.length; i += 1) { + m_data[index * data.length + i] = data[i]; + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return name of the source data + */ + //////////////////////////////////////////////////////////////////////////// + this.name = function () { + return m_name; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set name of the source data + */ + //////////////////////////////////////////////////////////////////////////// + this.setName = function (name) { + m_name = name; + }; + + + return this; + }; + + + vgl.sourceDataAnyfv = function (size, key, arg) { + 'use strict'; + if (!(this instanceof vgl.sourceDataAnyfv)) { + return new vgl.sourceDataAnyfv(size, key, arg); + } + + vgl.sourceData.call(this, arg); + this.addAttribute(key, vgl.GL.FLOAT, + 4, 0, size * 4, size, false); + + this.pushBack = function (value) { + this.insert(value); + }; + + return this; + }; + inherit(vgl.sourceDataAnyfv, vgl.sourceData); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceDataP3T3f + * + * @returns {vgl.sourceDataP3T3f} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceDataP3T3f = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceDataP3T3f)) { + return new vgl.sourceDataP3T3f(arg); + } + vgl.sourceData.call(this, arg); + + this.addAttribute(vgl.vertexAttributeKeys.Position, vgl.GL.FLOAT, 4, 0, 6 * 4, 3, + false); + this.addAttribute(vgl.vertexAttributeKeys.TextureCoordinate, vgl.GL.FLOAT, 4, 12, + 6 * 4, 3, false); + + this.pushBack = function (value) { + this.insert(value.m_position); + this.insert(value.m_texCoordinate); + }; + + return this; + }; + + inherit(vgl.sourceDataP3T3f, vgl.sourceData); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceDataP3N3f + * + * @returns {vgl.sourceDataP3N3f} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceDataP3N3f = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceDataP3N3f)) { + return new vgl.sourceDataP3N3f(arg); + } + + vgl.sourceData.call(this, arg); + + this.addAttribute(vgl.vertexAttributeKeys.Position, vgl.GL.FLOAT, 4, 0, 6 * 4, 3, + false); + this.addAttribute(vgl.vertexAttributeKeys.Normal, vgl.GL.FLOAT, 4, 12, 6 * 4, 3, + false); + + this.pushBack = function (value) { + this.insert(value.m_position); + this.insert(value.m_normal); + }; + + return this; + }; + + inherit(vgl.sourceDataP3N3f, vgl.sourceData); + + ///////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceDataP3fv + * + * @returns {vgl.sourceDataP3fv} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceDataP3fv = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceDataP3fv)) { + return new vgl.sourceDataP3fv(arg); + } + + vgl.sourceData.call(this, arg); + + this.addAttribute(vgl.vertexAttributeKeys.Position, vgl.GL.FLOAT, 4, 0, 3 * 4, 3, + false); + + this.pushBack = function (value) { + this.insert(value); + }; + + return this; + }; + + inherit(vgl.sourceDataP3fv, vgl.sourceData); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceDataT2fv + * + * @returns {vgl.sourceDataT2fv} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceDataT2fv = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceDataT2fv)) { + return new vgl.sourceDataT2fv(arg); + } + + vgl.sourceData.call(this, arg); + + this.addAttribute(vgl.vertexAttributeKeys.TextureCoordinate, vgl.GL.FLOAT, 4, 0, + 2 * 4, 2, false); + + this.pushBack = function (value) { + this.insert(value); + }; + + return this; + }; + + inherit(vgl.sourceDataT2fv, vgl.sourceData); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceDataC3fv + * + * @returns {vgl.sourceDataC3fv} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceDataC3fv = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceDataC3fv)) { + return new vgl.sourceDataC3fv(arg); + } + + vgl.sourceData.call(this, arg); + + this.addAttribute(vgl.vertexAttributeKeys.Color, vgl.GL.FLOAT, 4, 0, 3 * 4, 3, false); + + this.pushBack = function (value) { + this.insert(value); + }; + + return this; + }; + + inherit(vgl.sourceDataC3fv, vgl.sourceData); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceDataSf meant to hold scalar float values + * + * @class + * @returns {vgl.sourceDataSf} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceDataSf = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceDataSf)) { + return new vgl.sourceDataSf(arg); + } + + var m_min = null, + m_max = null, + m_fixedmin = null, + m_fixedmax = null; + + vgl.sourceData.call(this, arg); + + this.addAttribute(vgl.vertexAttributeKeys.Scalar, vgl.GL.FLOAT, 4, 0, 4, 1, false); + + this.pushBack = function (value) { + if (m_max === null || value > m_max) { + m_max = value; + } + if (m_min === null || value < m_min) { + m_min = value; + } + //this.insert(value); //no, slow on Safari + this.data()[this.data().length] = value; + }; + + this.insertAt = function (index, value) { + if (m_max === null || value > m_max) { + m_max = value; + } + if (m_min === null || value < m_min) { + m_min = value; + } + //call superclass ?? + //vgl.sourceData.insertAt.call(this, index, value); + this.data()[index] = value; + }; + + this.scalarRange = function () { + if (m_fixedmin === null || m_fixedmax === null) { + return [m_min, m_max]; + } + + return [m_fixedmin, m_fixedmax]; + }; + + this.setScalarRange = function (min, max) { + m_fixedmin = min; + m_fixedmax = max; + }; + + return this; + }; + + inherit(vgl.sourceDataSf, vgl.sourceData); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sourceDataDf meant to hold data float values + * + * This source array is the best way to pass a array of floats to the shader + * that has one entry for each of the vertices. + * + * @class + * @returns {vgl.sourceDataDf} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.sourceDataDf = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.sourceDataDf)) { + return new vgl.sourceDataDf(arg); + } + + vgl.sourceData.call(this, arg); + + this.addAttribute(vgl.vertexAttributeKeys.Scalar, vgl.GL.FLOAT, + 4, 0, 4, 1, false); + + this.pushBack = function (value) { + this.data()[this.data().length] = value; + }; + + this.insertAt = function (index, value) { + this.data()[index] = value; + }; + + return this; + }; + + inherit(vgl.sourceDataDf, vgl.sourceData); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class geometryData + * + * @class + * @returns {vgl.geometryData} + */ + ///////////////////////////////////////////////////////////////////////////// + vgl.geometryData = function () { + 'use strict'; + + if (!(this instanceof vgl.geometryData)) { + return new vgl.geometryData(); + } + vgl.data.call(this); + + /** @private */ + var m_name = '', + m_primitives = [], + m_sources = [], + m_bounds = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + m_computeBoundsTimestamp = vgl.timestamp(), + m_boundsDirtyTimestamp = vgl.timestamp(); + + //////////////////////////////////////////////////////////////////////////// + /** + * Return type + */ + //////////////////////////////////////////////////////////////////////////// + this.type = function () { + return vgl.data.geometry; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return ID of the geometry data + */ + //////////////////////////////////////////////////////////////////////////// + this.name = function () { + return m_name; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set name of the geometry data + */ + //////////////////////////////////////////////////////////////////////////// + this.setName = function (name) { + m_name = name; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add new source + */ + //////////////////////////////////////////////////////////////////////////// + this.addSource = function (source, sourceName) { + // @todo Check if the incoming source has duplicate keys + + if (sourceName !== undefined) { + source.setName(sourceName); + } + // NOTE This might not work on IE8 or lower + if (m_sources.indexOf(source) === -1) { + m_sources.push(source); + + if (source.hasKey(vgl.vertexAttributeKeys.Position)) { + m_boundsDirtyTimestamp.modified(); + } + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return source for a given index. Returns 0 if not found. + */ + //////////////////////////////////////////////////////////////////////////// + this.source = function (index) { + if (index < m_sources.length) { + return m_sources[index]; + } + + return 0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return source with a specified name. Returns 0 if not found. + */ + //////////////////////////////////////////////////////////////////////////// + this.sourceByName = function (sourceName) { + for (var i = 0; i < m_sources.length; i += 1) { + if (m_sources[i].name() === sourceName) { + return m_sources[i]; + } + } + return 0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return number of sources + */ + //////////////////////////////////////////////////////////////////////////// + this.numberOfSources = function () { + return m_sources.length; + }; + + /** + * Return source data given a key + */ + this.sourceData = function (key) { + var i; + + for (i = 0; i < m_sources.length; i += 1) { + if (m_sources[i].hasKey(key)) { + return m_sources[i]; + } + } + + return null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add new primitive + */ + //////////////////////////////////////////////////////////////////////////// + this.addPrimitive = function (primitive) { + m_primitives.push(primitive); + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return primitive for a given index. Returns null if not found. + */ + //////////////////////////////////////////////////////////////////////////// + this.primitive = function (index) { + if (index < m_primitives.length) { + return m_primitives[index]; + } + + return null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return number of primitives + */ + //////////////////////////////////////////////////////////////////////////// + this.numberOfPrimitives = function () { + return m_primitives.length; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return bounds [minX, maxX, minY, maxY, minZ, maxZ] + */ + //////////////////////////////////////////////////////////////////////////// + this.bounds = function () { + if (m_boundsDirtyTimestamp.getMTime() > m_computeBoundsTimestamp.getMTime()) { + this.computeBounds(); + } + return m_bounds; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if bounds are dirty or mark them as such. + * + * @param dirty: true to set bounds as dirty. + * Return true if bounds are dirty. + */ + //////////////////////////////////////////////////////////////////////////// + this.boundsDirty = function (dirty) { + if (dirty) { + m_boundsDirtyTimestamp.modified(); + } + return m_boundsDirtyTimestamp.getMTime() > m_computeBoundsTimestamp.getMTime(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Reset bounds + */ + //////////////////////////////////////////////////////////////////////////// + this.resetBounds = function () { + m_bounds[0] = 0.0; + m_bounds[1] = 0.0; + m_bounds[2] = 0.0; + m_bounds[3] = 0.0; + m_bounds[4] = 0.0; + m_bounds[5] = 0.0; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set bounds + */ + //////////////////////////////////////////////////////////////////////////// + this.setBounds = function (minX, maxX, minY, maxY, minZ, maxZ) { + 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(); + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute bounds + */ + //////////////////////////////////////////////////////////////////////////// + this.computeBounds = function () { + if (m_boundsDirtyTimestamp.getMTime() > m_computeBoundsTimestamp.getMTime()) { + var 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, + j, ib, jb, maxv, minv, + value = null, + vertexIndex; + + // We advance by index, not by byte + stride /= sizeOfDataType; + offset /= sizeOfDataType; + + this.resetBounds(); + + for (j = 0; j < numberOfComponents; j += 1) { + ib = j * 2; + jb = j * 2 + 1; + if (count) { + maxv = minv = m_bounds[jb] = data[offset + j]; + } else { + maxv = minv = 0; + } + for (vertexIndex = offset + stride + j; vertexIndex < count; + vertexIndex += stride) { + value = data[vertexIndex]; + if (value > maxv) { + maxv = value; + } + if (value < minv) { + minv = value; + } + } + m_bounds[ib] = minv; m_bounds[jb] = maxv; + } + + m_computeBoundsTimestamp.modified(); + } + }; + + + //////////////////////////////////////////////////////////////////////////// + /** + * Returns the vertex closest to a given position + */ + //////////////////////////////////////////////////////////////////////////// + this.findClosestVertex = function (point) { + 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, + minDist = Number.MAX_VALUE, + minIndex = null, + vi, vPos, dx, dy, dz, dist, i; + + // assume positions are always triplets + if (numberOfComponents !== 3) { + console.log('[warning] Find closest vertex assumes three' + + 'component vertex '); + } + + if (!point.z) { + point = {x: point.x, y: point.y, z: 0}; + } + + for (vi = offset, i = 0; vi < data.length; vi += stride, i += 1) { + vPos = [data[vi], + data[vi + 1], + data[vi + 2]]; + + dx = vPos[0] - point.x; + dy = vPos[1] - point.y; + dz = vPos[2] - point.z; + dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist < minDist) { + minDist = dist; + minIndex = i; + } + } + return minIndex; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Returns the requested vertex position + */ + //////////////////////////////////////////////////////////////////////////// + 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; + + // assume positions are always triplets + if (numberOfComponents !== 3) { + console.log('[warning] getPosition assumes three component data'); + } + + return [data[offset + index * stride], + data[offset + index * stride + 1], + data[offset + index * stride + 2]]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Returns the scalar corresponding to a given vertex index + */ + //////////////////////////////////////////////////////////////////////////// + this.getScalar = function (index) { + var attr = vgl.vertexAttributeKeys.Scalar, + sourceData = this.sourceData(attr), + numberOfComponents, sizeOfDataType, data, stride, offset; + + if (!sourceData) { + return null; + } + + numberOfComponents = sourceData.attributeNumberOfComponents(attr); + sizeOfDataType = sourceData.sizeOfAttributeDataType(attr); + data = sourceData.data(); + stride = sourceData.attributeStride(attr) / sizeOfDataType; + offset = sourceData.attributeOffset(attr) / sizeOfDataType; + + //console.log('index for scalar is ' + index); + //console.log('offset for scalar is ' + offset); + //console.log('stride for scalar is ' + stride); + + //console.log('have ' + data.length + ' scalars'); + + if (index * stride + offset >= data.length) { + console.log('access out of bounds in getScalar'); + } + + return data[index * stride + offset]; + }; + + return this; + }; + + inherit(vgl.geometryData, vgl.data); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, Float32Array, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class mapper + * + * @class + * @returns {vgl.mapper} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.mapper = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.mapper)) { + return new vgl.mapper(arg); + } + vgl.boundingObject.call(this); + + /** @private */ + arg = arg || {}; + + var m_dirty = true, + m_color = [0.0, 1.0, 1.0], + m_geomData = null, + m_buffers = [], + m_bufferVertexAttributeMap = {}, + m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, + m_glCompileTimestamp = vgl.timestamp(), + m_context = null, + m_this = this; + + //////////////////////////////////////////////////////////////////////////// + /** + * Delete cached VBO if any + */ + //////////////////////////////////////////////////////////////////////////// + this.deleteVertexBufferObjects = function (renderState) { + var i; + var context = m_context; + if (renderState) { + context = renderState.m_context; + } + if (context) { + for (i = 0; i < m_buffers.length; i += 1) { + context.deleteBuffer(m_buffers[i]); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create new VBO for all its geometryData sources and primitives + * + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function createVertexBufferObjects(renderState) { + if (m_geomData) { + if (renderState) { + m_context = renderState.m_context; + } + var numberOfSources = m_geomData.numberOfSources(), + i, j, k, bufferId = null, keys, ks, numberOfPrimitives, data; + + for (i = 0; i < numberOfSources; i += 1) { + bufferId = m_context.createBuffer(); + m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, bufferId); + data = m_geomData.source(i).data(); + if (!(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 = []; + + for (j = 0; j < keys.length; j += 1) { + ks.push(keys[j]); + } + + m_bufferVertexAttributeMap[i] = ks; + m_buffers[i] = bufferId; + } + + numberOfPrimitives = m_geomData.numberOfPrimitives(); + for (k = 0; k < numberOfPrimitives; 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(); + } + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Clear cache related to buffers + * + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function cleanUpDrawObjects(renderState) { + renderState = renderState; /* avoid unused warning */ + m_bufferVertexAttributeMap = {}; + m_buffers = []; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Setup draw objects; Delete old ones and create new ones + * + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function setupDrawObjects(renderState) { + // Delete buffer objects from past if any. + m_this.deleteVertexBufferObjects(renderState); + + // Clear any cache related to buffers + cleanUpDrawObjects(renderState); + + // Now construct the new ones. + createVertexBufferObjects(renderState); + + m_dirty = false; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute bounds of the data + */ + //////////////////////////////////////////////////////////////////////////// + this.computeBounds = function () { + if (m_geomData === null || typeof m_geomData === 'undefined') { + this.resetBounds(); + return; + } + + var computeBoundsTimestamp = this.computeBoundsTimestamp(), + boundsDirtyTimestamp = this.boundsDirtyTimestamp(), + geomBounds = null; + + if (boundsDirtyTimestamp.getMTime() > computeBoundsTimestamp.getMTime()) { + geomBounds = m_geomData.bounds(); + + this.setBounds(geomBounds[0], geomBounds[1], geomBounds[2], + geomBounds[3], geomBounds[4], geomBounds[5]) ; + + computeBoundsTimestamp.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get solid color of the geometry + */ + //////////////////////////////////////////////////////////////////////////// + this.color = function () { + return m_color; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set solid color of the geometry. Default is teal [1.0, 1.0, 1.0] + * + * @param r Red component of the color [0.0 - 1.0] + * @param g Green component of the color [0.0 - 1.0] + * @param b Blue component of the color [0.0 - 1.0] + */ + //////////////////////////////////////////////////////////////////////////// + this.setColor = function (r, g, b) { + m_color[0] = r; + m_color[1] = g; + m_color[2] = b; + + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return stored geometry data if any + */ + //////////////////////////////////////////////////////////////////////////// + this.geometryData = function () { + return m_geomData; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Connect mapper to its geometry data + */ + //////////////////////////////////////////////////////////////////////////// + this.setGeometryData = function (geom) { + if (m_geomData !== geom) { + m_geomData = geom; + + this.modified(); + this.boundsDirtyTimestamp().modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update the buffer used for a named source. + * + * @param {String} sourceName The name of the source to update. + * @param {Object[] or Float32Array} values The values to use for the source. + * If not specified, use the source's own buffer. + */ + //////////////////////////////////////////////////////////////////////////// + this.updateSourceBuffer = function (sourceName, values, renderState) { + if (renderState) { + m_context = renderState.m_context; + } + if (!m_context) { + return false; + } + var bufferIndex = -1; + for (var i = 0; i < m_geomData.numberOfSources(); i += 1) { + if (m_geomData.source(i).name() === sourceName) { + bufferIndex = i; + break; + } + } + if (bufferIndex < 0 || bufferIndex >= m_buffers.length) { + return false; + } + if (!values) { + values = m_geomData.source(i).dataToFloat32Array(); + } + m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_buffers[bufferIndex]); + if (values instanceof Float32Array) { + m_context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, values); + } else { + m_context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, + new Float32Array(values)); + } + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the buffer used for a named source. If the current buffer isn't a + * Float32Array, it is converted to one. This array can then be modified + * directly, after which updateSourceBuffer can be called to update the + * GL array. + * + * @param {String} sourceName The name of the source to update. + * @returns {Float32Array} An array used for this source. + */ + //////////////////////////////////////////////////////////////////////////// + this.getSourceBuffer = function (sourceName) { + var source = m_geomData.sourceByName(sourceName); + if (!source) { + return new Float32Array(); + } + return source.dataToFloat32Array(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render the mapper + */ + //////////////////////////////////////////////////////////////////////////// + this.render = function (renderState) { + if (this.getMTime() > m_glCompileTimestamp.getMTime() || + renderState.m_contextChanged) { + setupDrawObjects(renderState); + } + m_context = renderState.m_context; + + // Fixed vertex color + m_context.vertexAttrib3fv(vgl.vertexAttributeKeys.Color, this.color()); + + // TODO Use renderState + var bufferIndex = 0, + j = 0, i, noOfPrimitives = null, primitive = null; + + for (i in m_bufferVertexAttributeMap) { + if (m_bufferVertexAttributeMap.hasOwnProperty(i)) { + m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, + m_buffers[bufferIndex]); + for (j = 0; j < m_bufferVertexAttributeMap[i].length; j += 1) { + renderState.m_material + .bindVertexData(renderState, m_bufferVertexAttributeMap[i][j]); + } + bufferIndex += 1; + } + } + + noOfPrimitives = m_geomData.numberOfPrimitives(); + for (j = 0; j < noOfPrimitives; j += 1) { + m_context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_buffers[bufferIndex]); + bufferIndex += 1; + primitive = m_geomData.primitive(j); + switch (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()); + break; + } + m_context.bindBuffer (vgl.GL.ARRAY_BUFFER, null); + } + }; + + return this; + }; + + inherit(vgl.mapper, vgl.boundingObject); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + vgl.groupMapper = function () { + 'use strict'; + + if (!(this instanceof vgl.groupMapper)) { + return new vgl.groupMapper(); + } + vgl.mapper.call(this); + + /** @private */ + var m_createMappersTimestamp = vgl.timestamp(), + m_mappers = [], + m_geomDataArray = []; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return stored geometry data if any + * + * @param index optional + */ + //////////////////////////////////////////////////////////////////////////// + this.geometryData = function (index) { + if (index !== undefined && index < m_geomDataArray.length) { + return m_geomDataArray[index]; + } + + if (m_geomDataArray.length > 0) { + return m_geomDataArray[0]; + } + + return null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Connect mapper to its geometry data + * + * @param geom {vgl.geomData} + */ + //////////////////////////////////////////////////////////////////////////// + this.setGeometryData = function (geom) { + if (m_geomDataArray.length === 1) { + if (m_geomDataArray[0] === geom) { + return; + } + } + m_geomDataArray = []; + m_geomDataArray.push(geom); + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return stored geometry data array if any + */ + //////////////////////////////////////////////////////////////////////////// + this.geometryDataArray = function () { + return m_geomDataArray; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Connect mapper to its geometry data + * + * @param geoms {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.setGeometryDataArray = function (geoms) { + if (geoms instanceof Array) { + if (m_geomDataArray !== geoms) { + m_geomDataArray = []; + m_geomDataArray = geoms; + this.modified(); + return true; + } + } else { + console.log('[error] Requies array of geometry data'); + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute bounds of the data + */ + //////////////////////////////////////////////////////////////////////////// + this.computeBounds = function () { + if (m_geomDataArray === null || + m_geomDataArray === undefined) { + this.resetBounds(); + return; + } + + var computeBoundsTimestamp = this.computeBoundsTimestamp(), + boundsDirtyTimestamp = this.boundsDirtyTimestamp(), + m_bounds = this.bounds(), + geomBounds = null, + i = null; + + if (boundsDirtyTimestamp.getMTime() > + computeBoundsTimestamp.getMTime()) { + + for (i = 0; i < m_geomDataArray.length; i += 1) { + geomBounds = m_geomDataArray[i].bounds(); + + if (m_bounds[0] > geomBounds[0]) { + m_bounds[0] = geomBounds[0]; + } + if (m_bounds[1] < geomBounds[1]) { + m_bounds[1] = geomBounds[1]; + } + if (m_bounds[2] > geomBounds[2]) { + m_bounds[2] = geomBounds[2]; + } + if (m_bounds[3] < geomBounds[3]) { + m_bounds[3] = geomBounds[3]; + } + if (m_bounds[4] > geomBounds[4]) { + m_bounds[4] = geomBounds[4]; + } + if (m_bounds[5] < geomBounds[5]) { + m_bounds[5] = geomBounds[5]; + } + } + + this.modified(); + computeBoundsTimestamp.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render the mapper + */ + //////////////////////////////////////////////////////////////////////////// + this.render = function (renderState) { + var i = null; + + if (this.getMTime() > m_createMappersTimestamp.getMTime()) { + // NOTE Hoping that it will release the graphics resources + for (i = 0; i < m_geomDataArray.length; i += 1) { + m_mappers.push(vgl.mapper()); + m_mappers[i].setGeometryData(m_geomDataArray[i]); + } + m_createMappersTimestamp.modified(); + } + + for (i = 0; i < m_mappers.length; i += 1) { + m_mappers[i].render(renderState); + } + }; + + return this; + }; + + inherit(vgl.groupMapper, vgl.mapper); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + vgl.materialAttributeType = { + 'Undefined' : 0x0, + 'ShaderProgram' : 0x1, + 'Texture' : 0x2, + 'Blend' : 0x3, + 'Depth' : 0x4 + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class materialAttribute + * + * @class + * @param type + * @returns {vgl.materialAttribute} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.materialAttribute = function (type) { + 'use strict'; + + if (!(this instanceof vgl.materialAttribute)) { + return new vgl.materialAttribute(); + } + vgl.graphicsObject.call(this); + + /** @private */ + var m_this = this, + m_type = type, + m_enabled = true; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return tyep of the material attribute + * + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.type = function () { + return m_type; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return if material attribute is enabled or not + * + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.enabled = function () { + return m_enabled; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bind and activate vertex specific data + * + * @param renderState + * @param key + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.bindVertexData = function (renderState, key) { + renderState = renderState; /* unused parameter */ + key = key /* unused parameter */; + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Undo bind and deactivate vertex specific data + * + * @param renderState + * @param key + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.undoBindVertexData = function (renderState, key) { + renderState = renderState; /* unused parameter */ + key = key /* unused parameter */; + return false; + }; + + return m_this; + }; + + inherit(vgl.materialAttribute, vgl.graphicsObject); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of clas blendFunction + * + * @class + * @param source + * @param destination + * @returns {vgl.blendFunction} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.blendFunction = function (source, destination) { + 'use strict'; + + if (!(this instanceof vgl.blendFunction)) { + return new vgl.blendFunction(source, destination); + } + + /** @private */ + var m_source = source, + m_destination = destination; + + //////////////////////////////////////////////////////////////////////////// + /** + * Apply blend function to the current state + * + * @param {vgl.renderState} + */ + //////////////////////////////////////////////////////////////////////////// + this.apply = function (renderState) { + renderState.m_context.blendFuncSeparate(m_source, m_destination, + vgl.GL.ONE, vgl.GL.ONE_MINUS_SRC_ALPHA); + }; + + return this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class blend + * + * @returns {vgl.blend} + */ + //////////////////////////////////////////////////////////////////////////// + vgl.blend = function () { + 'use strict'; + + if (!(this instanceof vgl.blend)) { + return new vgl.blend(); + } + vgl.materialAttribute.call( + this, vgl.materialAttributeType.Blend); + + /** @private */ + var m_wasEnabled = false, + m_blendFunction = vgl.blendFunction(vgl.GL.SRC_ALPHA, + vgl.GL.ONE_MINUS_SRC_ALPHA); + + //////////////////////////////////////////////////////////////////////////// + /** + * Bind blend attribute + * + * @param {vgl.renderState} + */ + //////////////////////////////////////////////////////////////////////////// + this.bind = function (renderState) { + m_wasEnabled = renderState.m_context.isEnabled(vgl.GL.BLEND); + + if (this.enabled()) { + renderState.m_context.enable(vgl.GL.BLEND); + m_blendFunction.apply(renderState); + } else { + renderState.m_context.disable(vgl.GL.BLEND); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Undo bind blend attribute + * + * @param {vgl.renderState} + */ + //////////////////////////////////////////////////////////////////////////// + this.undoBind = function (renderState) { + if (m_wasEnabled) { + renderState.m_context.enable(vgl.GL.BLEND); + } else { + renderState.m_context.disable(vgl.GL.BLEND); + } + + return true; + }; + + return this; + }; + + inherit(vgl.blend, vgl.materialAttribute); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class material + * + * @class + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.material = function () { + 'use strict'; + + if (!(this instanceof vgl.material)) { + return new vgl.material(); + } + vgl.graphicsObject.call(this); + + // / Private member variables + var m_this = this, + m_shaderProgram = new vgl.shaderProgram(), + m_binNumber = 100, + m_textureAttributes = {}, + m_attributes = {}; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return bin number for the material + * + * @default 100 + * @returns {number} + */ + //////////////////////////////////////////////////////////////////////////// + this.binNumber = function () { + return m_binNumber; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set bin number for the material + * + * @param binNo + */ + //////////////////////////////////////////////////////////////////////////// + this.setBinNumber = function (binNo) { + m_binNumber = binNo; + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if incoming attribute already exists in the material + * + * @param attr + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.exists = function (attr) { + if (attr.type() === vgl.materialAttribute.Texture) { + return m_textureAttributes.hasOwnProperty(attr); + } + + return m_attributes.hasOwnProperty(attr); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get uniform given a name + + * @param name Uniform name + * @returns {vgl.uniform} + */ + //////////////////////////////////////////////////////////////////////////// + this.uniform = function (name) { + if (m_shaderProgram) { + return m_shaderProgram.uniform(name); + } + + return null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get material attribute + + * @param attr Attribute name + * @returns {vgl.materialAttribute} + */ + //////////////////////////////////////////////////////////////////////////// + this.attribute = function (name) { + if (m_attributes.hasOwnProperty(name)) { + return m_attributes[name]; + } + + if (m_textureAttributes.hasOwnProperty(name)) { + return m_textureAttributes[name]; + } + + return null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set a new attribute for the material + * + * This method replace any existing attribute except for textures as + * materials can have multiple textures. + * + * @param attr + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setAttribute = function (attr) { + if (attr.type() === vgl.materialAttributeType.Texture && + m_textureAttributes[attr.textureUnit()] !== attr) { + m_textureAttributes[attr.textureUnit()] = attr; + m_this.modified(); + return true; + } + + if (m_attributes[attr.type()] === attr) { + return false; + } + + // Shader is a very special attribute + if (attr.type() === vgl.materialAttributeType.ShaderProgram) { + m_shaderProgram = attr; + } + + m_attributes[attr.type()] = attr; + m_this.modified(); + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add a new attribute to the material. + * + * @param attr + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.addAttribute = function (attr) { + if (m_this.exists(attr)) { + return false; + } + + if (attr.type() === vgl.materialAttributeType.Texture) { + // TODO Currently we don't check if we are replacing or not. + // It would be nice to have a flag for it. + m_textureAttributes[attr.textureUnit()] = attr; + m_this.modified(); + return true; + } + + // Shader is a very special attribute + if (attr.type() === vgl.materialAttributeType.ShaderProgram) { + m_shaderProgram = attr; + } + + m_attributes[attr.type()] = attr; + m_this.modified(); + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return shader program used by the material + * + * @returns {vgl.shaderProgram} + */ + //////////////////////////////////////////////////////////////////////////// + this.shaderProgram = function () { + return m_shaderProgram; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Setup (initialize) the material attribute + * + * @param renderState + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this._setup = function (renderState) { + renderState = renderState; /* unused parameter */ + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove any resources acquired before deletion + * + * @param renderState + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this._cleanup = function (renderState) { + for (var key in m_attributes) { + if (m_attributes.hasOwnProperty(key)) { + m_attributes[key]._cleanup(renderState); + } + } + + for (key in m_textureAttributes) { + if (m_textureAttributes.hasOwnProperty(key)) { + m_textureAttributes[key]._cleanup(renderState); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bind and activate material states + * + * @param renderState + */ + //////////////////////////////////////////////////////////////////////////// + this.bind = function (renderState) { + var key = null; + + m_shaderProgram.bind(renderState); + + for (key in m_attributes) { + if (m_attributes.hasOwnProperty(key)) { + if (m_attributes[key] !== m_shaderProgram) { + m_attributes[key].bind(renderState); + } + } + } + + for (key in m_textureAttributes) { + if (m_textureAttributes.hasOwnProperty(key)) { + m_textureAttributes[key].bind(renderState); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Undo-bind and de-activate material states + * + * @param renderState + */ + //////////////////////////////////////////////////////////////////////////// + this.undoBind = function (renderState) { + var key = null; + for (key in m_attributes) { + if (m_attributes.hasOwnProperty(key)) { + m_attributes[key].undoBind(renderState); + } + } + + for (key in m_textureAttributes) { + if (m_textureAttributes.hasOwnProperty(key)) { + m_textureAttributes[key].undoBind(renderState); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bind vertex data + * + * @param renderState + * @param key + */ + //////////////////////////////////////////////////////////////////////////// + this.bindVertexData = function (renderState, key) { + var i = null; + + for (i in m_attributes) { + if (m_attributes.hasOwnProperty(i)) { + m_attributes[i].bindVertexData(renderState, key); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Undo bind vertex data + * + * @param renderState + * @param key + */ + //////////////////////////////////////////////////////////////////////////// + this.undoBindVertexData = function (renderState, key) { + var i = null; + + for (i in m_attributes) { + if (m_attributes.hasOwnProperty(i)) { + m_attributes.undoBindVertexData(renderState, key); + } + } + }; + + return m_this; + }; + + vgl.material.RenderBin = { + 'Base' : 0, + 'Default' : 100, + 'Opaque' : 100, + 'Transparent' : 1000, + 'Overlay' : 10000 + }; + + inherit(vgl.material, vgl.graphicsObject); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, vec2, vec3, vec4, mat4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class renderState + * + * @returns {vgl.renderState} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.renderState = function () { + 'use strict'; + + this.m_context = null; + this.m_modelViewMatrix = mat4.create(); + this.m_normalMatrix = mat4.create(); + this.m_projectionMatrix = null; + this.m_material = null; + this.m_mapper = null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class renderer * + * + * @returns {vgl.renderer} + */ + //////////////////////////////////////////////////////////////////////////// + vgl.renderer = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.renderer)) { + return new vgl.renderer(arg); + } + vgl.graphicsObject.call(this); + arg = arg || {}; + + /** @private */ + var m_this = this; + m_this.m_renderWindow = null; + m_this.m_contextChanged = false; + m_this.m_sceneRoot = new vgl.groupNode(); + m_this.m_camera = new vgl.camera(arg); + m_this.m_nearClippingPlaneTolerance = null; + m_this.m_x = 0; + m_this.m_y = 0; + m_this.m_width = 0; + m_this.m_height = 0; + m_this.m_resizable = true; + m_this.m_resetScene = true; + m_this.m_layer = 0; + m_this.m_renderPasses = null; + m_this.m_resetClippingRange = true; + m_this.m_depthBits = null; + + m_this.m_camera.addChild(m_this.m_sceneRoot); + + //////////////////////////////////////////////////////////////////////////// + /** + * Get width of the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.width = function () { + return m_this.m_width; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get height of the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.height = function () { + return m_this.m_height; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get layer this renderer is associated with + * + * @return {Number} + */ + //////////////////////////////////////////////////////////////////////////// + this.layer = function () { + return m_this.m_layer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set the layer this renderer is associated with. + * + * @param layerNo + */ + //////////////////////////////////////////////////////////////////////////// + this.setLayer = function (layerNo) { + m_this.m_layer = layerNo; + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * + */ + //////////////////////////////////////////////////////////////////////////// + this.isResizable = function () { + return m_this.m_resizable; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * + */ + //////////////////////////////////////////////////////////////////////////// + this.setResizable = function (r) { + m_this.m_resizable = r; + }; + + + //////////////////////////////////////////////////////////////////////////// + /** + * Return render window (owner) of the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.renderWindow = function () { + return m_this.m_renderWindow; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set render window for the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.setRenderWindow = function (renWin) { + if (m_this.m_renderWindow !== renWin) { + if (m_this.m_renderWindow) { + m_this.m_renderWindow.removeRenderer(this); + } + m_this.m_renderWindow = renWin; + m_this.m_contextChanged = true; + m_this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get background color + */ + //////////////////////////////////////////////////////////////////////////// + this.backgroundColor = function () { + return m_this.m_camera.clearColor(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set background color of the renderer + * + * @param r + * @param g + * @param b + * @param a + */ + //////////////////////////////////////////////////////////////////////////// + this.setBackgroundColor = function (r, g, b, a) { + m_this.m_camera.setClearColor(r, g, b, a); + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get scene root + * + * @returns {vgl.groupNode} + */ + //////////////////////////////////////////////////////////////////////////// + this.sceneRoot = function () { + return m_this.m_sceneRoot; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get main camera of the renderer + * + * @returns {vgl.camera} + */ + //////////////////////////////////////////////////////////////////////////// + this.camera = function () { + return m_this.m_camera; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render the scene + */ + //////////////////////////////////////////////////////////////////////////// + this.render = function () { + var i, renSt, children, actor = null, sortedActors = [], + mvMatrixInv = mat4.create(), clearColor = null; + + renSt = new vgl.renderState(); + renSt.m_renderer = m_this; + renSt.m_context = m_this.renderWindow().context(); + if (!m_this.m_depthBits || m_this.m_contextChanged) { + m_this.m_depthBits = renSt.m_context.getParameter(vgl.GL.DEPTH_BITS); + } + renSt.m_contextChanged = m_this.m_contextChanged; + + if (m_this.m_renderPasses) { + for (i = 0; i < m_this.m_renderPasses.length; i += 1) { + if (m_this.m_renderPasses[i].render(renSt)) { + // Stop the rendering if render pass returns false + console.log('returning'); + m_this.m_renderPasses[i].remove(renSt); + return; + } + m_this.m_renderPasses[i].remove(renSt); + } + } + + renSt.m_context.enable(vgl.GL.DEPTH_TEST); + renSt.m_context.depthFunc(vgl.GL.LEQUAL); + + /*jshint bitwise: false */ + if (m_this.m_camera.clearMask() & vgl.GL.COLOR_BUFFER_BIT) { + clearColor = m_this.m_camera.clearColor(); + renSt.m_context.clearColor(clearColor[0], clearColor[1], + clearColor[2], clearColor[3]); + } + + if (m_this.m_camera.clearMask() & vgl.GL.DEPTH_BUFFER_BIT) { + renSt.m_context.clearDepth(m_this.m_camera.clearDepth()); + } + /*jshint bitwise: true */ + + renSt.m_context.clear(m_this.m_camera.clearMask()); + + // Set the viewport for this renderer + renSt.m_context.viewport(m_this.m_x, m_this.m_y, + m_this.m_width, m_this.m_height); + + children = m_this.m_sceneRoot.children(); + + if (children.length > 0 && m_this.m_resetScene) { + m_this.resetCamera(); + m_this.m_resetScene = false; + } + + for (i = 0; i < children.length; i += 1) { + actor = children[i]; + + // Compute the bounds even if the actor is not visible + actor.computeBounds(); + + // If bin number is < 0, then don't even bother + // rendering the data + if (actor.visible() && actor.material().binNumber() >= 0) { + sortedActors.push([actor.material().binNumber(), actor]); + } + } + + // Now perform sorting + sortedActors.sort(function (a, b) {return a[0] - b[0];}); + + for (i = 0; i < sortedActors.length; i += 1) { + actor = sortedActors[i][1]; + if (actor.referenceFrame() === + vgl.boundingObject.ReferenceFrame.Relative) { + var view = m_this.m_camera.viewMatrix(); + /* If the view matrix is a plain array, keep it as such. This is + * intended to preserve precision, and will only be the case if the + * view matrix was created by delibrately setting it as an array. */ + if (view instanceof Array) { + renSt.m_modelViewMatrix = new Array(16); + } + mat4.multiply(renSt.m_modelViewMatrix, view, actor.matrix()); + renSt.m_projectionMatrix = m_this.m_camera.projectionMatrix(); + renSt.m_modelViewAlignment = m_this.m_camera.viewAlignment(); + } else { + renSt.m_modelViewMatrix = actor.matrix(); + renSt.m_modelViewAlignment = null; + renSt.m_projectionMatrix = mat4.create(); + mat4.ortho(renSt.m_projectionMatrix, + 0, m_this.m_width, 0, m_this.m_height, -1, 1); + } + + mat4.invert(mvMatrixInv, renSt.m_modelViewMatrix); + mat4.transpose(renSt.m_normalMatrix, mvMatrixInv); + renSt.m_material = actor.material(); + renSt.m_mapper = actor.mapper(); + + // TODO Fix this shortcut + renSt.m_material.bind(renSt); + renSt.m_mapper.render(renSt); + renSt.m_material.undoBind(renSt); + } + + renSt.m_context.finish(); + m_this.m_contextChanged = false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Automatically set up the camera based on visible actors + */ + //////////////////////////////////////////////////////////////////////////// + this.resetCamera = function () { + m_this.m_camera.computeBounds(); + + var vn = m_this.m_camera.directionOfProjection(), + visibleBounds = m_this.m_camera.bounds(), + center = [ + (visibleBounds[0] + visibleBounds[1]) / 2.0, + (visibleBounds[2] + visibleBounds[3]) / 2.0, + (visibleBounds[4] + visibleBounds[5]) / 2.0 + ], + diagonals = [ + visibleBounds[1] - visibleBounds[0], + visibleBounds[3] - visibleBounds[2], + visibleBounds[5] - visibleBounds[4] + ], + radius = 0.0, + aspect = m_this.m_camera.viewAspect(), + angle = m_this.m_camera.viewAngle(), + distance = null, + vup = null; + + if (diagonals[0] > diagonals[1]) { + if (diagonals[0] > diagonals[2]) { + radius = diagonals[0] / 2.0; + } else { + radius = diagonals[2] / 2.0; + } + } else { + if (diagonals[1] > diagonals[2]) { + radius = diagonals[1] / 2.0; + } else { + radius = diagonals[2] / 2.0; + } + } + + // @todo Need to figure out what's happening here + if (aspect >= 1.0) { + angle = 2.0 * Math.atan(Math.tan(angle * 0.5) / aspect); + } else { + angle = 2.0 * Math.atan(Math.tan(angle * 0.5) * aspect); + } + + distance = radius / Math.sin(angle * 0.5); + vup = m_this.m_camera.viewUpDirection(); + + if (Math.abs(vec3.dot(vup, vn)) > 0.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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check whether or not whether or not the bounds are valid + */ + //////////////////////////////////////////////////////////////////////////// + this.hasValidBounds = function (bounds) { + if (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) { + return false; + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Recalculate camera's clipping range + */ + //////////////////////////////////////////////////////////////////////////// + this.resetCameraClippingRange = function (bounds) { + if (typeof bounds === 'undefined') { + m_this.m_camera.computeBounds(); + bounds = m_this.m_camera.bounds(); + } + + if (!m_this.hasValidBounds(bounds)) { + return; + } + + 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) { + return; + } + + // Set the max near clipping plane and the min far clipping plane + range[0] = a * bounds[0] + b * bounds[2] + c * bounds[4] + d; + range[1] = 1e-18; + + // Find the closest / farthest bounding box vertex + for (k = 0; k < 2; k += 1) { + for (j = 0; j < 2; j += 1) { + for (i = 0; i < 2; i += 1) { + dist = a * bounds[i] + b * bounds[2 + j] + c * bounds[4 + k] + d; + range[0] = (dist < range[0]) ? (dist) : (range[0]); + range[1] = (dist > range[1]) ? (dist) : (range[1]); + } + } + } + + // Do not let the range behind the camera throw off the calculation. + if (range[0] < 0.0) { + range[0] = 0.0; + } + + // Give ourselves a little breathing room + range[0] = 0.99 * range[0] - (range[1] - range[0]) * 0.5; + range[1] = 1.01 * range[1] + (range[1] - range[0]) * 0.5; + + // Make sure near is not bigger than far + range[0] = (range[0] >= range[1]) ? (0.01 * range[1]) : (range[0]); + + // Make sure near is at least some fraction of far - this prevents near + // from being behind the camera or too close in front. How close is too + // close depends on the resolution of the depth buffer. + if (!m_this.m_nearClippingPlaneTolerance) { + m_this.m_nearClippingPlaneTolerance = 0.01; + + if (m_this.m_depthBits && m_this.m_depthBits > 16) { + m_this.m_nearClippingPlaneTolerance = 0.001; + } + } + + // make sure the front clipping range is not too far from the far clippnig + // range, this is to make sure that the zbuffer resolution is effectively + // used. + if (range[0] < m_this.m_nearClippingPlaneTolerance * range[1]) { + range[0] = m_this.m_nearClippingPlaneTolerance * range[1]; + } + + m_this.m_camera.setClippingRange(range[0], range[1]); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Resize viewport given a width and height + */ + //////////////////////////////////////////////////////////////////////////// + this.resize = function (width, height) { + if (!width || !height) { + return; + } + // @note: where do m_this.m_x and m_this.m_y come from? + m_this.positionAndResize(m_this.m_x, m_this.m_y, width, height); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Resize viewport given a position, width and height + */ + //////////////////////////////////////////////////////////////////////////// + this.positionAndResize = function (x, y, width, height) { + var i; + + // TODO move this code to camera + if (x < 0 || y < 0 || width <= 0 || height <= 0) { + console.log('[error] Invalid position and resize values', + x, y, width, height); + return; + } + + //If we're allowing this renderer to resize ... + 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(); + } + + if (m_this.m_renderPasses) { + for (i = 0; i < m_this.m_renderPasses.length; i += 1) { + m_this.m_renderPasses[i].resize(width, height); + m_this.m_renderPasses[i].renderer().positionAndResize(x, y, width, height); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add new actor to the collection + * + * @param actor + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.addActor = function (actor) { + if (actor instanceof vgl.actor) { + m_this.m_sceneRoot.addChild(actor); + m_this.modified(); + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return true if this renderer has this actor attached, false otherwise. + * + * @param actor + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.hasActor = function (actor) { + return m_this.m_sceneRoot.hasChild(actor); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add an array of actors to the collection + */ + //////////////////////////////////////////////////////////////////////////// + this.addActors = function (actors) { + var i = null; + if (actors instanceof Array) { + for (i = 0; i < actors.length; i += 1) { + m_this.m_sceneRoot.addChild(actors[i]); + } + m_this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove the actor from the collection + * + * @param actor + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.removeActor = function (actor) { + if (m_this.m_sceneRoot.children().indexOf(actor) !== -1) { + /* When we remove an actor, free the VBOs of the mapper and mark the + * mapper as modified; it will reallocate VBOs as necessary. */ + if (actor.mapper()) { + actor.mapper().deleteVertexBufferObjects(); + actor.mapper().modified(); + } + m_this.m_sceneRoot.removeChild(actor); + m_this.modified(); + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove actors from the collection + * + * @param actors + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.removeActors = function (actors) { + if (!(actors instanceof Array)) { + return false; + } + + var i; + for (i = 0; i < actors.length; i += 1) { + m_this.m_sceneRoot.removeChild(actors[i]); + } + m_this.modified(); + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove all actors for a renderer + * + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.removeAllActors = function () { + return m_this.m_sceneRoot.removeChildren(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Transform a point in the world space to display space + */ + //////////////////////////////////////////////////////////////////////////// + this.worldToDisplay = function (worldPt, viewMatrix, projectionMatrix, width, + height) { + var viewProjectionMatrix = mat4.create(), + winX = null, + winY = null, + winZ = null, + winW = null, + clipPt = null; + + + mat4.multiply(viewProjectionMatrix, projectionMatrix, viewMatrix); + + // Transform world to clipping coordinates + clipPt = vec4.create(); + vec4.transformMat4(clipPt, worldPt, viewProjectionMatrix); + + if (clipPt[3] !== 0.0) { + clipPt[0] = clipPt[0] / clipPt[3]; + clipPt[1] = clipPt[1] / clipPt[3]; + clipPt[2] = clipPt[2] / clipPt[3]; + clipPt[3] = 1.0; + } + + winX = (((clipPt[0]) + 1) / 2.0) * width; + // We calculate -point3D.getY() because the screen Y axis is + // oriented top->down + winY = ((1 - clipPt[1]) / 2.0) * height; + winZ = clipPt[2]; + winW = clipPt[3]; + + return vec4.fromValues(winX, winY, winZ, winW); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Transform a point in display space to world space + * @param displayPt + * @param viewMatrix + * @param projectionMatrix + * @param width + * @param height + * @returns {vec4} + */ + //////////////////////////////////////////////////////////////////////////// + this.displayToWorld = function (displayPt, viewMatrix, projectionMatrix, + width, height) { + var x = (2.0 * displayPt[0] / width) - 1, + y = -(2.0 * displayPt[1] / height) + 1, + z = displayPt[2], + viewProjectionInverse = mat4.create(), + worldPt = null; + + mat4.multiply(viewProjectionInverse, projectionMatrix, viewMatrix); + mat4.invert(viewProjectionInverse, viewProjectionInverse); + + worldPt = vec4.fromValues(x, y, z, 1); + vec4.transformMat4(worldPt, worldPt, viewProjectionInverse); + if (worldPt[3] !== 0.0) { + worldPt[0] = worldPt[0] / worldPt[3]; + worldPt[1] = worldPt[1] / worldPt[3]; + worldPt[2] = worldPt[2] / worldPt[3]; + worldPt[3] = 1.0; + } + + return worldPt; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the focusDisplayPoint + * @returns {vec4} + */ + //////////////////////////////////////////////////////////////////////////// + this.focusDisplayPoint = function () { + var focalPoint = m_this.m_camera.focalPoint(), + focusWorldPt = vec4.fromValues( + focalPoint[0], focalPoint[1], focalPoint[2], 1); + + return m_this.worldToDisplay( + focusWorldPt, m_this.m_camera.viewMatrix(), + m_this.m_camera.projectionMatrix(), m_this.m_width, m_this.m_height); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Will the scene be reset. + * @returns {bool} + */ + //////////////////////////////////////////////////////////////////////////// + this.resetScene = function () { + return m_this.m_resetScene; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * If true the scene will be reset, otherwise the scene will not be + * automatically reset. + * + * @param reset + */ + //////////////////////////////////////////////////////////////////////////// + this.setResetScene = function (reset) { + if (m_this.m_resetScene !== reset) { + m_this.m_resetScene = reset; + m_this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Will the clipping range be reset + * @returns {bool} + */ + //////////////////////////////////////////////////////////////////////////// + this.resetClippingRange = function () { + return m_this.m_resetClippingRange; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * If true the camera clipping range will be reset, otherwise the scene will + * not be automatically reset. + * + * @param reset + */ + //////////////////////////////////////////////////////////////////////////// + this.setResetClippingRange = function (reset) { + if (m_this.m_resetClippingRange !== reset) { + m_this.m_resetClippingRange = reset; + m_this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * + */ + //////////////////////////////////////////////////////////////////////////// + this.addRenderPass = function (renPass) { + var i; + + if (m_this.m_renderPasses) { + for (i = 0; i < m_this.m_renderPasses.length; i += 1) { + if (renPass === m_this.m_renderPasses[i]) { + return; + } + } + } + + m_this.m_renderPasses = []; + m_this.m_renderPasses.push(renPass); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * + */ + //////////////////////////////////////////////////////////////////////////// + this.removeRenderPass = function (renPass) { + renPass = renPass; // TODO Implement this + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * + */ + //////////////////////////////////////////////////////////////////////////// + this._cleanup = function (renderState) { + var children = m_this.m_sceneRoot.children(); + for (var i = 0; i < children.length; i += 1) { + var actor = children[i]; + actor.material()._cleanup(renderState); + actor.mapper()._cleanup(renderState); + } + + m_this.m_sceneRoot.removeChildren(); + }; + + return m_this; + }; + + inherit(vgl.renderer, vgl.graphicsObject); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, vec4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class renderWindow + * + * @class + * @returns {vgl.renderWindow} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.renderWindow = function (canvas) { + 'use strict'; + + if (!(this instanceof vgl.renderWindow)) { + return new vgl.renderWindow(canvas); + } + vgl.graphicsObject.call(this); + + /** @private */ + var m_this = this, + m_x = 0, + m_y = 0, + m_width = 400, + m_height = 400, + m_canvas = canvas, + m_activeRender = null, + m_renderers = [], + m_context = null; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get size of the render window + * + * @returns {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.windowSize = function () { + return [m_width, m_height]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set size of the render window + * + * @param width + * @param height + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setWindowSize = function (width, height) { + + if (m_width !== width || m_height !== height) { + m_width = width; + m_height = height; + + m_this.modified(); + + return true; + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get window position (top left coordinates) + * + * @returns {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.windowPosition = function () { + return [m_x, m_y]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set window position (top left coordinates) + * + * @param x + * @param y + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.setWindowPosition = function (x, y) { + if ((m_x !== x) || (m_y !== y)) { + m_x = x; + m_y = y; + m_this.modified(); + return true; + } + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return all renderers contained in the render window + * @returns {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.renderers = function () { + return m_renderers; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get active renderer of the the render window + * + * @returns vgl.renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.activeRenderer = function () { + return m_activeRender; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add renderer to the render window + * + * @param ren + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.addRenderer = function (ren) { + if (m_this.hasRenderer(ren) === false) { + m_renderers.push(ren); + ren.setRenderWindow(m_this); + if (m_activeRender === null) { + m_activeRender = ren; + } + if (ren.layer() !== 0) { + ren.camera().setClearMask(vgl.GL.DepthBufferBit); + } + m_this.modified(); + return true; + } + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove renderer from the render window + * + * @param ren + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.removeRenderer = function (ren) { + var index = m_renderers.indexOf(ren); + if (index !== -1) { + if (m_activeRender === ren) { + m_activeRender = null; + } + m_renderers.splice(index, 1); + m_this.modified(); + return true; + } + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return a renderer at a given index + * + * @param index + * @returns {vgl.renderer} + */ + //////////////////////////////////////////////////////////////////////////// + this.getRenderer = function (index) { + if (index < m_renderers.length) { + return m_renderers[index]; + } + + console.log('[WARNING] Out of index array'); + return null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if the renderer exists + * + * @param ren + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.hasRenderer = function (ren) { + var i; + for (i = 0; i < m_renderers.length; i += 1) { + if (ren === m_renderers[i]) { + return true; + } + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Resize window + * + * @param width + * @param height + */ + //////////////////////////////////////////////////////////////////////////// + this.resize = function (width, height) { + m_this.positionAndResize(m_x, m_y, width, height); + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Resize and reposition the window + * + * @param x + * @param y + * @param width + * @param height + */ + //////////////////////////////////////////////////////////////////////////// + this.positionAndResize = function (x, y, width, height) { + m_x = x; + m_y = y; + m_width = width; + m_height = height; + var i; + for (i = 0; i < m_renderers.length; i += 1) { + m_renderers[i].positionAndResize(m_x, m_y, m_width, m_height); + } + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create the window + * + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this._setup = function (renderState) { + renderState = renderState; /* unused parameter */ + m_context = null; + + try { + // Try to grab the standard context. If it fails, fallback to + // experimental. + m_context = m_canvas.getContext('webgl') || + m_canvas.getContext('experimental-webgl'); + + // Set width and height of renderers if not set already + var i; + for (i = 0; i < m_renderers.length; i += 1) { + if ((m_renderers[i].width() > m_width) || + m_renderers[i].width() === 0 || + (m_renderers[i].height() > m_height) || + m_renderers[i].height() === 0) { + m_renderers[i].resize(m_x, m_y, m_width, m_height); + } + } + + return true; + } + catch (e) { + } + + // If we don't have a GL context, give up now + if (!m_context) { + console('[ERROR] Unable to initialize WebGL. Your browser may not support it.'); + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return current GL context + */ + //////////////////////////////////////////////////////////////////////////// + this.context = function () { + return m_context; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Delete this window and release any graphics resources + */ + //////////////////////////////////////////////////////////////////////////// + this._cleanup = function (renderState) { + var i; + for (i = 0; i < m_renderers.length; i += 1) { + m_renderers[i]._cleanup(renderState); + } + vgl.clearCachedShaders(renderState ? renderState.m_context : null); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render the scene + */ + //////////////////////////////////////////////////////////////////////////// + this.render = function () { + var i; + m_renderers.sort(function (a, b) {return a.layer() - b.layer();}); + for (i = 0; i < m_renderers.length; i += 1) { + m_renderers[i].render(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the focusDisplayPoint from the activeRenderer + * @returns {vec4} + */ + //////////////////////////////////////////////////////////////////////////// + this.focusDisplayPoint = function () { + return m_activeRender.focusDisplayPoint(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Transform a point in display space to world space + * @param {Number} x + * @param {Number} y + * @param {vec4} focusDisplayPoint + * @returns {vec4} + */ + //////////////////////////////////////////////////////////////////////////// + this.displayToWorld = function (x, y, focusDisplayPoint, ren) { + ren = ren === undefined ? ren = m_activeRender : ren; + + var camera = ren.camera(); + if (!focusDisplayPoint) { + focusDisplayPoint = ren.focusDisplayPoint(); + } + + return ren.displayToWorld( + vec4.fromValues(x, y, focusDisplayPoint[2], 1.0), camera.viewMatrix(), + camera.projectionMatrix(), m_width, m_height); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Transform a point in display space to world space + * @param {Number} x + * @param {Number} y + * @param {vec4} focusDisplayPoint + * @returns {vec4} + */ + //////////////////////////////////////////////////////////////////////////// + this.worldToDisplay = function (x, y, z, ren) { + ren = ren === undefined ? ren = m_activeRender : ren; + var camera = ren.camera(); + return ren.worldToDisplay( + vec4.fromValues(x, y, z, 1.0), camera.viewMatrix(), + camera.projectionMatrix(), m_width, m_height); + }; + + return m_this; + }; + + inherit(vgl.renderWindow, vgl.graphicsObject); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, vec3, vec4, mat4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class camera + * + * @class + * @returns {vgl.camera} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.camera = function (arg) { + 'use strict'; + + if (!(this instanceof vgl.camera)) { + return new vgl.camera(arg); + } + vgl.groupNode.call(this); + arg = arg || {}; + + /** @private */ + var m_viewAngle = (Math.PI * 30) / 180.0, + m_position = vec4.fromValues(0.0, 0.0, 1.0, 1.0), + m_focalPoint = vec4.fromValues(0.0, 0.0, 0.0, 1.0), + m_centerOfRotation = vec3.fromValues(0.0, 0.0, 0.0), + m_viewUp = vec4.fromValues(0.0, 1.0, 0.0, 0.0), + m_rightDir = vec4.fromValues(1.0, 0.0, 0.0, 0.0), + m_near = 0.01, + m_far = 10000.0, + m_viewAspect = 1.0, + m_directionOfProjection = vec4.fromValues(0.0, 0.0, -1.0, 0.0), + m_viewPlaneNormal = vec4.fromValues(0.0, 0.0, 1.0, 0.0), + m_viewMatrix = mat4.create(), + m_projectionMatrix = mat4.create(), + m_computeModelViewMatrixTime = vgl.timestamp(), + m_computeProjectMatrixTime = vgl.timestamp(), + m_left = -1.0, + m_right = 1.0, + m_top = +1.0, + m_bottom = -1.0, + m_parallelExtents = {zoom: 1, tilesize: 256}, + m_enableTranslation = true, + m_enableRotation = true, + m_enableScale = true, + m_enableParallelProjection = false, + m_clearColor = [0.0, 0.0, 0.0, 0.0], + m_clearDepth = 1.0, + /*jshint bitwise: false */ + m_clearMask = vgl.GL.COLOR_BUFFER_BIT | + vgl.GL.DEPTH_BUFFER_BIT; + /*jshint bitwise: true */ + + if (arg.parallelProjection !== undefined) { + m_enableParallelProjection = arg.parallelProjection ? true : false; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Get view angle of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.viewAngle = function () { + return m_viewAngle; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set view angle of the camera in degrees, which is converted to radians. + */ + //////////////////////////////////////////////////////////////////////////// + this.setViewAngleDegrees = function (a) { + m_viewAngle = (Math.PI * a) / 180.0; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set view angle of the camera in degrees, which is converted to radians. + */ + //////////////////////////////////////////////////////////////////////////// + this.setViewAngle = function (a) { + if (m_enableScale) { + m_viewAngle = a; + this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get position of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.position = function () { + return m_position; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set position of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.setPosition = function (x, y, z) { + if (m_enableTranslation) { + m_position = vec4.fromValues(x, y, z, 1.0); + this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get focal point of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.focalPoint = function () { + return m_focalPoint; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set focal point of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.setFocalPoint = function (x, y, z) { + if (m_enableRotation && m_enableTranslation) { + m_focalPoint = vec4.fromValues(x, y, z, 1.0); + this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get view-up direction of camera + */ + //////////////////////////////////////////////////////////////////////////// + this.viewUpDirection = function () { + return m_viewUp; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set view-up direction of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.setViewUpDirection = function (x, y, z) { + m_viewUp = vec4.fromValues(x, y, z, 0); + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get center of rotation for camera + */ + //////////////////////////////////////////////////////////////////////////// + this.centerOfRotation = function () { + return m_centerOfRotation; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set center of rotation for camera + */ + //////////////////////////////////////////////////////////////////////////// + this.setCenterOfRotation = function (centerOfRotation) { + m_centerOfRotation = centerOfRotation; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get clipping range of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.clippingRange = function () { + return [m_near, m_far]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set clipping range of the camera + */ + //////////////////////////////////////////////////////////////////////////// + this.setClippingRange = function (near, far) { + m_near = near; + m_far = far; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get view aspect + */ + //////////////////////////////////////////////////////////////////////////// + this.viewAspect = function () { + return m_viewAspect; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set view aspect + */ + //////////////////////////////////////////////////////////////////////////// + this.setViewAspect = function (aspect) { + m_viewAspect = aspect; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return active mode for scaling (enabled / disabled) + */ + //////////////////////////////////////////////////////////////////////////// + this.enableScale = function () { + return m_enableScale; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Enable/disable scaling + */ + //////////////////////////////////////////////////////////////////////////// + this.setEnableScale = function (flag) { + if (flag !== m_enableScale) { + m_enableScale = flag; + this.modified(); + return true; + } + + return m_enableScale; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return active mode for rotation (enabled / disabled) + */ + //////////////////////////////////////////////////////////////////////////// + this.enableRotation = function () { + return m_enableRotation; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Enable / disable rotation + */ + //////////////////////////////////////////////////////////////////////////// + this.setEnableRotation = function (flag) { + if (flag !== m_enableRotation) { + m_enableRotation = flag; + this.modified(); + return true; + } + + return m_enableRotation; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return active mode for translation (enabled/disabled) + */ + //////////////////////////////////////////////////////////////////////////// + this.enableTranslation = function () { + return m_enableTranslation; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Enable / disable translation + */ + //////////////////////////////////////////////////////////////////////////// + this.setEnableTranslation = function (flag) { + if (flag !== m_enableTranslation) { + m_enableTranslation = flag; + this.modified(); + return true; + } + + return m_enableTranslation; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return if parallel projection is enabled + */ + //////////////////////////////////////////////////////////////////////////// + this.isEnabledParallelProjection = function () { + return m_enableParallelProjection; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Enable / disable parallel projection + */ + //////////////////////////////////////////////////////////////////////////// + this.enableParallelProjection = function (flag) { + if (flag !== m_enableParallelProjection) { + m_enableParallelProjection = flag; + this.modified(); + return true; + } + + return m_enableParallelProjection; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Enable / disable parallel projection + */ + //////////////////////////////////////////////////////////////////////////// + this.setEnableParallelProjection = function (flag) { + return this.enableParallelProjection(flag); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get parallel projection parameters + */ + //////////////////////////////////////////////////////////////////////////// + this.parallelProjection = function () { + return {left: m_left, right: m_right, top: m_top, bottom: m_bottom}; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set parallel projection parameters + */ + //////////////////////////////////////////////////////////////////////////// + this.setParallelProjection = function (left, right, top, bottom) { + m_left = left; + m_right = right; + m_top = top; + m_bottom = bottom; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get parallel projection extents parameters + */ + //////////////////////////////////////////////////////////////////////////// + this.parallelExtents = function () { + return m_parallelExtents; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set parallel projection extents parameters + */ + //////////////////////////////////////////////////////////////////////////// + this.setParallelExtents = function (extents) { + var prop = ['width', 'height', 'zoom', 'tilesize'], mod = false, i, key; + for (i = 0; i < prop.length; i += 1) { + key = prop[i]; + if (extents[key] !== undefined && + extents[key] !== m_parallelExtents[key]) { + m_parallelExtents[key] = extents[key]; + mod = true; + } + } + if (mod && m_parallelExtents.width && m_parallelExtents.height && + m_parallelExtents.zoom !== undefined && m_parallelExtents.tilesize) { + var unitsPerPixel = this.unitsPerPixel(m_parallelExtents.zoom, + m_parallelExtents.tilesize); + m_right = unitsPerPixel * m_parallelExtents.width / 2; + m_left = -m_right; + m_top = unitsPerPixel * m_parallelExtents.height / 2; + m_bottom = -m_top; + this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute the units per pixel. + * + * @param zoom: tile zoom level. + * @param tilesize: number of pixels per tile (defaults to 256). + * @returns: unitsPerPixel. + */ + //////////////////////////////////////////////////////////////////////////// + this.unitsPerPixel = function (zoom, tilesize) { + tilesize = tilesize || 256; + return 360.0 * Math.pow(2, -zoom) / tilesize; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return direction of projection + */ + //////////////////////////////////////////////////////////////////////////// + this.directionOfProjection = function () { + this.computeDirectionOfProjection(); + return m_directionOfProjection; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return view plane normal direction + */ + //////////////////////////////////////////////////////////////////////////// + this.viewPlaneNormal = function () { + this.computeViewPlaneNormal(); + return m_viewPlaneNormal; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return view-matrix for the camera This method does not compute the + * view-matrix for the camera. It is assumed that a call to computeViewMatrix + * has been made earlier. + * + * @returns {mat4} + */ + //////////////////////////////////////////////////////////////////////////// + this.viewMatrix = function () { + 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. + * @param {boolean} preserveType: if true, clone the input using slice. This + * can be used to ensure the array is a specific precision. + */ + //////////////////////////////////////////////////////////////////////////// + this.setViewMatrix = function (view, preserveType) { + if (!preserveType) { + mat4.copy(m_viewMatrix, view); + } else { + m_viewMatrix = view.slice(); + } + m_computeModelViewMatrixTime.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return camera projection matrix This method does not compute the + * projection-matrix for the camera. It is assumed that a call to + * computeProjectionMatrix has been made earlier. + * + * @returns {mat4} + */ + //////////////////////////////////////////////////////////////////////////// + this.projectionMatrix = function () { + 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 + * + * @returns {number} + */ + //////////////////////////////////////////////////////////////////////////// + this.clearMask = function () { + return m_clearMask; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set clear mask for camera + * + * @param mask + */ + //////////////////////////////////////////////////////////////////////////// + this.setClearMask = function (mask) { + m_clearMask = mask; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get clear color (background color) of the camera + * + * @returns {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.clearColor = function () { + return m_clearColor; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set clear color (background color) for the camera + * + * @param color RGBA + */ + //////////////////////////////////////////////////////////////////////////// + this.setClearColor = function (r, g, b, a) { + m_clearColor[0] = r; + m_clearColor[1] = g; + m_clearColor[2] = b; + m_clearColor[3] = a; + + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * + * @returns {{1.0: null}} + */ + //////////////////////////////////////////////////////////////////////////// + this.clearDepth = function () { + return m_clearDepth; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * + * @param depth + */ + //////////////////////////////////////////////////////////////////////////// + this.setClearDepth = function (depth) { + m_clearDepth = depth; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute direction of projection + */ + //////////////////////////////////////////////////////////////////////////// + this.computeDirectionOfProjection = function () { + vec3.subtract(m_directionOfProjection, m_focalPoint, m_position); + vec3.normalize(m_directionOfProjection, m_directionOfProjection); + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute view plane normal + */ + //////////////////////////////////////////////////////////////////////////// + this.computeViewPlaneNormal = function () { + m_viewPlaneNormal[0] = -m_directionOfProjection[0]; + m_viewPlaneNormal[1] = -m_directionOfProjection[1]; + m_viewPlaneNormal[2] = -m_directionOfProjection[2]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Move camera closer or further away from the scene + */ + //////////////////////////////////////////////////////////////////////////// + this.zoom = function (d, dir) { + if (d === 0) { + return; + } + + if (!m_enableTranslation) { + return; + } + + d = d * vec3.distance(m_focalPoint, m_position); + if (!dir) { + dir = m_directionOfProjection; + m_position[0] = m_focalPoint[0] - d * dir[0]; + m_position[1] = m_focalPoint[1] - d * dir[1]; + m_position[2] = m_focalPoint[2] - d * dir[2]; + } else { + m_position[0] = m_position[0] + d * dir[0]; + m_position[1] = m_position[1] + d * dir[1]; + m_position[2] = m_position[2] + d * dir[2]; + } + + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Move camera sideways + */ + //////////////////////////////////////////////////////////////////////////// + this.pan = function (dx, dy, dz) { + if (!m_enableTranslation) { + return; + } + + m_position[0] += dx; + m_position[1] += dy; + m_position[2] += dz; + + m_focalPoint[0] += dx; + m_focalPoint[1] += dy; + m_focalPoint[2] += dz; + + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute camera coordinate axes + */ + //////////////////////////////////////////////////////////////////////////// + this.computeOrthogonalAxes = function () { + this.computeDirectionOfProjection(); + vec3.cross(m_rightDir, m_directionOfProjection, m_viewUp); + vec3.normalize(m_rightDir, m_rightDir); + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Rotate camera around center of rotation + * @param dx Rotation around vertical axis in degrees + * @param dy Rotation around horizontal axis in degrees + */ + //////////////////////////////////////////////////////////////////////////// + this.rotate = function (dx, dy) { + if (!m_enableRotation) { + return; + } + + // Convert degrees into radians + dx = 0.5 * dx * (Math.PI / 180.0); + dy = 0.5 * dy * (Math.PI / 180.0); + + var mat = mat4.create(), + inverseCenterOfRotation = new vec3.create(); + + mat4.identity(mat); + + inverseCenterOfRotation[0] = -m_centerOfRotation[0]; + inverseCenterOfRotation[1] = -m_centerOfRotation[1]; + inverseCenterOfRotation[2] = -m_centerOfRotation[2]; + + mat4.translate(mat, mat, m_centerOfRotation); + mat4.rotate(mat, mat, dx, m_viewUp); + mat4.rotate(mat, mat, dy, m_rightDir); + mat4.translate(mat, mat, inverseCenterOfRotation); + + vec4.transformMat4(m_position, m_position, mat); + vec4.transformMat4(m_focalPoint, m_focalPoint, mat); + + // Update viewup vector + vec4.transformMat4(m_viewUp, m_viewUp, mat); + vec4.normalize(m_viewUp, m_viewUp); + + // Update direction of projection + this.computeOrthogonalAxes(); + + // Mark modified + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute camera view matrix + */ + //////////////////////////////////////////////////////////////////////////// + this.computeViewMatrix = function () { + if (m_computeModelViewMatrixTime.getMTime() < this.getMTime()) { + mat4.lookAt(m_viewMatrix, m_position, m_focalPoint, m_viewUp); + m_computeModelViewMatrixTime.modified(); + } + return m_viewMatrix; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Check if the texture should be aligned to the screen. Alignment only + * occurs if the parallel extents contain width, height, and a + * close-to-integer zoom level, and if the units-per-pixel value has been + * computed. The camera must either be in parallel projection mode OR must + * have a perspective camera which is oriented along the z-axis without any + * rotation. + * + * @returns: either null if no alignment should occur, or an alignment object + * with the rounding value and offsets. + */ + //////////////////////////////////////////////////////////////////////////// + this.viewAlignment = function () { + if (!m_enableParallelProjection) { + /* If we aren't in parallel projection mode, ensure that the projection + * matrix meets strict specifications */ + var proj = this.projectionMatrix(); + if (proj[1] || proj[2] || proj[3] || proj[4] || proj[6] || proj[7] || + proj[8] || proj[9] || proj[12] || proj[13] || proj[15]) { + return null; + } + } + var unitsPerPixel = this.unitsPerPixel(m_parallelExtents.zoom, + m_parallelExtents.tilesize); + /* If we don't have screen dimensions, we can't know how to align pixels */ + if (!m_parallelExtents.width || !m_parallelExtents.height || + !unitsPerPixel) { + return null; + } + /* If we aren't at an integer zoom level, we shouldn't align pixels. If + * we are really close to an integer zoom level, that is good enough. */ + if (parseFloat(m_parallelExtents.zoom.toFixed(6)) !== + parseFloat(m_parallelExtents.zoom.toFixed(0))) { + return null; + } + 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 + * the same reason. */ + if (m_parallelExtents.width % 2) { + align.dx = unitsPerPixel * 0.5; + } + if (m_parallelExtents.height % 2) { + align.dy = unitsPerPixel * 0.5; + } + return align; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute camera projection matrix + */ + //////////////////////////////////////////////////////////////////////////// + this.computeProjectionMatrix = function () { + if (m_computeProjectMatrixTime.getMTime() < this.getMTime()) { + if (!m_enableParallelProjection) { + mat4.perspective(m_projectionMatrix, m_viewAngle, m_viewAspect, m_near, m_far); + } else { + mat4.ortho(m_projectionMatrix, + m_left, m_right, m_bottom, m_top, m_near, m_far); + } + + m_computeProjectMatrixTime.modified(); + } + + return m_projectionMatrix; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Convert a zoom level and window size to a camera height. + */ + //////////////////////////////////////////////////////////////////////////// + this.zoomToHeight = function (zoom, width, height) { + return vgl.zoomToHeight(zoom, width, height, m_viewAngle); + }; + + this.computeDirectionOfProjection(); + + return this; + }; + + inherit(vgl.camera, vgl.groupNode); + + //////////////////////////////////////////////////////////////////////////// + /** + * Convert a zoom level and window size to a camera height. + * + * @param zoom: Zoom level, as used in OSM maps. + * @param width: width of the window. + * @param height: height of the window. + * @returns: perspective camera height. + */ + //////////////////////////////////////////////////////////////////////////// + vgl.zoomToHeight = function (zoom, width, height, viewAngle) { + 'use strict'; + viewAngle = viewAngle || (30 * Math.PI / 180.0); + var newZ = 360 * Math.pow(2, -zoom); + newZ /= Math.tan(viewAngle / 2) * 2 * 256 / height; + return newZ; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Convert a camera height and window size to a zoom level. + * + * @param z: perspective camera height. + * @param width: width of the window. + * @param height: height of the window. + * @returns: zoom level. + */ + //////////////////////////////////////////////////////////////////////////// + vgl.heightToZoom = function (z, width, height, viewAngle) { + 'use strict'; + viewAngle = viewAngle || (30 * Math.PI / 180.0); + z *= Math.tan(viewAngle / 2) * 2 * 256 / height; + var zoom = -Math.log2(z / 360); + return zoom; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit, $*/ + ////////////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class interactorStyle + * + * @class vgl.interactorStyle + * interactorStyle is a base class for all interactor styles + * @returns {vgl.interactorStyle} + */ + //////////////////////////////////////////////////////////////////////////// + vgl.interactorStyle = function () { + 'use strict'; + + if (!(this instanceof vgl.interactorStyle)) { + return new vgl.interactorStyle(); + } + vgl.object.call(this); + + // Private member variables + var m_that = this, + m_viewer = null; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return viewer referenced by the interactor style + * + * @returns {null} + */ + //////////////////////////////////////////////////////////////////////////// + this.viewer = function () { + return m_viewer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set viewer for the interactor style + * + * @param viewer + */ + //////////////////////////////////////////////////////////////////////////// + this.setViewer = function (viewer) { + if (viewer !== m_viewer) { + m_viewer = viewer; + $(m_viewer).on(vgl.event.mousePress, m_that.handleMouseDown); + $(m_viewer).on(vgl.event.mouseRelease, m_that.handleMouseUp); + $(m_viewer).on(vgl.event.mouseMove, m_that.handleMouseMove); + $(m_viewer).on(vgl.event.mouseOut, m_that.handleMouseOut); + $(m_viewer).on(vgl.event.mouseWheel, m_that.handleMouseWheel); + $(m_viewer).on(vgl.event.keyPress, m_that.handleKeyPress); + $(m_viewer).on(vgl.event.mouseContextMenu, m_that.handleContextMenu); + $(m_viewer).on(vgl.event.click, m_that.handleClick); + $(m_viewer).on(vgl.event.dblClick, m_that.handleDoubleClick); + this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse down event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseDown = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse up event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseUp = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseMove = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseOut = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse wheel event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseWheel = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle click event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleClick = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle double click event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleDoubleClick = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle key press event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleKeyPress = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle context menu event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleContextMenu = function (event) { + event = event; /* unused parameter */ + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Reset to default + */ + //////////////////////////////////////////////////////////////////////////// + this.reset = function () { + return true; + }; + + return this; + }; + + inherit(vgl.interactorStyle, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, vec4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of trackballInteractorStyle + * + * @class vgl.trackballInteractorStyle + * @returns {vgl.trackballInteractorStyle} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.trackballInteractorStyle = function () { + 'use strict'; + + if (!(this instanceof vgl.trackballInteractorStyle)) { + return new vgl.trackballInteractorStyle(); + } + vgl.interactorStyle.call(this); + var m_that = this, + m_leftMouseBtnDown = false, + m_rightMouseBtnDown = false, + m_midMouseBtnDown = false, + m_outsideCanvas, + m_currPos = {x: 0, y: 0}, + m_lastPos = {x: 0, y: 0}; + + + ///////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event + * + * @param event + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.handleMouseMove = function (event) { + var width = m_that.viewer().renderWindow().windowSize()[0], + height = m_that.viewer().renderWindow().windowSize()[1], + ren = m_that.viewer().renderWindow().activeRenderer(), + cam = ren.camera(), coords = m_that.viewer().relMouseCoords(event), + fp, fdp, fwp, dp1, dp2, wp1, wp2, dx, dy, dz, m_zTrans; + + m_outsideCanvas = false; + m_currPos = {x: 0, y: 0}; + + if ((coords.x < 0) || (coords.x > width)) { + m_currPos.x = 0; + m_outsideCanvas = true; + } else { + m_currPos.x = coords.x; + } + if ((coords.y < 0) || (coords.y > height)) { + m_currPos.y = 0; + m_outsideCanvas = true; + } else { + m_currPos.y = coords.y; + } + if (m_outsideCanvas === true) { + return; + } + + 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.0); + dp2 = vec4.fromValues(m_lastPos.x, m_lastPos.y, fdp[2], 1.0); + + 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]; + + if (m_midMouseBtnDown) { + cam.pan(-dx, -dy, -dz); + m_that.viewer().render(); + } + if (m_leftMouseBtnDown) { + cam.rotate((m_lastPos.x - m_currPos.x), + (m_lastPos.y - m_currPos.y)); + ren.resetCameraClippingRange(); + m_that.viewer().render(); + } + if (m_rightMouseBtnDown) { + /// 2.0 is the speed up factor + m_zTrans = 2.0 * (m_currPos.y - m_lastPos.y) / height; + + // Calculate zoom scale here + if (m_zTrans > 0) { + cam.zoom(1 - Math.abs(m_zTrans)); + } else { + 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; + return false; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse down event + * + * @param event + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.handleMouseDown = function (event) { + var coords; + + if (event.button === 0) { + m_leftMouseBtnDown = true; + } + if (event.button === 1) { + m_midMouseBtnDown = true; + } + if (event.button === 2) { + m_rightMouseBtnDown = true; + } + coords = m_that.viewer().relMouseCoords(event); + if (coords.x < 0) { + m_lastPos.x = 0; + } else { + m_lastPos.x = coords.x; + } + if (coords.y < 0) { + m_lastPos.y = 0; + } else { + m_lastPos.y = coords.y; + } + return false; + }; + + // @note We never get mouse up from scroll bar: See the bug report here + // http://bugs.jquery.com/ticket/8184 + ///////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse up event + * + * @param event + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.handleMouseUp = function (event) { + if (event.button === 0) { + m_leftMouseBtnDown = false; + } + if (event.button === 1) { + m_midMouseBtnDown = false; + } + if (event.button === 2) { + m_rightMouseBtnDown = false; + } + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse wheel event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseWheel = function (event) { + var ren = m_that.viewer().renderWindow().activeRenderer(), + cam = ren.camera(); + + // TODO Compute zoom factor intelligently + if (event.originalEvent.wheelDelta < 0) { + cam.zoom(0.9); + } else { + cam.zoom(1.1); + } + ren.resetCameraClippingRange(); + m_that.viewer().render(); + return true; + }; + + return this; + }; + inherit(vgl.trackballInteractorStyle, vgl.interactorStyle); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, vec4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of pvwInteractorStyle (for ParaViewWeb) + * + * @class vgl.pvwInteractorStyle + * @returns {vgl.pvwInteractorStyle} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.pvwInteractorStyle = function () { + 'use strict'; + + if (!(this instanceof vgl.pvwInteractorStyle)) { + return new vgl.pvwInteractorStyle(); + } + vgl.trackballInteractorStyle.call(this); + var m_that = this, + m_leftMouseButtonDown = false, + m_rightMouseButtonDown = false, + m_middleMouseButtonDown = false, + 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_mouseLastPos = { + x: 0, + y: 0 + }; + + function render() { + m_renderer.resetCameraClippingRange(); + m_that.viewer().render(); + } + + ///////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event + * + * @param event + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.handleMouseMove = function (event) { + var rens = [], i = null, secCameras = [], deltaxy = null; + 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 = false; + m_coords = m_that.viewer().relMouseCoords(event); + m_currentMousePos = { + x: 0, + y: 0 + }; + + // Get secondary cameras + rens = m_that.viewer().renderWindow().renderers(); + for (i = 0; i < rens.length; i += 1) { + if (m_renderer !== rens[i]) { + secCameras.push(rens[i].camera()); + } + } + + if ((m_coords.x < 0) || (m_coords.x > m_width)) { + m_currentMousePos.x = 0; + m_outsideCanvas = true; + } else { + m_currentMousePos.x = m_coords.x; + } + if ((m_coords.y < 0) || (m_coords.y > m_height)) { + m_currentMousePos.y = 0; + m_outsideCanvas = true; + } else { + m_currentMousePos.y = m_coords.y; + } + if (m_outsideCanvas === true) { + return; + } + 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.0); + m_displayPt2 = vec4.fromValues( + m_mouseLastPos.x, m_mouseLastPos.y, m_focusDisplayPt[2], 1.0); + 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]; + + if (m_middleMouseButtonDown) { + m_camera.pan(-m_dx, -m_dy, -m_dz); + render(); + } + if (m_leftMouseButtonDown) { + deltaxy = [(m_mouseLastPos.x - m_currentMousePos.x), + (m_mouseLastPos.y - m_currentMousePos.y)]; + m_camera.rotate(deltaxy[0], deltaxy[1]); + + // Apply rotation to all other cameras + for (i = 0; i < secCameras.length; i += 1) { + secCameras[i].rotate(deltaxy[0], deltaxy[1]); + } + + // Apply rotation to all other cameras + for (i = 0; i < rens.length; i += 1) { + rens[i].resetCameraClippingRange(); + } + render(); + } + if (m_rightMouseButtonDown) { + /// 2.0 is the speed up factor. + m_zTrans = 2.0 * (m_currentMousePos.y - m_mouseLastPos.y) / m_height; + + // Calculate zoom scale here + if (m_zTrans > 0) { + m_camera.zoom(1 - Math.abs(m_zTrans)); + } else { + m_camera.zoom(1 + Math.abs(m_zTrans)); + } + render(); + } + m_mouseLastPos.x = m_currentMousePos.x; + m_mouseLastPos.y = m_currentMousePos.y; + return false; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse down event + * + * @param event + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.handleMouseDown = function (event) { + if (event.button === 0) { + m_leftMouseButtonDown = true; + } + if (event.button === 1) { + m_middleMouseButtonDown = true; + } + if (event.button === 2) { + m_rightMouseButtonDown = true; + } + m_coords = m_that.viewer().relMouseCoords(event); + if (m_coords.x < 0) { + m_mouseLastPos.x = 0; + } else { + m_mouseLastPos.x = m_coords.x; + } + if (m_coords.y < 0) { + m_mouseLastPos.y = 0; + } else { + m_mouseLastPos.y = m_coords.y; + } + return false; + }; + + // @note We never get mouse up from scroll bar: See the bug report here + // http://bugs.jquery.com/ticket/8184 + ///////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse up event + * + * @param event + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.handleMouseUp = function (event) { + if (event.button === 0) { + m_leftMouseButtonDown = false; + } + if (event.button === 1) { + m_middleMouseButtonDown = false; + } + if (event.button === 2) { + m_rightMouseButtonDown = false; + } + return false; + }; + + return this; + }; + inherit(vgl.pvwInteractorStyle, vgl.trackballInteractorStyle); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global window, vgl, inherit, $*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class viewer + * + * @param canvas + * @returns {vgl.viewer} + */ + ////////////////////////////////////////////////////////////////////////////// + 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 = true, + m_interactorStyle = null, + m_renderer = vgl.renderer(options), + m_renderWindow = vgl.renderWindow(m_canvas); + + //////////////////////////////////////////////////////////////////////////// + /** + * Get canvas of the viewer + * + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.canvas = function () { + return m_canvas; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return render window of the viewer + * + * @returns {vgl.renderWindow} + */ + //////////////////////////////////////////////////////////////////////////// + this.renderWindow = function () { + return m_renderWindow; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize the viewer + * + * This is a must call or otherwise render context may not initialized + * properly. + */ + //////////////////////////////////////////////////////////////////////////// + this.init = function () { + if (m_renderWindow !== null) { + m_renderWindow._setup(); + } else { + console.log('[ERROR] No render window attached'); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove the viewer + */ + //////////////////////////////////////////////////////////////////////////// + this.exit = function (renderState) { + if (m_renderWindow !== null) { + m_renderWindow._cleanup(renderState); + } else { + console.log('[ERROR] No render window attached'); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get interactor style of the viewer + * + * @returns {vgl.interactorStyle} + */ + //////////////////////////////////////////////////////////////////////////// + this.interactorStyle = function () { + return m_interactorStyle; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set interactor style to be used by the viewer + * + * @param {vgl.interactorStyle} style + */ + //////////////////////////////////////////////////////////////////////////// + this.setInteractorStyle = function (style) { + if (style !== m_interactorStyle) { + m_interactorStyle = style; + m_interactorStyle.setViewer(this); + this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse down event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseDown = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + // Only prevent default action for right mouse button + if (event.button === 2) { + fixedEvent.preventDefault(); + } + fixedEvent.state = 'down'; + fixedEvent.type = vgl.event.mousePress; + $(m_that).trigger(fixedEvent); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse up event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseUp = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.state = 'up'; + fixedEvent.type = vgl.event.mouseRelease; + $(m_that).trigger(fixedEvent); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseMove = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.type = vgl.event.mouseMove; + $(m_that).trigger(fixedEvent); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse wheel scroll + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseWheel = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.type = vgl.event.mouseWheel; + $(m_that).trigger(fixedEvent); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleMouseOut = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.type = vgl.event.mouseOut; + $(m_that).trigger(fixedEvent); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle key press event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleKeyPress = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.type = vgl.event.keyPress; + $(m_that).trigger(fixedEvent); + } + + return true; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle context menu event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleContextMenu = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.type = vgl.event.contextMenu; + $(m_that).trigger(fixedEvent); + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle click event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleClick = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.type = vgl.event.click; + $(m_that).trigger(fixedEvent); + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle double click event + * + * @param event + * @returns {boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.handleDoubleClick = function (event) { + if (m_ready === true) { + var fixedEvent = $.event.fix(event || window.event); + fixedEvent.preventDefault(); + fixedEvent.type = vgl.event.dblClick; + $(m_that).trigger(fixedEvent); + } + + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get mouse coodinates related to canvas + * + * @param event + * @returns {{x: number, y: number}} + */ + //////////////////////////////////////////////////////////////////////////// + this.relMouseCoords = function (event) { + if (event.pageX === undefined || event.pageY === undefined) { + 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); + + canvasX = event.pageX - totalOffsetX; + canvasY = event.pageY - totalOffsetY; + + return { + x: canvasX, + y: canvasY + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render + */ + //////////////////////////////////////////////////////////////////////////// + this.render = function () { + m_renderWindow.render(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bind canvas mouse events to their default handlers + */ + //////////////////////////////////////////////////////////////////////////// + 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Undo earlier binded handlers for canvas mouse events + */ + //////////////////////////////////////////////////////////////////////////// + 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + this.bindEventHandlers(); + m_renderWindow.addRenderer(m_renderer); + }; + + this._init(); + return this; + }; + + inherit(vgl.viewer, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class shader + * + * @param type + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.shader = function (type) { + 'use strict'; + + if (!(this instanceof vgl.shader)) { + return new vgl.shader(type); + } + vgl.object.call(this); + + var m_shaderContexts = [], + m_shaderType = type, + m_shaderSource = ''; + + /** + * A shader can be associated with multiple contexts. Each context needs to + * be compiled and attached separately. These are tracked in the + * m_shaderContexts array. + * + * @param renderState a renderState that includes a m_context value. + * @return an object with context, compileTimestamp, and, if compiled, a + * shaderHandle entry. + */ + this._getContextEntry = function (renderState) { + var context = renderState.m_context, i, entry; + for (i = 0; i < m_shaderContexts.length; i += 1) { + if (m_shaderContexts[i].context === context) { + return m_shaderContexts[i]; + } + } + entry = { + context: context, + compileTimestamp: vgl.timestamp() + }; + m_shaderContexts.push(entry); + return entry; + }; + + /** + * Remove the context from the list of tracked contexts. This allows the + * associated shader handle to be GCed. Does nothing if the context is not + * in the list of tracked contexts. + * + * @param renderState a renderState that includes a m_context value. + */ + this.removeContext = function (renderState) { + var context = renderState.m_context, i; + for (i = 0; i < m_shaderContexts.length; i += 1) { + if (m_shaderContexts[i].context === context) { + m_shaderContexts.splice(i, 1); + return; + } + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get shader handle + */ + ///////////////////////////////////////////////////////////////////////////// + this.shaderHandle = function (renderState) { + var entry = this._getContextEntry(renderState); + return entry.shaderHandle; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get type of the shader + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.shaderType = function () { + return m_shaderType; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get shader source + * + * @returns {string} + */ + ///////////////////////////////////////////////////////////////////////////// + this.shaderSource = function () { + return m_shaderSource; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set shader source + * + * @param {string} source + */ + ///////////////////////////////////////////////////////////////////////////// + this.setShaderSource = function (source) { + m_shaderSource = source; + this.modified(); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Compile the shader + * + * @returns {null} + */ + ///////////////////////////////////////////////////////////////////////////// + this.compile = function (renderState) { + var entry = this._getContextEntry(renderState); + if (this.getMTime() < entry.compileTimestamp.getMTime()) { + return entry.shaderHandle; + } + + renderState.m_context.deleteShader(entry.shaderHandle); + entry.shaderHandle = renderState.m_context.createShader(m_shaderType); + renderState.m_context.shaderSource(entry.shaderHandle, m_shaderSource); + renderState.m_context.compileShader(entry.shaderHandle); + + // See if it compiled successfully + if (!renderState.m_context.getShaderParameter(entry.shaderHandle, + vgl.GL.COMPILE_STATUS)) { + console.log('[ERROR] An error occurred compiling the shaders: ' + + renderState.m_context.getShaderInfoLog(entry.shaderHandle)); + console.log(m_shaderSource); + renderState.m_context.deleteShader(entry.shaderHandle); + return null; + } + + entry.compileTimestamp.modified(); + + return entry.shaderHandle; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Attach shader to the program + * + * @param programHandle + */ + ///////////////////////////////////////////////////////////////////////////// + this.attachShader = function (renderState, programHandle) { + renderState.m_context.attachShader( + programHandle, this.shaderHandle(renderState)); + }; + }; + + inherit(vgl.shader, vgl.object); + + + /* We can use the same shader multiple times if it is identical. This caches + * the last N shaders and will reuse them when possible. The cache keeps the + * most recently requested shader at the front. If you are doing anything more + * to a shader then creating it and setting its source once, do not use this + * cache. + */ + (function () { + 'use strict'; + var m_shaderCache = [], + m_shaderCacheMaxSize = 10; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get a shader from the cache. Create a new shader if necessary using a + * specific source. + * + * @param type One of vgl.GL.*_SHADER + * @param context the GL context for the shader. + * @param {string} source the source code of the shader. + */ + ///////////////////////////////////////////////////////////////////////////// + vgl.getCachedShader = function (type, context, source) { + for (var i = 0; i < m_shaderCache.length; i += 1) { + if (m_shaderCache[i].type === type && + m_shaderCache[i].context === context && + m_shaderCache[i].source === source) { + if (i) { + m_shaderCache.splice(0, 0, m_shaderCache.splice(i, 1)[0]); + } + return m_shaderCache[0].shader; + } + } + var shader = new vgl.shader(type); + shader.setShaderSource(source); + m_shaderCache.unshift({ + type: type, + context: context, + source: source, + shader: shader + }); + if (m_shaderCache.length >= m_shaderCacheMaxSize) { + m_shaderCache.splice(m_shaderCacheMaxSize, + m_shaderCache.length - m_shaderCacheMaxSize); + } + return shader; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Clear the shader cache. + * + * @param context the GL context to clear, or null for clear all. + */ + ///////////////////////////////////////////////////////////////////////////// + vgl.clearCachedShaders = function (context) { + for (var i = m_shaderCache.length - 1; i >= 0; i -= 1) { + if (context === null || context === undefined || + m_shaderCache[i].context === context) { + m_shaderCache.splice(i, 1); + } + } + }; + })(); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit, $*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instace of class shaderProgram + * + * @class + * @returns {vgl.shaderProgram} + */ + ////////////////////////////////////////////////////////////////////////////// + + var getBaseUrl = (function () { + 'use strict'; + var baseUrl = '.'; + var scripts = document.getElementsByTagName('script'); + /* When run in certain environments, there may be no scripts loaded. For + * instance, jQuery's $.getScript won't add it to a script tag. */ + if (scripts.length > 0) { + var index = scripts.length - 1; + var vglScript = scripts[index]; + index = vglScript.src.lastIndexOf('/'); + baseUrl = vglScript.src.substring(0, index); + } + return function () { return baseUrl; }; + })(); + + + vgl.shaderProgram = function () { + 'use strict'; + + if (!(this instanceof vgl.shaderProgram)) { + return new vgl.shaderProgram(); + } + vgl.materialAttribute.call( + this, vgl.materialAttributeType.ShaderProgram); + + /** @private */ + var m_this = this, + m_programHandle = 0, + m_compileTimestamp = vgl.timestamp(), + m_bindTimestamp = vgl.timestamp(), + m_shaders = [], + m_uniforms = [], + m_vertexAttributes = {}, + m_uniformNameToLocation = {}, + m_vertexAttributeNameToLocation = {}; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Create a particular shader type using GLSL shader strings from a file + */ + ///////////////////////////////////////////////////////////////////////////// + this.loadFromFile = function (type, sourceUrl) { + var shader; + $.ajax({ + url: sourceUrl, + type: 'GET', + async: false, + success: function (result) { + //console.log(result); + shader = vgl.shader(type); + shader.setShaderSource(result); + m_this.addShader(shader); + } + }); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Create a particular shader type using GLSL shader strings from a file + * relative to VGL load URL. + */ + ///////////////////////////////////////////////////////////////////////////// + this.loadShader = function (type, file) { + this.loadFromFile(type, getBaseUrl() + '/shaders/' + file); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Query uniform location in the program + * + * @param name + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.queryUniformLocation = function (renderState, name) { + return renderState.m_context.getUniformLocation(m_programHandle, name); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Query attribute location in the program + * + * @param name + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.queryAttributeLocation = function (renderState, name) { + return renderState.m_context.getAttribLocation(m_programHandle, name); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Add a new shader to the program + * + * @param shader + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.addShader = function (shader) { + if (m_shaders.indexOf(shader) > -1) { + return false; + } + + var i; + for (i = 0; i < m_shaders.length; i += 1) { + if (m_shaders[i].shaderType() === shader.shaderType()) { + m_shaders.splice(m_shaders.indexOf(shader), 1); + } + } + + m_shaders.push(shader); + m_this.modified(); + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Add a new uniform to the program + * + * @param uniform + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.addUniform = function (uniform) { + if (m_uniforms.indexOf(uniform) > -1) { + return false; + } + + m_uniforms.push(uniform); + m_this.modified(); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Add a new vertex attribute to the program + * + * @param attr + * @param key + */ + ///////////////////////////////////////////////////////////////////////////// + this.addVertexAttribute = function (attr, key) { + m_vertexAttributes[key] = attr; + m_this.modified(); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get uniform location + * + * This method does not perform any query into the program but relies on + * the fact that it depends on a call to queryUniformLocation earlier. + * + * @param name + * @returns {number} + */ + ///////////////////////////////////////////////////////////////////////////// + this.uniformLocation = function (name) { + return m_uniformNameToLocation[name]; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get attribute location + * + * This method does not perform any query into the program but relies on the + * fact that it depends on a call to queryUniformLocation earlier. + * + * @param name + * @returns {number} + */ + ///////////////////////////////////////////////////////////////////////////// + this.attributeLocation = function (name) { + return m_vertexAttributeNameToLocation[name]; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get uniform object using name as the key + * + * @param name + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.uniform = function (name) { + var i; + for (i = 0; i < m_uniforms.length; i += 1) { + if (m_uniforms[i].name() === name) { + return m_uniforms[i]; + } + } + + return null; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Update all uniforms + * + * This method should be used directly unless required + */ + ///////////////////////////////////////////////////////////////////////////// + this.updateUniforms = function (renderState) { + var i; + + for (i = 0; i < m_uniforms.length; i += 1) { + m_uniforms[i].callGL(renderState, + m_uniformNameToLocation[m_uniforms[i].name()]); + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Link shader program + * + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.link = function (renderState) { + renderState.m_context.linkProgram(m_programHandle); + + // If creating the shader program failed, alert + if (!renderState.m_context.getProgramParameter(m_programHandle, + vgl.GL.LINK_STATUS)) { + console.log('[ERROR] Unable to initialize the shader program.'); + return false; + } + + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Use the shader program + */ + ///////////////////////////////////////////////////////////////////////////// + this.use = function (renderState) { + renderState.m_context.useProgram(m_programHandle); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Peform any initialization required + */ + ///////////////////////////////////////////////////////////////////////////// + this._setup = function (renderState) { + if (m_programHandle === 0) { + m_programHandle = renderState.m_context.createProgram(); + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Peform any clean up required when the program gets deleted + */ + ///////////////////////////////////////////////////////////////////////////// + this._cleanup = function (renderState) { + m_this.deleteVertexAndFragment(renderState); + m_this.deleteProgram(renderState); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Delete the shader program + */ + ///////////////////////////////////////////////////////////////////////////// + this.deleteProgram = function (renderState) { + renderState.m_context.deleteProgram(m_programHandle); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Delete vertex and fragment shaders + */ + ///////////////////////////////////////////////////////////////////////////// + this.deleteVertexAndFragment = function (renderState) { + var i; + for (i = 0; i < m_shaders.length; i += 1) { + renderState.m_context.detachShader(m_shaders[i].shaderHandle(renderState)); + renderState.m_context.deleteShader(m_shaders[i].shaderHandle(renderState)); + m_shaders[i].removeContext(renderState); + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Compile and link a shader + */ + ///////////////////////////////////////////////////////////////////////////// + this.compileAndLink = function (renderState) { + var i; + + if (m_compileTimestamp.getMTime() >= this.getMTime()) { + return; + } + + m_this._setup(renderState); + + // Compile shaders + for (i = 0; i < m_shaders.length; i += 1) { + m_shaders[i].compile(renderState); + m_shaders[i].attachShader(renderState, m_programHandle); + } + + m_this.bindAttributes(renderState); + + // link program + if (!m_this.link(renderState)) { + console.log('[ERROR] Failed to link Program'); + m_this._cleanup(renderState); + } + + m_compileTimestamp.modified(); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Bind the program with its shaders + * + * @param renderState + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.bind = function (renderState) { + var i = 0; + + if (m_bindTimestamp.getMTime() < m_this.getMTime()) { + + // Compile shaders + m_this.compileAndLink(renderState); + + m_this.use(renderState); + m_this.bindUniforms(renderState); + m_bindTimestamp.modified(); + } else { + m_this.use(renderState); + } + + // Call update callback. + for (i = 0; i < m_uniforms.length; i += 1) { + m_uniforms[i].update(renderState, m_this); + } + + // Now update values to GL. + m_this.updateUniforms(renderState); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Undo binding of the shader program + * + * @param renderState + */ + ///////////////////////////////////////////////////////////////////////////// + this.undoBind = function (renderState) { + // REF https://www.khronos.org/opengles/sdk/docs/man/xhtml/glUseProgram.xml + // If program is 0, then the current rendering state refers to an invalid + // program object, and the results of vertex and fragment shader execution + // due to any glDrawArrays or glDrawElements commands are undefined + renderState.m_context.useProgram(null); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Bind vertex data + * + * @param renderState + * @param key + */ + ///////////////////////////////////////////////////////////////////////////// + this.bindVertexData = function (renderState, key) { + if (m_vertexAttributes.hasOwnProperty(key)) { + m_vertexAttributes[key].bindVertexData(renderState, key); + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Undo bind vetex data + * + * @param renderState + * @param key + */ + ///////////////////////////////////////////////////////////////////////////// + this.undoBindVertexData = function (renderState, key) { + if (m_vertexAttributes.hasOwnProperty(key)) { + m_vertexAttributes[key].undoBindVertexData(renderState, key); + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Bind uniforms + */ + ///////////////////////////////////////////////////////////////////////////// + this.bindUniforms = function (renderState) { + var i; + for (i = 0; i < m_uniforms.length; i += 1) { + m_uniformNameToLocation[m_uniforms[i].name()] = this + .queryUniformLocation(renderState, m_uniforms[i].name()); + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Bind vertex attributes + */ + ///////////////////////////////////////////////////////////////////////////// + this.bindAttributes = function (renderState) { + var key, name; + for (key in m_vertexAttributes) { + if (m_vertexAttributes.hasOwnProperty(key)) { + name = m_vertexAttributes[key].name(); + renderState.m_context.bindAttribLocation(m_programHandle, key, name); + m_vertexAttributeNameToLocation[name] = key; + } + } + }; + + return m_this; + }; + + inherit(vgl.shaderProgram, vgl.materialAttribute); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global Uint8Array, vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class texture + * + * @class + * @returns {vgl.texture} + */ + /////////////////////////////////////////////////////////////////////////////// + vgl.texture = function () { + 'use strict'; + + if (!(this instanceof vgl.texture)) { + return new vgl.texture(); + } + vgl.materialAttribute.call( + this, vgl.materialAttributeType.Texture); + + this.m_width = 0; + this.m_height = 0; + this.m_depth = 0; + + this.m_textureHandle = null; + this.m_textureUnit = 0; + + this.m_pixelFormat = vgl.GL.RGBA; + this.m_pixelDataType = vgl.GL.UNSIGNED_BYTE; + this.m_internalFormat = vgl.GL.RGBA; + this.m_nearestPixel = false; + + this.m_image = null; + + var m_setupTimestamp = vgl.timestamp(), + m_that = this; + + function activateTextureUnit(renderState) { + switch (m_that.m_textureUnit) { + case 0: + renderState.m_context.activeTexture(vgl.GL.TEXTURE0); + break; + case 1: + renderState.m_context.activeTexture(vgl.GL.TEXTURE1); + break; + case 2: + renderState.m_context.activeTexture(vgl.GL.TEXTURE2); + break; + case 3: + renderState.m_context.activeTexture(vgl.GL.TEXTURE3); + break; + case 4: + renderState.m_context.activeTexture(vgl.GL.TEXTURE4); + break; + case 5: + renderState.m_context.activeTexture(vgl.GL.TEXTURE5); + break; + case 6: + renderState.m_context.activeTexture(vgl.GL.TEXTURE6); + break; + case 7: + renderState.m_context.activeTexture(vgl.GL.TEXTURE7); + break; + case 8: + renderState.m_context.activeTexture(vgl.GL.TEXTURE8); + break; + case 9: + renderState.m_context.activeTexture(vgl.GL.TEXTURE9); + break; + case 10: + renderState.m_context.activeTexture(vgl.GL.TEXTURE10); + break; + case 11: + renderState.m_context.activeTexture(vgl.GL.TEXTURE11); + break; + case 12: + renderState.m_context.activeTexture(vgl.GL.TEXTURE12); + break; + case 13: + renderState.m_context.activeTexture(vgl.GL.TEXTURE13); + break; + case 14: + renderState.m_context.activeTexture(vgl.GL.TEXTURE14); + break; + case 15: + renderState.m_context.activeTexture(vgl.GL.TEXTURE15); + break; + default: + throw '[error] Texture unit ' + m_that.m_textureUnit + + ' is not supported'; + } + } + + ///////////////////////////////////////////////////////////////////////////// + /** + * Create texture, update parameters, and bind data + * + * @param renderState + */ + ///////////////////////////////////////////////////////////////////////////// + this.setup = function (renderState) { + // Activate the texture unit first + activateTextureUnit(renderState); + + 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, + this.m_nearestPixel ? vgl.GL.NEAREST : vgl.GL.LINEAR); + renderState.m_context.texParameteri(vgl.GL.TEXTURE_2D, + vgl.GL.TEXTURE_MAG_FILTER, + this.m_nearestPixel ? vgl.GL.NEAREST : 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); + + if (this.m_image !== null) { + renderState.m_context.pixelStorei(vgl.GL.UNPACK_ALIGNMENT, 1); + renderState.m_context.pixelStorei(vgl.GL.UNPACK_FLIP_Y_WEBGL, true); + + this.updateDimensions(); + this.computeInternalFormatUsingImage(); + + // console.log('m_internalFormat ' + this.m_internalFormat); + // console.log('m_pixelFormat ' + this.m_pixelFormat); + // console.log('m_pixelDataType ' + this.m_pixelDataType); + + // FOR now support only 2D textures + renderState.m_context.texImage2D(vgl.GL.TEXTURE_2D, 0, this.m_internalFormat, + this.m_pixelFormat, this.m_pixelDataType, this.m_image); + } else { + renderState.m_context.texImage2D(vgl.GL.TEXTURE_2D, 0, this.m_internalFormat, + this.m_width, this.m_height, 0, this.m_pixelFormat, this.m_pixelDataType, null); + } + + renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, null); + m_setupTimestamp.modified(); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Create texture and if already created use it + * + * @param renderState + */ + ///////////////////////////////////////////////////////////////////////////// + this.bind = function (renderState) { + // TODO Call setup via material setup + if (this.getMTime() > m_setupTimestamp.getMTime()) { + this.setup(renderState); + } + + activateTextureUnit(renderState); + renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, this.m_textureHandle); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Turn off the use of this texture + * + * @param renderState + */ + ///////////////////////////////////////////////////////////////////////////// + this.undoBind = function (renderState) { + renderState.m_context.bindTexture(vgl.GL.TEXTURE_2D, null); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get image used by the texture + * + * @returns {vgl.image} + */ + ///////////////////////////////////////////////////////////////////////////// + this.image = function () { + return this.m_image; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set image for the texture + * + * @param {vgl.image} image + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setImage = function (image) { + if (image !== null) { + this.m_image = image; + this.updateDimensions(); + this.modified(); + return true; + } + + return false; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get nearest pixel flag for the texture + * + * @returns boolean + */ + ///////////////////////////////////////////////////////////////////////////// + this.nearestPixel = function () { + return this.m_nearestPixel; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set nearest pixel flag for the texture + * + * @param {boolean} nearest pixel flag + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setNearestPixel = function (nearest) { + nearest = nearest ? true : false; + if (nearest !== this.m_nearestPixel) { + this.m_nearestPixel = nearest; + this.modified(); + return true; + } + return false; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get texture unit of the texture + * + * @returns {number} + */ + ///////////////////////////////////////////////////////////////////////////// + this.textureUnit = function () { + return this.m_textureUnit; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set texture unit of the texture. Default is 0. + * + * @param {number} unit + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setTextureUnit = function (unit) { + if (this.m_textureUnit === unit) { + return false; + } + + this.m_textureUnit = unit; + this.modified(); + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get width of the texture + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.width = function () { + return this.m_width; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set width of the texture + * + * @param {number} width + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setWidth = function (width) { + if (m_that.m_width !== width) { + m_that.m_width = width; + m_that.modified(); + return true; + } + + return false; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get width of the texture + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.height = function () { + return m_that.m_height; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set height of the texture + * + * @param {number} height + * @returns {vgl.texture} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setHeight = function (height) { + if (m_that.m_height !== height) { + m_that.m_height = height; + m_that.modified(); + return true; + } + + return false; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get depth of the texture + * + * @returns {number} + */ + ///////////////////////////////////////////////////////////////////////////// + this.depth = function () { + return this.m_depth; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set depth of the texture + * + * @param {number} depth + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setDepth = function (depth) { + if (this.m_image === null) { + return false; + } + + this.m_depth = depth; + this.modified(); + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get the texture handle (id) of the texture + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.textureHandle = function () { + return this.m_textureHandle; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get internal format of the texture + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.internalFormat = function () { + return this.m_internalFormat; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set internal format of the texture + * + * @param internalFormat + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setInternalFormat = function (internalFormat) { + if (this.m_internalFormat !== internalFormat) { + this.m_internalFormat = internalFormat; + this.modified(); + return true; + } + + return false; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get pixel format of the texture + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.pixelFormat = function () { + return this.m_pixelFormat; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set pixel format of the texture + * + * @param pixelFormat + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setPixelFormat = function (pixelFormat) { + if (this.m_image === null) { + return false; + } + + this.m_pixelFormat = pixelFormat; + this.modified(); + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get pixel data type + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.pixelDataType = function () { + return this.m_pixelDataType; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set pixel data type + * + * @param pixelDataType + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setPixelDataType = function (pixelDataType) { + if (this.m_image === null) { + return false; + } + + this.m_pixelDataType = pixelDataType; + + this.modified(); + + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Compute internal format of the texture + */ + ///////////////////////////////////////////////////////////////////////////// + this.computeInternalFormatUsingImage = function () { + // Currently image does not define internal format + // and hence it's pixel format is the only way to query + // information on how color has been stored. + // switch (this.m_image.pixelFormat()) { + // case vgl.GL.RGB: + // this.m_internalFormat = vgl.GL.RGB; + // break; + // case vgl.GL.RGBA: + // this.m_internalFormat = vgl.GL.RGBA; + // break; + // case vgl.GL.Luminance: + // this.m_internalFormat = vgl.GL.Luminance; + // break; + // case vgl.GL.LuminanceAlpha: + // this.m_internalFormat = vgl.GL.LuminanceAlpha; + // break; + // // Do nothing when image pixel format is none or undefined. + // default: + // break; + // }; + + // TODO Fix this + this.m_internalFormat = vgl.GL.RGBA; + this.m_pixelFormat = vgl.GL.RGBA; + this.m_pixelDataType = vgl.GL.UNSIGNED_BYTE; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Update texture dimensions + */ + ///////////////////////////////////////////////////////////////////////////// + this.updateDimensions = function () { + if (this.m_image !== null) { + this.m_width = this.m_image.width; + this.m_height = this.m_image.height; + this.m_depth = 0; // Only 2D images are supported now + } + }; + + return this; + }; + + inherit(vgl.texture, vgl.materialAttribute); + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class lookupTable + * + * @class + * @returns {vgl.lookupTable} + */ + /////////////////////////////////////////////////////////////////////////////// + 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]; + + this.m_colorTable = //paraview bwr colortable + [0.07514311, 0.468049805, 1, 1, + 0.247872569, 0.498782363, 1, 1, + 0.339526309, 0.528909511, 1, 1, + 0.409505078, 0.558608486, 1, 1, + 0.468487184, 0.588057293, 1, 1, + 0.520796675, 0.617435078, 1, 1, + 0.568724526, 0.646924167, 1, 1, + 0.613686735, 0.676713218, 1, 1, + 0.656658579, 0.707001303, 1, 1, + 0.698372844, 0.738002964, 1, 1, + 0.739424025, 0.769954435, 1, 1, + 0.780330104, 0.803121429, 1, 1, + 0.821573924, 0.837809045, 1, 1, + 0.863634967, 0.874374691, 1, 1, + 0.907017747, 0.913245283, 1, 1, + 0.936129275, 0.938743558, 0.983038586, 1, + 0.943467973, 0.943498599, 0.943398095, 1, + 0.990146732, 0.928791426, 0.917447482, 1, + 1, 0.88332677, 0.861943246, 1, + 1, 0.833985467, 0.803839606, 1, + 1, 0.788626485, 0.750707739, 1, + 1, 0.746206642, 0.701389973, 1, + 1, 0.70590052, 0.654994046, 1, + 1, 0.667019783, 0.610806959, 1, + 1, 0.6289553, 0.568237474, 1, + 1, 0.591130233, 0.526775617, 1, + 1, 0.552955184, 0.485962266, 1, + 1, 0.513776083, 0.445364274, 1, + 1, 0.472800903, 0.404551679, 1, + 1, 0.428977855, 0.363073592, 1, + 1, 0.380759558, 0.320428137, 1, + 0.961891484, 0.313155629, 0.265499262, 1, + 0.916482116, 0.236630659, 0.209939162, 1].map( + function (x) {return x * 255;}); + + ///////////////////////////////////////////////////////////////////////////// + /** + * Create lookup table, initialize parameters, and bind data to it + * + * @param {vgl.renderState} renderState + */ + ///////////////////////////////////////////////////////////////////////////// + this.setup = function (renderState) { + if (this.textureUnit() === 0) { + renderState.m_context.activeTexture(vgl.GL.TEXTURE0); + } else if (this.textureUnit() === 1) { + 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(); + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get color table used by the lookup table + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.colorTable = function () { + return this.m_colorTable; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set color table used by the lookup table + * + * @param colors + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setColorTable = function (colors) { + if (this.m_colorTable === colors) { + return false; + } + + this.m_colorTable = colors; + this.modified(); + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get scalar range + * + * @returns {Array} + */ + ///////////////////////////////////////////////////////////////////////////// + this.range = function () { + return m_range; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set scalar range for the lookup table + * + * @param range + * @returns {boolean} + */ + ///////////////////////////////////////////////////////////////////////////// + this.setRange = function (range) { + if (m_range === range) { + return false; + } + m_range = range; + this.modified(); + return true; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Given a [min,max] range update the lookup table range + * + * @param range + */ + ///////////////////////////////////////////////////////////////////////////// + this.updateRange = function (range) { + if (!(range instanceof Array)) { + console.log('[error] Invalid data type for range. Requires array [min,max]'); + } + + if (range[0] < m_range[0]) { + m_range[0] = range[0]; + this.modified(); + } + + if (range[1] > m_range[1]) { + m_range[1] = range[1]; + this.modified(); + } + }; + + return this; + }; + + inherit(vgl.lookupTable, vgl.texture); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, mat4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class uniform + * + * @param type + * @param name + * @returns {vgl.uniform} OpenGL uniform encapsulation + */ + /////////////////////////////////////////////////////////////////////////////// + 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 = []; + + m_dataArray.length = this.getTypeNumberOfComponents(m_type); + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get name of the uniform + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.name = function () { + return m_name; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get type of the uniform + * + * @returns {*} + */ + ///////////////////////////////////////////////////////////////////////////// + this.type = function () { + return m_type; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Get value of the uniform + * + * @returns {Array} + */ + ///////////////////////////////////////////////////////////////////////////// + this.get = function () { + return m_dataArray; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Set value of the uniform + * + * @param value + */ + ///////////////////////////////////////////////////////////////////////////// + this.set = function (value) { + var i = 0; + if (m_dataArray.length === 16) { + for (i = 0; i < 16; i += 1) { + m_dataArray[i] = value[i]; + } + } else if (m_dataArray.length === 9) { + for (i = 0; i < 9; i += 1) { + m_dataArray[i] = value[i]; + } + } else if (m_dataArray.length === 4) { + for (i = 0; i < 4; i += 1) { + m_dataArray[i] = value[i]; + } + } else if (m_dataArray.length === 3) { + for (i = 0; i < 3; i += 1) { + m_dataArray[i] = value[i]; + } + } else if (m_dataArray.length === 2) { + for (i = 0; i < 2; i += 1) { + m_dataArray[i] = value[i]; + } + } else { + m_dataArray[0] = value; + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Call GL and pass updated values to the current shader + * + * @param location + */ + ///////////////////////////////////////////////////////////////////////////// + this.callGL = function (renderState, location) { + if (this.m_numberElements < 1) { + return; + } + + 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); + break; + default: + break; + } + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Virtual method to update the uniform + * + * Should be implemented by the derived class. + * + * @param renderState + * @param program + */ + ///////////////////////////////////////////////////////////////////////////// + this.update = function (renderState, program) { + renderState = renderState; /* unused parameter */ + program = program; /* unused parameter */ + // Should be implemented by the derived class + }; + + return this; + }; + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create new instance of class modelViewUniform + * + * @param name + * @returns {vgl.modelViewUniform} + */ + /////////////////////////////////////////////////////////////////////////////// + vgl.modelViewUniform = function (name) { + 'use strict'; + + if (!(this instanceof vgl.modelViewUniform)) { + return new vgl.modelViewUniform(name); + } + + if (name.length === 0) { + name = 'modelViewMatrix'; + } + + vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); + + this.set(mat4.create()); + + ///////////////////////////////////////////////////////////////////////////// + /** + * Update the uniform given a render state and shader program + * + * @param {vgl.renderState} renderState + * @param {vgl.shaderProgram} program + */ + ///////////////////////////////////////////////////////////////////////////// + this.update = function (renderState, program) { + program = program; /* unused parameter */ + this.set(renderState.m_modelViewMatrix); + }; + + return this; + }; + + inherit(vgl.modelViewUniform, vgl.uniform); + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create new instance of class modelViewOriginUniform. + * + * @param name + * @param {array} origin a triplet of floats. + * @returns {vgl.modelViewUniform} + */ + /////////////////////////////////////////////////////////////////////////////// + vgl.modelViewOriginUniform = function (name, origin) { + 'use strict'; + + if (!(this instanceof vgl.modelViewOriginUniform)) { + return new vgl.modelViewOriginUniform(name, origin); + } + + if (name.length === 0) { + name = 'modelViewMatrix'; + } + origin = origin || [0, 0, 0]; + + var m_origin = [origin[0], origin[1], origin[2] || 0]; + + vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); + + this.set(mat4.create()); + + /** + * Change the origin used by the uniform view matrix. + * + * @param {array} origin a triplet of floats. + */ + this.setOrigin = function (origin) { + origin = origin || [0, 0, 0]; + m_origin = [origin[0], origin[1], origin[2] || 0]; + }; + + ///////////////////////////////////////////////////////////////////////////// + /** + * Update the uniform given a render state and shader program. This offsets + * the modelViewMatrix by the origin, and, if the model view should be + * aligned, aligns it appropriately. The alignment must be done after the + * origin offset to maintain precision. + * + * @param {vgl.renderState} renderState + * @param {vgl.shaderProgram} program + */ + ///////////////////////////////////////////////////////////////////////////// + this.update = function (renderState, program) { + program = program; /* unused parameter */ + var view = mat4.create(); + mat4.translate(view, renderState.m_modelViewMatrix, m_origin); + if (renderState.m_modelViewAlignment) { + var align = renderState.m_modelViewAlignment; + /* view[12] and view[13] are the x and y offsets. align.round is the + * 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.roundx) * align.roundx + align.dx; + view[13] = Math.round(view[13] / align.roundy) * align.roundy + align.dy; + } + this.set(view); + }; + + return this; + }; + + inherit(vgl.modelViewOriginUniform, vgl.uniform); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class projectionUniform + * + * @param name + * @returns {vgl.projectionUniform} + */ + /////////////////////////////////////////////////////////////////////////////// + vgl.projectionUniform = function (name) { + 'use strict'; + + if (!(this instanceof vgl.projectionUniform)) { + return new vgl.projectionUniform(name); + } + + if (name.length === 0) { + name = 'projectionMatrix'; + } + + vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); + + this.set(mat4.create()); + + ///////////////////////////////////////////////////////////////////////////// + /** + * Update the uniform given a render state and shader program + * + * @param renderState + * @param program + */ + ///////////////////////////////////////////////////////////////////////////// + this.update = function (renderState, program) { + program = program; /* unused parameter */ + this.set(renderState.m_projectionMatrix); + }; + + return this; + }; + + inherit(vgl.projectionUniform, vgl.uniform); + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class floatUniform + * + * @param name + * @param value + * @returns {vgl.floatUniform} + */ + /////////////////////////////////////////////////////////////////////////////// + vgl.floatUniform = function (name, value) { + 'use strict'; + + if (!(this instanceof vgl.floatUniform)) { + return new vgl.floatUniform(name, value); + } + + if (name.length === 0) { + name = 'floatUniform'; + } + + value = value === undefined ? 1.0 : value; + + vgl.uniform.call(this, vgl.GL.FLOAT, name); + + this.set(value); + }; + + inherit(vgl.floatUniform, vgl.uniform); + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create new instance of class normalMatrixUniform + * + * @param name + * @returns {vgl.normalMatrixUniform} + */ + /////////////////////////////////////////////////////////////////////////////// + vgl.normalMatrixUniform = function (name) { + 'use strict'; + + if (!(this instanceof vgl.normalMatrixUniform)) { + return new vgl.normalMatrixUniform(name); + } + + if (name.length === 0) { + name = 'normalMatrix'; + } + + vgl.uniform.call(this, vgl.GL.FLOAT_MAT4, name); + + this.set(mat4.create()); + + ///////////////////////////////////////////////////////////////////////////// + /** + * Update the uniform given a render state and shader program + * + * @param {vgl.renderState} renderState + * @param {vgl.shaderProgram} program + */ + ///////////////////////////////////////////////////////////////////////////// + this.update = function (renderState, program) { + program = program; /* unused parameter */ + this.set(renderState.m_normalMatrix); + }; + + return this; + }; + + inherit(vgl.normalMatrixUniform, vgl.uniform); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Keys to identify vertex attributes + * + * @type {{Position: number, Normal: number, TextureCoordinate: number, + * Color: number, Scalar: number, Scalar2: number, Scalar3: number, + * Scalar4: number, Scalar5: number, Scalar6: number, Scalar7: number, + * CountAttributeIndex: number}} + */ + ////////////////////////////////////////////////////////////////////////////// + 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 + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of vertexAttribute + * + * @param {string} name + * @returns {vgl.vertexAttribute} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.vertexAttribute = function (name) { + 'use strict'; + + if (!(this instanceof vgl.vertexAttribute)) { + return new vgl.vertexAttribute(name); + } + + var m_name = name; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get name of the vertex attribute + * + * @returns {string} + */ + ////////////////////////////////////////////////////////////////////////////// + this.name = function () { + return m_name; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Bind vertex data to the given render state + * + * @param {vgl.renderState} renderState + * @param {vgl.vertexAttributeKeys} key + */ + ////////////////////////////////////////////////////////////////////////////// + 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)); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Undo bind vertex data for a given render state + * + * @param {vgl.renderState} renderState + * @param {vgl.vertexAttributeKeys} key + */ + ////////////////////////////////////////////////////////////////////////////// + this.undoBindVertexData = function (renderState, key) { + key = key; /* unused parameter */ + + var program = renderState.m_material.shaderProgram(); + + renderState.m_context.disableVertexAttribArray(program.attributeLocation(m_name)); + }; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + /////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class source + * + * @returns {vgl.source} + */ + /////////////////////////////////////////////////////////////////////////////// + vgl.source = function () { + 'use strict'; + + if (!(this instanceof vgl.source)) { + return new vgl.source(); + } + + vgl.object.call(this); + + ///////////////////////////////////////////////////////////////////////////// + /** + * Virtual function to create a source instance + */ + ///////////////////////////////////////////////////////////////////////////// + this.create = function () { + }; + + return this; + }; + + inherit(vgl.source, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class planeSource + * + * @class + * @returns {vgl.planeSource} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.planeSource = function () { + 'use strict'; + + if (!(this instanceof vgl.planeSource)) { + return new vgl.planeSource(); + } + vgl.source.call(this); + + var m_origin = [0.0, 0.0, 0.0], + m_point1 = [1.0, 0.0, 0.0], + m_point2 = [0.0, 1.0, 0.0], + m_normal = [0.0, 0.0, 1.0], + m_xresolution = 1, + m_yresolution = 1, + m_geom = null; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set origin of the plane + * + * @param x + * @param y + * @param z + */ + //////////////////////////////////////////////////////////////////////////// + this.setOrigin = function (x, y, z) { + m_origin[0] = x; + m_origin[1] = y; + m_origin[2] = z; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set point that defines the first axis of the plane + * + * @param x + * @param y + * @param z + */ + //////////////////////////////////////////////////////////////////////////// + this.setPoint1 = function (x, y, z) { + m_point1[0] = x; + m_point1[1] = y; + m_point1[2] = z; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set point that defines the first axis of the plane + * + * @param x + * @param y + * @param z + */ + //////////////////////////////////////////////////////////////////////////// + this.setPoint2 = function (x, y, z) { + m_point2[0] = x; + m_point2[1] = y; + m_point2[2] = z; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a plane geometry given input parameters + * + * @returns {null} + */ + //////////////////////////////////////////////////////////////////////////// + this.create = function () { + m_geom = new vgl.geometryData(); + + var x = [], tc = [], v1 = [], v2 = [], + pts = [], i, j, k, ii, numPts, numPolys, + posIndex = 0, normIndex = 0, colorIndex = 0, texCoordIndex = 0, + positions = [], normals = [], colors = [], + texCoords = [], indices = [], tristrip = null, + sourcePositions = null, sourceColors = null, sourceTexCoords; + + x.length = 3; + tc.length = 2; + v1.length = 3; + v2.length = 3; + pts.length = 3; + + // Check input + for (i = 0; i < 3; i += 1) { + v1[i] = m_point1[i] - m_origin[i]; + v2[i] = m_point2[i] - m_origin[i]; + } + + // TODO Compute center and normal + // Set things up; allocate memory + 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; + + for (k = 0, i = 0; i < (m_yresolution + 1); i += 1) { + tc[1] = i / m_yresolution; + + for (j = 0; j < (m_xresolution + 1); j += 1) { + tc[0] = j / m_xresolution; + + for (ii = 0; ii < 3; ii += 1) { + x[ii] = m_origin[ii] + tc[0] * v1[ii] + tc[1] * v2[ii]; + } + + //jshint plusplus: false + positions[posIndex++] = x[0]; + positions[posIndex++] = x[1]; + positions[posIndex++] = x[2]; + + colors[colorIndex++] = 1.0; + colors[colorIndex++] = 1.0; + colors[colorIndex++] = 1.0; + + normals[normIndex++] = m_normal[0]; + normals[normIndex++] = m_normal[1]; + normals[normIndex++] = m_normal[2]; + + texCoords[texCoordIndex++] = tc[0]; + texCoords[texCoordIndex++] = tc[1]; + //jshint plusplus: true + } + } + + /// Generate polygon connectivity + for (i = 0; i < m_yresolution; i += 1) { + for (j = 0; j < m_xresolution; 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; i < numPts; i += 1) { + indices[i] = i; + } + + 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); + + return m_geom; + }; + }; + + inherit(vgl.planeSource, vgl.source); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class pointSource + * + * @class + * @returns {vgl.pointSource} + */ + ////////////////////////////////////////////////////////////////////////////// + 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; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get positions for the points + */ + //////////////////////////////////////////////////////////////////////////// + this.getPositions = function () { + return m_positions; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set positions for the source + * + * @param positions + */ + //////////////////////////////////////////////////////////////////////////// + this.setPositions = function (positions) { + if (positions instanceof Array) { + m_positions = positions; + } else { + console + .log('[ERROR] Invalid data type for positions. Array is required.'); + } + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get colors for the points + */ + //////////////////////////////////////////////////////////////////////////// + this.getColors = function () { + return m_colors; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set colors for the points + * + * @param colors + */ + //////////////////////////////////////////////////////////////////////////// + this.setColors = function (colors) { + if (colors instanceof Array) { + m_colors = colors; + } else { + console.log('[ERROR] Invalid data type for colors. Array is required.'); + } + + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get size for the points + */ + //////////////////////////////////////////////////////////////////////////// + this.getSize = function () { + return m_size; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set colors for the points + * + * @param colors + */ + //////////////////////////////////////////////////////////////////////////// + this.setSize = function (size) { + m_size = size; + this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set texture coordinates for the points + * + * @param texcoords + */ + //////////////////////////////////////////////////////////////////////////// + this.setTextureCoordinates = function (texcoords) { + if (texcoords instanceof Array) { + m_textureCoords = texcoords; + } else { + console.log('[ERROR] Invalid data type for ' + + 'texture coordinates. Array is required.'); + } + m_this.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a point geometry given input parameters + */ + //////////////////////////////////////////////////////////////////////////// + this.create = function () { + m_geom = new vgl.geometryData(); + + if (m_positions.length % 3 !== 0) { + console.log('[ERROR] Invalid length of the points array'); + return; + } + + var numPts = m_positions.length / 3, + i = 0, + indices = [], + pointsPrimitive, + sourcePositions, + sourceColors, + sourceTexCoords, + sourceSize; + + indices.length = numPts; + for (i = 0; i < numPts; i += 1) { + indices[i] = i; + } + + /// Generate array of size if needed + sourceSize = vgl.sourceDataDf(); + if (numPts !== m_size.length) { + for (i = 0; i < numPts; i += 1) { + sourceSize.pushBack(m_size); + } + } else { + sourceSize.setData(m_size); + } + m_geom.addSource(sourceSize); + + pointsPrimitive = new vgl.points(); + pointsPrimitive.setIndices(indices); + + sourcePositions = vgl.sourceDataP3fv(); + sourcePositions.pushBack(m_positions); + m_geom.addSource(sourcePositions); + + if ((m_colors.length > 0) && m_colors.length === m_positions.length) { + sourceColors = vgl.sourceDataC3fv(); + sourceColors.pushBack(m_colors); + m_geom.addSource(sourceColors); + } else if ((m_colors.length > 0) && m_colors.length !== m_positions.length) { + console + .log('[ERROR] Number of colors are different than number of points'); + } + + if (m_textureCoords.length > 0 && + m_textureCoords.length === m_positions.length) { + sourceTexCoords = vgl.sourceDataT2fv(); + sourceTexCoords.pushBack(m_textureCoords); + m_geom.addSource(sourceTexCoords); + } else if (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); + + return m_geom; + }; + }; + + inherit(vgl.pointSource, vgl.source); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class lineSource + * + * @class + * @returns {vgl.lineSource} + */ + ////////////////////////////////////////////////////////////////////////////// + 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; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set start positions for the lines + * + * @param positions + */ + //////////////////////////////////////////////////////////////////////////// + this.setPositions = function (positions) { + if (positions instanceof Array) { + m_positions = positions; + this.modified(); + return true; + } + + console + .log('[ERROR] Invalid data type for positions. Array is required.'); + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Set colors for the lines + * + * @param colors + */ + //////////////////////////////////////////////////////////////////////////// + this.setColors = function (colors) { + if (colors instanceof Array) { + m_colors = colors; + this.modified(); + return true; + } + + console.log('[ERROR] Invalid data type for colors. Array is required.'); + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a point geometry given input parameters + */ + //////////////////////////////////////////////////////////////////////////// + this.create = function () { + if (!m_positions) { + console.log('[error] Invalid positions'); + return; + } + + if (m_positions.length % 3 !== 0) { + console.log('[error] Line source requires 3d points'); + return; + } + + if (m_positions.length % 3 !== 0) { + console.log('[ERROR] Invalid length of the points array'); + return; + } + + var m_geom = new vgl.geometryData(), + numPts = m_positions.length / 3, + i, + indices = [], + linesPrimitive, + sourcePositions, + sourceColors; + + indices.length = numPts; + + for (i = 0; i < numPts; i += 1) { + indices[i] = i; + } + + linesPrimitive = new vgl.lines(); + linesPrimitive.setIndices(indices); + + sourcePositions = vgl.sourceDataP3fv(); + sourcePositions.pushBack(m_positions); + m_geom.addSource(sourcePositions); + + if (m_colors && (m_colors.length > 0) && + m_colors.length === m_positions.length) { + sourceColors = vgl.sourceDataC3fv(); + sourceColors.pushBack(m_colors); + m_geom.addSource(sourceColors); + } else if (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); + + return m_geom; + }; + }; + + inherit(vgl.lineSource, vgl.source); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global document, vgl, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class utils + * + * Utility class provides helper functions such as functions to create + * shaders, geometry etc. + * + * @returns {vgl.utils} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils = function () { + 'use strict'; + + if (!(this instanceof vgl.utils)) { + return new vgl.utils(); + } + vgl.object.call(this); + + return this; + }; + + inherit(vgl.utils, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Helper function to compute power of 2 number + * + * @param value + * @param pow + * + * @returns {number} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.computePowerOfTwo = function (value, pow) { + 'use strict'; + pow = pow || 1; + while (pow < value) { + pow *= 2; + } + return pow; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of default vertex shader that uses a texture + * + * Helper function to create default vertex shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createTextureVertexShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, + vertexShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of default fragment shader that uses a texture + * + * Helper function to create default fragment shader with sampler + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createTextureFragmentShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, + fragmentShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create variation of createTextureFragmentShader which uses texture alpha + * + * Helper function to create default fragment shader with sampler + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createRgbaTextureFragmentShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, + fragmentShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of default vertex shader + * + * Helper function to create default vertex shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createVertexShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, + vertexShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of default vertex shader + * + * Helper function to create default vertex shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPointVertexShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, + vertexShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of vertex shader with a solid color + * + * Helper function to create default vertex shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createVertexShaderSolidColor = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, + vertexShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of vertex shader that passes values through + * for color mapping + * + * Helper function to create default vertex shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createVertexShaderColorMap = function (context, min, max) { + 'use strict'; + min = min; /* unused parameter */ + max = max; /* unused parameter */ + 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'); + return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, + vertexShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of default fragment shader + * + * Helper function to create default fragment shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createFragmentShader = function (context) { + 'use strict'; + var fragmentShaderSource = ['varying mediump vec3 iVertexColor;', + 'uniform mediump float opacity;', + 'void main(void) {', + 'gl_FragColor = vec4(iVertexColor, opacity);', + '}'].join('\n'); + return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, + fragmentShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a Phong vertex shader + * + * Helper function to create Phong vertex shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPhongVertexShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, + vertexShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of Phong fragment shader + * + * Helper function to create Phong fragment shader + * + * NOTE: Shader assumes directional light + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPhongFragmentShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, + fragmentShaderSource); + }; + + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of fragment shader with an assigned constant color. + * + * Helper function to create default fragment shader + * + * @param context + * @returns {vgl.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'); + return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, + fragmentShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of fragment shader that maps values into colors bia lookup table + * + * Helper function to create default fragment shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createFragmentShaderColorMap = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, + fragmentShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of vertex shader for point sprites + * + * Helper function to create default point sprites vertex shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPointSpritesVertexShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.VERTEX_SHADER, context, + vertexShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of fragment shader for point sprites + * + * Helper function to create default point sprites fragment shader + * + * @param context + * @returns {vgl.shader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPointSpritesFragmentShader = function (context) { + 'use strict'; + 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'); + return vgl.getCachedShader(vgl.GL.FRAGMENT_SHADER, context, + fragmentShaderSource); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of texture material + * + * Helper function to create a texture material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createTextureMaterial = function (isRgba, origin) { + 'use strict'; + var 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.0), + modelViewUniform, + projectionUniform = new vgl.projectionUniform('projectionMatrix'), + samplerUniform = new vgl.uniform(vgl.GL.INT, 'sampler2d'), + opacityUniform = null; + if (origin !== undefined) { + modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', + origin); + } else { + modelViewUniform = 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); + + if (isRgba) { + fragmentShader = vgl.utils.createRgbaTextureFragmentShader(vgl.GL); + } else { + fragmentShader = vgl.utils.createTextureFragmentShader(vgl.GL); + } + opacityUniform = new vgl.floatUniform('opacity', 1.0); + prog.addUniform(opacityUniform); + + prog.addShader(fragmentShader); + prog.addShader(vertexShader); + mat.addAttribute(prog); + mat.addAttribute(blend); + + return mat; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of geometry material + * + * Helper function to create geometry material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createGeometryMaterial = function () { + 'use strict'; + var mat = new vgl.material(), + prog = new vgl.shaderProgram(), + pointSize = 5.0, + opacity = 1.0, + 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'); + + 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); + + return mat; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of geometry material + * + * Helper function to create geometry material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPointGeometryMaterial = function (opacity) { + 'use strict'; + opacity = opacity === undefined ? 1.0 : 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'); + + 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); + + return mat; + }; + + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of geometry material with the phong shader + * + * Helper function to create color phong shaded geometry material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + 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.0), + modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), + normalUniform = new vgl.normalMatrixUniform('normalMatrix'), + projectionUniform = new vgl.projectionUniform('projectionMatrix'); + + 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(blend); + mat.addAttribute(prog); + + return mat; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of colored geometry material + * + * Helper function to create color geometry material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + 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.0), + opacityUniform = new vgl.floatUniform('opacity', 1.0), + modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), + projectionUniform = new vgl.projectionUniform('projectionMatrix'); + + 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); + + return mat; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of geometry material + * + * Helper function to create geometry material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createColorMappedMaterial = function (lut) { + 'use strict'; + if (!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.0), + opacityUniform = new vgl.floatUniform('opacity', 1.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.FLOAT, 'sampler2d'), + lookupTable = lut; + + 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); + + return mat; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Update color mapped material + * + * @param mat + * @param scalarRange + * @param lut + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.updateColorMappedMaterial = function (mat, lut) { + 'use strict'; + if (!mat) { + console.log('[warning] Invalid material. Nothing to update.'); + return; + } + + if (!lut) { + console.log('[warning] Invalid lookup table. Nothing to update.'); + return; + } + + + var lutMin = mat.shaderProgram().uniform('lutMin'), + lutMax = mat.shaderProgram().uniform('lutMax'); + + lutMin.set(lut.range()[0]); + lutMax.set(lut.range()[1]); + + // This will replace the existing lookup table + mat.setAttribute(lut); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of solid color material + * + * Helper function to create geometry material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createSolidColorMaterial = function (color) { + 'use strict'; + if (!color) { + color = [1.0, 1.0, 1.0]; + } + + 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.0), + opacityUniform = new vgl.floatUniform('opacity', 1.0), + modelViewUniform = new vgl.modelViewUniform('modelViewMatrix'), + projectionUniform = new vgl.projectionUniform('projectionMatrix'); + + 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); + + return mat; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of point sprites material + * + * Helper function to create point sprites material + * + * @returns {vgl.material} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPointSpritesMaterial = function (image, lut) { + 'use strict'; + var scalarRange = lut === undefined ? [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.0), + vertexColorWeightUniform = + new vgl.floatUniform('vertexColorWeight', 0.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(); + + samplerUniform.set(0); + scalarsToColors.set(1); + useScalarsToColors.set(0); + useVertexColors.set(0); + pointSize.set([1.0, 1.0]); + + 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); + + if (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); + return mat; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of an actor that contains a plane geometry + * + * Function to create a plane node This method will create a plane actor + * with texture coordinates, eventually normal, and plane material. + * + * @returns {vgl.actor} + */ + ////////////////////////////////////////////////////////////////////////////// + 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(); + + 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); + + return actor; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of an actor that contains a texture plane geometry + * + * Helper function to create a plane textured node This method will create + * a plane actor with texture coordinates, eventually normal, and plane + * material. + * + * @returns {vgl.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(); + + 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); + + return actor; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of an actor that contains points + * + * Helper function to create a point node This method will create a point + * actor with texture coordinates, eventually normal, and plane material. + * + * @returns {vgl.actor} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPoints = function (positions, size, colors, texcoords, opacity) { + 'use strict'; + if (!positions) { + console.log('[ERROR] Cannot create points without positions'); + return null; + } + + opacity = opacity === undefined ? 1.0 : opacity; + var mapper = new vgl.mapper(), + pointSource = new vgl.pointSource(), + mat = vgl.utils.createPointGeometryMaterial(opacity), + actor = new vgl.actor(); + + pointSource.setPositions(positions); + if (colors) { + pointSource.setColors(colors); + } + + if (texcoords) { + pointSource.setTextureCoordinates(texcoords); + } + + if (size) { + pointSource.setSize(size); + } else { + pointSource.setSize(1.0); + } + + mapper.setGeometryData(pointSource.create()); + actor.setMapper(mapper); + actor.setMaterial(mat); + + return actor; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of an actor that contains point sprites + * + * Helper function to create a point sprites node This method will create + * a point sprites actor with texture coordinates, normals, and a point sprites + * material. + * + * @returns {vgl.actor} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createPointSprites = function (image, positions, colors, + texcoords) { + 'use strict'; + if (!image) { + console.log('[ERROR] Point sprites requires an image'); + return null; + } + + if (!positions) { + console.log('[ERROR] Cannot create points without positions'); + return null; + } + + var mapper = new vgl.mapper(), + pointSource = new vgl.pointSource(), + mat = vgl.utils.createPointSpritesMaterial(image), + actor = new vgl.actor(); + + pointSource.setPositions(positions); + if (colors) { + pointSource.setColors(colors); + } + + if (texcoords) { + pointSource.setTextureCoordinates(texcoords); + } + + mapper.setGeometryData(pointSource.create()); + actor.setMapper(mapper); + actor.setMaterial(mat); + + return actor; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create lines given positions, colors, and desired length + * + * @param positions + * @param colors + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createLines = function (positions, colors) { + 'use strict'; + if (!positions) { + console.log('[ERROR] Cannot create points without positions'); + return null; + } + + var mapper = new vgl.mapper(), + lineSource = new vgl.lineSource(), + mat = vgl.utils.createGeometryMaterial(), + actor = new vgl.actor(); + + lineSource.setPositions(positions); + if (colors) { + lineSource.setColors(colors); + } + + mapper.setGeometryData(lineSource.create()); + actor.setMapper(mapper); + actor.setMaterial(mat); + + return actor; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create color legend + * + * @param lookupTable + * @param width + * @param height + * @param origin + * @param divs + * @returns {Array} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.createColorLegend = function (varname, lookupTable, origin, + width, height, countMajor, + countMinor) { + 'use strict'; + + if (!lookupTable) { + console.log('[error] Invalid lookup table'); + return []; + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create labels for the legend + * + * @param ticks + * @param range + * @param divs + */ + ////////////////////////////////////////////////////////////////////////////// + function createLabels(varname, positions, range) { + if (!positions) { + console.log('[error] Create labels requires positions (x,y,z) array'); + return; + } + + if (positions.length % 3 !== 0) { + console.log('[error] Create labels require positions array contain 3d points'); + return; + } + + if (!range) { + console.log('[error] Create labels requires Valid range'); + return; + } + + var actor = null, + size = vgl.utils.computePowerOfTwo(48), + index = 0, + actors = [], + origin = [], + pt1 = [], + pt2 = [], + delta = (positions[6] - positions[0]), + axisLabelOffset = 4, i; + + origin.length = 3; + pt1.length = 3; + pt2.length = 3; + + // For now just create labels for end points + for (i = 0; i < 2; 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], true); + + 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); + } + + // Create axis label + origin[0] = (positions[0] + positions[positions.length - 3] - size) * 0.5; + 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], true); + 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); + + return actors; + } + + ////////////////////////////////////////////////////////////////////////////// + // TODO Currently we assume that the ticks are laid on x-axis + // and this is on a 2D plane (ignoring Z axis. For now lets + // not draw minor ticks. + /** + * Create ticks and labels + * + * @param originX + * @param originY + * @param originZ + * @param pt1X + * @param pt1Y + * @param pt1Z + * @param pt2X + * @param pt2Y + * @param pt2Z + * @param divs + * @param heightMajor + * @param heightMinor + * @returns {Array} Returns array of vgl.actor + */ + ////////////////////////////////////////////////////////////////////////////// + function createTicksAndLabels(varname, lut, + originX, originY, originZ, + pt1X, pt1Y, pt1Z, + pt2X, pt2Y, pt2Z, + countMajor, countMinor, + heightMajor, heightMinor) { + heightMinor = heightMinor; /* unused parameter */ + var width = pt2X - pt1X, + index = null, + delta = width / countMajor, + positions = [], + actors = []; + + for (index = 0; index <= countMajor; 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); + } + + // TODO: Fix this + //actor = vgl.utils.createLines(positions, null); + //actor.setReferenceFrame(vgl.boundingObject.ReferenceFrame.Absolute); + //actor.material().setBinNumber(vgl.material.RenderBin.Overlay); + //actors.push(actor); + + actors = actors.concat(createLabels(varname, positions, lut.range())); + return actors; + } + + // TODO Currently we create only one type of legend + var pt1X = origin[0] + width, + pt1Y = origin[1], + pt1Z = 0.0, + pt2X = origin[0], + pt2Y = origin[1] + height, + pt2Z = 0.0, + actors = [], + actor = null, + mat = null, + group = vgl.groupNode(); + + actor = vgl.utils.createTexturePlane( + origin[0], origin[1], origin[2], + pt1X, pt1Y, pt1Z, + pt2X, pt2Y, pt2Z, true + ); + + 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)); + + // TODO This needs to change so that we can return a group node + // which should get appended to the scene graph + return actors; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create 2D texture by rendering text using canvas2D context + * + * @param textToWrite + * @param textSize + * @param color + * @returns {vgl.texture} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.utils.create2DTexture = function (textToWrite, textSize, + color, font, alignment, baseline, bold) { + 'use strict'; + + var canvas = document.getElementById('textRendering'), + ctx = null, + texture = vgl.texture(); + + font = font || 'sans-serif'; + alignment = alignment || 'center'; + baseline = baseline || 'bottom'; + + if (typeof bold === 'undefined') { + bold = true; + } + + if (!canvas) { + canvas = document.createElement('canvas'); + } + ctx = canvas.getContext('2d'); + + canvas.setAttribute('id', 'textRendering'); + canvas.style.display = 'none'; + + // Make width and height equal so that we get pretty looking text. + 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); + + // This determines the text colour, it can take a hex value or rgba value + // (e.g. rgba(255,0,0,0.5)) + ctx.fillStyle = 'rgba(200, 85, 10, 1.0)'; + + // This determines the alignment of text, e.g. left, center, right + ctx.textAlign = alignment; + + // This determines the baseline of the text, e.g. top, middle, bottom + ctx.textBaseline = baseline; + + // This determines the size of the text and the font family used + ctx.font = 4 * textSize + 'px ' + font; + if (bold) { + ctx.font = 'bold ' + ctx.font; + } + + ctx.fillText(textToWrite, canvas.width / 2, canvas.height / 2, canvas.width); + + texture.setImage(canvas); + texture.updateDimensions(); + + return texture; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, vec4, inherit*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class picker + * + * @class vgl.picker + * @returns {vgl.picker} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.picker = function () { + 'use strict'; + + if (!(this instanceof vgl.picker)) { + return new vgl.picker(); + } + vgl.object.call(this); + + /** @private */ + var m_actors = []; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get actors intersected + */ + //////////////////////////////////////////////////////////////////////////// + this.getActors = function () { + return m_actors; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Perform pick operation + */ + //////////////////////////////////////////////////////////////////////////// + this.pick = function (selectionX, selectionY, renderer) { + // Check if variables are acceptable + if (selectionX === undefined) { + return 0; + } + if (selectionY === undefined) { + return 0; + } + if (renderer === undefined) { + return 0; + } + + // Clean list of actors intersected previously + m_actors = []; + + // + var camera = renderer.camera(), + width = renderer.width(), + height = renderer.height(), + fpoint = camera.focalPoint(), + focusWorldPt = vec4.fromValues(fpoint[0], fpoint[1], fpoint[2], 1.0), + focusDisplayPt = renderer.worldToDisplay( + focusWorldPt, camera.viewMatrix(), + camera.projectionMatrix(), width, height), + displayPt = vec4.fromValues(selectionX, + selectionY, focusDisplayPt[2], 1.0), + // Convert selection point into world coordinates + worldPt = renderer.displayToWorld(displayPt, camera.viewMatrix(), + camera.projectionMatrix(), width, height), + cameraPos = camera.position(), ray = [], actors, count, i, bb, + tmin, tmax, tymin, tymax, tzmin, tzmax, actor; + + for (i = 0; i < 3; i += 1) { + ray[i] = worldPt[i] - cameraPos[i]; + } + + // Go through all actors and check if intersects + actors = renderer.sceneRoot().children(); + count = 0; + + for (i = 0; i < actors.length; i += 1) { + actor = actors[i]; + if (actor.visible() === true) { + bb = actor.bounds(); + // Ray-aabb intersection - Smits' method + if (ray[0] >= 0) { + tmin = (bb[0] - cameraPos[0]) / ray[0]; + tmax = (bb[1] - cameraPos[0]) / ray[0]; + } else { + tmin = (bb[1] - cameraPos[0]) / ray[0]; + tmax = (bb[0] - cameraPos[0]) / ray[0]; + } + if (ray[1] >= 0) { + tymin = (bb[2] - cameraPos[1]) / ray[1]; + tymax = (bb[3] - cameraPos[1]) / ray[1]; + } else { + tymin = (bb[3] - cameraPos[1]) / ray[1]; + tymax = (bb[2] - cameraPos[1]) / ray[1]; + } + if ((tmin > tymax) || (tymin > tmax)) { + //jscs:disable disallowKeywords + continue; + //jscs:enable disallowKeywords + } + + + if (tymin > tmin) { + tmin = tymin; + } + if (tymax < tmax) { + tmax = tymax; + } + if (ray[2] >= 0) { + tzmin = (bb[4] - cameraPos[2]) / ray[2]; + tzmax = (bb[5] - cameraPos[2]) / ray[2]; + } else { + tzmin = (bb[5] - cameraPos[2]) / ray[2]; + tzmax = (bb[4] - cameraPos[2]) / ray[2]; + } + if ((tmin > tzmax) || (tzmin > tmax)) { + //jscs:disable disallowKeywords + continue; + //jscs:enable disallowKeywords + } + if (tzmin > tmin) { + tmin = tzmin; + } + if (tzmax < tmax) { + tmax = tzmax; + } + + m_actors[count] = actor; + count += 1; + } + } + return count; + }; + + return this; + }; + + inherit(vgl.picker, vgl.object); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, $*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of shapefile reader + * + * This contains code that reads a shapefile and produces vgl geometries + * + * @class + * @returns {vgl.shapefileReader} + */ + ////////////////////////////////////////////////////////////////////////////// + vgl.shapefileReader = function () { + 'use strict'; + + if (!(this instanceof vgl.shapefileReader)) { + return new vgl.shapefileReader(); + } + + var m_that = this; + var SHP_NULL = 0; + var SHP_POINT = 1; + var SHP_POLYGON = 5; + var SHP_POLYLINE = 3; + + this.int8 = function (data, offset) { + return data.charCodeAt (offset); + }; + + /*jshint bitwise: false */ + this.bint32 = function (data, offset) { + return ( + ((data.charCodeAt (offset) & 0xff) << 24) + + ((data.charCodeAt (offset + 1) & 0xff) << 16) + + ((data.charCodeAt (offset + 2) & 0xff) << 8) + + (data.charCodeAt (offset + 3) & 0xff) + ); + }; + + this.lint32 = function (data, offset) { + return ( + ((data.charCodeAt (offset + 3) & 0xff) << 24) + + ((data.charCodeAt (offset + 2) & 0xff) << 16) + + ((data.charCodeAt (offset + 1) & 0xff) << 8) + + (data.charCodeAt (offset) & 0xff) + ); + }; + + this.bint16 = function (data, offset) { + return ( + ((data.charCodeAt (offset) & 0xff) << 8) + + (data.charCodeAt (offset + 1) & 0xff) + ); + }; + + this.lint16 = function (data, offset) { + return ( + ((data.charCodeAt (offset + 1) & 0xff) << 8) + + (data.charCodeAt (offset) & 0xff) + ); + }; + + this.ldbl64 = function (data, offset) { + var b0 = data.charCodeAt (offset) & 0xff; + var b1 = data.charCodeAt (offset + 1) & 0xff; + var b2 = data.charCodeAt (offset + 2) & 0xff; + var b3 = data.charCodeAt (offset + 3) & 0xff; + var b4 = data.charCodeAt (offset + 4) & 0xff; + var b5 = data.charCodeAt (offset + 5) & 0xff; + var b6 = data.charCodeAt (offset + 6) & 0xff; + var b7 = data.charCodeAt (offset + 7) & 0xff; + + var sign = 1 - 2 * (b7 >> 7); + var exp = (((b7 & 0x7f) << 4) + ((b6 & 0xf0) >> 4)) - 1023; + //var frac = (b6 & 0x0f) * Math.pow (2, -4) + b5 * Math.pow (2, -12) + b4 * + // Math.pow (2, -20) + b3 * Math.pow (2, -28) + b2 * Math.pow (2, -36) + b1 * + // Math.pow (2, -44) + b0 * Math.pow (2, -52); + + //return sign * (1 + frac) * Math.pow (2, exp); + var frac = (b6 & 0x0f) * 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 = data.charCodeAt (offset) & 0xff; + var b1 = data.charCodeAt (offset + 1) & 0xff; + var b2 = data.charCodeAt (offset + 2) & 0xff; + var b3 = data.charCodeAt (offset + 3) & 0xff; + + var sign = 1 - 2 * (b3 >> 7); + var exp = (((b3 & 0x7f) << 1) + ((b2 & 0xfe) >> 7)) - 127; + var frac = (b2 & 0x7f) * Math.pow (2, 16) + b1 * Math.pow (2, 8) + b0; + + return sign * (1 + frac * Math.pow (2, -23)) * Math.pow (2, exp); + }; + /*jshint bitwise: true */ + + this.str = function (data, offset, length) { + var chars = []; + var index = offset; + while (index < offset + length) { + var c = data[index]; + if (c.charCodeAt (0) !== 0) { + chars.push (c); + } else { + break; + } + index += 1; + } + return chars.join (''); + }; + + this.readHeader = function (data) { + var code = this.bint32(data, 0); + var length = this.bint32(data, 24); + var version = this.lint32(data, 28); + var shapetype = this.lint32(data, 32); + + /* + var xmin = this.ldbl64(data, 36); + var ymin = this.ldbl64(data, 44); + var xmax = this.ldbl64(data, 52); + var ymax = this.ldbl64(data, 60); + */ + return { + code: code, + length: length, + version: version, + shapetype: shapetype + // bounds: new Box (vect (xmin, ymin), vect (xmax, ymax)) + }; + }; + + this.loadShx = function (data) { + var indices = []; + var appendIndex = function (offset) { + indices.push (2 * m_that.bint32(data, offset)); + return offset + 8; + }; + var offset = 100; + while (offset < data.length) { + offset = appendIndex (offset); + } + return indices; + }; + + this.Shapefile = function (options) { + var path = options.path; + $.ajax ({ + url: path + '.shx', + mimeType: 'text/plain; charset=x-user-defined', + success: function (data) { + var indices = this.loadShx(data); + $.ajax ({ + url: path + '.shp', + mimeType: 'text/plain; charset=x-user-defined', + success: function (data) { + $.ajax ({ + url: path + '.dbf', + mimeType: 'text/plain; charset=x-user-defined', + success: function (dbf_data) { + var layer = this.loadShp (data, dbf_data, indices, options); + options.success (layer); + } + }); + } + }); + } + }); + }; + + this.localShapefile = function (options) { + var shxFile = options.shx; + var shpFile = options.shp; + var dbfFile = options.dbf; + var shxReader = new FileReader(); + shxReader.onloadend = function () { + var indices = m_that.loadShx(shxReader.result); + var shpReader = new FileReader(); + + shpReader.onloadend = function () { + var shpData = shpReader.result; + + var dbfReader = new FileReader(); + dbfReader.onloadend = function () { + var dbfData = dbfReader.result; + var layer = m_that.loadShp(shpData, dbfData, indices, options); + options.success(layer); + }; + dbfReader.readAsBinaryString(dbfFile); + }; + shpReader.readAsBinaryString(shpFile); + }; + shxReader.readAsBinaryString(shxFile); + }; + + this.loadDBF = function (data) { + var readHeader = function (offset) { + var name = m_that.str(data, offset, 10); + var type = m_that.str(data, offset + 11, 1); + var length = m_that.int8(data, offset + 16); + return { + name: name, + type: type, + length: length + }; + }; + + // Level of the dBASE file + var level = m_that.int8(data, 0); + if (level === 4) { + throw 'Level 7 dBASE not supported'; + } + + // Date of last update + /* + var year = m_that.int8(data, 1); + var month = m_that.int8(data, 2); + var day = m_that.int8(data, 3); + */ + + var num_entries = m_that.lint32(data, 4); + var header_size = m_that.lint16(data, 8); + var record_size = m_that.lint16(data, 10); + + var FIELDS_START = 32; + var HEADER_LENGTH = 32; + + var header_offset = FIELDS_START; + var headers = []; + while (header_offset < header_size - 1) { + headers.push (readHeader(header_offset)); + header_offset += HEADER_LENGTH; + } + + var records = []; + var record_offset = header_size; + while (record_offset < header_size + num_entries * record_size) { + var declare = m_that.str(data, record_offset, 1); + if (declare === '*') { + // Record size in the header include the size of the delete indicator + record_offset += record_size; + } else { + // Move offset to the start of the actual data + record_offset += 1; + var record = {}; + for (var i = 0; i < headers.length; i += 1) { + var header = headers[i]; + var value; + if (header.type === 'C') { + value = m_that.str(data, record_offset, header.length).trim (); + } else if (header.type === 'N') { + value = parseFloat (m_that.str (data, record_offset, header.length)); + } + record_offset += header.length; + record[header.name] = value; + } + records.push(record); + } + } + return records; + }; + + this.loadShp = function (data, dbf_data, indices, options) { + options = options; /* unused parameter */ + var features = []; + var readRing = function (offset, start, end) { + var ring = []; + for (var i = end - 1; i >= start; i -= 1) { + var x = m_that.ldbl64(data, offset + 16 * i); + var y = m_that.ldbl64(data, offset + 16 * i + 8); + ring.push ([x, y]); + } + //if (ring.length <= 3) + // return []; + return ring; + }; + + var readRecord = function (offset) { + // var index = m_that.bint32(data, offset); + // var record_length = m_that.bint32(data, offset + 4); + var record_offset = offset + 8; + var geom_type = m_that.lint32(data, record_offset); + var num_parts, num_points, parts_start, points_start, i, + start, end, ring, rings; + + if (geom_type === SHP_NULL) { + console.log ('NULL Shape'); + //return offset + 12; + } else if (geom_type === SHP_POINT) { + var x = m_that.ldbl64(data, record_offset + 4); + var y = m_that.ldbl64(data, record_offset + 12); + + features.push ({ + type: 'Point', + attr: {}, + geom: [[x, y]] + }); + } else if (geom_type === SHP_POLYGON) { + 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 = []; + for (i = 0; i < num_parts; i += 1) { + start = m_that.lint32(data, parts_start + i * 4); + if (i + 1 < num_parts) { + end = m_that.lint32(data, parts_start + (i + 1) * 4); + } else { + end = num_points; + } + ring = readRing (points_start, start, end); + rings.push (ring); + } + features.push ({ + type: 'Polygon', + attr: {}, + geom: [rings] + }); + } else if (geom_type === SHP_POLYLINE) { + 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 = []; + for (i = 0; i < num_parts; i += 1) { + start = m_that.lint32(data, parts_start + i * 4); + if (i + 1 < num_parts) { + end = m_that.lint32(data, parts_start + (i + 1) * 4); + } else { + end = num_points; + } + ring = readRing (points_start, start, end); + rings.push (ring); + } + features.push ({ + type: 'Polyline', + attr: {}, + geom: [rings] + }); + } else { + throw 'Not Implemented: ' + geom_type; + } + //return offset + 2 * record_length + SHP_HEADER_LEN; + }; + + var attr = this.loadDBF(dbf_data), i; + + //var offset = 100; + //while (offset < length * 2) { + // offset = readRecord (offset); + //} + for (i = 0; i < indices.length; i += 1) { + var offset = indices[i]; + readRecord (offset); + } + + var layer = []; //new Layer (); + + for (i = 0; i < features.length; i += 1) { + var feature = features[i]; + feature.attr = attr[i]; + layer.push (feature); + } + return layer; + }; + + return this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @module vgl + */ + + /*global vgl, mat4, unescape, Float32Array, Int8Array, Uint16Array*/ + ////////////////////////////////////////////////////////////////////////////// + + ////////////////////////////////////////////////////////////////////////////// + // + // vbgModule.vtkReader class + // This contains code that unpack a json base64 encoded vtkdataset, + // such as those produced by ParaView's webGL exporter (where much + // of the code originated from) and convert it to VGL representation. + // + ////////////////////////////////////////////////////////////////////////////// + + vgl.vtkReader = function () { + 'use strict'; + + if (!(this instanceof vgl.vtkReader)) { + return new vgl.vtkReader(); + } + + var m_base64Chars = + ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', + 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'], + m_reverseBase64Chars = [], + m_vtkRenderedList = {}, + m_vtkObjectCount = 0, + m_vtkScene = null, + m_node = null, + END_OF_INPUT = -1, + m_base64Str = '', + m_base64Count = 0, + m_pos = 0, + m_viewer = null, + i = 0; + + //initialize the array here if not already done. + if (m_reverseBase64Chars.length === 0) { + for (i = 0; i < m_base64Chars.length; i += 1) { + m_reverseBase64Chars[m_base64Chars[i]] = i; + } + } + + + + //////////////////////////////////////////////////////////////////////////// + /** + * ntos + * + * @param n + * @returns unescaped n + */ + //////////////////////////////////////////////////////////////////////////// + this.ntos = function (n) { + var unN; + + unN = n.toString(16); + if (unN.length === 1) { + unN = '0' + unN; + } + unN = '%' + unN; + + return unescape(unN); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * readReverseBase64 + * + * @returns + */ + //////////////////////////////////////////////////////////////////////////// + this.readReverseBase64 = function () { + var nextCharacter; + + if (!m_base64Str) { + return END_OF_INPUT; + } + + while (true) { + if (m_base64Count >= m_base64Str.length) { + return END_OF_INPUT; + } + nextCharacter = m_base64Str.charAt(m_base64Count); + m_base64Count += 1; + + if (m_reverseBase64Chars[nextCharacter]) { + return m_reverseBase64Chars[nextCharacter]; + } + if (nextCharacter === 'A') { + return 0; + } + } + + return END_OF_INPUT; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * decode64 + * + * @param str + * @returns result + */ + //////////////////////////////////////////////////////////////////////////// + this.decode64 = function (str) { + var result = '', + inBuffer = new Array(4), + done = false; + + m_base64Str = str; + m_base64Count = 0; + + while (!done && + (inBuffer[0] = this.readReverseBase64()) !== END_OF_INPUT && + (inBuffer[1] = this.readReverseBase64()) !== END_OF_INPUT) { + inBuffer[2] = this.readReverseBase64(); + inBuffer[3] = this.readReverseBase64(); + /*jshint bitwise: false */ + result += this.ntos((((inBuffer[0] << 2) & 0xff) | inBuffer[1] >> 4)); + if (inBuffer[2] !== END_OF_INPUT) { + result += this.ntos((((inBuffer[1] << 4) & 0xff) | inBuffer[2] >> 2)); + if (inBuffer[3] !== END_OF_INPUT) { + result += this.ntos((((inBuffer[2] << 6) & 0xff) | inBuffer[3])); + } else { + done = true; + } + } else { + done = true; + } + /*jshint bitwise: true */ + } + + return result; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * readNumber + * + * @param ss + * @returns v + */ + //////////////////////////////////////////////////////////////////////////// + this.readNumber = function (ss) { + //jshint plusplus: false, bitwise: false + var v = ((ss[m_pos++]) + + (ss[m_pos++] << 8) + + (ss[m_pos++] << 16) + + (ss[m_pos++] << 24)); + //jshint plusplus: true, bitwise: true + return v; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * readF3Array + * + * @param numberOfPoints + * @param ss + * @returns points + */ + //////////////////////////////////////////////////////////////////////////// + this.readF3Array = function (numberOfPoints, ss) { + var size = numberOfPoints * 4 * 3, test = new Int8Array(size), + points = null, i; + + for (i = 0; i < size; i += 1) { + test[i] = ss[m_pos]; + m_pos += 1; + } + + points = new Float32Array(test.buffer); + + return points; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * readColorArray + * + * @param numberOfPoints + * @param ss + * @param vglcolors + * @returns points + */ + //////////////////////////////////////////////////////////////////////////// + this.readColorArray = function (numberOfPoints, ss, vglcolors) { + var i, idx = 0, tmp = new Array(numberOfPoints * 3); + //jshint plusplus: false + for (i = 0; i < numberOfPoints; i += 1) { + tmp[idx++] = ss[m_pos++] / 255.0; + tmp[idx++] = ss[m_pos++] / 255.0; + tmp[idx++] = ss[m_pos++] / 255.0; + m_pos++; + } + //jshint plusplus: true + vglcolors.insert(tmp); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * parseObject + * + * @param buffer + */ + //////////////////////////////////////////////////////////////////////////// + this.parseObject = function (vtkObject) { + var geom = new vgl.geometryData(), mapper = vgl.mapper(), ss = [], + type = null, data = null, size, matrix = null, material = null, + actor, colorMapData, shaderProg, opacityUniform, lookupTable, + colorTable, windowSize, width, height, position; + + //dehexlify + //data = this.decode64(vtkObject.data); + data = atob(vtkObject.data); + //jshint bitwise: false + for (i = 0; i < data.length; i += 1) { + ss[i] = data.charCodeAt(i) & 0xff; + } + //jshint bitwise: true + + //Determine the Object type + m_pos = 0; + size = this.readNumber(ss); + type = String.fromCharCode(ss[m_pos]); + m_pos += 1; + geom.setName(type); + + // Lines + if (type === 'L') { + matrix = this.parseLineData(geom, ss); + material = vgl.utils.createGeometryMaterial(); + // Mesh + } else if (type === 'M') { + matrix = this.parseMeshData(geom, ss); + material = vgl.utils.createPhongMaterial(); + // Points + } else if (type === 'P') { + matrix = this.parsePointData(geom, ss); + material = vgl.utils.createGeometryMaterial(); + // ColorMap + } else if (type === 'C') { + colorMapData = this.parseColorMapData(geom, ss, size); + colorTable = []; + + for (i = 0; i < colorMapData.colors.length; i += 1) { + colorTable.push(colorMapData.colors[i][1]); + colorTable.push(colorMapData.colors[i][2]); + colorTable.push(colorMapData.colors[i][3]); + colorTable.push(colorMapData.colors[i][0] * 255); + } + + lookupTable = new vgl.lookupTable(); + lookupTable.setColorTable(colorTable); + + windowSize = m_viewer.renderWindow().windowSize(); + width = colorMapData.size[0] * windowSize[0]; + height = colorMapData.size[1] * windowSize[1]; + + position = [colorMapData.position[0] * windowSize[0], + (1 - colorMapData.position[1]) * windowSize[1], 0]; + position[1] = position[1] - height; + + // For now hardcode the height + height = 30; + + return vgl.utils.createColorLegend(colorMapData.title, + lookupTable, position, width, height, 3, 0); + // Unknown + } else { + console.log('Ignoring unrecognized encoded data type ' + type); + } + + mapper.setGeometryData(geom); + + //default opacity === solid. If were transparent, set it lower. + if (vtkObject.hasTransparency) { + shaderProg = material.shaderProgram(); + opacityUniform = shaderProg.uniform('opacity'); + console.log('opacity ', vtkObject.opacity); + opacityUniform.set(vtkObject.opacity); + material.setBinNumber(1000); + } + + actor = vgl.actor(); + actor.setMapper(mapper); + actor.setMaterial(material); + actor.setMatrix(mat4.transpose(mat4.create(), matrix)); + + return [actor]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * parseLineData + * + * @param geom, ss + * @returns matrix + */ + //////////////////////////////////////////////////////////////////////////// + this.parseLineData = function (geom, ss) { + var vglpoints = null, vglcolors = null, vgllines = null, + matrix = mat4.create(), + numberOfIndex, numberOfPoints, points, + temp, index, size, m, i, + p = null, idx = 0; + + numberOfPoints = this.readNumber(ss); + p = new Array(numberOfPoints * 3); + + //Getting Points + vglpoints = new vgl.sourceDataP3fv(); + points = this.readF3Array(numberOfPoints, ss); + + //jshint plusplus: false + for (i = 0; i < numberOfPoints; i += 1) { + p[idx++] = points[i * 3/*+0*/]; + p[idx++] = points[i * 3 + 1]; + p[idx++] = points[i * 3 + 2]; + } + //jshint plusplus: true + vglpoints.insert(p); + geom.addSource(vglpoints); + + //Getting Colors + vglcolors = new vgl.sourceDataC3fv(); + this.readColorArray(numberOfPoints, ss, vglcolors); + geom.addSource(vglcolors); + + //Getting connectivity + vgllines = new vgl.lines(); + geom.addPrimitive(vgllines); + numberOfIndex = this.readNumber(ss); + + temp = new Int8Array(numberOfIndex * 2); + for (i = 0; i < numberOfIndex * 2; i += 1) { + temp[i] = ss[m_pos]; + m_pos += 1; + } + + index = new Uint16Array(temp.buffer); + vgllines.setIndices(index); + vgllines.setPrimitiveType(vgl.GL.LINES); + + //Getting Matrix + size = 16 * 4; + temp = new Int8Array(size); + for (i = 0; i < size; i += 1) { + temp[i] = ss[m_pos]; + m_pos += 1; + } + + m = new Float32Array(temp.buffer); + mat4.copy(matrix, m); + + return matrix; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * parseMeshData + * + * @param geom, ss + * @returns matrix + */ + //////////////////////////////////////////////////////////////////////////// + this.parseMeshData = function (geom, ss) { + var vglpoints = null, vglcolors = null, + normals = null, matrix = mat4.create(), + vgltriangles = null, numberOfIndex, numberOfPoints, + points, temp, index, size, m, i, tcoord, + pn = null, idx = 0; + + numberOfPoints = this.readNumber(ss); + pn = new Array(numberOfPoints * 6); + //Getting Points + vglpoints = new vgl.sourceDataP3N3f(); + points = this.readF3Array(numberOfPoints, ss); + + //Getting Normals + normals = this.readF3Array(numberOfPoints, ss); + //jshint plusplus: false + for (i = 0; i < numberOfPoints; i += 1) { + pn[idx++] = points[i * 3/*+0*/]; + pn[idx++] = points[i * 3 + 1]; + pn[idx++] = points[i * 3 + 2]; + pn[idx++] = normals[i * 3/*+0*/]; + pn[idx++] = normals[i * 3 + 1]; + pn[idx++] = normals[i * 3 + 2]; + } + //jshint plusplus: true + vglpoints.insert(pn); + geom.addSource(vglpoints); + + //Getting Colors + vglcolors = new vgl.sourceDataC3fv(); + this.readColorArray(numberOfPoints, ss, vglcolors); + geom.addSource(vglcolors); + + //Getting connectivity + temp = []; + vgltriangles = new vgl.triangles(); + numberOfIndex = this.readNumber(ss); + + temp = new Int8Array(numberOfIndex * 2); + for (i = 0; i < numberOfIndex * 2; i += 1) { + temp[i] = ss[m_pos]; + m_pos += 1; + } + + index = new Uint16Array(temp.buffer); + vgltriangles.setIndices(index); + geom.addPrimitive(vgltriangles); + + //Getting Matrix + size = 16 * 4; + temp = new Int8Array(size); + for (i = 0; i < size; i += 1) { + temp[i] = ss[m_pos]; + m_pos += 1; + } + + m = new Float32Array(temp.buffer); + mat4.copy(matrix, m); + + //Getting TCoord + //TODO: renderer is not doing anything with this yet + tcoord = null; + + return matrix; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * parsePointData + * + * @param geom, ss + * @returns matrix + */ + //////////////////////////////////////////////////////////////////////////// + this.parsePointData = function (geom, ss) { + var numberOfPoints, points, indices, temp, size, + matrix = mat4.create(), vglpoints = null, + vglcolors = null, vglVertexes = null, m, + p = null, idx = 0; + + numberOfPoints = this.readNumber(ss); + p = new Array(numberOfPoints * 3); + + //Getting Points and creating 1:1 connectivity + vglpoints = new vgl.sourceDataP3fv(); + points = this.readF3Array(numberOfPoints, ss); + + indices = new Uint16Array(numberOfPoints); + + //jshint plusplus: false + for (i = 0; i < numberOfPoints; i += 1) { + indices[i] = i; + p[idx++] = points[i * 3/*+0*/]; + p[idx++] = points[i * 3 + 1]; + p[idx++] = points[i * 3 + 2]; + } + //jshint plusplus: true + vglpoints.insert(p); + geom.addSource(vglpoints); + + //Getting Colors + vglcolors = new vgl.sourceDataC3fv(); + this.readColorArray(numberOfPoints, ss, vglcolors); + geom.addSource(vglcolors); + + //Getting connectivity + vglVertexes = new vgl.points(); + vglVertexes.setIndices(indices); + geom.addPrimitive(vglVertexes); + + //Getting matrix + size = 16 * 4; + temp = new Int8Array(size); + for (i = 0; i < size; i += 1) { + temp[i] = ss[m_pos]; + m_pos += 1; + } + + m = new Float32Array(temp.buffer); + mat4.copy(matrix, m); + + return matrix; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * parseColorMapData + * + * @param geom, ss + * @returns matrix + */ + //////////////////////////////////////////////////////////////////////////// + this.parseColorMapData = function (geom, ss, numColors) { + + var tmpArray, size, xrgb, i, c, obj = {}; + + // Set number of colors + obj.numOfColors = numColors; + + // Getting Position + size = 8; + tmpArray = new Int8Array(size); + for (i = 0; i < size; i += 1) { + tmpArray[i] = ss[m_pos]; + m_pos += 1; + } + obj.position = new Float32Array(tmpArray.buffer); + + // Getting Size + size = 8; + tmpArray = new Int8Array(size); + for (i = 0; i < size; i += 1) { + tmpArray[i] = ss[m_pos]; + m_pos += 1; + } + obj.size = new Float32Array(tmpArray.buffer); + + //Getting Colors + obj.colors = []; + //jshint plusplus: false + for (c = 0; c < obj.numOfColors; c += 1) { + tmpArray = new Int8Array(4); + for (i = 0; i < 4; 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; + } + + obj.orientation = ss[m_pos++]; + obj.numOfLabels = ss[m_pos++]; + obj.title = ''; + while (m_pos < ss.length) { + obj.title += String.fromCharCode(ss[m_pos++]); + } + //jshint plusplus: true + + return obj; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * parseSceneMetadata + * + * @param data + * @returns renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.parseSceneMetadata = function (renderer, layer) { + + var sceneRenderer = m_vtkScene.Renderers[layer], + camera = renderer.camera(), bgc, localWidth, localHeight; + + localWidth = (sceneRenderer.size[0] - sceneRenderer.origin[0]) * m_node.width; + localHeight = (sceneRenderer.size[1] - sceneRenderer.origin[1]) * m_node.height; + renderer.resize(localWidth, localHeight); + + /// We are setting the center to the focal point because of + /// a possible paraview web bug. The center of rotation isn't + /// getting updated correctly on resetCamera. + camera.setCenterOfRotation( + [sceneRenderer.LookAt[1], sceneRenderer.LookAt[2], + sceneRenderer.LookAt[3]]); + camera.setViewAngleDegrees(sceneRenderer.LookAt[0]); + camera.setPosition( + sceneRenderer.LookAt[7], sceneRenderer.LookAt[8], + sceneRenderer.LookAt[9]); + camera.setFocalPoint( + sceneRenderer.LookAt[1], sceneRenderer.LookAt[2], + sceneRenderer.LookAt[3]); + camera.setViewUpDirection( + sceneRenderer.LookAt[4], sceneRenderer.LookAt[5], + sceneRenderer.LookAt[6]); + + if (layer === 0) { + bgc = sceneRenderer.Background1; + renderer.setBackgroundColor(bgc[0], bgc[1], bgc[2], 1); + } else { + renderer.setResizable(false); + } + renderer.setLayer(layer); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * initScene + * + * @returns viewer + */ + //////////////////////////////////////////////////////////////////////////// + this.initScene = function () { + var renderer, layer; + + if (m_vtkScene === null) { + return m_viewer; + } + for (layer = m_vtkScene.Renderers.length - 1; layer >= 0; layer -= 1) { + + renderer = this.getRenderer(layer); + this.parseSceneMetadata(renderer, layer); + } + + return m_viewer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * createViewer - Creates a viewer object. + * + * @param + * + * @returns viewer + */ + //////////////////////////////////////////////////////////////////////////// + this.createViewer = function (node) { + var interactorStyle; + + if (m_viewer === null) { + 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); + } + + return m_viewer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * deleteViewer - Deletes the viewer object associated with the reader. + * + * @returns void + */ + //////////////////////////////////////////////////////////////////////////// + this.deleteViewer = function () { + m_vtkRenderedList = {}; + m_viewer = null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * updateCanvas - + * + * @param + * + * @returns void + */ + //////////////////////////////////////////////////////////////////////////// + this.updateCanvas = function (node) { + m_node = node; + m_viewer.renderWindow().resize(node.width, node.height); + + return m_viewer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * clearVtkObjectData - Clear out the list of VTK geometry data. + * + * @param void + * @returns void + */ + //////////////////////////////////////////////////////////////////////////// + this.numObjects = function () { + return m_vtkObjectCount; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * getRenderer - Gets (or creates) the renderer for a layer. + * + * @param layer + * @returns renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.getRenderer = function (layer) { + var renderer; + + renderer = m_vtkRenderedList[layer]; + if (renderer === null || typeof renderer === 'undefined') { + renderer = new vgl.renderer(); + renderer.setResetScene(false); + renderer.setResetClippingRange(false); + m_viewer.renderWindow().addRenderer(renderer); + + if (layer !== 0) { + renderer.camera().setClearMask(vgl.GL.DepthBufferBit); + } + + m_vtkRenderedList[layer] = renderer; + } + + return renderer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * setVtkScene - Set the VTK scene data for camera initialization. + * + * @param scene + * @returns void + */ + //////////////////////////////////////////////////////////////////////////// + this.setVtkScene = function (scene) { + m_vtkScene = scene; + }; + + return this; + }; + + vgl.DataBuffers = function (initialSize) { + 'use strict'; + if (!(this instanceof vgl.DataBuffers)) { + return new vgl.DataBuffers(initialSize); + } + + var data = {}; + + var size; + if (!initialSize && initialSize !== 0) { + size = 256; + } else { + size = initialSize; + } + + var current = 0; + + var copyArray = function (dst, src, start, count) { + if (!dst) { + console.log ('ack'); + } + if (!start) { + start = 0; + } + if (!count) { + count = src.length; + } + for (var i = 0; i < count; i += 1) { + dst[start + i] = src[i]; + } + }; + + var resize = function (min_expand) { + var new_size = size; + /* If the array would increase substantially, don't just double its + * size. If the array has been increasing gradually, double it as the + * expectation is that it will increase again. */ + if (new_size * 2 < min_expand) { + new_size = min_expand; + } + while (new_size < min_expand) { + new_size *= 2; + } + size = new_size; + for (var name in data) { + if (data.hasOwnProperty(name)) { + var newArray = new Float32Array (new_size * data[name].len); + var oldArray = data[name].array; + copyArray (newArray, oldArray); + data[name].array = newArray; + data[name].dirty = true; + } + } + }; + + this.create = function (name, len) { + if (!len) { + throw 'Length of buffer must be a positive integer'; + } + var array = new Float32Array (size * len); + data[name] = { + array: array, + len: len, + dirty: false + }; + return data[name].array; + }; + + this.alloc = function (num) { + if ((current + num) >= size) { + resize (current + num); + } + var start = current; + current += num; + return 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 = true; + }; + + this.repeat = function (name, elem, start, count) { + for (var i = 0; i < count; i += 1) { + copyArray (data[name].array, elem, + (start + i) * data[name].len, data[name].len); + } + data[name].dirty = true; + }; + + this.count = function () { + return current; + }; + + this.data = function (name) { + return data[name].array; + }; + }; + + return vgl; + + })); + + + +/***/ }, +/* 8 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = { + create: __webpack_require__(9) + , clone: __webpack_require__(10) + , copy: __webpack_require__(11) + , identity: __webpack_require__(12) + , transpose: __webpack_require__(13) + , invert: __webpack_require__(14) + , adjoint: __webpack_require__(15) + , determinant: __webpack_require__(16) + , multiply: __webpack_require__(17) + , translate: __webpack_require__(18) + , scale: __webpack_require__(19) + , rotate: __webpack_require__(20) + , rotateX: __webpack_require__(21) + , rotateY: __webpack_require__(22) + , rotateZ: __webpack_require__(23) + , fromRotationTranslation: __webpack_require__(24) + , fromQuat: __webpack_require__(25) + , frustum: __webpack_require__(26) + , perspective: __webpack_require__(27) + , perspectiveFromFieldOfView: __webpack_require__(28) + , ortho: __webpack_require__(29) + , lookAt: __webpack_require__(30) + , str: __webpack_require__(31) + } + +/***/ }, +/* 9 */ +/***/ function(module, exports) { + + module.exports = create; + + /** + * Creates a new identity mat4 + * + * @returns {mat4} a new 4x4 matrix + */ + function create() { + var out = new Float32Array(16); + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = 1; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 1; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; + }; + +/***/ }, +/* 10 */ +/***/ function(module, exports) { + + module.exports = clone; + + /** + * Creates a new mat4 initialized with values from an existing matrix + * + * @param {mat4} a matrix to clone + * @returns {mat4} a new 4x4 matrix + */ + function clone(a) { + var out = new Float32Array(16); + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + out[9] = a[9]; + out[10] = a[10]; + out[11] = a[11]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + return out; + }; + +/***/ }, +/* 11 */ +/***/ function(module, exports) { + + module.exports = copy; + + /** + * Copy the values from one mat4 to another + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ + function copy(out, a) { + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[8] = a[8]; + out[9] = a[9]; + out[10] = a[10]; + out[11] = a[11]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + return out; + }; + +/***/ }, +/* 12 */ +/***/ function(module, exports) { + + module.exports = identity; + + /** + * Set a mat4 to the identity matrix + * + * @param {mat4} out the receiving matrix + * @returns {mat4} out + */ + function identity(out) { + out[0] = 1; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = 1; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 1; + out[11] = 0; + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; + }; + +/***/ }, +/* 13 */ +/***/ function(module, exports) { + + module.exports = transpose; + + /** + * Transpose the values of a mat4 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ + function transpose(out, a) { + // If we are transposing ourselves we can skip a few steps but have to cache some values + if (out === a) { + var a01 = a[1], a02 = a[2], a03 = a[3], + a12 = a[6], a13 = a[7], + a23 = a[11]; + + out[1] = a[4]; + out[2] = a[8]; + out[3] = a[12]; + out[4] = a01; + out[6] = a[9]; + out[7] = a[13]; + out[8] = a02; + out[9] = a12; + out[11] = a[14]; + out[12] = a03; + out[13] = a13; + out[14] = a23; + } else { + out[0] = a[0]; + out[1] = a[4]; + out[2] = a[8]; + out[3] = a[12]; + out[4] = a[1]; + out[5] = a[5]; + out[6] = a[9]; + out[7] = a[13]; + out[8] = a[2]; + out[9] = a[6]; + out[10] = a[10]; + out[11] = a[14]; + out[12] = a[3]; + out[13] = a[7]; + out[14] = a[11]; + out[15] = a[15]; + } + + return out; + }; + +/***/ }, +/* 14 */ +/***/ function(module, exports) { + + module.exports = invert; + + /** + * Inverts a mat4 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ + function invert(out, a) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], + + b00 = a00 * a11 - a01 * a10, + b01 = a00 * a12 - a02 * a10, + b02 = a00 * a13 - a03 * a10, + b03 = a01 * a12 - a02 * a11, + b04 = a01 * a13 - a03 * a11, + b05 = a02 * a13 - a03 * a12, + b06 = a20 * a31 - a21 * a30, + b07 = a20 * a32 - a22 * a30, + b08 = a20 * a33 - a23 * a30, + b09 = a21 * a32 - a22 * a31, + b10 = a21 * a33 - a23 * a31, + b11 = a22 * a33 - a23 * a32, + + // Calculate the determinant + det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + + if (!det) { + return null; + } + det = 1.0 / det; + + out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det; + out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det; + out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det; + out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det; + out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det; + out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det; + out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det; + out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det; + out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det; + out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det; + out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det; + out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det; + out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det; + out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det; + out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det; + out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det; + + return out; + }; + +/***/ }, +/* 15 */ +/***/ function(module, exports) { + + module.exports = adjoint; + + /** + * Calculates the adjugate of a mat4 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the source matrix + * @returns {mat4} out + */ + function adjoint(out, a) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; + + out[0] = (a11 * (a22 * a33 - a23 * a32) - a21 * (a12 * a33 - a13 * a32) + a31 * (a12 * a23 - a13 * a22)); + out[1] = -(a01 * (a22 * a33 - a23 * a32) - a21 * (a02 * a33 - a03 * a32) + a31 * (a02 * a23 - a03 * a22)); + out[2] = (a01 * (a12 * a33 - a13 * a32) - a11 * (a02 * a33 - a03 * a32) + a31 * (a02 * a13 - a03 * a12)); + out[3] = -(a01 * (a12 * a23 - a13 * a22) - a11 * (a02 * a23 - a03 * a22) + a21 * (a02 * a13 - a03 * a12)); + out[4] = -(a10 * (a22 * a33 - a23 * a32) - a20 * (a12 * a33 - a13 * a32) + a30 * (a12 * a23 - a13 * a22)); + out[5] = (a00 * (a22 * a33 - a23 * a32) - a20 * (a02 * a33 - a03 * a32) + a30 * (a02 * a23 - a03 * a22)); + out[6] = -(a00 * (a12 * a33 - a13 * a32) - a10 * (a02 * a33 - a03 * a32) + a30 * (a02 * a13 - a03 * a12)); + out[7] = (a00 * (a12 * a23 - a13 * a22) - a10 * (a02 * a23 - a03 * a22) + a20 * (a02 * a13 - a03 * a12)); + out[8] = (a10 * (a21 * a33 - a23 * a31) - a20 * (a11 * a33 - a13 * a31) + a30 * (a11 * a23 - a13 * a21)); + out[9] = -(a00 * (a21 * a33 - a23 * a31) - a20 * (a01 * a33 - a03 * a31) + a30 * (a01 * a23 - a03 * a21)); + out[10] = (a00 * (a11 * a33 - a13 * a31) - a10 * (a01 * a33 - a03 * a31) + a30 * (a01 * a13 - a03 * a11)); + out[11] = -(a00 * (a11 * a23 - a13 * a21) - a10 * (a01 * a23 - a03 * a21) + a20 * (a01 * a13 - a03 * a11)); + out[12] = -(a10 * (a21 * a32 - a22 * a31) - a20 * (a11 * a32 - a12 * a31) + a30 * (a11 * a22 - a12 * a21)); + out[13] = (a00 * (a21 * a32 - a22 * a31) - a20 * (a01 * a32 - a02 * a31) + a30 * (a01 * a22 - a02 * a21)); + out[14] = -(a00 * (a11 * a32 - a12 * a31) - a10 * (a01 * a32 - a02 * a31) + a30 * (a01 * a12 - a02 * a11)); + out[15] = (a00 * (a11 * a22 - a12 * a21) - a10 * (a01 * a22 - a02 * a21) + a20 * (a01 * a12 - a02 * a11)); + return out; + }; + +/***/ }, +/* 16 */ +/***/ function(module, exports) { + + module.exports = determinant; + + /** + * Calculates the determinant of a mat4 + * + * @param {mat4} a the source matrix + * @returns {Number} determinant of a + */ + function determinant(a) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15], + + b00 = a00 * a11 - a01 * a10, + b01 = a00 * a12 - a02 * a10, + b02 = a00 * a13 - a03 * a10, + b03 = a01 * a12 - a02 * a11, + b04 = a01 * a13 - a03 * a11, + b05 = a02 * a13 - a03 * a12, + b06 = a20 * a31 - a21 * a30, + b07 = a20 * a32 - a22 * a30, + b08 = a20 * a33 - a23 * a30, + b09 = a21 * a32 - a22 * a31, + b10 = a21 * a33 - a23 * a31, + b11 = a22 * a33 - a23 * a32; + + // Calculate the determinant + return b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06; + }; + +/***/ }, +/* 17 */ +/***/ function(module, exports) { + + module.exports = multiply; + + /** + * Multiplies two mat4's + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the first operand + * @param {mat4} b the second operand + * @returns {mat4} out + */ + function multiply(out, a, b) { + var a00 = a[0], a01 = a[1], a02 = a[2], a03 = a[3], + a10 = a[4], a11 = a[5], a12 = a[6], a13 = a[7], + a20 = a[8], a21 = a[9], a22 = a[10], a23 = a[11], + a30 = a[12], a31 = a[13], a32 = a[14], a33 = a[15]; + + // Cache only the current line of the second matrix + var b0 = b[0], b1 = b[1], b2 = b[2], b3 = b[3]; + out[0] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[1] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[2] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[3] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[4]; b1 = b[5]; b2 = b[6]; b3 = b[7]; + out[4] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[5] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[6] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[7] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[8]; b1 = b[9]; b2 = b[10]; b3 = b[11]; + out[8] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[9] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[10] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[11] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + + b0 = b[12]; b1 = b[13]; b2 = b[14]; b3 = b[15]; + out[12] = b0*a00 + b1*a10 + b2*a20 + b3*a30; + out[13] = b0*a01 + b1*a11 + b2*a21 + b3*a31; + out[14] = b0*a02 + b1*a12 + b2*a22 + b3*a32; + out[15] = b0*a03 + b1*a13 + b2*a23 + b3*a33; + return out; + }; + +/***/ }, +/* 18 */ +/***/ function(module, exports) { + + module.exports = translate; + + /** + * Translate a mat4 by the given vector + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to translate + * @param {vec3} v vector to translate by + * @returns {mat4} out + */ + function translate(out, a, v) { + var x = v[0], y = v[1], z = v[2], + a00, a01, a02, a03, + a10, a11, a12, a13, + a20, a21, a22, a23; + + if (a === out) { + out[12] = a[0] * x + a[4] * y + a[8] * z + a[12]; + out[13] = a[1] * x + a[5] * y + a[9] * z + a[13]; + out[14] = a[2] * x + a[6] * y + a[10] * z + a[14]; + out[15] = a[3] * x + a[7] * y + a[11] * z + a[15]; + } else { + a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; + a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; + a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; + + out[0] = a00; out[1] = a01; out[2] = a02; out[3] = a03; + out[4] = a10; out[5] = a11; out[6] = a12; out[7] = a13; + out[8] = a20; out[9] = a21; out[10] = a22; out[11] = a23; + + out[12] = a00 * x + a10 * y + a20 * z + a[12]; + out[13] = a01 * x + a11 * y + a21 * z + a[13]; + out[14] = a02 * x + a12 * y + a22 * z + a[14]; + out[15] = a03 * x + a13 * y + a23 * z + a[15]; + } + + return out; + }; + +/***/ }, +/* 19 */ +/***/ function(module, exports) { + + module.exports = scale; + + /** + * Scales the mat4 by the dimensions in the given vec3 + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to scale + * @param {vec3} v the vec3 to scale the matrix by + * @returns {mat4} out + **/ + function scale(out, a, v) { + var x = v[0], y = v[1], z = v[2]; + + out[0] = a[0] * x; + out[1] = a[1] * x; + out[2] = a[2] * x; + out[3] = a[3] * x; + out[4] = a[4] * y; + out[5] = a[5] * y; + out[6] = a[6] * y; + out[7] = a[7] * y; + out[8] = a[8] * z; + out[9] = a[9] * z; + out[10] = a[10] * z; + out[11] = a[11] * z; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + return out; + }; + +/***/ }, +/* 20 */ +/***/ function(module, exports) { + + module.exports = rotate; + + /** + * Rotates a mat4 by the given angle + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @param {vec3} axis the axis to rotate around + * @returns {mat4} out + */ + function rotate(out, a, rad, axis) { + var x = axis[0], y = axis[1], z = axis[2], + len = Math.sqrt(x * x + y * y + z * z), + s, c, t, + a00, a01, a02, a03, + a10, a11, a12, a13, + a20, a21, a22, a23, + b00, b01, b02, + b10, b11, b12, + b20, b21, b22; + + if (Math.abs(len) < 0.000001) { return null; } + + len = 1 / len; + x *= len; + y *= len; + z *= len; + + s = Math.sin(rad); + c = Math.cos(rad); + t = 1 - c; + + a00 = a[0]; a01 = a[1]; a02 = a[2]; a03 = a[3]; + a10 = a[4]; a11 = a[5]; a12 = a[6]; a13 = a[7]; + a20 = a[8]; a21 = a[9]; a22 = a[10]; a23 = a[11]; + + // Construct the elements of the rotation matrix + b00 = x * x * t + c; b01 = y * x * t + z * s; b02 = z * x * t - y * s; + b10 = x * y * t - z * s; b11 = y * y * t + c; b12 = z * y * t + x * s; + b20 = x * z * t + y * s; b21 = y * z * t - x * s; b22 = z * z * t + c; + + // Perform rotation-specific matrix multiplication + out[0] = a00 * b00 + a10 * b01 + a20 * b02; + out[1] = a01 * b00 + a11 * b01 + a21 * b02; + out[2] = a02 * b00 + a12 * b01 + a22 * b02; + out[3] = a03 * b00 + a13 * b01 + a23 * b02; + out[4] = a00 * b10 + a10 * b11 + a20 * b12; + out[5] = a01 * b10 + a11 * b11 + a21 * b12; + out[6] = a02 * b10 + a12 * b11 + a22 * b12; + out[7] = a03 * b10 + a13 * b11 + a23 * b12; + out[8] = a00 * b20 + a10 * b21 + a20 * b22; + out[9] = a01 * b20 + a11 * b21 + a21 * b22; + out[10] = a02 * b20 + a12 * b21 + a22 * b22; + out[11] = a03 * b20 + a13 * b21 + a23 * b22; + + if (a !== out) { // If the source and destination differ, copy the unchanged last row + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + return out; + }; + +/***/ }, +/* 21 */ +/***/ function(module, exports) { + + module.exports = rotateX; + + /** + * Rotates a matrix by the given angle around the X axis + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat4} out + */ + function rotateX(out, a, rad) { + var s = Math.sin(rad), + c = Math.cos(rad), + a10 = a[4], + a11 = a[5], + a12 = a[6], + a13 = a[7], + a20 = a[8], + a21 = a[9], + a22 = a[10], + a23 = a[11]; + + if (a !== out) { // If the source and destination differ, copy the unchanged rows + out[0] = a[0]; + out[1] = a[1]; + out[2] = a[2]; + out[3] = a[3]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + + // Perform axis-specific matrix multiplication + out[4] = a10 * c + a20 * s; + out[5] = a11 * c + a21 * s; + out[6] = a12 * c + a22 * s; + out[7] = a13 * c + a23 * s; + out[8] = a20 * c - a10 * s; + out[9] = a21 * c - a11 * s; + out[10] = a22 * c - a12 * s; + out[11] = a23 * c - a13 * s; + return out; + }; + +/***/ }, +/* 22 */ +/***/ function(module, exports) { + + module.exports = rotateY; + + /** + * Rotates a matrix by the given angle around the Y axis + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat4} out + */ + function rotateY(out, a, rad) { + var s = Math.sin(rad), + c = Math.cos(rad), + a00 = a[0], + a01 = a[1], + a02 = a[2], + a03 = a[3], + a20 = a[8], + a21 = a[9], + a22 = a[10], + a23 = a[11]; + + if (a !== out) { // If the source and destination differ, copy the unchanged rows + out[4] = a[4]; + out[5] = a[5]; + out[6] = a[6]; + out[7] = a[7]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + + // Perform axis-specific matrix multiplication + out[0] = a00 * c - a20 * s; + out[1] = a01 * c - a21 * s; + out[2] = a02 * c - a22 * s; + out[3] = a03 * c - a23 * s; + out[8] = a00 * s + a20 * c; + out[9] = a01 * s + a21 * c; + out[10] = a02 * s + a22 * c; + out[11] = a03 * s + a23 * c; + return out; + }; + +/***/ }, +/* 23 */ +/***/ function(module, exports) { + + module.exports = rotateZ; + + /** + * Rotates a matrix by the given angle around the Z axis + * + * @param {mat4} out the receiving matrix + * @param {mat4} a the matrix to rotate + * @param {Number} rad the angle to rotate the matrix by + * @returns {mat4} out + */ + function rotateZ(out, a, rad) { + var s = Math.sin(rad), + c = Math.cos(rad), + a00 = a[0], + a01 = a[1], + a02 = a[2], + a03 = a[3], + a10 = a[4], + a11 = a[5], + a12 = a[6], + a13 = a[7]; + + if (a !== out) { // If the source and destination differ, copy the unchanged last row + out[8] = a[8]; + out[9] = a[9]; + out[10] = a[10]; + out[11] = a[11]; + out[12] = a[12]; + out[13] = a[13]; + out[14] = a[14]; + out[15] = a[15]; + } + + // Perform axis-specific matrix multiplication + out[0] = a00 * c + a10 * s; + out[1] = a01 * c + a11 * s; + out[2] = a02 * c + a12 * s; + out[3] = a03 * c + a13 * s; + out[4] = a10 * c - a00 * s; + out[5] = a11 * c - a01 * s; + out[6] = a12 * c - a02 * s; + out[7] = a13 * c - a03 * s; + return out; + }; + +/***/ }, +/* 24 */ +/***/ function(module, exports) { + + module.exports = fromRotationTranslation; + + /** + * Creates a matrix from a quaternion rotation and vector translation + * This is equivalent to (but much faster than): + * + * mat4.identity(dest); + * mat4.translate(dest, vec); + * var quatMat = mat4.create(); + * quat4.toMat4(quat, quatMat); + * mat4.multiply(dest, quatMat); + * + * @param {mat4} out mat4 receiving operation result + * @param {quat4} q Rotation quaternion + * @param {vec3} v Translation vector + * @returns {mat4} out + */ + function fromRotationTranslation(out, q, v) { + // Quaternion math + var x = q[0], y = q[1], z = q[2], w = q[3], + x2 = x + x, + y2 = y + y, + z2 = z + z, + + xx = x * x2, + xy = x * y2, + xz = x * z2, + yy = y * y2, + yz = y * z2, + zz = z * z2, + wx = w * x2, + wy = w * y2, + wz = w * z2; + + out[0] = 1 - (yy + zz); + out[1] = xy + wz; + out[2] = xz - wy; + out[3] = 0; + out[4] = xy - wz; + out[5] = 1 - (xx + zz); + out[6] = yz + wx; + out[7] = 0; + out[8] = xz + wy; + out[9] = yz - wx; + out[10] = 1 - (xx + yy); + out[11] = 0; + out[12] = v[0]; + out[13] = v[1]; + out[14] = v[2]; + out[15] = 1; + + return out; + }; + +/***/ }, +/* 25 */ +/***/ function(module, exports) { + + module.exports = fromQuat; + + /** + * Creates a matrix from a quaternion rotation. + * + * @param {mat4} out mat4 receiving operation result + * @param {quat4} q Rotation quaternion + * @returns {mat4} out + */ + function fromQuat(out, q) { + var x = q[0], y = q[1], z = q[2], w = q[3], + x2 = x + x, + y2 = y + y, + z2 = z + z, + + xx = x * x2, + yx = y * x2, + yy = y * y2, + zx = z * x2, + zy = z * y2, + zz = z * z2, + wx = w * x2, + wy = w * y2, + wz = w * z2; + + out[0] = 1 - yy - zz; + out[1] = yx + wz; + out[2] = zx - wy; + out[3] = 0; + + out[4] = yx - wz; + out[5] = 1 - xx - zz; + out[6] = zy + wx; + out[7] = 0; + + out[8] = zx + wy; + out[9] = zy - wx; + out[10] = 1 - xx - yy; + out[11] = 0; + + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + + return out; + }; + +/***/ }, +/* 26 */ +/***/ function(module, exports) { + + module.exports = frustum; + + /** + * Generates a frustum matrix with the given bounds + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {Number} left Left bound of the frustum + * @param {Number} right Right bound of the frustum + * @param {Number} bottom Bottom bound of the frustum + * @param {Number} top Top bound of the frustum + * @param {Number} near Near bound of the frustum + * @param {Number} far Far bound of the frustum + * @returns {mat4} out + */ + function frustum(out, left, right, bottom, top, near, far) { + var rl = 1 / (right - left), + tb = 1 / (top - bottom), + nf = 1 / (near - far); + out[0] = (near * 2) * rl; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = (near * 2) * tb; + out[6] = 0; + out[7] = 0; + out[8] = (right + left) * rl; + out[9] = (top + bottom) * tb; + out[10] = (far + near) * nf; + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[14] = (far * near * 2) * nf; + out[15] = 0; + return out; + }; + +/***/ }, +/* 27 */ +/***/ function(module, exports) { + + module.exports = perspective; + + /** + * Generates a perspective projection matrix with the given bounds + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {number} fovy Vertical field of view in radians + * @param {number} aspect Aspect ratio. typically viewport width/height + * @param {number} near Near bound of the frustum + * @param {number} far Far bound of the frustum + * @returns {mat4} out + */ + function perspective(out, fovy, aspect, near, far) { + var f = 1.0 / Math.tan(fovy / 2), + nf = 1 / (near - far); + out[0] = f / aspect; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = f; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = (far + near) * nf; + out[11] = -1; + out[12] = 0; + out[13] = 0; + out[14] = (2 * far * near) * nf; + out[15] = 0; + return out; + }; + +/***/ }, +/* 28 */ +/***/ function(module, exports) { + + module.exports = perspectiveFromFieldOfView; + + /** + * Generates a perspective projection matrix with the given field of view. + * This is primarily useful for generating projection matrices to be used + * with the still experiemental WebVR API. + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {number} fov Object containing the following values: upDegrees, downDegrees, leftDegrees, rightDegrees + * @param {number} near Near bound of the frustum + * @param {number} far Far bound of the frustum + * @returns {mat4} out + */ + function perspectiveFromFieldOfView(out, fov, near, far) { + var upTan = Math.tan(fov.upDegrees * Math.PI/180.0), + downTan = Math.tan(fov.downDegrees * Math.PI/180.0), + leftTan = Math.tan(fov.leftDegrees * Math.PI/180.0), + rightTan = Math.tan(fov.rightDegrees * Math.PI/180.0), + xScale = 2.0 / (leftTan + rightTan), + yScale = 2.0 / (upTan + downTan); + + out[0] = xScale; + out[1] = 0.0; + out[2] = 0.0; + out[3] = 0.0; + out[4] = 0.0; + out[5] = yScale; + out[6] = 0.0; + out[7] = 0.0; + out[8] = -((leftTan - rightTan) * xScale * 0.5); + out[9] = ((upTan - downTan) * yScale * 0.5); + out[10] = far / (near - far); + out[11] = -1.0; + out[12] = 0.0; + out[13] = 0.0; + out[14] = (far * near) / (near - far); + out[15] = 0.0; + return out; + } + + + +/***/ }, +/* 29 */ +/***/ function(module, exports) { + + module.exports = ortho; + + /** + * Generates a orthogonal projection matrix with the given bounds + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {number} left Left bound of the frustum + * @param {number} right Right bound of the frustum + * @param {number} bottom Bottom bound of the frustum + * @param {number} top Top bound of the frustum + * @param {number} near Near bound of the frustum + * @param {number} far Far bound of the frustum + * @returns {mat4} out + */ + function ortho(out, left, right, bottom, top, near, far) { + var lr = 1 / (left - right), + bt = 1 / (bottom - top), + nf = 1 / (near - far); + out[0] = -2 * lr; + out[1] = 0; + out[2] = 0; + out[3] = 0; + out[4] = 0; + out[5] = -2 * bt; + out[6] = 0; + out[7] = 0; + out[8] = 0; + out[9] = 0; + out[10] = 2 * nf; + out[11] = 0; + out[12] = (left + right) * lr; + out[13] = (top + bottom) * bt; + out[14] = (far + near) * nf; + out[15] = 1; + return out; + }; + +/***/ }, +/* 30 */ +/***/ function(module, exports, __webpack_require__) { + + var identity = __webpack_require__(12); + + module.exports = lookAt; + + /** + * Generates a look-at matrix with the given eye position, focal point, and up axis + * + * @param {mat4} out mat4 frustum matrix will be written into + * @param {vec3} eye Position of the viewer + * @param {vec3} center Point the viewer is looking at + * @param {vec3} up vec3 pointing up + * @returns {mat4} out + */ + function lookAt(out, eye, center, up) { + var x0, x1, x2, y0, y1, y2, z0, z1, z2, len, + eyex = eye[0], + eyey = eye[1], + eyez = eye[2], + upx = up[0], + upy = up[1], + upz = up[2], + centerx = center[0], + centery = center[1], + centerz = center[2]; + + if (Math.abs(eyex - centerx) < 0.000001 && + Math.abs(eyey - centery) < 0.000001 && + Math.abs(eyez - centerz) < 0.000001) { + return identity(out); + } + + z0 = eyex - centerx; + z1 = eyey - centery; + z2 = eyez - centerz; + + len = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2); + z0 *= len; + z1 *= len; + z2 *= len; + + x0 = upy * z2 - upz * z1; + x1 = upz * z0 - upx * z2; + x2 = upx * z1 - upy * z0; + len = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2); + if (!len) { + x0 = 0; + x1 = 0; + x2 = 0; + } else { + len = 1 / len; + x0 *= len; + x1 *= len; + x2 *= len; + } + + y0 = z1 * x2 - z2 * x1; + y1 = z2 * x0 - z0 * x2; + y2 = z0 * x1 - z1 * x0; + + len = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2); + if (!len) { + y0 = 0; + y1 = 0; + y2 = 0; + } else { + len = 1 / len; + y0 *= len; + y1 *= len; + y2 *= len; + } + + out[0] = x0; + out[1] = y0; + out[2] = z0; + out[3] = 0; + out[4] = x1; + out[5] = y1; + out[6] = z1; + out[7] = 0; + out[8] = x2; + out[9] = y2; + out[10] = z2; + out[11] = 0; + out[12] = -(x0 * eyex + x1 * eyey + x2 * eyez); + out[13] = -(y0 * eyex + y1 * eyey + y2 * eyez); + out[14] = -(z0 * eyex + z1 * eyey + z2 * eyez); + out[15] = 1; + + return out; + }; + +/***/ }, +/* 31 */ +/***/ function(module, exports) { + + module.exports = str; + + /** + * Returns a string representation of a mat4 + * + * @param {mat4} mat matrix to represent as a string + * @returns {String} string representation of the matrix + */ + function str(a) { + return 'mat4(' + a[0] + ', ' + a[1] + ', ' + a[2] + ', ' + a[3] + ', ' + + a[4] + ', ' + a[5] + ', ' + a[6] + ', ' + a[7] + ', ' + + a[8] + ', ' + a[9] + ', ' + a[10] + ', ' + a[11] + ', ' + + a[12] + ', ' + a[13] + ', ' + a[14] + ', ' + a[15] + ')'; + }; + +/***/ }, +/* 32 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = { + create: __webpack_require__(33), + clone: __webpack_require__(34), + fromValues: __webpack_require__(35), + copy: __webpack_require__(36), + set: __webpack_require__(37), + add: __webpack_require__(38), + subtract: __webpack_require__(39), + multiply: __webpack_require__(40), + divide: __webpack_require__(41), + min: __webpack_require__(42), + max: __webpack_require__(43), + scale: __webpack_require__(44), + scaleAndAdd: __webpack_require__(45), + distance: __webpack_require__(46), + squaredDistance: __webpack_require__(47), + length: __webpack_require__(48), + squaredLength: __webpack_require__(49), + negate: __webpack_require__(50), + inverse: __webpack_require__(51), + normalize: __webpack_require__(52), + dot: __webpack_require__(53), + lerp: __webpack_require__(54), + random: __webpack_require__(55), + transformMat4: __webpack_require__(56), + transformQuat: __webpack_require__(57) + } + + +/***/ }, +/* 33 */ +/***/ function(module, exports) { + + module.exports = create + + /** + * Creates a new, empty vec4 + * + * @returns {vec4} a new 4D vector + */ + function create () { + var out = new Float32Array(4) + out[0] = 0 + out[1] = 0 + out[2] = 0 + out[3] = 0 + return out + } + + +/***/ }, +/* 34 */ +/***/ function(module, exports) { + + module.exports = clone + + /** + * Creates a new vec4 initialized with values from an existing vector + * + * @param {vec4} a vector to clone + * @returns {vec4} a new 4D vector + */ + function clone (a) { + var out = new Float32Array(4) + out[0] = a[0] + out[1] = a[1] + out[2] = a[2] + out[3] = a[3] + return out + } + + +/***/ }, +/* 35 */ +/***/ function(module, exports) { + + module.exports = fromValues + + /** + * Creates a new vec4 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {vec4} a new 4D vector + */ + function fromValues (x, y, z, w) { + var out = new Float32Array(4) + out[0] = x + out[1] = y + out[2] = z + out[3] = w + return out + } + + +/***/ }, +/* 36 */ +/***/ function(module, exports) { + + module.exports = copy + + /** + * Copy the values from one vec4 to another + * + * @param {vec4} out the receiving vector + * @param {vec4} a the source vector + * @returns {vec4} out + */ + function copy (out, a) { + out[0] = a[0] + out[1] = a[1] + out[2] = a[2] + out[3] = a[3] + return out + } + + +/***/ }, +/* 37 */ +/***/ function(module, exports) { + + module.exports = set + + /** + * Set the components of a vec4 to the given values + * + * @param {vec4} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @param {Number} w W component + * @returns {vec4} out + */ + function set (out, x, y, z, w) { + out[0] = x + out[1] = y + out[2] = z + out[3] = w + return out + } + + +/***/ }, +/* 38 */ +/***/ function(module, exports) { + + module.exports = add + + /** + * Adds two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ + function add (out, a, b) { + out[0] = a[0] + b[0] + out[1] = a[1] + b[1] + out[2] = a[2] + b[2] + out[3] = a[3] + b[3] + return out + } + + +/***/ }, +/* 39 */ +/***/ function(module, exports) { + + module.exports = subtract + + /** + * Subtracts vector b from vector a + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ + function subtract (out, a, b) { + out[0] = a[0] - b[0] + out[1] = a[1] - b[1] + out[2] = a[2] - b[2] + out[3] = a[3] - b[3] + return out + } + + +/***/ }, +/* 40 */ +/***/ function(module, exports) { + + module.exports = multiply + + /** + * Multiplies two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ + function multiply (out, a, b) { + out[0] = a[0] * b[0] + out[1] = a[1] * b[1] + out[2] = a[2] * b[2] + out[3] = a[3] * b[3] + return out + } + + +/***/ }, +/* 41 */ +/***/ function(module, exports) { + + module.exports = divide + + /** + * Divides two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ + function divide (out, a, b) { + out[0] = a[0] / b[0] + out[1] = a[1] / b[1] + out[2] = a[2] / b[2] + out[3] = a[3] / b[3] + return out + } + + +/***/ }, +/* 42 */ +/***/ function(module, exports) { + + module.exports = min + + /** + * Returns the minimum of two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ + function min (out, a, b) { + out[0] = Math.min(a[0], b[0]) + out[1] = Math.min(a[1], b[1]) + out[2] = Math.min(a[2], b[2]) + out[3] = Math.min(a[3], b[3]) + return out + } + + +/***/ }, +/* 43 */ +/***/ function(module, exports) { + + module.exports = max + + /** + * Returns the maximum of two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {vec4} out + */ + function max (out, a, b) { + out[0] = Math.max(a[0], b[0]) + out[1] = Math.max(a[1], b[1]) + out[2] = Math.max(a[2], b[2]) + out[3] = Math.max(a[3], b[3]) + return out + } + + +/***/ }, +/* 44 */ +/***/ function(module, exports) { + + module.exports = scale + + /** + * Scales a vec4 by a scalar number + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec4} out + */ + function scale (out, a, b) { + out[0] = a[0] * b + out[1] = a[1] * b + out[2] = a[2] * b + out[3] = a[3] * b + return out + } + + +/***/ }, +/* 45 */ +/***/ function(module, exports) { + + module.exports = scaleAndAdd + + /** + * Adds two vec4's after scaling the second operand by a scalar value + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec4} out + */ + function scaleAndAdd (out, a, b, scale) { + out[0] = a[0] + (b[0] * scale) + out[1] = a[1] + (b[1] * scale) + out[2] = a[2] + (b[2] * scale) + out[3] = a[3] + (b[3] * scale) + return out + } + + +/***/ }, +/* 46 */ +/***/ function(module, exports) { + + module.exports = distance + + /** + * Calculates the euclidian distance between two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} distance between a and b + */ + function distance (a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2], + w = b[3] - a[3] + return Math.sqrt(x * x + y * y + z * z + w * w) + } + + +/***/ }, +/* 47 */ +/***/ function(module, exports) { + + module.exports = squaredDistance + + /** + * Calculates the squared euclidian distance between two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} squared distance between a and b + */ + function squaredDistance (a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2], + w = b[3] - a[3] + return x * x + y * y + z * z + w * w + } + + +/***/ }, +/* 48 */ +/***/ function(module, exports) { + + module.exports = length + + /** + * Calculates the length of a vec4 + * + * @param {vec4} a vector to calculate length of + * @returns {Number} length of a + */ + function length (a) { + var x = a[0], + y = a[1], + z = a[2], + w = a[3] + return Math.sqrt(x * x + y * y + z * z + w * w) + } + + +/***/ }, +/* 49 */ +/***/ function(module, exports) { + + module.exports = squaredLength + + /** + * Calculates the squared length of a vec4 + * + * @param {vec4} a vector to calculate squared length of + * @returns {Number} squared length of a + */ + function squaredLength (a) { + var x = a[0], + y = a[1], + z = a[2], + w = a[3] + return x * x + y * y + z * z + w * w + } + + +/***/ }, +/* 50 */ +/***/ function(module, exports) { + + module.exports = negate + + /** + * Negates the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to negate + * @returns {vec4} out + */ + function negate (out, a) { + out[0] = -a[0] + out[1] = -a[1] + out[2] = -a[2] + out[3] = -a[3] + return out + } + + +/***/ }, +/* 51 */ +/***/ function(module, exports) { + + module.exports = inverse + + /** + * Returns the inverse of the components of a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to invert + * @returns {vec4} out + */ + function inverse (out, a) { + out[0] = 1.0 / a[0] + out[1] = 1.0 / a[1] + out[2] = 1.0 / a[2] + out[3] = 1.0 / a[3] + return out + } + + +/***/ }, +/* 52 */ +/***/ function(module, exports) { + + module.exports = normalize + + /** + * Normalize a vec4 + * + * @param {vec4} out the receiving vector + * @param {vec4} a vector to normalize + * @returns {vec4} out + */ + function normalize (out, a) { + var x = a[0], + y = a[1], + z = a[2], + w = a[3] + var len = x * x + y * y + z * z + w * w + if (len > 0) { + len = 1 / Math.sqrt(len) + out[0] = x * len + out[1] = y * len + out[2] = z * len + out[3] = w * len + } + return out + } + + +/***/ }, +/* 53 */ +/***/ function(module, exports) { + + module.exports = dot + + /** + * Calculates the dot product of two vec4's + * + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @returns {Number} dot product of a and b + */ + function dot (a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3] + } + + +/***/ }, +/* 54 */ +/***/ function(module, exports) { + + module.exports = lerp + + /** + * Performs a linear interpolation between two vec4's + * + * @param {vec4} out the receiving vector + * @param {vec4} a the first operand + * @param {vec4} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {vec4} out + */ + function lerp (out, a, b, t) { + var ax = a[0], + ay = a[1], + az = a[2], + aw = a[3] + out[0] = ax + t * (b[0] - ax) + out[1] = ay + t * (b[1] - ay) + out[2] = az + t * (b[2] - az) + out[3] = aw + t * (b[3] - aw) + return out + } + + +/***/ }, +/* 55 */ +/***/ function(module, exports, __webpack_require__) { + + var vecNormalize = __webpack_require__(52) + var vecScale = __webpack_require__(44) + + module.exports = random + + /** + * Generates a random vector with the given scale + * + * @param {vec4} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec4} out + */ + function random (out, scale) { + scale = scale || 1.0 + + // TODO: This is a pretty awful way of doing this. Find something better. + out[0] = Math.random() + out[1] = Math.random() + out[2] = Math.random() + out[3] = Math.random() + vecNormalize(out, out) + vecScale(out, out, scale) + return out + } + + +/***/ }, +/* 56 */ +/***/ function(module, exports) { + + module.exports = transformMat4 + + /** + * Transforms the vec4 with a mat4. + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec4} out + */ + function transformMat4 (out, a, m) { + var x = a[0], y = a[1], z = a[2], w = a[3] + out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w + out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w + out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w + out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w + return out + } + + +/***/ }, +/* 57 */ +/***/ function(module, exports) { + + module.exports = transformQuat + + /** + * Transforms the vec4 with a quat + * + * @param {vec4} out the receiving vector + * @param {vec4} a the vector to transform + * @param {quat} q quaternion to transform with + * @returns {vec4} out + */ + function transformQuat (out, a, q) { + var x = a[0], y = a[1], z = a[2], + qx = q[0], qy = q[1], qz = q[2], qw = q[3], + + // calculate quat * vec + ix = qw * x + qy * z - qz * y, + iy = qw * y + qz * x - qx * z, + iz = qw * z + qx * y - qy * x, + iw = -qx * x - qy * y - qz * z + + // calculate result * inverse quat + out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy + out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz + out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx + out[3] = a[3] + return out + } + + +/***/ }, +/* 58 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = { + create: __webpack_require__(59) + , clone: __webpack_require__(60) + , angle: __webpack_require__(61) + , fromValues: __webpack_require__(62) + , copy: __webpack_require__(65) + , set: __webpack_require__(66) + , add: __webpack_require__(67) + , subtract: __webpack_require__(68) + , multiply: __webpack_require__(69) + , divide: __webpack_require__(70) + , min: __webpack_require__(71) + , max: __webpack_require__(72) + , scale: __webpack_require__(73) + , scaleAndAdd: __webpack_require__(74) + , distance: __webpack_require__(75) + , squaredDistance: __webpack_require__(76) + , length: __webpack_require__(77) + , squaredLength: __webpack_require__(78) + , negate: __webpack_require__(79) + , inverse: __webpack_require__(80) + , normalize: __webpack_require__(63) + , dot: __webpack_require__(64) + , cross: __webpack_require__(81) + , lerp: __webpack_require__(82) + , random: __webpack_require__(83) + , transformMat4: __webpack_require__(84) + , transformMat3: __webpack_require__(85) + , transformQuat: __webpack_require__(86) + , rotateX: __webpack_require__(87) + , rotateY: __webpack_require__(88) + , rotateZ: __webpack_require__(89) + , forEach: __webpack_require__(90) + } + +/***/ }, +/* 59 */ +/***/ function(module, exports) { + + module.exports = create; + + /** + * Creates a new, empty vec3 + * + * @returns {vec3} a new 3D vector + */ + function create() { + var out = new Float32Array(3) + out[0] = 0 + out[1] = 0 + out[2] = 0 + return out + } + +/***/ }, +/* 60 */ +/***/ function(module, exports) { + + module.exports = clone; + + /** + * Creates a new vec3 initialized with values from an existing vector + * + * @param {vec3} a vector to clone + * @returns {vec3} a new 3D vector + */ + function clone(a) { + var out = new Float32Array(3) + out[0] = a[0] + out[1] = a[1] + out[2] = a[2] + return out + } + +/***/ }, +/* 61 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = angle + + var fromValues = __webpack_require__(62) + var normalize = __webpack_require__(63) + var dot = __webpack_require__(64) + + /** + * Get the angle between two 3D vectors + * @param {vec3} a The first operand + * @param {vec3} b The second operand + * @returns {Number} The angle in radians + */ + function angle(a, b) { + var tempA = fromValues(a[0], a[1], a[2]) + var tempB = fromValues(b[0], b[1], b[2]) + + normalize(tempA, tempA) + normalize(tempB, tempB) + + var cosine = dot(tempA, tempB) + + if(cosine > 1.0){ + return 0 + } else { + return Math.acos(cosine) + } + } + + +/***/ }, +/* 62 */ +/***/ function(module, exports) { + + module.exports = fromValues; + + /** + * Creates a new vec3 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @returns {vec3} a new 3D vector + */ + function fromValues(x, y, z) { + var out = new Float32Array(3) + out[0] = x + out[1] = y + out[2] = z + return out + } + +/***/ }, +/* 63 */ +/***/ function(module, exports) { + + module.exports = normalize; + + /** + * Normalize a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to normalize + * @returns {vec3} out + */ + function normalize(out, a) { + var x = a[0], + y = a[1], + z = a[2] + var len = x*x + y*y + z*z + if (len > 0) { + //TODO: evaluate use of glm_invsqrt here? + len = 1 / Math.sqrt(len) + out[0] = a[0] * len + out[1] = a[1] * len + out[2] = a[2] * len + } + return out + } + +/***/ }, +/* 64 */ +/***/ function(module, exports) { + + module.exports = dot; + + /** + * Calculates the dot product of two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} dot product of a and b + */ + function dot(a, b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + } + +/***/ }, +/* 65 */ +/***/ function(module, exports) { + + module.exports = copy; + + /** + * Copy the values from one vec3 to another + * + * @param {vec3} out the receiving vector + * @param {vec3} a the source vector + * @returns {vec3} out + */ + function copy(out, a) { + out[0] = a[0] + out[1] = a[1] + out[2] = a[2] + return out + } + +/***/ }, +/* 66 */ +/***/ function(module, exports) { + + module.exports = set; + + /** + * Set the components of a vec3 to the given values + * + * @param {vec3} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @param {Number} z Z component + * @returns {vec3} out + */ + function set(out, x, y, z) { + out[0] = x + out[1] = y + out[2] = z + return out + } + +/***/ }, +/* 67 */ +/***/ function(module, exports) { + + module.exports = add; + + /** + * Adds two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ + function add(out, a, b) { + out[0] = a[0] + b[0] + out[1] = a[1] + b[1] + out[2] = a[2] + b[2] + return out + } + +/***/ }, +/* 68 */ +/***/ function(module, exports) { + + module.exports = subtract; + + /** + * Subtracts vector b from vector a + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ + function subtract(out, a, b) { + out[0] = a[0] - b[0] + out[1] = a[1] - b[1] + out[2] = a[2] - b[2] + return out + } + +/***/ }, +/* 69 */ +/***/ function(module, exports) { + + module.exports = multiply; + + /** + * Multiplies two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ + function multiply(out, a, b) { + out[0] = a[0] * b[0] + out[1] = a[1] * b[1] + out[2] = a[2] * b[2] + return out + } + +/***/ }, +/* 70 */ +/***/ function(module, exports) { + + module.exports = divide; + + /** + * Divides two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ + function divide(out, a, b) { + out[0] = a[0] / b[0] + out[1] = a[1] / b[1] + out[2] = a[2] / b[2] + return out + } + +/***/ }, +/* 71 */ +/***/ function(module, exports) { + + module.exports = min; + + /** + * Returns the minimum of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ + function min(out, a, b) { + out[0] = Math.min(a[0], b[0]) + out[1] = Math.min(a[1], b[1]) + out[2] = Math.min(a[2], b[2]) + return out + } + +/***/ }, +/* 72 */ +/***/ function(module, exports) { + + module.exports = max; + + /** + * Returns the maximum of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ + function max(out, a, b) { + out[0] = Math.max(a[0], b[0]) + out[1] = Math.max(a[1], b[1]) + out[2] = Math.max(a[2], b[2]) + return out + } + +/***/ }, +/* 73 */ +/***/ function(module, exports) { + + module.exports = scale; + + /** + * Scales a vec3 by a scalar number + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec3} out + */ + function scale(out, a, b) { + out[0] = a[0] * b + out[1] = a[1] * b + out[2] = a[2] * b + return out + } + +/***/ }, +/* 74 */ +/***/ function(module, exports) { + + module.exports = scaleAndAdd; + + /** + * Adds two vec3's after scaling the second operand by a scalar value + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec3} out + */ + function scaleAndAdd(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale) + out[1] = a[1] + (b[1] * scale) + out[2] = a[2] + (b[2] * scale) + return out + } + +/***/ }, +/* 75 */ +/***/ function(module, exports) { + + module.exports = distance; + + /** + * Calculates the euclidian distance between two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} distance between a and b + */ + function distance(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2] + return Math.sqrt(x*x + y*y + z*z) + } + +/***/ }, +/* 76 */ +/***/ function(module, exports) { + + module.exports = squaredDistance; + + /** + * Calculates the squared euclidian distance between two vec3's + * + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {Number} squared distance between a and b + */ + function squaredDistance(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1], + z = b[2] - a[2] + return x*x + y*y + z*z + } + +/***/ }, +/* 77 */ +/***/ function(module, exports) { + + module.exports = length; + + /** + * Calculates the length of a vec3 + * + * @param {vec3} a vector to calculate length of + * @returns {Number} length of a + */ + function length(a) { + var x = a[0], + y = a[1], + z = a[2] + return Math.sqrt(x*x + y*y + z*z) + } + +/***/ }, +/* 78 */ +/***/ function(module, exports) { + + module.exports = squaredLength; + + /** + * Calculates the squared length of a vec3 + * + * @param {vec3} a vector to calculate squared length of + * @returns {Number} squared length of a + */ + function squaredLength(a) { + var x = a[0], + y = a[1], + z = a[2] + return x*x + y*y + z*z + } + +/***/ }, +/* 79 */ +/***/ function(module, exports) { + + module.exports = negate; + + /** + * Negates the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to negate + * @returns {vec3} out + */ + function negate(out, a) { + out[0] = -a[0] + out[1] = -a[1] + out[2] = -a[2] + return out + } + +/***/ }, +/* 80 */ +/***/ function(module, exports) { + + module.exports = inverse; + + /** + * Returns the inverse of the components of a vec3 + * + * @param {vec3} out the receiving vector + * @param {vec3} a vector to invert + * @returns {vec3} out + */ + function inverse(out, a) { + out[0] = 1.0 / a[0] + out[1] = 1.0 / a[1] + out[2] = 1.0 / a[2] + return out + } + +/***/ }, +/* 81 */ +/***/ function(module, exports) { + + module.exports = cross; + + /** + * Computes the cross product of two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @returns {vec3} out + */ + function cross(out, a, b) { + var ax = a[0], ay = a[1], az = a[2], + bx = b[0], by = b[1], bz = b[2] + + out[0] = ay * bz - az * by + out[1] = az * bx - ax * bz + out[2] = ax * by - ay * bx + return out + } + +/***/ }, +/* 82 */ +/***/ function(module, exports) { + + module.exports = lerp; + + /** + * Performs a linear interpolation between two vec3's + * + * @param {vec3} out the receiving vector + * @param {vec3} a the first operand + * @param {vec3} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {vec3} out + */ + function lerp(out, a, b, t) { + var ax = a[0], + ay = a[1], + az = a[2] + out[0] = ax + t * (b[0] - ax) + out[1] = ay + t * (b[1] - ay) + out[2] = az + t * (b[2] - az) + return out + } + +/***/ }, +/* 83 */ +/***/ function(module, exports) { + + module.exports = random; + + /** + * Generates a random vector with the given scale + * + * @param {vec3} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec3} out + */ + function random(out, scale) { + scale = scale || 1.0 + + var r = Math.random() * 2.0 * Math.PI + var z = (Math.random() * 2.0) - 1.0 + var zScale = Math.sqrt(1.0-z*z) * scale + + out[0] = Math.cos(r) * zScale + out[1] = Math.sin(r) * zScale + out[2] = z * scale + return out + } + +/***/ }, +/* 84 */ +/***/ function(module, exports) { + + module.exports = transformMat4; + + /** + * Transforms the vec3 with a mat4. + * 4th vector component is implicitly '1' + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec3} out + */ + function transformMat4(out, a, m) { + var x = a[0], y = a[1], z = a[2], + w = m[3] * x + m[7] * y + m[11] * z + m[15] + w = w || 1.0 + out[0] = (m[0] * x + m[4] * y + m[8] * z + m[12]) / w + out[1] = (m[1] * x + m[5] * y + m[9] * z + m[13]) / w + out[2] = (m[2] * x + m[6] * y + m[10] * z + m[14]) / w + return out + } + +/***/ }, +/* 85 */ +/***/ function(module, exports) { + + module.exports = transformMat3; + + /** + * Transforms the vec3 with a mat3. + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {mat4} m the 3x3 matrix to transform with + * @returns {vec3} out + */ + function transformMat3(out, a, m) { + var x = a[0], y = a[1], z = a[2] + out[0] = x * m[0] + y * m[3] + z * m[6] + out[1] = x * m[1] + y * m[4] + z * m[7] + out[2] = x * m[2] + y * m[5] + z * m[8] + return out + } + +/***/ }, +/* 86 */ +/***/ function(module, exports) { + + module.exports = transformQuat; + + /** + * Transforms the vec3 with a quat + * + * @param {vec3} out the receiving vector + * @param {vec3} a the vector to transform + * @param {quat} q quaternion to transform with + * @returns {vec3} out + */ + function transformQuat(out, a, q) { + // benchmarks: http://jsperf.com/quaternion-transform-vec3-implementations + + var x = a[0], y = a[1], z = a[2], + qx = q[0], qy = q[1], qz = q[2], qw = q[3], + + // calculate quat * vec + ix = qw * x + qy * z - qz * y, + iy = qw * y + qz * x - qx * z, + iz = qw * z + qx * y - qy * x, + iw = -qx * x - qy * y - qz * z + + // calculate result * inverse quat + out[0] = ix * qw + iw * -qx + iy * -qz - iz * -qy + out[1] = iy * qw + iw * -qy + iz * -qx - ix * -qz + out[2] = iz * qw + iw * -qz + ix * -qy - iy * -qx + return out + } + +/***/ }, +/* 87 */ +/***/ function(module, exports) { + + module.exports = rotateX; + + /** + * Rotate a 3D vector around the x-axis + * @param {vec3} out The receiving vec3 + * @param {vec3} a The vec3 point to rotate + * @param {vec3} b The origin of the rotation + * @param {Number} c The angle of rotation + * @returns {vec3} out + */ + function rotateX(out, a, b, c){ + var p = [], r=[] + //Translate point to the origin + p[0] = a[0] - b[0] + p[1] = a[1] - b[1] + p[2] = a[2] - b[2] + + //perform rotation + r[0] = p[0] + r[1] = p[1]*Math.cos(c) - p[2]*Math.sin(c) + r[2] = p[1]*Math.sin(c) + p[2]*Math.cos(c) + + //translate to correct position + out[0] = r[0] + b[0] + out[1] = r[1] + b[1] + out[2] = r[2] + b[2] + + return out + } + +/***/ }, +/* 88 */ +/***/ function(module, exports) { + + module.exports = rotateY; + + /** + * Rotate a 3D vector around the y-axis + * @param {vec3} out The receiving vec3 + * @param {vec3} a The vec3 point to rotate + * @param {vec3} b The origin of the rotation + * @param {Number} c The angle of rotation + * @returns {vec3} out + */ + function rotateY(out, a, b, c){ + var p = [], r=[] + //Translate point to the origin + p[0] = a[0] - b[0] + p[1] = a[1] - b[1] + p[2] = a[2] - b[2] + + //perform rotation + r[0] = p[2]*Math.sin(c) + p[0]*Math.cos(c) + r[1] = p[1] + r[2] = p[2]*Math.cos(c) - p[0]*Math.sin(c) + + //translate to correct position + out[0] = r[0] + b[0] + out[1] = r[1] + b[1] + out[2] = r[2] + b[2] + + return out + } + +/***/ }, +/* 89 */ +/***/ function(module, exports) { + + module.exports = rotateZ; + + /** + * Rotate a 3D vector around the z-axis + * @param {vec3} out The receiving vec3 + * @param {vec3} a The vec3 point to rotate + * @param {vec3} b The origin of the rotation + * @param {Number} c The angle of rotation + * @returns {vec3} out + */ + function rotateZ(out, a, b, c){ + var p = [], r=[] + //Translate point to the origin + p[0] = a[0] - b[0] + p[1] = a[1] - b[1] + p[2] = a[2] - b[2] + + //perform rotation + r[0] = p[0]*Math.cos(c) - p[1]*Math.sin(c) + r[1] = p[0]*Math.sin(c) + p[1]*Math.cos(c) + r[2] = p[2] + + //translate to correct position + out[0] = r[0] + b[0] + out[1] = r[1] + b[1] + out[2] = r[2] + b[2] + + return out + } + +/***/ }, +/* 90 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = forEach; + + var vec = __webpack_require__(59)() + + /** + * Perform some operation over an array of vec3s. + * + * @param {Array} a the array of vectors to iterate over + * @param {Number} stride Number of elements between the start of each vec3. If 0 assumes tightly packed + * @param {Number} offset Number of elements to skip at the beginning of the array + * @param {Number} count Number of vec3s to iterate over. If 0 iterates over entire array + * @param {Function} fn Function to call for each vector in the array + * @param {Object} [arg] additional argument to pass to fn + * @returns {Array} a + * @function + */ + function forEach(a, stride, offset, count, fn, arg) { + var i, l + if(!stride) { + stride = 3 + } + + if(!offset) { + offset = 0 + } + + if(count) { + l = Math.min((count * stride) + offset, a.length) + } else { + l = a.length + } + + for(i = offset; i < l; i += stride) { + vec[0] = a[i] + vec[1] = a[i+1] + vec[2] = a[i+2] + fn(vec, vec, arg) + a[i] = vec[0] + a[i+1] = vec[1] + a[i+2] = vec[2] + } + + return a + } + +/***/ }, +/* 91 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = { + create: __webpack_require__(92) + , clone: __webpack_require__(93) + , fromValues: __webpack_require__(94) + , copy: __webpack_require__(95) + , set: __webpack_require__(96) + , add: __webpack_require__(97) + , subtract: __webpack_require__(98) + , multiply: __webpack_require__(99) + , divide: __webpack_require__(100) + , min: __webpack_require__(101) + , max: __webpack_require__(102) + , scale: __webpack_require__(103) + , scaleAndAdd: __webpack_require__(104) + , distance: __webpack_require__(105) + , squaredDistance: __webpack_require__(106) + , length: __webpack_require__(107) + , squaredLength: __webpack_require__(108) + , negate: __webpack_require__(109) + , normalize: __webpack_require__(110) + , dot: __webpack_require__(111) + , cross: __webpack_require__(112) + , lerp: __webpack_require__(113) + , random: __webpack_require__(114) + , transformMat2: __webpack_require__(115) + , transformMat2d: __webpack_require__(116) + , transformMat3: __webpack_require__(117) + , transformMat4: __webpack_require__(118) + , forEach: __webpack_require__(119) + } + +/***/ }, +/* 92 */ +/***/ function(module, exports) { + + module.exports = create + + /** + * Creates a new, empty vec2 + * + * @returns {vec2} a new 2D vector + */ + function create() { + var out = new Float32Array(2) + out[0] = 0 + out[1] = 0 + return out + } + +/***/ }, +/* 93 */ +/***/ function(module, exports) { + + module.exports = clone + + /** + * Creates a new vec2 initialized with values from an existing vector + * + * @param {vec2} a vector to clone + * @returns {vec2} a new 2D vector + */ + function clone(a) { + var out = new Float32Array(2) + out[0] = a[0] + out[1] = a[1] + return out + } + +/***/ }, +/* 94 */ +/***/ function(module, exports) { + + module.exports = fromValues + + /** + * Creates a new vec2 initialized with the given values + * + * @param {Number} x X component + * @param {Number} y Y component + * @returns {vec2} a new 2D vector + */ + function fromValues(x, y) { + var out = new Float32Array(2) + out[0] = x + out[1] = y + return out + } + +/***/ }, +/* 95 */ +/***/ function(module, exports) { + + module.exports = copy + + /** + * Copy the values from one vec2 to another + * + * @param {vec2} out the receiving vector + * @param {vec2} a the source vector + * @returns {vec2} out + */ + function copy(out, a) { + out[0] = a[0] + out[1] = a[1] + return out + } + +/***/ }, +/* 96 */ +/***/ function(module, exports) { + + module.exports = set + + /** + * Set the components of a vec2 to the given values + * + * @param {vec2} out the receiving vector + * @param {Number} x X component + * @param {Number} y Y component + * @returns {vec2} out + */ + function set(out, x, y) { + out[0] = x + out[1] = y + return out + } + +/***/ }, +/* 97 */ +/***/ function(module, exports) { + + module.exports = add + + /** + * Adds two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ + function add(out, a, b) { + out[0] = a[0] + b[0] + out[1] = a[1] + b[1] + return out + } + +/***/ }, +/* 98 */ +/***/ function(module, exports) { + + module.exports = subtract + + /** + * Subtracts vector b from vector a + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ + function subtract(out, a, b) { + out[0] = a[0] - b[0] + out[1] = a[1] - b[1] + return out + } + +/***/ }, +/* 99 */ +/***/ function(module, exports) { + + module.exports = multiply + + /** + * Multiplies two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ + function multiply(out, a, b) { + out[0] = a[0] * b[0] + out[1] = a[1] * b[1] + return out + } + +/***/ }, +/* 100 */ +/***/ function(module, exports) { + + module.exports = divide + + /** + * Divides two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ + function divide(out, a, b) { + out[0] = a[0] / b[0] + out[1] = a[1] / b[1] + return out + } + +/***/ }, +/* 101 */ +/***/ function(module, exports) { + + module.exports = min + + /** + * Returns the minimum of two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ + function min(out, a, b) { + out[0] = Math.min(a[0], b[0]) + out[1] = Math.min(a[1], b[1]) + return out + } + +/***/ }, +/* 102 */ +/***/ function(module, exports) { + + module.exports = max + + /** + * Returns the maximum of two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec2} out + */ + function max(out, a, b) { + out[0] = Math.max(a[0], b[0]) + out[1] = Math.max(a[1], b[1]) + return out + } + +/***/ }, +/* 103 */ +/***/ function(module, exports) { + + module.exports = scale + + /** + * Scales a vec2 by a scalar number + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to scale + * @param {Number} b amount to scale the vector by + * @returns {vec2} out + */ + function scale(out, a, b) { + out[0] = a[0] * b + out[1] = a[1] * b + return out + } + +/***/ }, +/* 104 */ +/***/ function(module, exports) { + + module.exports = scaleAndAdd + + /** + * Adds two vec2's after scaling the second operand by a scalar value + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @param {Number} scale the amount to scale b by before adding + * @returns {vec2} out + */ + function scaleAndAdd(out, a, b, scale) { + out[0] = a[0] + (b[0] * scale) + out[1] = a[1] + (b[1] * scale) + return out + } + +/***/ }, +/* 105 */ +/***/ function(module, exports) { + + module.exports = distance + + /** + * Calculates the euclidian distance between two vec2's + * + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {Number} distance between a and b + */ + function distance(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1] + return Math.sqrt(x*x + y*y) + } + +/***/ }, +/* 106 */ +/***/ function(module, exports) { + + module.exports = squaredDistance + + /** + * Calculates the squared euclidian distance between two vec2's + * + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {Number} squared distance between a and b + */ + function squaredDistance(a, b) { + var x = b[0] - a[0], + y = b[1] - a[1] + return x*x + y*y + } + +/***/ }, +/* 107 */ +/***/ function(module, exports) { + + module.exports = length + + /** + * Calculates the length of a vec2 + * + * @param {vec2} a vector to calculate length of + * @returns {Number} length of a + */ + function length(a) { + var x = a[0], + y = a[1] + return Math.sqrt(x*x + y*y) + } + +/***/ }, +/* 108 */ +/***/ function(module, exports) { + + module.exports = squaredLength + + /** + * Calculates the squared length of a vec2 + * + * @param {vec2} a vector to calculate squared length of + * @returns {Number} squared length of a + */ + function squaredLength(a) { + var x = a[0], + y = a[1] + return x*x + y*y + } + +/***/ }, +/* 109 */ +/***/ function(module, exports) { + + module.exports = negate + + /** + * Negates the components of a vec2 + * + * @param {vec2} out the receiving vector + * @param {vec2} a vector to negate + * @returns {vec2} out + */ + function negate(out, a) { + out[0] = -a[0] + out[1] = -a[1] + return out + } + +/***/ }, +/* 110 */ +/***/ function(module, exports) { + + module.exports = normalize + + /** + * Normalize a vec2 + * + * @param {vec2} out the receiving vector + * @param {vec2} a vector to normalize + * @returns {vec2} out + */ + function normalize(out, a) { + var x = a[0], + y = a[1] + var len = x*x + y*y + if (len > 0) { + //TODO: evaluate use of glm_invsqrt here? + len = 1 / Math.sqrt(len) + out[0] = a[0] * len + out[1] = a[1] * len + } + return out + } + +/***/ }, +/* 111 */ +/***/ function(module, exports) { + + module.exports = dot + + /** + * Calculates the dot product of two vec2's + * + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {Number} dot product of a and b + */ + function dot(a, b) { + return a[0] * b[0] + a[1] * b[1] + } + +/***/ }, +/* 112 */ +/***/ function(module, exports) { + + module.exports = cross + + /** + * Computes the cross product of two vec2's + * Note that the cross product must by definition produce a 3D vector + * + * @param {vec3} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @returns {vec3} out + */ + function cross(out, a, b) { + var z = a[0] * b[1] - a[1] * b[0] + out[0] = out[1] = 0 + out[2] = z + return out + } + +/***/ }, +/* 113 */ +/***/ function(module, exports) { + + module.exports = lerp + + /** + * Performs a linear interpolation between two vec2's + * + * @param {vec2} out the receiving vector + * @param {vec2} a the first operand + * @param {vec2} b the second operand + * @param {Number} t interpolation amount between the two inputs + * @returns {vec2} out + */ + function lerp(out, a, b, t) { + var ax = a[0], + ay = a[1] + out[0] = ax + t * (b[0] - ax) + out[1] = ay + t * (b[1] - ay) + return out + } + +/***/ }, +/* 114 */ +/***/ function(module, exports) { + + module.exports = random + + /** + * Generates a random vector with the given scale + * + * @param {vec2} out the receiving vector + * @param {Number} [scale] Length of the resulting vector. If ommitted, a unit vector will be returned + * @returns {vec2} out + */ + function random(out, scale) { + scale = scale || 1.0 + var r = Math.random() * 2.0 * Math.PI + out[0] = Math.cos(r) * scale + out[1] = Math.sin(r) * scale + return out + } + +/***/ }, +/* 115 */ +/***/ function(module, exports) { + + module.exports = transformMat2 + + /** + * Transforms the vec2 with a mat2 + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat2} m matrix to transform with + * @returns {vec2} out + */ + function transformMat2(out, a, m) { + var x = a[0], + y = a[1] + out[0] = m[0] * x + m[2] * y + out[1] = m[1] * x + m[3] * y + return out + } + +/***/ }, +/* 116 */ +/***/ function(module, exports) { + + module.exports = transformMat2d + + /** + * Transforms the vec2 with a mat2d + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat2d} m matrix to transform with + * @returns {vec2} out + */ + function transformMat2d(out, a, m) { + var x = a[0], + y = a[1] + out[0] = m[0] * x + m[2] * y + m[4] + out[1] = m[1] * x + m[3] * y + m[5] + return out + } + +/***/ }, +/* 117 */ +/***/ function(module, exports) { + + module.exports = transformMat3 + + /** + * Transforms the vec2 with a mat3 + * 3rd vector component is implicitly '1' + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat3} m matrix to transform with + * @returns {vec2} out + */ + function transformMat3(out, a, m) { + var x = a[0], + y = a[1] + out[0] = m[0] * x + m[3] * y + m[6] + out[1] = m[1] * x + m[4] * y + m[7] + return out + } + +/***/ }, +/* 118 */ +/***/ function(module, exports) { + + module.exports = transformMat4 + + /** + * Transforms the vec2 with a mat4 + * 3rd vector component is implicitly '0' + * 4th vector component is implicitly '1' + * + * @param {vec2} out the receiving vector + * @param {vec2} a the vector to transform + * @param {mat4} m matrix to transform with + * @returns {vec2} out + */ + function transformMat4(out, a, m) { + var x = a[0], + y = a[1] + out[0] = m[0] * x + m[4] * y + m[12] + out[1] = m[1] * x + m[5] * y + m[13] + return out + } + +/***/ }, +/* 119 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = forEach + + var vec = __webpack_require__(92)() + + /** + * Perform some operation over an array of vec2s. + * + * @param {Array} a the array of vectors to iterate over + * @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed + * @param {Number} offset Number of elements to skip at the beginning of the array + * @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array + * @param {Function} fn Function to call for each vector in the array + * @param {Object} [arg] additional argument to pass to fn + * @returns {Array} a + * @function + */ + function forEach(a, stride, offset, count, fn, arg) { + var i, l + if(!stride) { + stride = 2 + } + + if(!offset) { + offset = 0 + } + + if(count) { + l = Math.min((count * stride) + offset, a.length) + } else { + l = a.length + } + + for(i = offset; i < l; i += stride) { + vec[0] = a[i] + vec[1] = a[i+1] + fn(vec, vec, arg) + a[i] = vec[0] + a[i+1] = vec[1] + } + + return a + } + +/***/ }, +/* 120 */ +/***/ function(module, exports, __webpack_require__) { + + var $ = __webpack_require__(1); + + /** + * @module geo.util + */ + var util = __webpack_require__(121); + $.extend(util, __webpack_require__(122)); + util.DistanceGrid = __webpack_require__(123); + util.ClusterGroup = __webpack_require__(124); + + module.exports = util; + + +/***/ }, +/* 121 */ +/***/ function(module, exports, __webpack_require__) { + + + (function () { + 'use strict'; + + var $ = __webpack_require__(1); + var geo = {util: {}}; + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + var m_timingData = {}, + m_timingKeepRecent = 200, + m_threshold = 15, + m_originalRequestAnimationFrame; + + /** + * Takes a variable number of arguments and returns the first numeric value + * it finds. + * @private + */ + function setNumeric() { + var i; + for (i = 0; i < arguments.length; i += 1) { + if (isFinite(arguments[i])) { + return arguments[i]; + } + } + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * Contains utility classes and methods used by geojs. + * @namespace + */ + ////////////////////////////////////////////////////////////////////////////// + geo.util = { + /** + * Returns true if the given point lies in the given polygon. + * Algorithm description: + * http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html + * @param {geo.screenPosition} point The test point + * @param {geo.screenPosition[]} outer The outer boundary of the polygon + * @param {geo.screenPosition[][]?} inner Inner boundaries (holes) + */ + pointInPolygon: function (point, outer, inner) { + var inside = false, n = outer.length; + + if (n < 3) { + // we need 3 coordinates for this to make sense + return false; + } + + outer.forEach(function (vert, i) { + var j = (n + i - 1) % n; + var intersect = ( + ((outer[i].y > point.y) !== (outer[j].y > point.y)) && + (point.x < (outer[j].x - outer[i].x) * + (point.y - outer[i].y) / + (outer[j].y - outer[i].y) + outer[i].x) + ); + if (intersect) { + inside = !inside; + } + }); + + (inner || []).forEach(function (hole) { + inside = inside && !geo.util.pointInPolygon(point, hole); + }); + + return inside; + }, + + /** + * Returns true if the argument is a function. + */ + isFunction: function (f) { + return typeof f === 'function'; + }, + + /** + * Returns the argument if it is function, otherwise returns a function + * that returns the argument. + */ + ensureFunction: function (f) { + if (geo.util.isFunction(f)) { + return f; + } else { + return function () { return f; }; + } + }, + + /** + * Return a random string of length n || 8. + */ + randomString: function (n) { + var s, i, r; + n = n || 8; + s = ''; + for (i = 0; i < n; i += 1) { + r = Math.floor(Math.random() * chars.length); + s += chars.substring(r, r + 1); + } + return s; + }, + + /** + * Convert a color from hex value or css name to rgb objects + */ + convertColor: function (color) { + if (color.r !== undefined && color.g !== undefined && + color.b !== undefined) { + return color; + } + if (typeof color === 'string') { + if (geo.util.cssColors.hasOwnProperty(color)) { + color = geo.util.cssColors[color]; + } else if (color.charAt(0) === '#') { + color = parseInt(color.slice(1), 16); + } + } + if (isFinite(color)) { + color = { + r: ((color & 0xff0000) >> 16) / 255, + g: ((color & 0xff00) >> 8) / 255, + b: ((color & 0xff)) / 255 + }; + } + return color; + }, + + /** + * Normalize a coordinate object into {x: ..., y: ..., z: ... } form. + * Accepts 2-3d arrays, + * latitude -> lat -> y + * longitude -> lon -> lng -> x + */ + normalizeCoordinates: function (p) { + p = p || {}; + if (Array.isArray(p)) { + return { + x: p[0], + y: p[1], + z: p[2] || 0 + }; + } + return { + 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 + ) + }; + }, + + /** + * 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; + }, + + /** + * Compare two arrays and return if their contents are equal. + * @param {array} a1 first array to compare + * @param {array} a2 second array to compare + * @returns {boolean} true if the contents of the arrays are equal. + */ + compareArrays: function (a1, a2) { + return (a1.length === a2.length && a1.every(function (el, idx) { + return el === a2[idx]; + })); + }, + + /** + * Create a vec3 that is always an array. This should only be used if it + * will not be used in a WebGL context. Plain arrays usually use 64-bit + * float values, whereas vec3 defaults to 32-bit floats. + * + * @returns {Array} zeroed-out vec3 compatible array. + */ + vec3AsArray: function () { + return [0, 0, 0]; + }, + + /** + * Create a mat4 that is always an array. This should only be used if it + * will not be used in a WebGL context. Plain arrays usually use 64-bit + * float values, whereas mat4 defaults to 32-bit floats. + * + * @returns {Array} identity mat4 compatible array. + */ + mat4AsArray: function () { + return [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]; + }, + + /** + * Get a buffer for a vgl geometry source. If a buffer already exists and + * is the correct size, return it. Otherwise, allocate a new buffer; any + * data in an old buffer is discarded. + * + * @param geom: the geometry to reference and modify. + * @param srcName: the name of the source. + * @param len: the number of elements for the array. + * @returns {Float32Array} + */ + getGeomBuffer: function (geom, srcName, len) { + var src = geom.sourceByName(srcName), data; + + data = src.data(); + if (data instanceof Float32Array && data.length === len) { + return data; + } + data = new Float32Array(len); + src.setData(data); + return data; + }, + + /** + * Report on one or all of the tracked timings. + * + * @param {string} name name to report on, or undefined to report all. + */ + timeReport: function (name) { + $.each(m_timingData, function (key, item) { + /* calculate the standard deviation of each item. */ + if (item.count) { + item.stddev = Math.sqrt(Math.abs(( + item.sum2 - item.sum * item.sum / item.count) / item.count)); + item.average = item.sum / item.count; + } else { + item.stddev = 0; + item.average = 0; + } + }); + if (name) { + return m_timingData[name]; + } + return m_timingData; + }, + + /** + * Note the start time of a function (or any other section of code). This + * should be paired with timeFunctionStop, which will collect statistics on + * the amount of time spent in a function. + * + * @param {string} name name to use for tracking the timing. + * @param {boolean} reset if true, clear old tracking data. + */ + timeFunctionStart: function (name, reset) { + if (!m_timingData[name] || reset) { + m_timingData[name] = { + count: 0, sum: 0, sum2: 0, max: 0, recent: [] + }; + } + m_timingData[name].start = window.performance.now(); + }, + + /** + * Note the stop time of a function (or any other section of code). This + * should be paired with timeFunctionStart. + * + * @param {string} name name to use for tracking the timing. + */ + timeFunctionStop: function (name) { + if (!m_timingData[name] || !m_timingData[name].start) { + return; + } + var duration = window.performance.now() - m_timingData[name].start; + m_timingData[name].start = null; + m_timingData[name].sum += duration; + m_timingData[name].sum2 += duration * duration; + m_timingData[name].count += 1; + m_timingData[name].max = Math.max( + m_timingData[name].max, duration); + m_timingData[name].recent.push(duration); + if (m_timingData[name].recent.length > m_timingKeepRecent) { + m_timingData[name].recent.splice( + 0, m_timingData[name].recent.length - m_timingKeepRecent); + } + }, + + /** + * Start or stop tracking the time spent in requestAnimationFrame. If + * tracked, the results can be fetched via + * timeFunctionReport('requestAnimationFrame'). + * + * @param {boolean} stop falsy to start tracking, truthy to start tracking. + * @param {boolean} reset if true, reset the statistics. + * @param {number} threshold if present, set the threshold used in tracking + * slow callbacks. + * @param {number} keep if present, set the number of recent frame times + * to track. + */ + timeRequestAnimationFrame: function (stop, reset, threshold, keep) { + if (!m_timingData.requestAnimationFrame || reset) { + m_timingData.requestAnimationFrame = { + count: 0, sum: 0, sum2: 0, max: 0, above_threshold: 0, + recent: [], recentsub: [] + }; + } + if (threshold) { + m_threshold = threshold; + } + if (keep) { + m_timingKeepRecent = keep; + } + if (stop && m_originalRequestAnimationFrame) { + window.requestAnimationFrame = m_originalRequestAnimationFrame; + m_originalRequestAnimationFrame = null; + } else if (!stop && !m_originalRequestAnimationFrame) { + m_originalRequestAnimationFrame = window.requestAnimationFrame; + window.requestAnimationFrame = function (callback) { + m_originalRequestAnimationFrame.call(window, function (timestamp) { + var track = m_timingData.requestAnimationFrame, recent; + if (track.timestamp !== timestamp) { + track.timestamp = timestamp; + track.subcalls = track.subcalls || 0; + track.start = { + sum: track.sum, + sum2: track.sum2, + count: track.count, + max: track.max, + above_threshold: track.above_threshold + }; + track.recent.push([0]); + track.recentsub.push([]); + if (track.recent.length > m_timingKeepRecent) { + track.recent.splice( + 0, track.recent.length - m_timingKeepRecent); + track.recentsub.splice( + 0, track.recentsub.length - m_timingKeepRecent); + } + } + track.subcalls += 1; + callback.apply(this, arguments); + var duration = window.performance.now() - timestamp; + track.sum = track.start.sum + duration; + track.sum2 = track.start.sum2 + duration * duration; + track.count = track.start.count + 1; + track.max = Math.max(track.max, duration); + track.above_threshold = track.start.above_threshold + ( + duration >= m_threshold ? 1 : 0); + track.recent[track.recent.length - 1] = duration; + recent = track.recentsub[track.recent.length - 1]; + recent.push({ + total_duration: duration, + duration: duration - (recent.length ? + recent[recent.length - 1].total_duration : 0), + callback: callback.name || callback + }); + }); + }; + } + } + }; + + geo.util.cssColors = { + aliceblue: 0xf0f8ff, + antiquewhite: 0xfaebd7, + aqua: 0x00ffff, + aquamarine: 0x7fffd4, + azure: 0xf0ffff, + beige: 0xf5f5dc, + bisque: 0xffe4c4, + black: 0x000000, + blanchedalmond: 0xffebcd, + blue: 0x0000ff, + blueviolet: 0x8a2be2, + brown: 0xa52a2a, + burlywood: 0xdeb887, + cadetblue: 0x5f9ea0, + chartreuse: 0x7fff00, + chocolate: 0xd2691e, + coral: 0xff7f50, + cornflowerblue: 0x6495ed, + cornsilk: 0xfff8dc, + crimson: 0xdc143c, + cyan: 0x00ffff, + darkblue: 0x00008b, + darkcyan: 0x008b8b, + darkgoldenrod: 0xb8860b, + darkgray: 0xa9a9a9, + darkgreen: 0x006400, + darkgrey: 0xa9a9a9, + darkkhaki: 0xbdb76b, + darkmagenta: 0x8b008b, + darkolivegreen: 0x556b2f, + darkorange: 0xff8c00, + darkorchid: 0x9932cc, + darkred: 0x8b0000, + darksalmon: 0xe9967a, + darkseagreen: 0x8fbc8f, + darkslateblue: 0x483d8b, + darkslategray: 0x2f4f4f, + darkslategrey: 0x2f4f4f, + darkturquoise: 0x00ced1, + darkviolet: 0x9400d3, + deeppink: 0xff1493, + deepskyblue: 0x00bfff, + dimgray: 0x696969, + dimgrey: 0x696969, + dodgerblue: 0x1e90ff, + firebrick: 0xb22222, + floralwhite: 0xfffaf0, + forestgreen: 0x228b22, + fuchsia: 0xff00ff, + gainsboro: 0xdcdcdc, + ghostwhite: 0xf8f8ff, + gold: 0xffd700, + goldenrod: 0xdaa520, + gray: 0x808080, + green: 0x008000, + greenyellow: 0xadff2f, + grey: 0x808080, + honeydew: 0xf0fff0, + hotpink: 0xff69b4, + indianred: 0xcd5c5c, + indigo: 0x4b0082, + ivory: 0xfffff0, + khaki: 0xf0e68c, + lavender: 0xe6e6fa, + lavenderblush: 0xfff0f5, + lawngreen: 0x7cfc00, + lemonchiffon: 0xfffacd, + lightblue: 0xadd8e6, + lightcoral: 0xf08080, + lightcyan: 0xe0ffff, + lightgoldenrodyellow: 0xfafad2, + lightgray: 0xd3d3d3, + lightgreen: 0x90ee90, + lightgrey: 0xd3d3d3, + lightpink: 0xffb6c1, + lightsalmon: 0xffa07a, + lightseagreen: 0x20b2aa, + lightskyblue: 0x87cefa, + lightslategray: 0x778899, + lightslategrey: 0x778899, + lightsteelblue: 0xb0c4de, + lightyellow: 0xffffe0, + lime: 0x00ff00, + limegreen: 0x32cd32, + linen: 0xfaf0e6, + magenta: 0xff00ff, + maroon: 0x800000, + mediumaquamarine: 0x66cdaa, + mediumblue: 0x0000cd, + mediumorchid: 0xba55d3, + mediumpurple: 0x9370db, + mediumseagreen: 0x3cb371, + mediumslateblue: 0x7b68ee, + mediumspringgreen: 0x00fa9a, + mediumturquoise: 0x48d1cc, + mediumvioletred: 0xc71585, + midnightblue: 0x191970, + mintcream: 0xf5fffa, + mistyrose: 0xffe4e1, + moccasin: 0xffe4b5, + navajowhite: 0xffdead, + navy: 0x000080, + oldlace: 0xfdf5e6, + olive: 0x808000, + olivedrab: 0x6b8e23, + orange: 0xffa500, + orangered: 0xff4500, + orchid: 0xda70d6, + palegoldenrod: 0xeee8aa, + palegreen: 0x98fb98, + paleturquoise: 0xafeeee, + palevioletred: 0xdb7093, + papayawhip: 0xffefd5, + peachpuff: 0xffdab9, + peru: 0xcd853f, + pink: 0xffc0cb, + plum: 0xdda0dd, + powderblue: 0xb0e0e6, + purple: 0x800080, + red: 0xff0000, + rosybrown: 0xbc8f8f, + royalblue: 0x4169e1, + saddlebrown: 0x8b4513, + salmon: 0xfa8072, + sandybrown: 0xf4a460, + seagreen: 0x2e8b57, + seashell: 0xfff5ee, + sienna: 0xa0522d, + silver: 0xc0c0c0, + skyblue: 0x87ceeb, + slateblue: 0x6a5acd, + slategray: 0x708090, + slategrey: 0x708090, + snow: 0xfffafa, + springgreen: 0x00ff7f, + steelblue: 0x4682b4, + tan: 0xd2b48c, + teal: 0x008080, + thistle: 0xd8bfd8, + tomato: 0xff6347, + turquoise: 0x40e0d0, + violet: 0xee82ee, + wheat: 0xf5deb3, + white: 0xffffff, + whitesmoke: 0xf5f5f5, + yellow: 0xffff00, + yellowgreen: 0x9acd32 + }; + + module.exports = geo.util; + }()); + + +/***/ }, +/* 122 */ +/***/ function(module, exports) { + + /** + * @file + * Based on the following jquery throttle / debounce plugin: + * + * jQuery throttle / debounce - v1.1 - 3/7/2010 + * http://benalman.com/projects/jquery-throttle-debounce-plugin/ + * + * @copyright 2010 "Cowboy" Ben Alman + * Dual licensed under the MIT and GPL licenses. + * http://benalman.com/about/license/ + * + * The implementation included here is modified to support a callback + * method that can accumulate values between actual invocations of + * the throttled method. + */ + + (function (window) { + 'use strict'; + + /** + * Throttle execution of a function. Especially useful for rate limiting + * execution of handlers on events like resize and scroll. If you want to + * rate-limit execution of a function to a single time see + * {@link geo.util.debounce}. + * + * In this visualization, | is a throttled-function call and X is the actual + * callback execution: + * + * :: + * Throttled with `no_trailing` specified as false or unspecified: + * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + * X X X X X X X X X X X X + * + * Throttled with `no_trailing` specified as true: + * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + * X X X X X X X X X X + * + * @function geo.util.throttle + * @param {number} delay A zero-or-greater delay in milliseconds. For event + * callbacks, values around 100 or 250 (or even higher) are most useful. + * @param {boolean} [no_trailing=false] If no_trailing is + * true, callback will only execute every `delay` milliseconds while the + * throttled-function is being called. If no_trailing is false or + * unspecified, callback will be executed one final time after the last + * throttled-function call. (After the throttled-function has not been + * called for `delay` milliseconds, the internal counter is reset) + * @param {function} callback A function to be executed after `delay` + * milliseconds. The `this` context and all arguments are passed through, + * as-is, to `callback` when the throttled-function is executed. + * @param {function} [accumulator] A function to be executed (synchronously) + * during **each** call to the wrapped function. Typically, this + * this method is used to accumulate values that the callback uses + * when it finally executes. + * + * @returns {function} The throttled version of `callback` + * + * @example + * var throttled = geo.util.throttle( delay, [ no_trailing, ] callback ); + * $('selector').bind( 'someevent', throttled ); + * $('selector').unbind( 'someevent', throttled ); + */ + var throttle = function (delay, no_trailing, + callback, accumulator, debounce_mode) { + // After wrapper has stopped being called, this timeout ensures that + // `callback` is executed at the proper times in `throttle` and `end` + // debounce modes. + var timeout_id, + + // Keep track of the last time `callback` was executed. + last_exec = 0; + + // `no_trailing` defaults to falsy. + if (typeof no_trailing !== 'boolean') { + debounce_mode = accumulator; + accumulator = callback; + callback = no_trailing; + no_trailing = undefined; + } + + // accumulator defaults to no-op + if (typeof accumulator !== 'function') { + debounce_mode = accumulator; + accumulator = function () {}; + } + + // The `wrapper` function encapsulates all of the throttling / debouncing + // functionality and when executed will limit the rate at which `callback` + // is executed. + function wrapper() { + var that = this, + elapsed = +new Date() - last_exec, + args = arguments; + + // Execute `callback` and update the `last_exec` timestamp. + function exec() { + last_exec = +new Date(); + callback.apply(that, args); + } + + // If `debounce_mode` is true (at_begin) this is used to clear the flag + // to allow future `callback` executions. + function clear() { + timeout_id = undefined; + } + + // always call the accumulator first + accumulator.apply(that, args); + + if (debounce_mode && !timeout_id) { + // Since `wrapper` is being called for the first time and + // `debounce_mode` is true (at_begin), execute `callback`. + exec(); + } + + // Clear any existing timeout. + void ( + timeout_id && clearTimeout(timeout_id) + ); + + if (debounce_mode === undefined && elapsed > delay) { + // In throttle mode, if `delay` time has been exceeded, execute + // `callback`. + exec(); + + } else if (no_trailing !== true) { + // In trailing throttle mode, since `delay` time has not been + // exceeded, schedule `callback` to execute `delay` ms after most + // recent execution. + // + // If `debounce_mode` is true (at_begin), schedule `clear` to execute + // after `delay` ms. + // + // If `debounce_mode` is false (at end), schedule `callback` to + // execute after `delay` ms. + timeout_id = setTimeout( + debounce_mode ? + clear : + exec, + debounce_mode === undefined ? + delay - elapsed : + delay + ); + } + } + + // Return the wrapper function. + return wrapper; + }; + + /** + * Debounce execution of a function. Debouncing, unlike throttling, + * guarantees that a function is only executed a single time, either at the + * very beginning of a series of calls, or at the very end. If you want to + * simply rate-limit execution of a function, see the + * method. + * + * In this visualization, | is a debounced-function call and X is the actual + * callback execution: + * + * :: + * + * Debounced with `at_begin` specified as false or unspecified: + * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + * X X + * + * Debounced with `at_begin` specified as true: + * ||||||||||||||||||||||||| (pause) ||||||||||||||||||||||||| + * X X + * + * + * @param {number} delay A zero-or-greater delay in milliseconds. For event + * callbacks, values around 100 or 250 (or even higher) are most useful. + * @param {boolean} [at_begin=false] If at_begin is false or + * unspecified, callback will only be executed `delay` milliseconds after + * the last debounced-function call. If at_begin is true, callback will be + * executed only at the first debounced-function call. (After the + * throttled-function has not been called for `delay` milliseconds, the + * internal counter is reset) + * @param {function} callback A function to be executed after delay milliseconds. + * The `this` context and all arguments are passed through, as-is, to + * `callback` when the debounced-function is executed. + * @param {function} [accumulator] A function to be executed (synchronously) + * during **each** call to the wrapped function. Typically, this + * this method is used to accumulate values that the callback uses + * when it finally executes. + * + * @returns {function} A new, debounced, function. + * + * @example + * var debounced = geo.util.debounce( delay, [ at_begin, ] callback ); + * $('selector').bind( 'someevent', debounced ); + * $('selector').unbind( 'someevent', debounced ); + * + */ + + var debounce = function (delay, at_begin, callback, accumulator) { + if (typeof at_begin !== 'boolean') { + accumulator = callback; + callback = at_begin; + at_begin = false; + } + accumulator = accumulator || function () {}; + return throttle(delay, false, callback, accumulator, !!at_begin); + }; + + module.exports = { + throttle: throttle, + debounce: debounce + }; + })(this); + + +/***/ }, +/* 123 */ +/***/ function(module, exports, __webpack_require__) { + + /* + markercluster plugin: + + Copyright 2012 David Leaver + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + Leaflet utilities: + + Copyright (c) 2010-2015, Vladimir Agafonkin + Copyright (c) 2010-2011, CloudMade + All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are + permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other + materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR + TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + /** + * @file + * Code taken from https://github.com/Leaflet/Leaflet.markercluster + * to support faster hierarchical clustering of features. + * @copyright 2012, David Leaver + */ + + (function () { + "use strict"; + + var $ = __webpack_require__(1); + var L = {}; + L.Util = { + // return unique ID of an object + stamp: function (obj) { + obj._leaflet_id = obj._leaflet_id || ++L.Util.lastId; + return obj._leaflet_id; + }, + lastId: 0 + }; + + var DistanceGrid = function (cellSize) { + this._cellSize = cellSize; + this._sqCellSize = cellSize * cellSize; + this._grid = {}; + this._objectPoint = {}; + }; + + 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); + }, + + //Returns true if the object was found + removeObject: 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] || [], + i, len; + + delete this._objectPoint[L.Util.stamp(obj)]; + + for (i = 0, len = cell.length; i < len; i++) { + if (cell[i] === obj) { + + cell.splice(i, 1); + + if (len === 1) { + delete row[x]; + } + + return true; + } + } + + }, + + 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) { + cell = row[j]; + + for (k = 0, len = cell.length; k < len; k++) { + removed = fn.call(context, cell[k]); + if (removed) { + k--; + len--; + } + } + } + } + }, + + getNearObject: function (point) { + var x = this._getCoord(point.x), + y = this._getCoord(point.y), + i, j, k, row, cell, len, obj, dist, + objectPoint = this._objectPoint, + closestDistSq = this._sqCellSize, + closest = null; + + for (i = y - 1; i <= y + 1; i++) { + row = this._grid[i]; + if (row) { + + for (j = x - 1; j <= x + 1; j++) { + cell = row[j]; + if (cell) { + + for (k = 0, len = cell.length; k < len; k++) { + obj = cell[k]; + dist = this._sqDist( + objectPoint[L.Util.stamp(obj)], + point + ); + if (dist < closestDistSq) { + closestDistSq = dist; + closest = obj; + } + } + } + } + } + } + return closest; + }, + + /* return the point coordinates contained in the structure */ + 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; + } + }; + + module.exports = DistanceGrid; + })(); + + +/***/ }, +/* 124 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * @file + * Using methods adapted from leaflet to cluster an array of positions + * hierarchically given an array of length scales (zoom levels). + */ + + (function () { + 'use strict'; + + var $ = __webpack_require__(1); + var vgl = __webpack_require__(6); + + /** + * This class manages a group of nearby points that are clustered as a + * single object for display purposes. The class constructor is private + * and only meant to be created by the ClusterGroup object. + * + * This is a tree-like data structure. Each node in the tree is a + * cluster containing child clusters and unclustered points. + * + * @class + * @private + * + * @param {geo.util.ClusterGroup} group The source cluster group + * @param {number} zoom The zoom level of the current node + * @param {object[]} children An array of ClusterTrees or point objects + */ + function ClusterTree(group, zoom, children) { + this._group = group; + this._zoom = zoom; + this._points = []; // Unclustered points + this._clusters = []; // Child clusters + this._count = 0; // Total number of points + this._parent = null; + this._coord = null; // The cached coordinates + var that = this; + + // add the children provided in the constructor call + (children || []).forEach(function (c) { + that._add(c); + }); + } + + /** + * Add a point or cluster as a child to the current cluster. + * @param {object} pt A ClusterTree or point object + * @private + */ + ClusterTree.prototype._add = function (pt) { + var inc = 1; + + if (pt instanceof ClusterTree) { + // add a child cluster + this._clusters.push(pt); + inc = pt._count; + } else { + this._points.push(pt); + } + pt._parent = this; + + // increment the counter + this._increment(inc); + }; + + /** + * Increment the child counter for this and the parent. + * @param {number} inc The value to increment by + * @private + */ + ClusterTree.prototype._increment = function (inc) { + this._coord = null; + this._count += inc; + if (this._parent) { + this._parent._increment(inc); + } + }; + + /** + * Return the total number of child points contained in the cluster. + * @returns {number} Total points contained + */ + ClusterTree.prototype.count = function () { + return this._count; + }; + + /** + * Recursively call a function on all points contained in the cluster. + * Calls the function with `this` as the current ClusterTree object, and + * arguments to arguments the point object and the zoom level: + * func.call(this, point, zoom) + */ + ClusterTree.prototype.each = function (func) { + var i; + for (i = 0; i < this._points.length; i += 1) { + func.call(this, this._points[i], this._zoom); + } + for (i = 0; i < this._clusters.length; i += 1) { + this._clusters[i].each.call( + this._clusters[i], + func + ); + } + }; + + /** + * Get the coordinates of the cluster (the mean position of all the points + * contained). This is lazily calculated and cached. + */ + ClusterTree.prototype.coords = function () { + var i, center = {x: 0, y: 0}; + if (this._coord) { + return this._coord; + } + // first add up the points at the node + for (i = 0; i < this._points.length; i += 1) { + center.x += this._points[i].x; + center.y += this._points[i].y; + } + + // add up the contribution from the clusters + for (i = 0; i < this._clusters.length; i += 1) { + center.x += this._clusters[i].coords().x * this._clusters[i].count(); + center.y += this._clusters[i].coords().y * this._clusters[i].count(); + } + + return { + x: center.x / this.count(), + y: center.y / this.count() + }; + }; + + /** + * This class manages clustering of an array of positions hierarchically. + * The algorithm and code was adapted from the Leaflet marker cluster + * plugin by David Leaver: https://github.com/Leaflet/Leaflet.markercluster + * + * @class geo.util.ClusterGroup + * @param {object} opts An options object + * @param {number} width The width of the window; used for scaling. + * @param {number} height The height of the window; used for scaling. + * @param {number} maxZoom The maximimum zoom level to calculate + * @param {number} radius Proportional to the clustering radius in pixels + */ + function C(opts, width, height) { + + var DistanceGrid = __webpack_require__(123); + + // store the options + this._opts = $.extend({ + maxZoom: 18, + radius: 0.05 + }, opts); + this._opts.width = this._opts.width || width || 256; + this._opts.height = this._opts.height || height || 256; + + // generate the initial datastructures + this._clusters = {}; // clusters at each zoom level + this._points = {}; // unclustered points at each zoom level + + 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 DistanceGrid(scl); + this._points[zoom] = new DistanceGrid(scl); + } + this._topClusterLevel = new ClusterTree(this, -1); + } + + /** + * Returns a characteristic distance scale at a particular zoom level. This + * scale is used to control the clustering radius. When the renderer supports + * it, this call should be replaced by a calculation involving the view port + * size in point coordinates at a particular zoom level. + * @private + */ + C.prototype._scaleAtLevel = function (zoom, width, height) { + return vgl.zoomToHeight(zoom, width, height) / 2 * this._opts.radius; + }; + + /** + * Add a position to the cluster group. + * @protected + */ + C.prototype.addPoint = function (point) { + var zoom, closest, parent, newCluster, lastParent, z; + + // start at the maximum zoom level and search for nearby + // + // 1. existing clusters + // 2. unclustered points + // + // otherwise add the point as a new unclustered point + + for (zoom = this._opts.maxZoom; zoom >= 0; zoom -= 1) { + + // find near cluster + closest = this._clusters[zoom].getNearObject(point); + if (closest) { + // add the point to the cluster and return + closest._add(point); + return; + } + + // find near point + closest = this._points[zoom].getNearObject(point); + if (closest) { + parent = closest._parent; + if (parent) { + // remove the point from the 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; + } + } + } + + if (!parent) { + $.noop(); + } + // create a new cluster with these two points + newCluster = new ClusterTree(this, zoom, [closest, point]); + this._clusters[zoom].addObject(newCluster, newCluster.coords()); + + // create intermediate parent clusters that don't exist + lastParent = newCluster; + for (z = zoom - 1; z > parent._zoom; z -= 1) { + lastParent = new ClusterTree(this, z, [lastParent]); + this._clusters[z].addObject(lastParent, lastParent.coords()); + } + parent._add(lastParent); + + // remove closest from this zoom level and any above (replace with newCluster) + for (z = zoom; z >= 0; z -= 1) { + if (!this._points[z].removeObject(closest, closest)) { + break; + } + } + + return; + } + + // add an unclustered point + this._points[zoom].addObject(point, point); + } + + // otherwise add to the top + this._topClusterLevel._add(point); + }; + + /** + * Return the unclustered points contained at a given zoom level. + * @param {number} zoom The zoom level + * @return {object[]} The array of unclustered points + */ + C.prototype.points = function (zoom) { + zoom = Math.min(Math.max(Math.floor(zoom), 0), this._opts.maxZoom - 1); + return this._points[Math.floor(zoom)].contents(); + }; + + /** + * Return the clusters contained at a given zoom level. + * @param {number} zoom The zoom level + * @return {ClusterTree[]} The array of clusters + */ + C.prototype.clusters = function (zoom) { + zoom = Math.min(Math.max(Math.floor(zoom), 0), this._opts.maxZoom - 1); + return this._clusters[Math.floor(zoom)].contents(); + }; + + module.exports = C; + })(); + + +/***/ }, +/* 125 */ +/***/ function(module, exports) { + + ////////////////////////////////////////////////////////////////////////////// + /** + * Common object containing all event types that are provided by the GeoJS + * API. Each property contained here is a valid target for event handling + * via {@link geo.object#geoOn}. The event object provided to handlers is + * different for each event type. Each handler will generally be called + * with a the this context being the class that caused the event.
+ *
+ * The following properties are common to all event objects: + * + * @namespace geo.event + * @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) { + * // event is an object with type: {@link geo.event.layerAdd} + * }); + * + */ + ////////////////////////////////////////////////////////////////////////////// + var geo_event = {}; + + ////////////////////////////////////////////////////////////////////////////// + /* + * Event types + */ + ////////////////////////////////////////////////////////////////////////////// + + // 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.layerSelect = 'geo_layerSelect'; + // geo.event.layerUnselect = 'geo_layerUnselect'; + // geo.event.query = 'geo_query'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered when a layer is added to the map. + * + * @property {geo.map} target The current map + * @property {geo.layer} layer The new layer + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.layerAdd = 'geo_layerAdd'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered when a layer is removed from the map. + * + * @property {geo.map} target The current map + * @property {geo.layer} layer The old layer + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.layerRemove = 'geo_layerRemove'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered when the map's zoom level is changed. Note that zoom is never + * triggered on the map itself. Instead it is triggered individually on + * layers, starting with the base layer. + * + * @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 current map center (pointing + * downward so that positive angles are clockwise rotations). + * + * @property {number} angle The angle of the rotation in radians + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.rotate = 'geo_rotate'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered when the map is panned either by user interaction or map + * transition. + * + * @property {object} screenDelta The number of pixels to pan the map by + * @property {object} center The new map center + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.pan = 'geo_pan'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered when the map's canvas is resized. + * + * @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.worldChanged = 'geo_worldChanged'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered on every call to {@link geo.map#draw} before the map is rendered. + * + * @property {geo.map} target The current map + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.draw = 'geo_draw'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered on every call to {@link geo.map#draw} after the map is rendered. + * + * @property {geo.map} target The current map + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.drawEnd = 'geo_drawEnd'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered on every 'mousemove' during a brushing selection. + * The event object extends {@link geo.brushSelection}. + * @mixes geo.brushSelection + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.brush = 'geo_brush'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered after a brush selection ends. + * The event object extends {@link geo.brushSelection}. + * @mixes geo.brushSelection + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.brushend = 'geo_brushend'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered when a brush selection starts. + * The event object extends {@link geo.brushSelection}. + * @mixes geo.brushSelection + */ + ////////////////////////////////////////////////////////////////////////////// + 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 if a map navigation animation is cancelled. + * + * @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.transitioncancel = 'geo_transitioncancel'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 + */ + //////////////////////////////////////////////////////////////////////////// + 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 + */ + //////////////////////////////////////////////////////////////////////////// + geo_event.camera = {}; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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.camera} camera The camera instance + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.camera.view = 'geo_camera_view'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered after a pan in the x/y plane (no zoom level change). + * + * @property {geo.camera} camera The camera instance + * @property {object} delta The translation delta in world coordinates. + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.camera.pan = 'geo_camera_pan'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered after a view matrix change that is not a simple pan. This + * includes, but is not limited to, pure zooms. + * + * @property {geo.camera} camera The camera instance + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.camera.zoom = 'geo_camera_zoom'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered after a projection change. + * + * @property {geo.camera} camera The camera instance + * @property {string} type The projection type ('perspective'|'parallel') + */ + ////////////////////////////////////////////////////////////////////////////// + geo_event.camera.projection = 'geo_camera_projection'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Triggered after a viewport change. + * + * @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.camera.viewport = 'geo_camera_viewport'; + + module.exports = geo_event; + + +/***/ }, +/* 126 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class choroplethFeature + * + * @class geo.choroplethFeature + * @extends geo.feature + * @returns {geo.choroplethFeature} + * + */ + ////////////////////////////////////////////////////////////////////////////// + var choroplethFeature = function (arg) { + 'use strict'; + if (!(this instanceof choroplethFeature)) { + return new choroplethFeature(arg); + } + arg = arg || {}; + feature.call(this, arg); + + var $ = __webpack_require__(1); + var ensureFunction = __webpack_require__(120).ensureFunction; + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var d3 = __webpack_require__(132), + 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 (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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set choropleth accessor + * + * @returns {geo.feature.choropleth} + */ + //////////////////////////////////////////////////////////////////////////// + this.choropleth = function (arg1, arg2) { + var choropleth; + + if (arg1 === undefined) { + return m_choropleth; + } + if (typeof arg1 === 'string' && arg2 === undefined) { + return m_choropleth[arg1]; + } + 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.modified(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.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); + } + } + return all; + } + return ensureFunction(m_choropleth[key]); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * A method that adds polygons from a given feature to the current layer. + * + * @param {} geoJsonFeature + * @param geo.color + * @return [{geo.feature}] + */ + //////////////////////////////////////////////////////////////////////////// + this._featureToPolygons = function (feature, fillValue) { + return m_this + ._addPolygonFeature(feature, fillValue); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * A method that sets a choropleth scale's domain and range. + * + * @param {undefined | function({})} valueAccessor + * @return {geo.feature.choropleth} + */ + //////////////////////////////////////////////////////////////////////////// + this._generateScale = function (valueAccessor) { + var extent = + d3.extent(m_this.scalar(), valueAccessor || undefined); + + m_this.choropleth() + .scale + .domain(extent) + .range(m_this.choropleth().colorRange); + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /**sr + * Generate scale for choropleth.data(), make polygons from features. + * @returns: [ [geo.feature.polygon, ...] , ... ] + */ + //////////////////////////////////////////////////////////////////////////// + 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); + }); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + + if (m_choropleth) { + m_this.dataTime().modified(); + } + }; + + this._init(arg); + return this; + }; + + inherit(choroplethFeature, feature); + module.exports = choroplethFeature; + + +/***/ }, +/* 127 */ +/***/ function(module, exports, __webpack_require__) { + + var $ = __webpack_require__(1); + var inherit = __webpack_require__(4); + var sceneObject = __webpack_require__(128); + var timestamp = __webpack_require__(129); + var geo_event = __webpack_require__(125); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class feature + * + * @class geo.feature + * @extends geo.sceneObject + * @returns {geo.feature} + */ + ////////////////////////////////////////////////////////////////////////////// + var feature = function (arg) { + 'use strict'; + if (!(this instanceof feature)) { + return new feature(arg); + } + sceneObject.call(this); + + var util = __webpack_require__(120); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + 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, + 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 = timestamp(), + m_buildTime = timestamp(), + m_updateTime = timestamp(), + m_selectedFeatures = []; + + //////////////////////////////////////////////////////////////////////////// + /** + * Private method to bind mouse handlers on the map element. + */ + //////////////////////////////////////////////////////////////////////////// + this._bindMouseHandlers = function () { + + // Don't bind handlers for improved performance on features that don't + // require it. + if (!m_selectionAPI) { + return; + } + + // 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Private method to unbind mouse handlers on the map element. + */ + //////////////////////////////////////////////////////////////////////////// + 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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. + * }); + */ + //////////////////////////////////////////////////////////////////////////// + + //////////////////////////////////////////////////////////////////////////// + /** + * 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} + */ + //////////////////////////////////////////////////////////////////////////// + this.pointSearch = function () { + // base class method does nothing + return { + index: [], + found: [] + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Private mousemove handler + */ + //////////////////////////////////////////////////////////////////////////// + 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; + + // Get the index of the element that was previously on top + if (m_selectedFeatures.length) { + lastTop = m_selectedFeatures[m_selectedFeatures.length - 1]; + } + + // 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; + }); + + 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: feature.eventID, + top: idx === newFeatures.length - 1 + }, true); + }); + + 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: feature.eventID, + top: idx === oldFeatures.length - 1 + }, true); + }); + + 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: feature.eventID, + top: idx === over.index.length - 1 + }, true); + }); + + // Replace the selected features array + m_selectedFeatures = over.index; + + // Get the index of the element that is now on top + if (m_selectedFeatures.length) { + top = m_selectedFeatures[m_selectedFeatures.length - 1]; + } + + 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 (top !== -1) { + m_this.geoTrigger(geo_event.feature.mouseon, { + data: data[top], + index: top, + mouse: mouse + }, true); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Private mouseclick handler + */ + //////////////////////////////////////////////////////////////////////////// + this._handleMouseclick = function () { + var mouse = m_this.layer().map().interactor().mouse(), + data = m_this.data(), + over = m_this.pointSearch(mouse.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: feature.eventID, + top: idx === over.index.length - 1 + }, true); + }); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Private brush handler. + */ + //////////////////////////////////////////////////////////////////////////// + this._handleBrush = function (brush) { + var idx = m_this.boxSearch(brush.gcs.lowerLeft, brush.gcs.upperRight), + data = m_this.data(); + + 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: feature.eventID, + top: idx === idx.length - 1 + }, true); + }); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Private brushend handler. + */ + //////////////////////////////////////////////////////////////////////////// + this._handleBrushend = function (brush) { + var idx = m_this.boxSearch(brush.gcs.lowerLeft, brush.gcs.upperRight), + data = m_this.data(); + + 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: feature.eventID, + top: idx === idx.length - 1 + }, true); + }); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set style used by the feature + */ + //////////////////////////////////////////////////////////////////////////// + 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(); + 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; + } + if (key.toLowerCase().match(/color$/)) { + if (util.isFunction(m_style[key])) { + tmp = util.ensureFunction(m_style[key]); + out = function () { + return util.convertColor( + tmp.apply(this, arguments) + ); + }; + } else { + // if the color is not a function, only convert it once + out = util.ensureFunction(util.convertColor(m_style[key])); + } + } else { + out = util.ensureFunction(m_style[key]); + } + return out; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get layer referenced by the feature + */ + //////////////////////////////////////////////////////////////////////////// + this.layer = function () { + return m_layer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get renderer used by the feature + */ + //////////////////////////////////////////////////////////////////////////// + this.renderer = function () { + return m_renderer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set projection of the feature + */ + //////////////////////////////////////////////////////////////////////////// + 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; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Convert from the renderer's input gcs coordinates to display coordinates. + * + * @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.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); + } + return c; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set visibility of the feature + */ + //////////////////////////////////////////////////////////////////////////// + this.visible = function (val) { + if (val === undefined) { + return m_visible; + } else { + m_visible = val; + m_this.modified(); + + // bind or unbind mouse handlers on visibility change + if (m_visible) { + m_this._bindMouseHandlers(); + } else { + m_this._unbindMouseHandlers(); + } + + return m_this; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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(); + return m_this; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set timestamp of last time update happened + */ + //////////////////////////////////////////////////////////////////////////// + this.updateTime = function (val) { + if (val === undefined) { + return m_updateTime; + } else { + m_updateTime = val; + m_this.modified(); + return m_this; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set the data array for the feature. + * + * @returns {Array|this} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Query if the selection API is enabled for this feature. + * @returns {bool} + */ + //////////////////////////////////////////////////////////////////////////// + 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(); + }; + + this._init(arg); + return this; + }; + + /** + * The most recent feature event triggered. + * @type {number} + */ + 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. + */ + + /** + * 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} + */ + feature.create = function (layer, spec) { + 'use strict'; + + var type = spec.type; + + // Check arguments + if (!(layer instanceof __webpack_require__(130))) { + 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(feature, sceneObject); + module.exports = feature; + + +/***/ }, +/* 128 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var object = __webpack_require__(5); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sceneObject, which extends the object's + * event handling with a tree-based event propagation. + * + * @class geo.sceneObject + * @extends geo.object + * @returns {geo.sceneObject} + */ + ////////////////////////////////////////////////////////////////////////////// + var sceneObject = function (arg) { + 'use strict'; + if (!(this instanceof sceneObject)) { + return new sceneObject(); + } + 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; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Override object.addPromise to propagate up the scene tree. + */ + ////////////////////////////////////////////////////////////////////////////// + this.addPromise = function (promise) { + if (m_parent) { + m_parent.addPromise(promise); + } else { + s_addPromise(promise); + } + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Override object.onIdle to propagate up the scene tree. + */ + ////////////////////////////////////////////////////////////////////////////// + this.onIdle = function (handler) { + if (m_parent) { + m_parent.onIdle(handler); + } else { + s_onIdle(handler); + } + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get/set parent of the object + * @param {?geo.sceneObject} parent + */ + ////////////////////////////////////////////////////////////////////////////// + this.parent = function (arg) { + if (arg === undefined) { + return m_parent; + } + m_parent = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Add a child (or an array of children) to the object + */ + ////////////////////////////////////////////////////////////////////////////// + this.addChild = function (child) { + if (Array.isArray(child)) { + child.forEach(m_this.addChild); + return m_this; + } + child.parent(m_this); + m_children.push(child); + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Remove a child (or array of children) from the object + */ + ////////////////////////////////////////////////////////////////////////////// + this.removeChild = function (child) { + if (Array.isArray(child)) { + child.forEach(m_this.removeChild); + return m_this; + } + m_children = m_children.filter(function (c) { return c !== child; }); + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get an array of child objects + */ + ////////////////////////////////////////////////////////////////////////////// + this.children = function () { + return m_children.slice(); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Force redraw of a scene object, to be implemented by subclasses. + * Base class just calls draw of child objects. + */ + ////////////////////////////////////////////////////////////////////////////// + this.draw = function (arg) { + m_this.children().forEach(function (child) { + child.draw(arg); + }); + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Trigger an event (or events) on this object and call all handlers. + * @param {String} event the event to trigger + * @param {Object} args arbitrary argument to pass to the handler + * @param {Boolean} childrenOnly if true, only propagate down the tree + */ + ////////////////////////////////////////////////////////////////////////////// + this.geoTrigger = function (event, args, childrenOnly) { + + var geoArgs; + + args = args || {}; + geoArgs = args.geo || {}; + args.geo = geoArgs; + + // stop propagation if requested by the handler + if (geoArgs.stopPropagation) { + return m_this; + } + + // If the event was not triggered by the parent, just propagate up the tree + if (!childrenOnly && m_parent && geoArgs._triggeredBy !== m_parent) { + geoArgs._triggeredBy = m_this; + m_parent.geoTrigger(event, args); + return m_this; + } + + // call the object's own handlers + s_trigger.call(m_this, event, args); + + // stop propagation if requested by the handler + if (geoArgs.stopPropagation) { + return m_this; + } + + // trigger the event on the children + m_children.forEach(function (child) { + if (child.geoTrigger) { + geoArgs._triggeredBy = m_this; + child.geoTrigger(event, args); + } + }); + + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Free all resources and destroy the object. + */ + ////////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.children = []; + delete m_this.parent; + s_exit(); + }; + + return this; + }; + + inherit(sceneObject, object); + module.exports = sceneObject; + + +/***/ }, +/* 129 */ +/***/ function(module, exports, __webpack_require__) { + + var vgl = __webpack_require__(6); + var inherit = __webpack_require__(4); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class timestamp + * + * @class geo.timestamp + * @extends vgl.timestamp + * @returns {geo.timestamp} + */ + ////////////////////////////////////////////////////////////////////////////// + var timestamp = function () { + 'use strict'; + if (!(this instanceof timestamp)) { + return new timestamp(); + } + vgl.timestamp.call(this); + }; + + inherit(timestamp, vgl.timestamp); + module.exports = timestamp; + + +/***/ }, +/* 130 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var sceneObject = __webpack_require__(128); + var feature = __webpack_require__(127); + var checkRenderer = __webpack_require__(131).checkRenderer; + + ////////////////////////////////////////////////////////////////////////////// + /** + * @class geo.layer + * @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} + */ + ////////////////////////////////////////////////////////////////////////////// + var layer = function (arg) { + 'use strict'; + + if (!(this instanceof layer)) { + return new layer(arg); + } + arg = arg || {}; + sceneObject.call(this, arg); + + var $ = __webpack_require__(1); + var timestamp = __webpack_require__(129); + var createRenderer = __webpack_require__(131).createRenderer; + var newLayerId = __webpack_require__(120).newLayerId; + var geo_event = __webpack_require__(125); + var camera = __webpack_require__(3); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_exit = this._exit, + m_id = arg.id === undefined ? 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 = timestamp(), + m_updateTime = 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; + + m_rendererName = checkRenderer(m_rendererName); + + if (!m_map) { + throw new Error('Layers must be initialized on a map.'); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the name of the renderer. + * + * @returns {string} + */ + //////////////////////////////////////////////////////////////////////////// + this.rendererName = function () { + return m_rendererName; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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. + * + * @param {number} [zIndex] The new z-index + * @returns {number|this} + */ + //////////////////////////////////////////////////////////////////////////// + this.zIndex = function (zIndex) { + if (zIndex === undefined) { + return m_zIndex; + } + m_zIndex = zIndex; + m_node.css('z-index', m_zIndex); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bring the layer above the given number of layers. This will rotate the + * current z-indices for this and the next `n` layers. + * + * @param {number} [n=1] The number of positions to move + * @returns {this} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + } + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bring the layer below the given number of layers. This will rotate the + * current z-indices for this and the previous `n` layers. + * + * @param {number} [n=1] The number of positions to move + * @returns {this} + */ + //////////////////////////////////////////////////////////////////////////// + this.moveDown = function (n) { + if (n === undefined) { + n = 1; + } + return m_this.moveUp(-n); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bring the layer to the top of the map layers. + * + * @returns {this} + */ + //////////////////////////////////////////////////////////////////////////// + this.moveToTop = function () { + return m_this.moveUp(m_this.map().children().length - 1); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Bring the layer to the bottom of the map layers. + * + * @returns {this} + */ + //////////////////////////////////////////////////////////////////////////// + this.moveToBottom = function () { + return m_this.moveDown(m_this.map().children().length - 1); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get whether or not the layer is sticky (navigates with the map). + * + * @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. + * + * @returns {Boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.active = function () { + return m_active; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set root node of the layer + * + * @returns {div} + */ + //////////////////////////////////////////////////////////////////////////// + this.node = function () { + return m_node; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set id of the layer + * + * @returns {String} + */ + //////////////////////////////////////////////////////////////////////////// + this.id = function (val) { + if (val === undefined) { + return m_id; + } + m_id = newLayerId(); + m_this.modified(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set name of the layer + * + * @returns {String} + */ + //////////////////////////////////////////////////////////////////////////// + this.name = function (val) { + if (val === undefined) { + return m_name; + } + m_name = val; + m_this.modified(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set map of the layer + */ + //////////////////////////////////////////////////////////////////////////// + this.map = function () { + return m_map; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get renderer for the layer if any + */ + //////////////////////////////////////////////////////////////////////////// + this.renderer = function () { + return m_renderer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get canvas of the layer + * + */ + //////////////////////////////////////////////////////////////////////////// + this.canvas = function () { + return m_canvas; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return last time data got changed + */ + //////////////////////////////////////////////////////////////////////////// + this.dataTime = function () { + return m_dataTime; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the modified time for the last update that did something + */ + //////////////////////////////////////////////////////////////////////////// + this.updateTime = function () { + return m_updateTime; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set if the layer has been initialized + */ + //////////////////////////////////////////////////////////////////////////// + this.initialized = function (val) { + if (val !== undefined) { + m_initialized = val; + return m_this; + } + return m_initialized; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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) { + camera.applyTransform(m_this._toLocalMatrix, input); + } + return input; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Transform coordinates from a local coordinate system to world coordinates. + */ + //////////////////////////////////////////////////////////////////////////// + this.fromLocal = function (input) { + if (m_this._fromLocalMatrix) { + camera.applyTransform(m_this._fromLocalMatrix, input); + } + return input; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the attribution html content that will displayed with the + * layer. By default, nothing will be displayed. Note, this content + * is **not** html escaped, so care should be taken when renderering + * user provided content. + * @param {string?} arg An html fragment + * @returns {string|this} Chainable as a setter + */ + //////////////////////////////////////////////////////////////////////////// + this.attribution = function (arg) { + if (arg !== undefined) { + m_attribution = arg; + m_this.map().updateAttribution(); + return m_this; + } + return m_attribution; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Init layer + * + * @param {boolean} noEvents if a subclass of this intends to bind the + * resize, pan, and zoom events itself, set this flag to true to avoid + * binding them here. + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (noEvents) { + if (m_initialized) { + return m_this; + } + + m_map.node().append(m_node); + + /* Pass along the arguments, but not the map reference */ + var options = $.extend({}, arg); + delete options.map; + + 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 = createRenderer(m_rendererName, m_this, m_canvas, + options); + } else { + m_renderer = createRenderer(m_rendererName, m_this, undefined, + options); + m_canvas = m_renderer.canvas(); + } + + if (!m_this.active()) { + m_node.css('pointerEvents', 'none'); + } + + m_initialized = true; + + if (!noEvents) { + /// 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.rotate, function (event) { + m_this._update({event: event}); + }); + + m_this.geoOn(geo_event.zoom, function (event) { + m_this._update({event: event}); + }); + } + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Clean up resouces + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.geoOff(); + if (m_renderer) { + m_renderer._exit(); + } + m_node.off(); + m_node.remove(); + arg = {}; + m_canvas = null; + m_renderer = null; + s_exit(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update layer + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the width of the layer in pixels. + * **DEPRECIATED: use map.size instead. + */ + //////////////////////////////////////////////////////////////////////////// + this.width = function () { + return m_this.map().size().width; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the height of the layer in pixels + * **DEPRECIATED: use map.size instead. + */ + //////////////////////////////////////////////////////////////////////////// + this.height = function () { + return m_this.map().size().height; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the current layer opacity. + */ + //////////////////////////////////////////////////////////////////////////// + this.opacity = function (opac) { + if (opac !== undefined) { + m_opacity = opac; + m_node.css('opacity', m_opacity); + return m_this; + } + return m_opacity; + }; + + 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; + }; + + /** + * Gets a new id number for a layer. + * @protected + * @instance + * @returns {number} + */ + layer.newLayerId = (function () { + 'use strict'; + var currentId = 1; + return function () { + var id = currentId; + currentId += 1; + return id; + }; + }()); + + /** + * General object specification for feature types. + * @typedef geo.layer.spec + * @type {object} + * @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 {geo.feature.spec[]} [features=[]] Features + * to add to the layer + */ + + /** + * Create a layer from an object. Any errors in the creation + * of the layer will result in returning null. + * @param {geo.map} map The map to add the layer to + * @param {geo.layer.spec} spec The object specification + * @returns {geo.layer|null} + */ + layer.create = function (map, spec) { + 'use strict'; + + spec = spec || {}; + + // add osmLayer later + spec.type = 'feature'; + if (spec.type !== 'feature') { + console.warn('Unsupported layer type'); + return null; + } + + spec.renderer = spec.renderer || 'vgl'; + spec.renderer = checkRenderer(spec.renderer); + + if (!spec.renderer) { + console.warn('Invalid renderer'); + return null; + } + + var layer = map.createLayer(spec.type, spec); + if (!layer) { + console.warn('Unable to create a layer'); + return null; + } + + // probably move this down to featureLayer eventually + spec.features.forEach(function (f) { + f.data = f.data || spec.data; + f.feature = feature.create(layer, f); + }); + + return layer; + }; + + inherit(layer, sceneObject); + module.exports = layer; + + +/***/ }, +/* 131 */ +/***/ function(module, exports, __webpack_require__) { + + var $ = __webpack_require__(1); + var widgets = { + dom: {} + }; + var layers = {}; + var renderers = {}; + var features = {}; + var fileReaders = {}; + var rendererLayerAdjustments = {}; + var util = {}; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Register a new file reader type + */ + ////////////////////////////////////////////////////////////////////////////// + util.registerFileReader = function (name, func) { + fileReaders[name] = func; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new file reader + */ + ////////////////////////////////////////////////////////////////////////////// + util.createFileReader = function (name, opts) { + if (fileReaders.hasOwnProperty(name)) { + return fileReaders[name](opts); + } + return null; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Register a new renderer type + */ + ////////////////////////////////////////////////////////////////////////////// + util.registerRenderer = function (name, func) { + renderers[name] = func; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create new instance of the renderer + */ + ////////////////////////////////////////////////////////////////////////////// + util.createRenderer = function (name, layer, canvas, options) { + if (renderers.hasOwnProperty(name)) { + var ren = renderers[name]( + {layer: layer, canvas: canvas, options: options} + ); + ren._init(); + return ren; + } + return null; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Check if the named renderer is supported. If not, display a warning and get + * the name of a fallback renderer. Ideally, we would pass a list of desired + * features, and, if the renderer is unavailable, this would choose a fallback + * that would support those features. + * + * @params {string|null} name name of the desired renderer + * @params {boolean} noFallack if true, don't recommend a fallback + * @return {string|null|false} the name of the renderer that should be used + * of false if no valid renderer can be determined. + */ + ////////////////////////////////////////////////////////////////////////////// + util.checkRenderer = function (name, noFallback) { + if (name === null) { + return name; + } + if (renderers.hasOwnProperty(name)) { + var ren = renderers[name]; + if (!ren.supported || ren.supported()) { + return name; + } + if (!ren.fallback || noFallback) { + return false; + } + var fallback = util.checkRenderer(ren.fallback(), true); + if (fallback !== false) { + console.warn(name + ' renderer is unavailable, using ' + fallback + + ' renderer instead'); + } + return fallback; + } + return false; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Register a new feature type + */ + ////////////////////////////////////////////////////////////////////////////// + util.registerFeature = function (category, name, func) { + if (features === undefined) { + features = {}; + } + + if (!(category in features)) { + features[category] = {}; + } + + // TODO Add warning if the name already exists + features[category][name] = func; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create new instance of the renderer + */ + ////////////////////////////////////////////////////////////////////////////// + util.createFeature = function (name, layer, renderer, arg) { + var category = renderer.api(), + options = {'layer': layer, 'renderer': renderer}; + if (category in features && name in features[category]) { + if (arg !== undefined) { + $.extend(true, options, arg); + } + var feature = features[category][name](options); + layer.gcs = function () { + return layer.map().gcs(); + }; + return feature; + } + return null; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Register a layer adjustment. + */ + ////////////////////////////////////////////////////////////////////////////// + util.registerLayerAdjustment = function (category, name, func) { + if (rendererLayerAdjustments === undefined) { + rendererLayerAdjustments = {}; + } + + if (!(category in rendererLayerAdjustments)) { + rendererLayerAdjustments[category] = {}; + } + + // TODO Add warning if the name already exists + 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. + */ + ////////////////////////////////////////////////////////////////////////////// + util.adjustLayerForRenderer = function (name, layer) { + var rendererName = layer.rendererName(); + if (rendererName) { + if (rendererLayerAdjustments && + rendererLayerAdjustments[rendererName] && + rendererLayerAdjustments[rendererName][name]) { + rendererLayerAdjustments[rendererName][name].apply(layer); + } + } + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Register a new layer type + */ + ////////////////////////////////////////////////////////////////////////////// + util.registerLayer = function (name, func) { + layers[name] = func; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create new instance of the layer + */ + ////////////////////////////////////////////////////////////////////////////// + util.createLayer = function (name, map, arg) { + /// Default renderer is vgl + var options = {'map': map, 'renderer': 'vgl'}, + layer = null; + + if (name in layers) { + if (arg !== undefined) { + $.extend(true, options, arg); + } + layer = layers[name](options); + layer._init(); + return layer; + } else { + return null; + } + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Register a new widget type + */ + ////////////////////////////////////////////////////////////////////////////// + util.registerWidget = function (category, name, func) { + if (!(category in widgets)) { + widgets[category] = {}; + } + + // TODO Add warning if the name already exists + widgets[category][name] = func; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create new instance of the widget + */ + ////////////////////////////////////////////////////////////////////////////// + util.createWidget = function (name, layer, arg) { + var options = { + layer: layer + }; + + if (name in widgets.dom) { + if (arg !== undefined) { + $.extend(true, options, arg); + } + + return widgets.dom[name](options); + } + + throw new Error('Cannot create unknown widget ' + name); + }; + + module.exports = util; + + +/***/ }, +/* 132 */ +/***/ function(module, exports) { + + module.exports = __WEBPACK_EXTERNAL_MODULE_132__; + +/***/ }, +/* 133 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var object = __webpack_require__(5); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Stores the current time for a map, triggers time keeping events, and + * handles the animation state and interaction. + * + * @class geo.clock + * @extends geo.object + * @returns {geo.clock} + */ + ////////////////////////////////////////////////////////////////////////////// + var clock = function (opts) { + 'use strict'; + + if (!(this instanceof clock)) { + return new clock(opts); + } + opts = opts || {}; + object.call(this, opts); + + var geo_event = __webpack_require__(125); + + ////////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + 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; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the geo.object to trigger events on. + */ + ////////////////////////////////////////////////////////////////////////////// + this.object = function (arg) { + if (arg === undefined) { + return m_object; + } + m_object = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Returns true if attached to a valid geo.object. + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + this._attached = function () { + return (m_object instanceof object); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the current time. + */ + ////////////////////////////////////////////////////////////////////////////// + this.now = function (arg) { + var previous = m_now; + if (arg === undefined) { + return m_now; + } + m_now = arg; + + if (m_now !== previous && + m_this._attached()) { + m_this.object().geoTrigger(geo_event.clock.change, { + previous: previous, + current: m_now, + clock: m_this + }); + } + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the animation start time. + */ + ////////////////////////////////////////////////////////////////////////////// + this.start = function (arg) { + if (arg === undefined) { + return m_start; + } + m_start = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the animation end time. + */ + ////////////////////////////////////////////////////////////////////////////// + this.end = function (arg) { + if (arg === undefined) { + return m_end; + } + m_end = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the animation time step. + */ + ////////////////////////////////////////////////////////////////////////////// + this.step = function (arg) { + if (arg === undefined) { + return m_step; + } + m_step = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set looping control of the clock. This controls how many times the + * animation will repeat before stopping. Default + * ``Number.POSITIVE_INFINITY``, the animation repeats forever. + */ + ////////////////////////////////////////////////////////////////////////////// + this.loop = function (arg) { + if (arg === undefined) { + return m_loop; + } + m_loop = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the animation state. Valid values are: + * + * * 'stop' + * * 'play' + * * 'pause' + * + * This will also trigger relevant events, but they may be fired + * asynchronously. + */ + ////////////////////////////////////////////////////////////////////////////// + this.state = function (arg, step) { + + if (arg === undefined) { + return m_state; + } + if (['stop', 'play', 'pause'].indexOf(arg) < 0) { + console.log('WARNING: Ignored invalid state: ' + arg); + return m_this; + } + + if (arg === 'play' && m_state === 'stop') { + // reset animation parameters + m_currentLoop = 0; + m_this.now(m_this.start()); + } + + if (arg === 'play' && m_state !== 'play') { + // Start a new animation. + m_state = arg; + m_this._animate(step || 1); + } + + m_state = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the animation frame rate. This is approximately the number + * of frames displayed per second. A null value will use the browser's + * native requestAnimationFrame to draw new frames. + */ + ////////////////////////////////////////////////////////////////////////////// + this.framerate = function (arg) { + if (arg === undefined) { + return m_rate; + } + m_rate = arg; + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Step to the next frame in the animation. Pauses the animation if it is + * playing. + */ + ////////////////////////////////////////////////////////////////////////////// + this.stepForward = function () { + m_this.state('pause'); + m_this._setNextFrame(1); + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Step to the previous frame in the animation. Pauses the animation if it is + * playing. + */ + ////////////////////////////////////////////////////////////////////////////// + this.stepBackward = function () { + m_this.state('pause'); + m_this._setNextFrame(-1); + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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(clock, object); + module.exports = clock; + + +/***/ }, +/* 134 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class contourFeature + * + * @class geo.contourFeature + * @extends geo.feature + * @returns {geo.contourFeature} + * + */ + ////////////////////////////////////////////////////////////////////////////// + var contourFeature = function (arg) { + 'use strict'; + if (!(this instanceof contourFeature)) { + return new contourFeature(arg); + } + + var $ = __webpack_require__(1); + var util = __webpack_require__(120); + + arg = arg || {}; + feature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + 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; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Override the parent data method to keep track of changes to the + * internal coordinates. + */ + //////////////////////////////////////////////////////////////////////////// + this.data = function (arg) { + var ret = s_data(arg); + return ret; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set contour accessor + * + * @returns {geo.pointFeature} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.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 util.ensureFunction(m_contour[key]); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set position accessor + * + * @returns {geo.pointFeature} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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}, + util.convertColor(contour.get('minColor')())); + result.maxColor = $.extend({a: contour.get('maxOpacity')() || 0}, + util.convertColor(contour.get('maxColor')())); + contour.get('colorRange')().forEach(function (clr, idx) { + result.colorMap.push($.extend( + {a: opacityRange && opacityRange[idx] !== undefined ? + opacityRange[idx] : 1}, 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, 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; + } + }, + arg.style === undefined ? {} : arg.style + ); + + m_this.style(defaultStyle); + + if (m_contour) { + m_this.dataTime().modified(); + } + }; + + this._init(arg); + return this; + }; + + inherit(contourFeature, feature); + module.exports = contourFeature; + + /* 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. + */ + + +/***/ }, +/* 135 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var renderer = __webpack_require__(136); + var registerRenderer = __webpack_require__(131).registerRenderer; + + /** + * @class geo.domRenderer + * @extends geo.renderer + */ + var domRenderer = function (arg) { + 'use strict'; + + if (!(this instanceof domRenderer)) { + return new domRenderer(arg); + } + renderer.call(this, arg); + + arg = arg || {}; + + var m_this = this; + + this.api = function () { + return 'dom'; + }; + + 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]); + } + }; + + this._init(arg); + return this; + }; + + inherit(domRenderer, renderer); + registerRenderer('dom', domRenderer); + module.exports = domRenderer; + + +/***/ }, +/* 136 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var object = __webpack_require__(5); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class renderer + * + * @class geo.renderer + * @extends geo.object + * @returns {geo.renderer} + */ + ////////////////////////////////////////////////////////////////////////////// + var renderer = function (arg) { + 'use strict'; + + if (!(this instanceof renderer)) { + return new renderer(arg); + } + object.call(this); + + arg = arg || {}; + var m_this = this, + m_layer = arg.layer === undefined ? null : arg.layer, + m_canvas = arg.canvas === undefined ? null : arg.canvas, + m_initialized = false; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get layer of the renderer + * + * @returns {*} + */ + //////////////////////////////////////////////////////////////////////////// + this.layer = function () { + return m_layer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get canvas for the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.canvas = function (val) { + if (val === undefined) { + return m_canvas; + } else { + m_canvas = val; + m_this.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get map that this renderer belongs to + */ + //////////////////////////////////////////////////////////////////////////// + this.map = function () { + if (m_layer) { + return m_layer.map(); + } else { + return null; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get base layer that belongs to this renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.baseLayer = function () { + if (m_this.map()) { + return m_this.map().baseLayer(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set if renderer has been initialized + */ + //////////////////////////////////////////////////////////////////////////// + this.initialized = function (val) { + if (val === undefined) { + return m_initialized; + } else { + m_initialized = val; + return m_this; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle resize event + */ + //////////////////////////////////////////////////////////////////////////// + this._resize = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render + */ + //////////////////////////////////////////////////////////////////////////// + this._render = function () { + }; + + return this; + }; + + inherit(renderer, object); + module.exports = renderer; + + +/***/ }, +/* 137 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var layer = __webpack_require__(130); + var geo_event = __webpack_require__(125); + var registry = __webpack_require__(131); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Layer to draw points, lines, and polygons on the map The polydata layer + * provide mechanisms to create and draw geometrical shapes such as points, + * lines, and polygons. + * @class geo.featureLayer + * @extends geo.layer + * @returns {geo.featureLayer} + */ + ////////////////////////////////////////////////////////////////////////////// + var featureLayer = function (arg) { + 'use strict'; + if (!(this instanceof featureLayer)) { + return new featureLayer(arg); + } + layer.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + m_features = [], + s_init = this._init, + s_exit = this._exit, + s_update = this._update, + s_draw = this.draw; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create feature give a name + * + * @returns {geo.Feature} Will return a new feature + */ + //////////////////////////////////////////////////////////////////////////// + this.createFeature = function (featureName, arg) { + + var newFeature = registry.createFeature( + featureName, m_this, m_this.renderer(), arg); + + m_this.addChild(newFeature); + m_features.push(newFeature); + m_this.features(m_features); + m_this.modified(); + return newFeature; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Delete feature + * + */ + //////////////////////////////////////////////////////////////////////////// + this.deleteFeature = function (feature) { + var i; + + for (i = 0; i < m_features.length; i += 1) { + if (m_features[i] === feature) { + m_features[i]._exit(); + m_this.dataTime().modified(); + m_this.modified(); + m_features.splice(i, 1); + } + } + m_this.removeChild(feature); + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set drawables + * + * @returns {Array} + */ + //////////////////////////////////////////////////////////////////////////// + this.features = function (val) { + if (val === undefined) { + return m_features; + } else { + m_features = val.slice(0); + m_this.dataTime().modified(); + m_this.modified(); + return m_this; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + if (m_this.initialized()) { + return m_this; + } + + /// Call super class init + s_init.call(m_this, true); + + /// Bind events to handlers + m_this.geoOn(geo_event.resize, function (event) { + if (m_this.renderer()) { + m_this.renderer()._resize(event.x, event.y, event.width, event.height); + m_this._update({event: event}); + m_this.renderer()._render(); + } else { + m_this._update({event: event}); + } + }); + + m_this.geoOn(geo_event.pan, function (event) { + m_this._update({event: event}); + if (m_this.renderer()) { + m_this.renderer()._render(); + } + }); + + m_this.geoOn(geo_event.rotate, function (event) { + m_this._update({event: event}); + if (m_this.renderer()) { + m_this.renderer()._render(); + } + }); + + m_this.geoOn(geo_event.zoom, function (event) { + m_this._update({event: event}); + if (m_this.renderer()) { + m_this.renderer()._render(); + } + }); + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update layer + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function (request) { + var i; + + if (!m_features.length) { + return m_this; + } + + /// Call base class update + s_update.call(m_this, request); + + if (m_features && m_features.length === 0) { + console.log('[info] No valid data source found.'); + return; + } + + if (m_this.dataTime().getMTime() > m_this.updateTime().getMTime()) { + for (i = 0; i < m_features.length; i += 1) { + m_features[i].renderer(m_this.renderer()); + } + } + + for (i = 0; i < m_features.length; i += 1) { + m_features[i]._update(); + } + + m_this.updateTime().modified(); + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Free all resources + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.clear(); + s_exit(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Draw + */ + //////////////////////////////////////////////////////////////////////////// + this.draw = function () { + // Call sceneObject.draw, which calls draw on all child objects. + s_draw(); + + // 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. + if (m_this.renderer()) { + m_this.renderer()._render(); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Clear all features in layer + */ + //////////////////////////////////////////////////////////////////////////// + this.clear = function () { + var i; + + if (!m_features.length) { + return m_this; + } + + for (i = 0; i < m_features.length; i += 1) { + m_features[i]._exit(); + m_this.removeChild(m_features[i]); + } + + m_this.dataTime().modified(); + m_this.modified(); + m_features = []; + + return m_this; + }; + + return m_this; + }; + + inherit(featureLayer, layer); + registry.registerLayer('feature', featureLayer); + module.exports = featureLayer; + + +/***/ }, +/* 138 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = (function () { + 'use strict'; + + var $ = __webpack_require__(1); + + ////////////////////////////////////////////////////////////////////////////// + /** + * This class implements a queue for Deferred objects. Whenever one of the + * objects in the queue completes (resolved or rejected), another item in the + * queue is processed. The number of concurrently processing items can be + * adjusted. At this time (2015-12-29) most major browsers support 6 + * concurrent requests from any given server, so, when using the queue for + * tile images, thie number of concurrent requests should be 6 * (number of + * subdomains serving tiles). + * + * @class geo.fetchQueue + * + * @param {Object?} [options] A configuration object for the queue + * @param {Number} [options.size=6] The maximum number of concurrent deferred + * objects. + * @param {Number} [options.track=600] The number of objects that are tracked + * that trigger checking if any of them have been abandoned. The fetch + * queue can grow to the greater of this size and the number of items that + * are still needed. Setting this to a low number will increase + * processing time, to a high number can increase memory. Ideally, it + * should reflect the number of items that are kept in memory elsewhere. + * If needed is null, this is ignored. + * @param {function} [options.needed=null] If set, this function is passed a + * Deferred object and must return a truthy value if the object is still + * needed. + */ + ////////////////////////////////////////////////////////////////////////////// + var fetchQueue = function (options) { + if (!(this instanceof fetchQueue)) { + return new fetchQueue(options); + } + + options = options || {}; + this._size = options.size || 6; + this._track = options.track || 600; + this._needed = options.needed || null; + this._batch = false; + + var m_this = this, + m_next_batch = 1; + + /** + * Get/set the maximum concurrent deferred object size. + */ + Object.defineProperty(this, 'size', { + get: function () { return this._size; }, + set: function (n) { + this._size = n; + this.next_item(); + } + }); + + /** + * Get the current queue size. + */ + Object.defineProperty(this, 'length', { + get: function () { return this._queue.length; } + }); + + /** + * Get the current number of processing items. + */ + Object.defineProperty(this, 'processing', { + get: function () { return this._processing; } + }); + + /** + * Remove all items from the queue. + */ + this.clear = function () { + this._queue = []; + this._processing = 0; + return this; + }; + + /** + * Add a Deferred object to the queue. + * @param {Deferred} defer Deferred object to add to the queue. + * @param {function} callback a function to call when the item's turn is + * granted. + * @param {boolean} atEnd if false, add the item to the front of the queue + * if batching is turned off or at the end of the current batch if it is + * turned on. If true, always add the item to the end of the queue. + */ + this.add = function (defer, callback, atEnd) { + if (defer.__fetchQueue) { + var pos = $.inArray(defer, this._queue); + if (pos >= 0) { + this._queue.splice(pos, 1); + this._addToQueue(defer, atEnd); + return defer; + } + } + var wait = new $.Deferred(); + var process = new $.Deferred(); + wait.then(function () { + $.when(callback.call(defer)).always(process.resolve); + }, process.resolve); + defer.__fetchQueue = wait; + this._addToQueue(defer, atEnd); + $.when(wait, process).always(function () { + if (m_this._processing > 0) { + m_this._processing -= 1; + } + m_this.next_item(); + }).promise(defer); + m_this.next_item(); + return defer; + }; + + /** + * Add an item to the queue. If batches are being used, add it at after + * other items in the same batch. + * @param {Deferred} defer Deferred object to add to the queue. + * @param {boolean} atEnd if false, add the item to the front of the queue + * if batching is turned off or at the end of the current batch if it is + * turned on. If true, always add the item to the end of the queue. + */ + this._addToQueue = function (defer, atEnd) { + defer.__fetchQueue._batch = this._batch; + if (atEnd) { + this._queue.push(defer); + } else if (!this._batch) { + this._queue.unshift(defer); + } else { + for (var i = 0; i < this._queue.length; i += 1) { + if (this._queue[i].__fetchQueue._batch !== this._batch) { + break; + } + } + this._queue.splice(i, 0, defer); + } + }; + + /** + * Get the position of a deferred object in the queue. + * @param {Deferred} defer Deferred object to get the position of. + * @returns {number} -1 if not in the queue, or the position in the queue. + */ + this.get = function (defer) { + return $.inArray(defer, this._queue); + }; + + /** + * Remove a Deferred object from the queue. + * @param {Deferred} defer Deferred object to add to the queue. + * @returns {bool} true if the object was removed + */ + this.remove = function (defer) { + var pos = $.inArray(defer, this._queue); + if (pos >= 0) { + this._queue.splice(pos, 1); + return true; + } + return false; + }; + + /** + * Start a new batch or clear using batches. + * @param {boolean} start true to start a new batch, false to turn off + * using batches. Undefined to return the current + * state of batches. + * @return {Number|boolean|Object} the current batch state or this object. + */ + this.batch = function (start) { + if (start === undefined) { + return this._batch; + } + if (!start) { + this._batch = false; + } else { + this._batch = m_next_batch; + m_next_batch += 1; + } + return this; + }; + + /** + * Check if any items are queued and if there if there are not too many + * deferred objects being processed. If so, process more items. + */ + this.next_item = function () { + if (m_this._innextitem) { + return; + } + m_this._innextitem = true; + /* if the queue is greater than the track size, check each item to see + * if it is still needed. */ + if (m_this._queue.length > m_this._track && this._needed) { + for (var i = m_this._queue.length - 1; i >= 0; i -= 1) { + if (!m_this._needed(m_this._queue[i])) { + var discard = m_this._queue.splice(i, 1)[0]; + m_this._processing += 1; + discard.__fetchQueue.reject(); + delete discard.__fetchQueue; + } + } + } + while (m_this._processing < m_this._size && m_this._queue.length) { + var defer = m_this._queue.shift(); + if (defer.__fetchQueue) { + m_this._processing += 1; + var needed = m_this._needed ? m_this._needed(defer) : true; + if (needed) { + defer.__fetchQueue.resolve(); + } else { + defer.__fetchQueue.reject(); + } + delete defer.__fetchQueue; + } + } + m_this._innextitem = false; + }; + + this.clear(); + return this; + }; + + return fetchQueue; + })(); + + +/***/ }, +/* 139 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var featureLayer = __webpack_require__(137); + var object = __webpack_require__(5); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class fileReader + * + * @class geo.fileReader + * @extends geo.object + * @returns {geo.fileReader} + */ + ////////////////////////////////////////////////////////////////////////////// + var fileReader = function (arg) { + 'use strict'; + if (!(this instanceof fileReader)) { + return new fileReader(arg); + } + object.call(this); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + arg = arg || {}; + + if (!(arg.layer instanceof featureLayer)) { + throw 'fileReader must be given a feature layer'; + } + + var m_layer = arg.layer; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the feature layer attached to the reader + */ + //////////////////////////////////////////////////////////////////////////// + this.layer = function () { + return m_layer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Tells the caller if it can handle the given file by returning a boolean. + */ + //////////////////////////////////////////////////////////////////////////// + this.canRead = function () { + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.read = function (file, done) { + done(false); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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); + }; + 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._getString = function (file, done, progress) { + var reader = newFileReader(done, progress); + reader.readAsText(file); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Like _getString, but returns an ArrayBuffer object. + */ + //////////////////////////////////////////////////////////////////////////// + this._getArrayBuffer = function (file, done, progress) { + var reader = newFileReader(done, progress); + reader.readAsText(file); + }; + + return this; + }; + + inherit(fileReader, object); + module.exports = fileReader; + + +/***/ }, +/* 140 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class geomFeature + * + * @class geo.geomFeature + * @extends geo.feature + * @returns {geo.geomFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var geomFeature = function (arg) { + 'use strict'; + if (!(this instanceof geomFeature)) { + return new geomFeature(arg); + } + + var $ = __webpack_require__(1); + + arg = arg || {}; + 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); + + return this; + }; + + inherit(geomFeature, feature); + module.exports = geomFeature; + + +/***/ }, +/* 141 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class graphFeature + * + * @class geo.greaphFeature + * @extends geo.feature + * @returns {geo.graphFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var graphFeature = function (arg) { + 'use strict'; + + if (!(this instanceof graphFeature)) { + return new graphFeature(arg); + } + arg = arg || {}; + feature.call(this, arg); + + var $ = __webpack_require__(1); + var util = __webpack_require__(120); + var registry = __webpack_require__(131); + + //////////////////////////////////////////////////////////////////////////// + /** + * @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; }); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Call child _build methods + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + m_this.children().forEach(function (child) { + child._build(); + }); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Call child _update methods + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + m_this.children().forEach(function (child) { + child._update(); + }); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set links accessor. + */ + //////////////////////////////////////////////////////////////////////////// + this.links = function (arg) { + if (arg === undefined) { + return m_children; + } + + m_children = util.ensureFunction(arg); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set nodes + */ + //////////////////////////////////////////////////////////////////////////// + this.nodes = function (val) { + if (val === undefined) { + return m_nodes; + } + m_nodes = val; + m_this.modified(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get internal node feature + */ + //////////////////////////////////////////////////////////////////////////// + this.nodeFeature = function () { + return m_points; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get internal link features + */ + //////////////////////////////////////////////////////////////////////////// + this.linkFeatures = function () { + return m_links; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build the feature for drawing + */ + //////////////////////////////////////////////////////////////////////////// + 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); + + // get links from node connections + data.forEach(function (source) { + (source.children || []).forEach(function (target) { + var link; + nLinks += 1; + if (m_links.length < nLinks) { + link = registry.createFeature( + style.linkType, layer, layer.renderer() + ).style(style.links); + m_this.addChild(link); + m_links.push(link); + } + m_links[nLinks - 1].data([source, target]); + }); + }); + + m_links.splice(nLinks, m_links.length - nLinks).forEach(function (l) { + l._exit(); + m_this.removeChild(l); + }); + + s_draw(); + return m_this; + }; + + m_points = registry.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(graphFeature, feature); + module.exports = graphFeature; + + +/***/ }, +/* 142 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var tile = __webpack_require__(143); + + module.exports = (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 geo.imageTile + * @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] + */ + ////////////////////////////////////////////////////////////////////////////// + var imageTile = function (spec) { + if (!(this instanceof imageTile)) { + return new imageTile(spec); + } + + var $ = __webpack_require__(1); + + spec.size = spec.size || {x: 256, y: 256}; + this._image = null; + + // Cache the coordinate scaling + this._cors = spec.crossDomain || 'anonymous'; + + // Call superclass constructor + 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 = defer.resolve; + this._image.onerror = defer.reject; + this._image.src = this._url; + + // attach a promise interface to `this` + defer.then(function () { + this._fetched = true; + }.bind(this)).promise(this); + } + 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(imageTile, tile); + return imageTile; + })(); + + +/***/ }, +/* 143 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = (function () { + 'use strict'; + + var $ = __webpack_require__(1); + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 geo.tile + * @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] + */ + ////////////////////////////////////////////////////////////////////////////// + var tile = function (spec) { + if (!(this instanceof tile)) { + return new 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; + this._queue = spec.queue || null; + + /** + * 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).then(function () { + this._fetched = true; + }.bind(this)).promise(this); + } + return this; + }; + + /** + * Return whether this tile has been fetched already. + * + * @returns {boolean} True if the tile has been fetched. + */ + this.fetched = function () { + return this._fetched; + }; + + /** + * 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) { + // both fetch and _queueAdd can replace the current then method + if (!this.fetched() && this._queue && this._queue.add && (!this.state || + this.state() === 'pending')) { + this._queue.add(this, this.fetch); + } else { + this.fetch(); + } + // 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; + }; + }; + return tile; + })(); + + +/***/ }, +/* 144 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFileReader = __webpack_require__(131).registerFileReader; + var fileReader = __webpack_require__(139); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class jsonReader + * + * @class geo.jsonReader + * @extends geo.fileReader + * @returns {geo.jsonReader} + */ + ////////////////////////////////////////////////////////////////////////////// + var jsonReader = function (arg) { + 'use strict'; + if (!(this instanceof jsonReader)) { + return new jsonReader(arg); + } + + var $ = __webpack_require__(1); + + 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); + + 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; + }; + + 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); + }); + } + } + } + + if (file instanceof File) { + m_this._getString(file, onDone, progress); + } else if (typeof file === 'string') { + onDone(file); + } else { + done(file); + } + }; + + 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; + }; + + 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._getCoordinates = function (spec) { + var geometry = spec.geometry || {}, + coordinates = geometry.coordinates || [], elv; + + if ((coordinates.length === 2 || coordinates.length === 3) && + (isFinite(coordinates[0]) && isFinite(coordinates[1]))) { + + // Do we have a elevation component + if (isFinite(coordinates[2])) { + elv = coordinates[2]; + } + + // special handling for single point coordinates + return [{x: coordinates[0], y: coordinates[1], z: elv}]; + } + + // 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 an array of points for LineString, MultiPoint, etc... + return 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); + 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); + } + }); + + if (done) { + done(allFeatures); + } + } + + m_this._readObject(file, _done, progress); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 + }; + }); + }; + + 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; + }); + } + return feature; + }; + + }; + + inherit(jsonReader, fileReader); + registerFileReader('jsonReader', jsonReader); + module.exports = jsonReader; + + +/***/ }, +/* 145 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class lineFeature + * + * @class geo.lineFeature + * @extends geo.feature + * @returns {geo.lineFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var lineFeature = function (arg) { + 'use strict'; + if (!(this instanceof lineFeature)) { + return new lineFeature(arg); + } + + var $ = __webpack_require__(1); + + arg = arg || {}; + feature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set line accessor + * + * @returns {geo.pointFeature} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set position accessor + * + * @returns {geo.pointFeature} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.pointSearch = function (p) { + var data, pt, line, width, indices = [], found = [], pos; + data = m_this.data(); + if (!data || !data.length) { + return { + found: [], + index: [] + }; + } + + line = m_this.line(); + width = m_this.style.get('strokeWidth'); + pos = m_this.position(); + pt = m_this.featureGcsToDisplay(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) + } + ); + } + + // 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 { + data: found, + index: indices + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Returns an array of line indices that are contained in the given box. + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + 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._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} + */ + lineFeature.create = function (layer, spec) { + 'use strict'; + + spec.type = 'line'; + return feature.create(layer, spec); + }; + + inherit(lineFeature, feature); + module.exports = lineFeature; + + +/***/ }, +/* 146 */ +/***/ function(module, exports, __webpack_require__) { + + var $ = __webpack_require__(1); + var vgl = __webpack_require__(6); + var inherit = __webpack_require__(4); + var sceneObject = __webpack_require__(128); + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 geo.map + * @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} [rotation=0] Clockwise rotation in radians + * @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. + * @param {boolean} [allowRotation=true] False prevents rotation, true allows + * any rotation. If a function, the function is called with a rotation + * (angle in radians) and returns a valid rotation (this can be used to + * constrain the rotation to a range or specific values). + * + * *** 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} + */ + ////////////////////////////////////////////////////////////////////////////// + var map = function (arg) { + 'use strict'; + if (!(this instanceof map)) { + return new map(arg); + } + arg = arg || {}; + + if (arg.node === undefined || arg.node === null) { + console.warn('map creation requires a node'); + return this; + } + + sceneObject.call(this, arg); + + var camera = __webpack_require__(3); + var transform = __webpack_require__(147); + var util = __webpack_require__(120); + var registry = __webpack_require__(131); + var geo_event = __webpack_require__(125); + var mapInteractor = __webpack_require__(217); + var clock = __webpack_require__(133); + var uiLayer = __webpack_require__(218); + + //////////////////////////////////////////////////////////////////////////// + /** + * 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_rotation = 0, + 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_allowRotation = (typeof arg.allowRotation === 'function' ? + arg.allowRotation : (arg.allowRotation === undefined ? + true : !!arg.allowRotation)), + m_maxBounds = arg.maxBounds || {}, + m_camera = arg.camera || camera(), + m_unitsPerPixel, + m_clampBoundsX, + m_clampBoundsY, + m_clampZoom, + m_origin, + m_scale = {x: 1, y: 1, z: 1}; // constant and ignored 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. */ + var mcx = ((m_maxBounds.left || 0) + (m_maxBounds.right || 0)) / 2, + mcy = ((m_maxBounds.bottom || 0) + (m_maxBounds.top || 0)) / 2; + m_maxBounds.left = transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: m_maxBounds.left !== undefined ? m_maxBounds.left : -180, y: mcy + }])[0].x; + m_maxBounds.right = transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: m_maxBounds.right !== undefined ? m_maxBounds.right : 180, y: mcy + }])[0].x; + m_maxBounds.top = (m_maxBounds.top !== undefined ? + transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: mcx, y: m_maxBounds.top}])[0].y : m_maxBounds.right); + m_maxBounds.bottom = (m_maxBounds.bottom !== undefined ? + transform.transformCoordinates(m_ingcs, m_gcs, [{ + x: mcx, 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 = 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/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.unitsPerPixel = function (zoom, unit) { + zoom = zoom || 0; + if (unit) { + // get the units at level 0 + m_unitsPerPixel = Math.pow(2, zoom) * unit; + + // redraw all the things + m_this.draw(); + return m_this; + } + return Math.pow(2, -zoom) * m_unitsPerPixel; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/set the clampBoundsX setting. If changed, adjust the bounds of the + * map as needed. + * + * @param {boolean?} clamp The new clamp value. + * @returns {boolean|this} + */ + //////////////////////////////////////////////////////////////////////////// + this.clampBoundsX = function (clamp) { + if (clamp === undefined) { + return m_clampBoundsX; + } + if (clamp !== m_clampBoundsX) { + m_clampBoundsX = !!clamp; + m_this.pan({x: 0, y: 0}); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/set the clampBoundsY setting. If changed, adjust the bounds of the + * map as needed. + * + * @param {boolean?} clamp The new clamp value. + * @returns {boolean|this} + */ + //////////////////////////////////////////////////////////////////////////// + this.clampBoundsY = function (clamp) { + if (clamp === undefined) { + return m_clampBoundsY; + } + if (clamp !== m_clampBoundsY) { + m_clampBoundsY = !!clamp; + m_this.pan({x: 0, y: 0}); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/set the clampZoom setting. If changed, adjust the bounds of the map + * as needed. + * + * @param {boolean?} clamp The new clamp value. + * @returns {boolean|this} + */ + //////////////////////////////////////////////////////////////////////////// + this.clampZoom = function (clamp) { + if (clamp === undefined) { + return m_clampZoom; + } + if (clamp !== m_clampZoom) { + m_clampZoom = !!clamp; + reset_minimum_zoom(); + m_this.zoom(m_zoom); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/set the allowRotation setting. If changed, adjust the map as needed. + * + * @param {boolean|function} allowRotation the new allowRotation value. + * @returns {boolean|function|this} + */ + //////////////////////////////////////////////////////////////////////////// + this.allowRotation = function (allowRotation) { + if (allowRotation === undefined) { + return m_allowRotation; + } + if (typeof allowRotation !== 'function') { + allowRotation = !!allowRotation; + } + if (allowRotation !== m_allowRotation) { + m_allowRotation = allowRotation; + m_this.rotation(m_rotation); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the map's world coordinate origin in gcs coordinates + * + * @returns {object} + */ + //////////////////////////////////////////////////////////////////////////// + this.origin = function () { + return $.extend({}, m_origin); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the map's world coordinate scaling relative gcs units + * + * @returns {object} + */ + //////////////////////////////////////////////////////////////////////////// + this.scale = function () { + return $.extend({}, m_scale); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the camera + * + * @returns {geo.camera} + */ + //////////////////////////////////////////////////////////////////////////// + this.camera = function () { + return m_camera; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get map gcs + * + * @returns {string} + */ + //////////////////////////////////////////////////////////////////////////// + this.gcs = function (arg) { + if (arg === undefined) { + return m_gcs; + } + if (arg !== m_gcs) { + var oldCenter = m_this.center(undefined, undefined); + m_gcs = arg; + reset_minimum_zoom(); + var newZoom = fix_zoom(m_zoom); + if (newZoom !== m_zoom) { + m_this.zoom(newZoom); + } + m_this.center(oldCenter, undefined); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get map interface gcs + * + * @returns {string} + */ + //////////////////////////////////////////////////////////////////////////// + this.ingcs = function (arg) { + if (arg === undefined) { + return m_ingcs; + } + m_ingcs = arg; + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get root node of the map + * + * @returns {object} + */ + //////////////////////////////////////////////////////////////////////////// + this.node = function () { + return m_node; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set zoom level of the map + * + * @param {number} val if undefined, return the current zoom level. + * Otherwise, the new zoom level to set. + * @param {object} origin if present, an object with 'geo' containing the + * gcs coordinates where the action started and 'map' containing the + * display coordinates of the same location before the zoom is applied. + * @param {boolean} ignoreDiscreteZoom if true, ignore the discreteZoom + * option when determining the new view. + * @returns {Number|geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.zoom = function (val, origin, ignoreDiscreteZoom) { + if (val === undefined) { + return m_zoom; + } + var evt, bounds; + + /* 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; + } + + m_zoom = val; + + bounds = m_this.boundsFromZoomAndCenter(val, m_center, m_rotation, null, + ignoreDiscreteZoom); + m_this.modified(); + + camera_bounds(bounds, m_rotation); + evt = { + geo: {}, + zoomLevel: m_zoom, + screenPosition: origin ? origin.map : undefined + }; + m_this.geoTrigger(geo_event.zoom, evt); + + 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}, + ignoreDiscreteZoom); + } else { + m_this.pan({x: 0, y: 0}, ignoreDiscreteZoom); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Pan the map by (x: dx, y: dy) pixels. + * + * @param {Object} delta x and y delta in display pixels + * @param {boolean} ignoreDiscreteZoom if true, ignore the discreteZoom + * option when determining the new view. + * @returns {geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.pan = function (delta, ignoreDiscreteZoom) { + var evt, unit; + evt = { + geo: {}, + screenDelta: delta + }; + + unit = m_this.unitsPerPixel(m_zoom); + + var sinr = Math.sin(m_rotation), cosr = Math.cos(m_rotation); + m_camera.pan({ + x: (delta.x * cosr - (-delta.y) * sinr) * unit, + y: (delta.x * sinr + (-delta.y) * cosr) * unit + }); + /* If m_clampBounds* is true, clamp the pan */ + var bounds = fix_bounds(m_camera.bounds, m_rotation); + if (bounds !== m_camera.bounds) { + var panPos = m_this.gcsToDisplay({ + x: m_camera.bounds.left, y: m_camera.bounds.top}, null); + bounds = m_this.boundsFromZoomAndCenter(m_zoom, { + x: (bounds.left + bounds.right) / 2, + y: (bounds.top + bounds.bottom) / 2 + }, m_rotation, null, ignoreDiscreteZoom); + camera_bounds(bounds, m_rotation); + 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 map rotation. The rotation is performed around the current + * view center. + * + * @param {Object} rotation angle in radians (positive is clockwise) + * @param {Object} origin is specified, rotate about this origin + * @param {boolean} ignoreRotationFunc if true, don't constrain the rotation. + * @returns {geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.rotation = function (rotation, origin, ignoreRotationFunc) { + if (rotation === undefined) { + return m_rotation; + } + rotation = fix_rotation(rotation, ignoreRotationFunc); + if (rotation === m_rotation) { + return m_this; + } + m_rotation = rotation; + + var bounds = m_this.boundsFromZoomAndCenter( + m_zoom, m_center, m_rotation, null, ignoreRotationFunc); + m_this.modified(); + + camera_bounds(bounds, m_rotation); + + var evt = { + geo: {}, + rotation: m_rotation, + screenPosition: origin ? origin.map : undefined + }; + + m_this.geoTrigger(geo_event.rotate, evt); + + 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}); + } + /* Changing the rotation can change our minimum zoom */ + reset_minimum_zoom(); + m_this.zoom(m_zoom, ignoreRotationFunc); + return m_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 + * @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. + * @param {boolean} ignoreDiscreteZoom if true, ignore the discreteZoom + * option when determining the new view. + * @returns {Object|geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.center = function (coordinates, gcs, ignoreDiscreteZoom) { + var center; + if (coordinates === undefined) { + center = $.extend({}, m_this.worldToGcs(m_center, gcs)); + return center; + } + + // 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, m_rotation, null, ignoreDiscreteZoom), m_rotation); + m_this.modified(); + // trigger a pan event + m_this.geoTrigger( + geo_event.pan, + { + geo: coordinates, + screenDelta: null + } + ); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Add layer to the map + * + * @param {geo.layer} layer to be added to the map + * @return {geom.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.createLayer = function (layerName, arg) { + arg = arg || {}; + var newLayer = registry.createLayer( + layerName, m_this, arg); + + if (newLayer) { + m_this.addChild(newLayer); + m_this.children().forEach(function (c) { + if (c instanceof uiLayer) { + c.moveToTop(); + } + }); + newLayer._update(); + m_this.modified(); + + m_this.geoTrigger(geo_event.layerAdd, { + type: geo_event.layerAdd, + target: m_this, + layer: newLayer + }); + } + + return newLayer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Remove layer from the map + * + * @param {geo.layer} layer that should be removed from the map + * @return {geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + 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 + }); + } + + /// 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the rotated size of the map. This is the width and height of the + * non-rotated area necessary to enclose the rotated area in pixels. + * + * @returns {Object} An object containing width and height as keys + */ + //////////////////////////////////////////////////////////////////////////// + this.rotatedSize = function () { + if (!this.rotation()) { + return { + width: m_width, + height: m_height + }; + } + var bds = rotate_bounds_center( + {x: 0, y: 0}, {width: m_width, height: m_height}, this.rotation()); + return { + width: Math.abs(bds.right - bds.left), + height: Math.abs(bds.top - bds.bottom) + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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) { + + // 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; + + reset_minimum_zoom(); + var newZoom = fix_zoom(m_zoom); + if (newZoom !== m_zoom) { + m_this.zoom(newZoom); + } + m_this.camera().viewport = {width: w, height: h}; + m_this.center(oldCenter); + + m_this.geoTrigger(geo_event.resize, { + type: geo_event.resize, + target: m_this, + x: m_x, + y: m_y, + width: w, + height: h + }); + + m_this.modified(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 + */ + //////////////////////////////////////////////////////////////////////////// + this.gcsToWorld = function (c, gcs) { + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (gcs !== m_gcs) { + c = transform.transformCoordinates(gcs, m_gcs, [c])[0]; + } + return transform.affineForward( + {origin: m_origin}, + [c] + )[0]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.worldToGcs = function (c, gcs) { + c = transform.affineInverse( + {origin: m_origin}, + [c] + )[0]; + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (gcs !== m_gcs) { + c = transform.transformCoordinates(m_gcs, gcs, [c])[0]; + } + return c; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.worldToDisplay = function (c) { + return m_camera.worldToDisplay(c); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Convert from display to gcs coordinates + * + * displayToWorld | worldToGcs + * + * @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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.displayToWorld = function (c) { + return m_camera.displayToWorld(c); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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(); + } + + m_this.geoTrigger(geo_event.drawEnd, { + type: geo_event.drawEnd, + target: m_this + }); + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 = registry.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'); + /* make sure the map node has nothing left in it */ + m_this.node().empty(); + $(window).off('resize', resizeSelf); + s_exit(); + }; + + 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; + + function done() { + m_this.draw(); + } + + if (reader) { + evt.stopPropagation(); + evt.preventDefault(); + + 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 + } + } + } + }); + + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the map interactor + */ + //////////////////////////////////////////////////////////////////////////// + this.interactor = function (arg) { + if (arg === undefined) { + return m_interactor; + } + m_interactor = arg; + + // 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the map clock + */ + //////////////////////////////////////////////////////////////////////////// + this.clock = function (arg) { + if (arg === undefined) { + return m_clock; + } + m_clock = arg; + + if (m_clock) { + m_clock.object(m_this); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get or set the min/max zoom range. + * + * @param {Object} arg {min: minimumzoom, max: maximumzom} + * @param {boolean} noRefresh if true, don't update the map if the zoom level + * has changed. + * @returns {Object|geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.zoomRange = function (arg, noRefresh) { + if (arg === undefined) { + return $.extend({}, m_validZoomRange); + } + if (arg.max !== undefined) { + m_validZoomRange.max = arg.max; + } + if (arg.min !== undefined) { + m_validZoomRange.min = m_validZoomRange.origMin = arg.min; + } + reset_minimum_zoom(); + if (!noRefresh) { + m_this.zoom(m_zoom); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Start an animated zoom/pan/rotate. If a second transition is requested + * while a transition is already in progress, a new transition is created + * that is functionally from whereever the map has moved to (possibly partway + * through the first transition) going to the end point of the new + * transition. + * + * Options: + *
+	   *   opts = {
+	   *     center: { x: ... , y: ... } // the new center
+	   *     zoom: ... // the new zoom level
+	   *     zoomOrigin: ... // an origin to use when zooming.  Optional.
+	   *     rotation: ... // the new rotation angle
+	   *     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.transition = function (opts, gcs, animTime) { + + if (opts === undefined) { + return m_transition; + } + + if (m_transition) { + /* The queued transition needs to combine the current transition's + * endpoint, any other queued transition, and the new transition to be + * complete. */ + var transitionEnd = $.extend(true, {}, m_transition.end); + if (transitionEnd.center && m_gcs !== m_ingcs) { + transitionEnd.center = transform.transformCoordinates( + m_gcs, m_ingcs, [transitionEnd.center])[0]; + } + m_queuedTransition = $.extend( + {}, transitionEnd || {}, m_queuedTransition || {}, opts); + return m_this; + } + + function interp1(p0, p1, t) { + return p0 + (p1 - p0) * t; + } + function defaultInterp(p0, p1) { + return function (t) { + var result = []; + $.each(p0, function (idx) { + result.push(interp1(p0[idx], p1[idx], t)); + }); + return result; + }; + } + + 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(), + rotation: m_this.rotation(), + 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 = $.extend(true, {}, opts); + opts.center = util.normalizeCoordinates(opts.center); + if (gcs !== m_gcs) { + opts.center = transform.transformCoordinates(gcs, m_gcs, [ + opts.center])[0]; + } + } + opts = $.extend(true, {}, defaultOpts, opts); + + m_transition = { + start: { + center: m_this.center(undefined, null), + zoom: m_this.zoom(), + rotation: m_this.rotation() + }, + end: { + center: opts.center, + zoom: fix_zoom(opts.zoom), + rotation: fix_rotation(opts.rotation, undefined, true) + }, + ease: opts.ease, + zCoord: opts.zCoord, + done: opts.done, + duration: opts.duration, + zoomOrigin: opts.zoomOrigin + }; + + if (opts.zCoord) { + m_transition.interp = opts.interp( + [ + m_transition.start.center.x, + m_transition.start.center.y, + zoom2z(m_transition.start.zoom), + m_transition.start.rotation + ], + [ + m_transition.end.center.x, + m_transition.end.center.y, + zoom2z(m_transition.end.zoom), + m_transition.end.rotation + ] + ); + } else { + m_transition.interp = opts.interp( + [ + m_transition.start.center.x, + m_transition.start.center.y, + m_transition.start.zoom, + m_transition.start.rotation + ], + [ + m_transition.end.center.x, + m_transition.end.center.y, + m_transition.end.zoom, + m_transition.end.rotation + ] + ); + } + + function anim(time) { + var done = m_transition.done, + next = m_queuedTransition; + if (m_transition.cancel === true) { + /* Finish cancelling a transition. */ + m_this.geoTrigger(geo_event.transitioncancel, opts); + if (done) { + done({ + cancel: true, + source: m_transition.cancelSource, + transition: m_transition + }); + } + m_transition = null; + /* There will only be a queuedTransition if it was created after this + * transition was cancelled */ + if (m_queuedTransition) { + next = m_queuedTransition; + m_queuedTransition = null; + m_this.transition(next, undefined, time); + } + return; + } + + if (!m_transition.start.time) { + m_transition.start.time = time; + m_transition.end.time = time + opts.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, m_transition.zoomOrigin); + m_this.rotation(fix_rotation(m_transition.end.rotation)); + } + + m_this.geoTrigger(geo_event.transitionend, opts); + + if (done) { + done({next: !!next}); + } + + m_transition = null; + if (m_queuedTransition) { + next = m_queuedTransition; + m_queuedTransition = null; + m_this.transition(next, undefined, time); + } + + return; + } + + var z = m_transition.ease( + (time - m_transition.start.time) / opts.duration + ); + + var p = m_transition.interp(z); + if (m_transition.zCoord) { + p[2] = z2zoom(p[2]); + } + if (fix_zoom(p[2], true) === m_zoom) { + m_this.center({ + x: p[0], + y: p[1] + }, null, true); + } else { + m_center = m_this.gcsToWorld({x: p[0], y: p[1]}, null); + m_this.zoom(p[2], m_transition.zoomOrigin, true); + } + m_this.rotation(p[3], undefined, true); + + window.requestAnimationFrame(anim); + } + + m_this.geoTrigger(geo_event.transitionstart, opts); + + if (geo_event.cancelNavigation) { + m_transition = null; + m_this.geoTrigger(geo_event.transitionend, opts); + return m_this; + } else if (geo_event.cancelAnimation) { + // run the navigation synchronously + opts.duration = 0; + anim(0); + } else if (animTime) { + anim(animTime); + } else { + window.requestAnimationFrame(anim); + } + return m_this; + }; + + /** + * Cancel any existing transition. The transition will send a cancel event + * at the next animation frame, but no further activity occurs. + * + * @param {string} [source] optional cause of the cancel. This can be any + * value, but something like . is + * recommended to allow other functions to determine the + * source and cause of the transition being canceled. + * @returns {bool} true if a transition was in progress. + */ + this.transitionCancel = function (source) { + if (m_transition && (m_transition.cancel !== true || m_queuedTransition)) { + m_transition.cancel = true; + m_transition.cancelSource = source || m_transition.cancelSource || ''; + m_queuedTransition = null; + return true; + } + return false; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/set the locations of the current map corners as latitudes/longitudes. + * When provided the argument should be an object containing the keys left, + * top, right, bottom declaring the desired new map bounds. The new bounds + * will contain at least the min/max lat/lngs provided modified by clamp + * settings. 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.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 = 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, m_rotation); + nav = m_this.zoomAndCenterFromBounds(bds, m_rotation, null); + + // This might have consequences 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, m_rotation, gcs, + true); + }; + + this.maxBounds = function (bounds, gcs) { + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (bounds === undefined) { + return { + left: transform.transformCoordinates(m_gcs, gcs, [{ + x: m_maxBounds.left, y: 0}])[0].x, + right: transform.transformCoordinates(m_gcs, gcs, [{ + x: m_maxBounds.right, y: 0}])[0].x, + bottom: transform.transformCoordinates(m_gcs, gcs, [{ + x: 0, y: m_maxBounds.bottom}])[0].y, + top: transform.transformCoordinates(m_gcs, gcs, [{ + x: 0, y: m_maxBounds.top}])[0].y + }; + } + var cx = ((bounds.left || 0) + (bounds.right || 0)) / 2, + cy = ((bounds.bottom || 0) + (bounds.top || 0)) / 2; + if (bounds.left !== undefined) { + m_maxBounds.left = transform.transformCoordinates(gcs, m_gcs, [{ + x: bounds.left, y: cy}])[0].x; + } + if (bounds.right !== undefined) { + m_maxBounds.right = transform.transformCoordinates(gcs, m_gcs, [{ + x: bounds.right, y: cy}])[0].x; + } + if (bounds.bottom !== undefined) { + m_maxBounds.bottom = transform.transformCoordinates(gcs, m_gcs, [{ + x: cx, y: bounds.bottom}])[0].y; + } + if (bounds.top !== undefined) { + m_maxBounds.top = transform.transformCoordinates(gcs, m_gcs, [{ + x: cx, y: bounds.top}])[0].y; + } + reset_minimum_zoom(); + m_this.zoom(m_zoom); + m_this.pan({x: 0, y: 0}); + return this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the center zoom level necessary to display the given lat/lon bounds. + * + * @param {geo.geoBounds} [bds] The requested map bounds + * @param {number} rotation Rotation in clockwise radians. + * @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.zoomAndCenterFromBounds = function (bounds, rotation, gcs) { + var center, zoom; + + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + if (gcs !== m_gcs) { + var trans = 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'); + } + + // calculate the zoom to fit the bounds + zoom = fix_zoom(calculate_zoom(bounds, rotation)); + + // clamp bounds if necessary + bounds = fix_bounds(bounds, rotation); + + /* 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 + }; + if (gcs !== m_gcs) { + center = transform.transformCoordinates(m_gcs, gcs, [center])[0]; + } + return { + zoom: zoom, + center: center + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the bounds that will be displayed with the given zoom and center. + * + * 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 {number} rotation The requested rotation + * @param {string|geo.transform} [gcs] undefined to use the interface gcs, + * null to use the map gcs, or any other transform. + * @param {boolean} ignoreDiscreteZoom if true, ignore the discreteZoom + * option when determining the new view. + * @return {geo.geoBounds} + */ + //////////////////////////////////////////////////////////////////////////// + this.boundsFromZoomAndCenter = function (zoom, center, rotation, gcs, + ignoreDiscreteZoom) { + var width, height, halfw, halfh, bounds, units; + + gcs = (gcs === null ? m_gcs : (gcs === undefined ? m_ingcs : gcs)); + // preprocess the arguments + zoom = fix_zoom(zoom, ignoreDiscreteZoom); + units = m_this.unitsPerPixel(zoom); + center = m_this.gcsToWorld(center, gcs); + + // get half the width and height in world coordinates + width = m_width * units; + height = m_height * units; + halfw = width / 2; + halfh = height / 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. + + if (rotation) { + center.x += m_origin.x; + center.y += m_origin.y; + bounds = rotate_bounds_center( + center, {width: width, height: height}, rotation); + // correct the bounds when clamping is enabled + bounds.width = width; + bounds.height = height; + bounds = fix_bounds(bounds, rotation); + } else { + bounds = { + left: center.x - halfw + m_origin.x, + right: center.x + halfw + m_origin.x, + bottom: center.y - halfh + m_origin.y, + top: center.y + halfh + m_origin.y + }; + // correct the bounds when clamping is enabled + bounds = fix_bounds(bounds, 0); + } + if (gcs !== m_gcs) { + var bds = transform.transformCoordinates( + m_gcs, gcs, + [[bounds.left, bounds.top], [bounds.right, bounds.bottom]]); + bounds = { + left: bds[0][0], top: bds[0][1], right: bds[1][0], bottom: bds[1][1] + }; + } + /* Add the original width and height of the viewport before rotation. */ + bounds.width = width; + bounds.height = height; + return bounds; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/set the discrete zoom flag. + * + * @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.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())); + } + m_this.interactor().options({discreteZoom: m_discreteZoom}); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the layers contained in the map. + * Alias of {@linkcode geo.sceneObject.children}. + */ + //////////////////////////////////////////////////////////////////////////// + this.layers = this.children; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 + * + * 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.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. + // + //////////////////////////////////////////////////////////////////////////// + /** + * 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 + */ + 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; + + if (ar_bds > ar_vp) { + // fit left and right + sclx = 1; + + // grow top and bottom + scly = ar_bds / ar_vp; + } else { + // fit top and bottom + scly = 1; + + // grow left and right + sclx = ar_vp / ar_bds; + } + return {x: sclx, y: scly}; + } + + /** + * Adjust a set of bounds based on a rotation. + * @private. + */ + function rotate_bounds(bounds, rotation) { + if (rotation) { + var center = { + x: (bounds.left + bounds.right) / 2, + y: (bounds.top + bounds.bottom) / 2 + }; + var size = { + width: Math.abs(bounds.left - bounds.right), + height: Math.abs(bounds.top - bounds.bottom) + }; + bounds = rotate_bounds_center(center, size, rotation); + } + return bounds; + } + + /** + * Generate a set of bounds based on a center point, a width and height, and + * a rotation. + * @private. + */ + function rotate_bounds_center(center, size, rotation) { + // calculate the half width and height + var width = size.width / 2, height = size.height / 2; + var sinr = Math.sin(rotation), cosr = Math.cos(rotation); + var ul = {}, ur = {}, ll = {}, lr = {}; + ul.x = center.x + (-width) * cosr - (-height) * sinr; + ul.y = center.y + (-width) * sinr + (-height) * cosr; + ur.x = center.x + width * cosr - (-height) * sinr; + ur.y = center.y + width * sinr + (-height) * cosr; + ll.x = center.x + (-width) * cosr - height * sinr; + ll.y = center.y + (-width) * sinr + height * cosr; + lr.x = center.x + width * cosr - height * sinr; + lr.y = center.y + width * sinr + height * cosr; + return { + left: Math.min(ul.x, ur.x, ll.x, lr.x), + right: Math.max(ul.x, ur.x, ll.x, lr.x), + bottom: Math.min(ul.y, ur.y, ll.y, lr.y), + top: Math.max(ul.y, ur.y, ll.y, lr.y) + }; + } + + /** + * 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, rotation) { + if (rotation === undefined) { + rotation = m_rotation; + } + bounds = rotate_bounds(bounds, rotation); + // 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; + } + + /** + * 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; + } + } + + /** + * Return the nearest valid zoom level to the requested zoom. + * @private + * @param {number} zoom a zoom level to adjust to current settings + * @param {boolean} ignoreDiscreteZoom if true, ignore the discreteZoom + * option when determining the new view. + * @returns {number} the zoom level clamped to the allowed zoom range and + * with other settings applied. + */ + 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; + } + + /** + * Return a valid rotation angle. + * @private + */ + function fix_rotation(rotation, ignoreRotationFunc, noRangeLimit) { + if (!m_allowRotation) { + return 0; + } + if (!ignoreRotationFunc && typeof m_allowRotation === 'function') { + rotation = m_allowRotation(rotation); + } + /* Ensure that the rotation is in the range [0, 2pi) */ + if (!noRangeLimit) { + var range = Math.PI * 2; + rotation = (rotation % range) + (rotation >= 0 ? 0 : range); + if (Math.min(Math.abs(rotation), Math.abs(rotation - range)) < 0.00001) { + rotation = 0; + } + } + return rotation; + } + + /** + * Return the nearest valid bounds maintaining the + * width and height. Does nothing if m_clampBounds* is + * false. + * @private + */ + function fix_bounds(bounds, rotation) { + if (!m_clampBoundsX && !m_clampBoundsY) { + return bounds; + } + var dx, dy, maxBounds = m_maxBounds; + if (rotation) { + maxBounds = $.extend({}, m_maxBounds); + /* When rotated, expand the maximum bounds so that they will allow the + * corners to be visible. We know the rotated bounding box, plus the + * original maximum bounds. To fit the corners of the maximum bounds, we + * can expand the total bounds by the same factor that the rotated + * bounding box is expanded from the non-rotated bounding box (for a + * small rotation, this is sin(rotation) * (original bounding box height) + * in the width). This feels like appropriate behaviour with one of the + * two bounds clamped. With both, it seems mildly peculiar. */ + var bw = Math.abs(bounds.right - bounds.left), + bh = Math.abs(bounds.top - bounds.bottom), + absinr = Math.abs(Math.sin(rotation)), + abcosr = Math.abs(Math.cos(rotation)), + ow, oh; + if (bounds.width && bounds.height) { + ow = bounds.width; + oh = bounds.height; + } else if (Math.abs(absinr - abcosr) < 0.0005) { + /* If we are close to a 45 degree rotation, it is ill-determined to + * compute the original (pre-rotation) bounds width and height. In + * this case, assume that we are using the map's aspect ratio. */ + if (m_width && m_height) { + var aspect = Math.abs(m_width / m_height); + var fac = Math.pow(1 + Math.pow(aspect, 2), 0.5); + ow = Math.max(bw, bh) / fac; + oh = ow * aspect; + } else { + /* Fallback if we don't have width or height */ + ow = bw * abcosr; + oh = bh * absinr; + } + } else { + /* Compute the pre-rotation (original) bounds width and height */ + ow = (abcosr * bw - absinr * bh) / (abcosr * abcosr - absinr * absinr); + oh = (abcosr * bh - absinr * bw) / (abcosr * abcosr - absinr * absinr); + } + /* Our maximum bounds are expanded based on the projected length of a + * tilted side of the original bounding box in the rotated bounding box. + * To handle all rotations, take the minimum difference in width or + * height. */ + var bdx = bw - Math.max(abcosr * ow, absinr * oh), + bdy = bh - Math.max(abcosr * oh, absinr * ow); + maxBounds.left -= bdx; + maxBounds.right += bdx; + maxBounds.top += bdy; + maxBounds.bottom -= bdy; + } + if (m_clampBoundsX) { + if (bounds.right - bounds.left > maxBounds.right - maxBounds.left) { + dx = maxBounds.left - ((bounds.right - bounds.left - ( + maxBounds.right - maxBounds.left)) / 2) - bounds.left; + } else if (bounds.left < maxBounds.left) { + dx = maxBounds.left - bounds.left; + } else if (bounds.right > maxBounds.right) { + dx = maxBounds.right - bounds.right; + } + if (dx) { + bounds = { + left: bounds.left += dx, + right: bounds.right += dx, + top: bounds.top, + bottom: bounds.bottom + }; + } + } + if (m_clampBoundsY) { + if (bounds.top - bounds.bottom > maxBounds.top - maxBounds.bottom) { + dy = maxBounds.bottom - ((bounds.top - bounds.bottom - ( + maxBounds.top - maxBounds.bottom)) / 2) - bounds.bottom; + } else if (bounds.top > maxBounds.top) { + dy = maxBounds.top - bounds.top; + } else if (bounds.bottom < maxBounds.bottom) { + dy = maxBounds.bottom - bounds.bottom; + } + if (dy) { + bounds = { + top: bounds.top += dy, + bottom: bounds.bottom += dy, + left: bounds.left, + right: bounds.right + }; + } + } + return bounds; + } + + /** + * Call the camera bounds method with the given bounds, but + * correct for the viewport aspect ratio. + * @private + */ + function camera_bounds(bounds, rotation) { + m_camera.rotation = rotation || 0; + /* When dealing with rotation, use the original width and height of the + * bounds, as the rotation will have expanded them. */ + if (bounds.width && bounds.height && rotation) { + var cx = (bounds.left + bounds.right) / 2, + cy = (bounds.top + bounds.bottom) / 2; + m_camera.viewFromCenterSizeRotation({x: cx, y: cy}, bounds, rotation); + } else { + m_camera.bounds = bounds; + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // All the methods are now defined. From here, we are initializing all + // internal variables and event handlers. + // + //////////////////////////////////////////////////////////////////////////// + + // Set the world origin + m_origin = {x: 0, y: 0}; + + // Fix the zoom level (minimum and initial) + this.zoomRange(arg, true); + m_zoom = fix_zoom(m_zoom); + m_rotation = fix_rotation(m_rotation); + // Now update to the correct center and zoom level + this.center($.extend({}, arg.center || m_center), undefined); + + this.interactor(arg.interactor || mapInteractor({discreteZoom: m_discreteZoom})); + this.clock(arg.clock || clock()); + + function resizeSelf() { + m_this.resize(0, 0, m_node.width(), m_node.height()); + } + + 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 + */ + + /** + * 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} + */ + map.create = function (spec) { + 'use strict'; + + var _map = map(spec), + layer = __webpack_require__(130); + + /* If the spec is bad, we still end up with an object, but it won't have a + * zoom function */ + if (!_map || !_map.zoom) { + console.warn('Could not create map.'); + return null; + } + + spec.data = spec.data || []; + spec.layers = spec.layers || []; + + spec.layers.forEach(function (l) { + l.data = l.data || spec.data; + l.layer = layer.create(_map, l); + }); + + return _map; + }; + + inherit(map, sceneObject); + module.exports = map; + + +/***/ }, +/* 147 */ +/***/ function(module, exports, __webpack_require__) { + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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/ + * + * The default transforms lat/long coordinates into web mercator + * for use with standard tile sets. + * + * 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 geo.transform + * @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} + */ + ////////////////////////////////////////////////////////////////////////////// + + var transform = function (options) { + 'use strict'; + if (!(this instanceof transform)) { + return new transform(options); + } + + var proj4 = __webpack_require__(148); + + var m_this = this, + m_proj, // The raw proj4js object + m_source, // The source projection + m_target; // The target projection + + /** + * Generate the internal proj4 object. + * @private + */ + function generate_proj4() { + m_proj = new proj4( + m_this.source(), + m_this.target() + ); + } + + /** + * Get/Set the source projection + */ + this.source = function (arg) { + if (arg === undefined) { + return m_source || 'EPSG:4326'; + } + m_source = arg; + generate_proj4(); + return m_this; + }; + + /** + * Get/Set the target projection + */ + this.target = function (arg) { + if (arg === undefined) { + return m_target || 'EPSG:3857'; + } + m_target = arg; + generate_proj4(); + return m_this; + }; + + /** + * 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) + * + * @returns {object} A point object in the target coordinates + */ + this._forward = function (point) { + var pt = m_proj.forward(point); + pt.z = point.z || 0; + return pt; + }; + + /** + * Perform an inverse transformation (target -> source) + * @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) + * + * @returns {object} A point object in the source coordinates + */ + this._inverse = function (point) { + var pt = m_proj.inverse(point); + pt.z = point.z || 0; + return pt; + }; + + /** + * 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) + * + * @returns {object} A point object or array in the target coordinates + */ + this.forward = function (point) { + if (Array.isArray(point)) { + return point.map(m_this._forward); + } + return m_this._forward(point); + }; + + /** + * 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) + * + * @returns {object} A point object in the source coordinates + */ + this.inverse = function (point) { + if (Array.isArray(point)) { + return point.map(m_this._inverse); + } + return m_this._inverse(point); + }; + + // Set defaults given by the constructor + options = options || {}; + try { + this.source(options.source); + } catch (err) { + console.error('Can\'t use transform source: ' + options.source); + this.source('EPSG:4326'); + } + try { + this.target(options.target); + } catch (err) { + console.error('Can\'t use transform target: ' + options.target); + this.target('EPSG:3857'); + } + + return this; + }; + + /** + * 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 {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 + */ + transform.transformCoordinates = function ( + srcPrj, tgtPrj, coordinates, numberOfComponents) { + 'use strict'; + + if (srcPrj === tgtPrj) { + return coordinates; + } + + var i, count, offset, xAcc, yAcc, zAcc, writer, output, projPoint, + trans = transform({source: srcPrj, target: tgtPrj}); + + /// Default Z accessor + zAcc = function () { + return 0.0; + }; + + /// 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; + + 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; + + 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; + + 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'; + } + } + } + + /// 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; + }; + + 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; + }; + + 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'; + } + } + + if (coordinates instanceof Array) { + output = []; + output.length = coordinates.length; + count = coordinates.length; + + if (coordinates[0] instanceof Array || + coordinates[0] instanceof Object) { + offset = 1; + + 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'; + } + } + + 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); + } + return output; + }; + + /** + * 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 {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 + */ + transform.affineForward = function (def, coords) { + 'use strict'; + 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; + }; + + /** + * 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. + * + * (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 + */ + transform.affineInverse = function (def, coords) { + 'use strict'; + 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; + }; + + module.exports = transform; + + +/***/ }, +/* 148 */ +/***/ function(module, exports, __webpack_require__) { + + var proj4 = __webpack_require__(149); + proj4.defaultDatum = 'WGS84'; //default datum + proj4.Proj = __webpack_require__(150); + proj4.WGS84 = new proj4.Proj('WGS84'); + proj4.Point = __webpack_require__(175); + proj4.toPoint = __webpack_require__(174); + proj4.defs = __webpack_require__(152); + proj4.transform = __webpack_require__(171); + proj4.mgrs = __webpack_require__(176); + proj4.version = __webpack_require__(177).version; + __webpack_require__(178)(proj4); + module.exports = proj4; + +/***/ }, +/* 149 */ +/***/ function(module, exports, __webpack_require__) { + + var proj = __webpack_require__(150); + var transform = __webpack_require__(171); + var wgs84 = proj('WGS84'); + + function transformer(from, to, coords) { + var transformedArray; + if (Array.isArray(coords)) { + transformedArray = transform(from, to, coords); + if (coords.length === 3) { + return [transformedArray.x, transformedArray.y, transformedArray.z]; + } + else { + return [transformedArray.x, transformedArray.y]; + } + } + else { + return transform(from, to, coords); + } + } + + function checkProj(item) { + if (item instanceof proj) { + return item; + } + if (item.oProj) { + return item.oProj; + } + return proj(item); + } + function proj4(fromProj, toProj, coord) { + fromProj = checkProj(fromProj); + var single = false; + var obj; + if (typeof toProj === 'undefined') { + toProj = fromProj; + fromProj = wgs84; + single = true; + } + else if (typeof toProj.x !== 'undefined' || Array.isArray(toProj)) { + coord = toProj; + toProj = fromProj; + fromProj = wgs84; + single = true; + } + toProj = checkProj(toProj); + if (coord) { + return transformer(fromProj, toProj, coord); + } + else { + obj = { + forward: function(coords) { + return transformer(fromProj, toProj, coords); + }, + inverse: function(coords) { + return transformer(toProj, fromProj, coords); + } + }; + if (single) { + obj.oProj = toProj; + } + return obj; + } + } + module.exports = proj4; + +/***/ }, +/* 150 */ +/***/ function(module, exports, __webpack_require__) { + + var parseCode = __webpack_require__(151); + var extend = __webpack_require__(158); + var projections = __webpack_require__(159); + var deriveConstants = __webpack_require__(167); + + function Projection(srsCode,callback) { + if (!(this instanceof Projection)) { + return new Projection(srsCode); + } + callback = callback || function(error){ + if(error){ + throw error; + } + }; + var json = parseCode(srsCode); + if(typeof json !== 'object'){ + callback(srsCode); + return; + } + var modifiedJSON = deriveConstants(json); + var ourProj = Projection.projections.get(modifiedJSON.projName); + if(ourProj){ + extend(this, modifiedJSON); + extend(this, ourProj); + this.init(); + callback(null, this); + }else{ + callback(srsCode); + } + } + Projection.projections = projections; + Projection.projections.start(); + module.exports = Projection; + + +/***/ }, +/* 151 */ +/***/ function(module, exports, __webpack_require__) { + + var defs = __webpack_require__(152); + var wkt = __webpack_require__(157); + var projStr = __webpack_require__(154); + function testObj(code){ + return typeof code === 'string'; + } + function testDef(code){ + return code in defs; + } + function testWKT(code){ + var codeWords = ['GEOGCS','GEOCCS','PROJCS','LOCAL_CS']; + return codeWords.reduce(function(a,b){ + return a+1+code.indexOf(b); + },0); + } + function testProj(code){ + return code[0] === '+'; + } + function parse(code){ + if (testObj(code)) { + //check to see if this is a WKT string + if (testDef(code)) { + return defs[code]; + } + else if (testWKT(code)) { + return wkt(code); + } + else if (testProj(code)) { + return projStr(code); + } + }else{ + return code; + } + } + + module.exports = parse; + +/***/ }, +/* 152 */ +/***/ function(module, exports, __webpack_require__) { + + var globals = __webpack_require__(153); + var parseProj = __webpack_require__(154); + var wkt = __webpack_require__(157); + + function defs(name) { + /*global console*/ + var that = this; + if (arguments.length === 2) { + var def = arguments[1]; + if (typeof def === 'string') { + if (def.charAt(0) === '+') { + defs[name] = parseProj(arguments[1]); + } + else { + defs[name] = wkt(arguments[1]); + } + } else { + defs[name] = def; + } + } + else if (arguments.length === 1) { + if (Array.isArray(name)) { + return name.map(function(v) { + if (Array.isArray(v)) { + defs.apply(that, v); + } + else { + defs(v); + } + }); + } + else if (typeof name === 'string') { + if (name in defs) { + return defs[name]; + } + } + else if ('EPSG' in name) { + defs['EPSG:' + name.EPSG] = name; + } + else if ('ESRI' in name) { + defs['ESRI:' + name.ESRI] = name; + } + else if ('IAU2000' in name) { + defs['IAU2000:' + name.IAU2000] = name; + } + else { + console.log(name); + } + return; + } + + + } + globals(defs); + module.exports = defs; + + +/***/ }, +/* 153 */ +/***/ function(module, exports) { + + module.exports = function(defs) { + defs('EPSG:4326', "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees"); + defs('EPSG:4269', "+title=NAD83 (long/lat) +proj=longlat +a=6378137.0 +b=6356752.31414036 +ellps=GRS80 +datum=NAD83 +units=degrees"); + defs('EPSG:3857', "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs"); + + defs.WGS84 = defs['EPSG:4326']; + defs['EPSG:3785'] = defs['EPSG:3857']; // maintain backward compat, official code is 3857 + defs.GOOGLE = defs['EPSG:3857']; + defs['EPSG:900913'] = defs['EPSG:3857']; + defs['EPSG:102113'] = defs['EPSG:3857']; + }; + + +/***/ }, +/* 154 */ +/***/ function(module, exports, __webpack_require__) { + + var D2R = 0.01745329251994329577; + var PrimeMeridian = __webpack_require__(155); + var units = __webpack_require__(156); + + module.exports = function(defData) { + var self = {}; + var paramObj = {}; + defData.split("+").map(function(v) { + return v.trim(); + }).filter(function(a) { + return a; + }).forEach(function(a) { + var split = a.split("="); + split.push(true); + paramObj[split[0].toLowerCase()] = split[1]; + }); + var paramName, paramVal, paramOutname; + var params = { + proj: 'projName', + datum: 'datumCode', + rf: function(v) { + self.rf = parseFloat(v); + }, + lat_0: function(v) { + self.lat0 = v * D2R; + }, + lat_1: function(v) { + self.lat1 = v * D2R; + }, + lat_2: function(v) { + self.lat2 = v * D2R; + }, + lat_ts: function(v) { + self.lat_ts = v * D2R; + }, + lon_0: function(v) { + self.long0 = v * D2R; + }, + lon_1: function(v) { + self.long1 = v * D2R; + }, + lon_2: function(v) { + self.long2 = v * D2R; + }, + alpha: function(v) { + self.alpha = parseFloat(v) * D2R; + }, + lonc: function(v) { + self.longc = v * D2R; + }, + x_0: function(v) { + self.x0 = parseFloat(v); + }, + y_0: function(v) { + self.y0 = parseFloat(v); + }, + k_0: function(v) { + self.k0 = parseFloat(v); + }, + k: function(v) { + self.k0 = parseFloat(v); + }, + a: function(v) { + self.a = parseFloat(v); + }, + b: function(v) { + self.b = parseFloat(v); + }, + r_a: function() { + self.R_A = true; + }, + zone: function(v) { + self.zone = parseInt(v, 10); + }, + south: function() { + self.utmSouth = true; + }, + towgs84: function(v) { + self.datum_params = v.split(",").map(function(a) { + return parseFloat(a); + }); + }, + to_meter: function(v) { + self.to_meter = parseFloat(v); + }, + units: function(v) { + self.units = v; + if (units[v]) { + self.to_meter = units[v].to_meter; + } + }, + from_greenwich: function(v) { + self.from_greenwich = v * D2R; + }, + pm: function(v) { + self.from_greenwich = (PrimeMeridian[v] ? PrimeMeridian[v] : parseFloat(v)) * D2R; + }, + nadgrids: function(v) { + if (v === '@null') { + self.datumCode = 'none'; + } + else { + self.nadgrids = v; + } + }, + axis: function(v) { + var legalAxis = "ewnsud"; + if (v.length === 3 && legalAxis.indexOf(v.substr(0, 1)) !== -1 && legalAxis.indexOf(v.substr(1, 1)) !== -1 && legalAxis.indexOf(v.substr(2, 1)) !== -1) { + self.axis = v; + } + } + }; + for (paramName in paramObj) { + paramVal = paramObj[paramName]; + if (paramName in params) { + paramOutname = params[paramName]; + if (typeof paramOutname === 'function') { + paramOutname(paramVal); + } + else { + self[paramOutname] = paramVal; + } + } + else { + self[paramName] = paramVal; + } + } + if(typeof self.datumCode === 'string' && self.datumCode !== "WGS84"){ + self.datumCode = self.datumCode.toLowerCase(); + } + return self; + }; + + +/***/ }, +/* 155 */ +/***/ function(module, exports) { + + exports.greenwich = 0.0; //"0dE", + exports.lisbon = -9.131906111111; //"9d07'54.862\"W", + exports.paris = 2.337229166667; //"2d20'14.025\"E", + exports.bogota = -74.080916666667; //"74d04'51.3\"W", + exports.madrid = -3.687938888889; //"3d41'16.58\"W", + exports.rome = 12.452333333333; //"12d27'8.4\"E", + exports.bern = 7.439583333333; //"7d26'22.5\"E", + exports.jakarta = 106.807719444444; //"106d48'27.79\"E", + exports.ferro = -17.666666666667; //"17d40'W", + exports.brussels = 4.367975; //"4d22'4.71\"E", + exports.stockholm = 18.058277777778; //"18d3'29.8\"E", + exports.athens = 23.7163375; //"23d42'58.815\"E", + exports.oslo = 10.722916666667; //"10d43'22.5\"E" + +/***/ }, +/* 156 */ +/***/ function(module, exports) { + + exports.ft = {to_meter: 0.3048}; + exports['us-ft'] = {to_meter: 1200 / 3937}; + + +/***/ }, +/* 157 */ +/***/ function(module, exports, __webpack_require__) { + + var D2R = 0.01745329251994329577; + var extend = __webpack_require__(158); + + function mapit(obj, key, v) { + obj[key] = v.map(function(aa) { + var o = {}; + sExpr(aa, o); + return o; + }).reduce(function(a, b) { + return extend(a, b); + }, {}); + } + + function sExpr(v, obj) { + var key; + if (!Array.isArray(v)) { + obj[v] = true; + return; + } + else { + key = v.shift(); + if (key === 'PARAMETER') { + key = v.shift(); + } + if (v.length === 1) { + if (Array.isArray(v[0])) { + obj[key] = {}; + sExpr(v[0], obj[key]); + } + else { + obj[key] = v[0]; + } + } + else if (!v.length) { + obj[key] = true; + } + else if (key === 'TOWGS84') { + obj[key] = v; + } + else { + obj[key] = {}; + if (['UNIT', 'PRIMEM', 'VERT_DATUM'].indexOf(key) > -1) { + obj[key] = { + name: v[0].toLowerCase(), + convert: v[1] + }; + if (v.length === 3) { + obj[key].auth = v[2]; + } + } + else if (key === 'SPHEROID') { + obj[key] = { + name: v[0], + a: v[1], + rf: v[2] + }; + if (v.length === 4) { + obj[key].auth = v[3]; + } + } + else if (['GEOGCS', 'GEOCCS', 'DATUM', 'VERT_CS', 'COMPD_CS', 'LOCAL_CS', 'FITTED_CS', 'LOCAL_DATUM'].indexOf(key) > -1) { + v[0] = ['name', v[0]]; + mapit(obj, key, v); + } + else if (v.every(function(aa) { + return Array.isArray(aa); + })) { + mapit(obj, key, v); + } + else { + sExpr(v, obj[key]); + } + } + } + } + + function rename(obj, params) { + var outName = params[0]; + var inName = params[1]; + if (!(outName in obj) && (inName in obj)) { + obj[outName] = obj[inName]; + if (params.length === 3) { + obj[outName] = params[2](obj[outName]); + } + } + } + + function d2r(input) { + return input * D2R; + } + + function cleanWKT(wkt) { + if (wkt.type === 'GEOGCS') { + wkt.projName = 'longlat'; + } + else if (wkt.type === 'LOCAL_CS') { + wkt.projName = 'identity'; + wkt.local = true; + } + else { + if (typeof wkt.PROJECTION === "object") { + wkt.projName = Object.keys(wkt.PROJECTION)[0]; + } + else { + wkt.projName = wkt.PROJECTION; + } + } + if (wkt.UNIT) { + wkt.units = wkt.UNIT.name.toLowerCase(); + if (wkt.units === 'metre') { + wkt.units = 'meter'; + } + if (wkt.UNIT.convert) { + if (wkt.type === 'GEOGCS') { + if (wkt.DATUM && wkt.DATUM.SPHEROID) { + wkt.to_meter = parseFloat(wkt.UNIT.convert, 10)*wkt.DATUM.SPHEROID.a; + } + } else { + wkt.to_meter = parseFloat(wkt.UNIT.convert, 10); + } + } + } + + if (wkt.GEOGCS) { + //if(wkt.GEOGCS.PRIMEM&&wkt.GEOGCS.PRIMEM.convert){ + // wkt.from_greenwich=wkt.GEOGCS.PRIMEM.convert*D2R; + //} + if (wkt.GEOGCS.DATUM) { + wkt.datumCode = wkt.GEOGCS.DATUM.name.toLowerCase(); + } + else { + wkt.datumCode = wkt.GEOGCS.name.toLowerCase(); + } + if (wkt.datumCode.slice(0, 2) === 'd_') { + wkt.datumCode = wkt.datumCode.slice(2); + } + if (wkt.datumCode === 'new_zealand_geodetic_datum_1949' || wkt.datumCode === 'new_zealand_1949') { + wkt.datumCode = 'nzgd49'; + } + if (wkt.datumCode === "wgs_1984") { + if (wkt.PROJECTION === 'Mercator_Auxiliary_Sphere') { + wkt.sphere = true; + } + wkt.datumCode = 'wgs84'; + } + if (wkt.datumCode.slice(-6) === '_ferro') { + wkt.datumCode = wkt.datumCode.slice(0, - 6); + } + if (wkt.datumCode.slice(-8) === '_jakarta') { + wkt.datumCode = wkt.datumCode.slice(0, - 8); + } + if (~wkt.datumCode.indexOf('belge')) { + wkt.datumCode = "rnb72"; + } + if (wkt.GEOGCS.DATUM && wkt.GEOGCS.DATUM.SPHEROID) { + wkt.ellps = wkt.GEOGCS.DATUM.SPHEROID.name.replace('_19', '').replace(/[Cc]larke\_18/, 'clrk'); + if (wkt.ellps.toLowerCase().slice(0, 13) === "international") { + wkt.ellps = 'intl'; + } + + wkt.a = wkt.GEOGCS.DATUM.SPHEROID.a; + wkt.rf = parseFloat(wkt.GEOGCS.DATUM.SPHEROID.rf, 10); + } + if (~wkt.datumCode.indexOf('osgb_1936')) { + wkt.datumCode = "osgb36"; + } + } + if (wkt.b && !isFinite(wkt.b)) { + wkt.b = wkt.a; + } + + function toMeter(input) { + var ratio = wkt.to_meter || 1; + return parseFloat(input, 10) * ratio; + } + var renamer = function(a) { + return rename(wkt, a); + }; + var list = [ + ['standard_parallel_1', 'Standard_Parallel_1'], + ['standard_parallel_2', 'Standard_Parallel_2'], + ['false_easting', 'False_Easting'], + ['false_northing', 'False_Northing'], + ['central_meridian', 'Central_Meridian'], + ['latitude_of_origin', 'Latitude_Of_Origin'], + ['latitude_of_origin', 'Central_Parallel'], + ['scale_factor', 'Scale_Factor'], + ['k0', 'scale_factor'], + ['latitude_of_center', 'Latitude_of_center'], + ['lat0', 'latitude_of_center', d2r], + ['longitude_of_center', 'Longitude_Of_Center'], + ['longc', 'longitude_of_center', d2r], + ['x0', 'false_easting', toMeter], + ['y0', 'false_northing', toMeter], + ['long0', 'central_meridian', d2r], + ['lat0', 'latitude_of_origin', d2r], + ['lat0', 'standard_parallel_1', d2r], + ['lat1', 'standard_parallel_1', d2r], + ['lat2', 'standard_parallel_2', d2r], + ['alpha', 'azimuth', d2r], + ['srsCode', 'name'] + ]; + list.forEach(renamer); + if (!wkt.long0 && wkt.longc && (wkt.projName === 'Albers_Conic_Equal_Area' || wkt.projName === "Lambert_Azimuthal_Equal_Area")) { + wkt.long0 = wkt.longc; + } + if (!wkt.lat_ts && wkt.lat1 && (wkt.projName === 'Stereographic_South_Pole' || wkt.projName === 'Polar Stereographic (variant B)')) { + wkt.lat0 = d2r(wkt.lat1 > 0 ? 90 : -90); + wkt.lat_ts = wkt.lat1; + } + } + module.exports = function(wkt, self) { + var lisp = JSON.parse(("," + wkt).replace(/\s*\,\s*([A-Z_0-9]+?)(\[)/g, ',["$1",').slice(1).replace(/\s*\,\s*([A-Z_0-9]+?)\]/g, ',"$1"]').replace(/,\["VERTCS".+/,'')); + var type = lisp.shift(); + var name = lisp.shift(); + lisp.unshift(['name', name]); + lisp.unshift(['type', type]); + lisp.unshift('output'); + var obj = {}; + sExpr(lisp, obj); + cleanWKT(obj.output); + return extend(self, obj.output); + }; + + +/***/ }, +/* 158 */ +/***/ function(module, exports) { + + module.exports = function(destination, source) { + destination = destination || {}; + var value, property; + if (!source) { + return destination; + } + for (property in source) { + value = source[property]; + if (value !== undefined) { + destination[property] = value; + } + } + return destination; + }; + + +/***/ }, +/* 159 */ +/***/ function(module, exports, __webpack_require__) { + + var projs = [ + __webpack_require__(160), + __webpack_require__(166) + ]; + var names = {}; + var projStore = []; + + function add(proj, i) { + var len = projStore.length; + if (!proj.names) { + console.log(i); + return true; + } + projStore[len] = proj; + proj.names.forEach(function(n) { + names[n.toLowerCase()] = len; + }); + return this; + } + + exports.add = add; + + exports.get = function(name) { + if (!name) { + return false; + } + var n = name.toLowerCase(); + if (typeof names[n] !== 'undefined' && projStore[names[n]]) { + return projStore[names[n]]; + } + }; + exports.start = function() { + projs.forEach(add); + }; + + +/***/ }, +/* 160 */ +/***/ function(module, exports, __webpack_require__) { + + var msfnz = __webpack_require__(161); + var HALF_PI = Math.PI/2; + var EPSLN = 1.0e-10; + var R2D = 57.29577951308232088; + var adjust_lon = __webpack_require__(162); + var FORTPI = Math.PI/4; + var tsfnz = __webpack_require__(164); + var phi2z = __webpack_require__(165); + exports.init = function() { + var con = this.b / this.a; + this.es = 1 - con * con; + if(!('x0' in this)){ + this.x0 = 0; + } + if(!('y0' in this)){ + this.y0 = 0; + } + this.e = Math.sqrt(this.es); + if (this.lat_ts) { + if (this.sphere) { + this.k0 = Math.cos(this.lat_ts); + } + else { + this.k0 = msfnz(this.e, Math.sin(this.lat_ts), Math.cos(this.lat_ts)); + } + } + else { + if (!this.k0) { + if (this.k) { + this.k0 = this.k; + } + else { + this.k0 = 1; + } + } + } + }; + + /* Mercator forward equations--mapping lat,long to x,y + --------------------------------------------------*/ + + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + // convert to radians + if (lat * R2D > 90 && lat * R2D < -90 && lon * R2D > 180 && lon * R2D < -180) { + return null; + } + + var x, y; + if (Math.abs(Math.abs(lat) - HALF_PI) <= EPSLN) { + return null; + } + else { + if (this.sphere) { + x = this.x0 + this.a * this.k0 * adjust_lon(lon - this.long0); + y = this.y0 + this.a * this.k0 * Math.log(Math.tan(FORTPI + 0.5 * lat)); + } + else { + var sinphi = Math.sin(lat); + var ts = tsfnz(this.e, lat, sinphi); + x = this.x0 + this.a * this.k0 * adjust_lon(lon - this.long0); + y = this.y0 - this.a * this.k0 * Math.log(ts); + } + p.x = x; + p.y = y; + return p; + } + }; + + + /* Mercator inverse equations--mapping x,y to lat/long + --------------------------------------------------*/ + exports.inverse = function(p) { + + var x = p.x - this.x0; + var y = p.y - this.y0; + var lon, lat; + + if (this.sphere) { + lat = HALF_PI - 2 * Math.atan(Math.exp(-y / (this.a * this.k0))); + } + else { + var ts = Math.exp(-y / (this.a * this.k0)); + lat = phi2z(this.e, ts); + if (lat === -9999) { + return null; + } + } + lon = adjust_lon(this.long0 + x / (this.a * this.k0)); + + p.x = lon; + p.y = lat; + return p; + }; + + exports.names = ["Mercator", "Popular Visualisation Pseudo Mercator", "Mercator_1SP", "Mercator_Auxiliary_Sphere", "merc"]; + + +/***/ }, +/* 161 */ +/***/ function(module, exports) { + + module.exports = function(eccent, sinphi, cosphi) { + var con = eccent * sinphi; + return cosphi / (Math.sqrt(1 - con * con)); + }; + +/***/ }, +/* 162 */ +/***/ function(module, exports, __webpack_require__) { + + var TWO_PI = Math.PI * 2; + // SPI is slightly greater than Math.PI, so values that exceed the -180..180 + // degree range by a tiny amount don't get wrapped. This prevents points that + // have drifted from their original location along the 180th meridian (due to + // floating point error) from changing their sign. + var SPI = 3.14159265359; + var sign = __webpack_require__(163); + + module.exports = function(x) { + return (Math.abs(x) <= SPI) ? x : (x - (sign(x) * TWO_PI)); + }; + +/***/ }, +/* 163 */ +/***/ function(module, exports) { + + module.exports = function(x) { + return x<0 ? -1 : 1; + }; + +/***/ }, +/* 164 */ +/***/ function(module, exports) { + + var HALF_PI = Math.PI/2; + + module.exports = function(eccent, phi, sinphi) { + var con = eccent * sinphi; + var com = 0.5 * eccent; + con = Math.pow(((1 - con) / (1 + con)), com); + return (Math.tan(0.5 * (HALF_PI - phi)) / con); + }; + +/***/ }, +/* 165 */ +/***/ function(module, exports) { + + var HALF_PI = Math.PI/2; + module.exports = function(eccent, ts) { + var eccnth = 0.5 * eccent; + var con, dphi; + var phi = HALF_PI - 2 * Math.atan(ts); + for (var i = 0; i <= 15; i++) { + con = eccent * Math.sin(phi); + dphi = HALF_PI - 2 * Math.atan(ts * (Math.pow(((1 - con) / (1 + con)), eccnth))) - phi; + phi += dphi; + if (Math.abs(dphi) <= 0.0000000001) { + return phi; + } + } + //console.log("phi2z has NoConvergence"); + return -9999; + }; + +/***/ }, +/* 166 */ +/***/ function(module, exports) { + + exports.init = function() { + //no-op for longlat + }; + + function identity(pt) { + return pt; + } + exports.forward = identity; + exports.inverse = identity; + exports.names = ["longlat", "identity"]; + + +/***/ }, +/* 167 */ +/***/ function(module, exports, __webpack_require__) { + + var Datum = __webpack_require__(168); + var Ellipsoid = __webpack_require__(169); + var extend = __webpack_require__(158); + var datum = __webpack_require__(170); + var EPSLN = 1.0e-10; + // ellipoid pj_set_ell.c + var SIXTH = 0.1666666666666666667; + /* 1/6 */ + var RA4 = 0.04722222222222222222; + /* 17/360 */ + var RA6 = 0.02215608465608465608; + module.exports = function(json) { + // DGR 2011-03-20 : nagrids -> nadgrids + if (json.datumCode && json.datumCode !== 'none') { + var datumDef = Datum[json.datumCode]; + if (datumDef) { + json.datum_params = datumDef.towgs84 ? datumDef.towgs84.split(',') : null; + json.ellps = datumDef.ellipse; + json.datumName = datumDef.datumName ? datumDef.datumName : json.datumCode; + } + } + if (!json.a) { // do we have an ellipsoid? + var ellipse = Ellipsoid[json.ellps] ? Ellipsoid[json.ellps] : Ellipsoid.WGS84; + extend(json, ellipse); + } + if (json.rf && !json.b) { + json.b = (1.0 - 1.0 / json.rf) * json.a; + } + if (json.rf === 0 || Math.abs(json.a - json.b) < EPSLN) { + json.sphere = true; + json.b = json.a; + } + json.a2 = json.a * json.a; // used in geocentric + json.b2 = json.b * json.b; // used in geocentric + json.es = (json.a2 - json.b2) / json.a2; // e ^ 2 + json.e = Math.sqrt(json.es); // eccentricity + if (json.R_A) { + json.a *= 1 - json.es * (SIXTH + json.es * (RA4 + json.es * RA6)); + json.a2 = json.a * json.a; + json.b2 = json.b * json.b; + json.es = 0; + } + json.ep2 = (json.a2 - json.b2) / json.b2; // used in geocentric + if (!json.k0) { + json.k0 = 1.0; //default value + } + //DGR 2010-11-12: axis + if (!json.axis) { + json.axis = "enu"; + } + + if (!json.datum) { + json.datum = datum(json); + } + return json; + }; + + +/***/ }, +/* 168 */ +/***/ function(module, exports) { + + exports.wgs84 = { + towgs84: "0,0,0", + ellipse: "WGS84", + datumName: "WGS84" + }; + exports.ch1903 = { + towgs84: "674.374,15.056,405.346", + ellipse: "bessel", + datumName: "swiss" + }; + exports.ggrs87 = { + towgs84: "-199.87,74.79,246.62", + ellipse: "GRS80", + datumName: "Greek_Geodetic_Reference_System_1987" + }; + exports.nad83 = { + towgs84: "0,0,0", + ellipse: "GRS80", + datumName: "North_American_Datum_1983" + }; + exports.nad27 = { + nadgrids: "@conus,@alaska,@ntv2_0.gsb,@ntv1_can.dat", + ellipse: "clrk66", + datumName: "North_American_Datum_1927" + }; + exports.potsdam = { + towgs84: "606.0,23.0,413.0", + ellipse: "bessel", + datumName: "Potsdam Rauenberg 1950 DHDN" + }; + exports.carthage = { + towgs84: "-263.0,6.0,431.0", + ellipse: "clark80", + datumName: "Carthage 1934 Tunisia" + }; + exports.hermannskogel = { + towgs84: "653.0,-212.0,449.0", + ellipse: "bessel", + datumName: "Hermannskogel" + }; + exports.ire65 = { + towgs84: "482.530,-130.596,564.557,-1.042,-0.214,-0.631,8.15", + ellipse: "mod_airy", + datumName: "Ireland 1965" + }; + exports.rassadiran = { + towgs84: "-133.63,-157.5,-158.62", + ellipse: "intl", + datumName: "Rassadiran" + }; + exports.nzgd49 = { + towgs84: "59.47,-5.04,187.44,0.47,-0.1,1.024,-4.5993", + ellipse: "intl", + datumName: "New Zealand Geodetic Datum 1949" + }; + exports.osgb36 = { + towgs84: "446.448,-125.157,542.060,0.1502,0.2470,0.8421,-20.4894", + ellipse: "airy", + datumName: "Airy 1830" + }; + exports.s_jtsk = { + towgs84: "589,76,480", + ellipse: 'bessel', + datumName: 'S-JTSK (Ferro)' + }; + exports.beduaram = { + towgs84: '-106,-87,188', + ellipse: 'clrk80', + datumName: 'Beduaram' + }; + exports.gunung_segara = { + towgs84: '-403,684,41', + ellipse: 'bessel', + datumName: 'Gunung Segara Jakarta' + }; + exports.rnb72 = { + towgs84: "106.869,-52.2978,103.724,-0.33657,0.456955,-1.84218,1", + ellipse: "intl", + datumName: "Reseau National Belge 1972" + }; + +/***/ }, +/* 169 */ +/***/ function(module, exports) { + + exports.MERIT = { + a: 6378137.0, + rf: 298.257, + ellipseName: "MERIT 1983" + }; + exports.SGS85 = { + a: 6378136.0, + rf: 298.257, + ellipseName: "Soviet Geodetic System 85" + }; + exports.GRS80 = { + a: 6378137.0, + rf: 298.257222101, + ellipseName: "GRS 1980(IUGG, 1980)" + }; + exports.IAU76 = { + a: 6378140.0, + rf: 298.257, + ellipseName: "IAU 1976" + }; + exports.airy = { + a: 6377563.396, + b: 6356256.910, + ellipseName: "Airy 1830" + }; + exports.APL4 = { + a: 6378137, + rf: 298.25, + ellipseName: "Appl. Physics. 1965" + }; + exports.NWL9D = { + a: 6378145.0, + rf: 298.25, + ellipseName: "Naval Weapons Lab., 1965" + }; + exports.mod_airy = { + a: 6377340.189, + b: 6356034.446, + ellipseName: "Modified Airy" + }; + exports.andrae = { + a: 6377104.43, + rf: 300.0, + ellipseName: "Andrae 1876 (Den., Iclnd.)" + }; + exports.aust_SA = { + a: 6378160.0, + rf: 298.25, + ellipseName: "Australian Natl & S. Amer. 1969" + }; + exports.GRS67 = { + a: 6378160.0, + rf: 298.2471674270, + ellipseName: "GRS 67(IUGG 1967)" + }; + exports.bessel = { + a: 6377397.155, + rf: 299.1528128, + ellipseName: "Bessel 1841" + }; + exports.bess_nam = { + a: 6377483.865, + rf: 299.1528128, + ellipseName: "Bessel 1841 (Namibia)" + }; + exports.clrk66 = { + a: 6378206.4, + b: 6356583.8, + ellipseName: "Clarke 1866" + }; + exports.clrk80 = { + a: 6378249.145, + rf: 293.4663, + ellipseName: "Clarke 1880 mod." + }; + exports.clrk58 = { + a: 6378293.645208759, + rf: 294.2606763692654, + ellipseName: "Clarke 1858" + }; + exports.CPM = { + a: 6375738.7, + rf: 334.29, + ellipseName: "Comm. des Poids et Mesures 1799" + }; + exports.delmbr = { + a: 6376428.0, + rf: 311.5, + ellipseName: "Delambre 1810 (Belgium)" + }; + exports.engelis = { + a: 6378136.05, + rf: 298.2566, + ellipseName: "Engelis 1985" + }; + exports.evrst30 = { + a: 6377276.345, + rf: 300.8017, + ellipseName: "Everest 1830" + }; + exports.evrst48 = { + a: 6377304.063, + rf: 300.8017, + ellipseName: "Everest 1948" + }; + exports.evrst56 = { + a: 6377301.243, + rf: 300.8017, + ellipseName: "Everest 1956" + }; + exports.evrst69 = { + a: 6377295.664, + rf: 300.8017, + ellipseName: "Everest 1969" + }; + exports.evrstSS = { + a: 6377298.556, + rf: 300.8017, + ellipseName: "Everest (Sabah & Sarawak)" + }; + exports.fschr60 = { + a: 6378166.0, + rf: 298.3, + ellipseName: "Fischer (Mercury Datum) 1960" + }; + exports.fschr60m = { + a: 6378155.0, + rf: 298.3, + ellipseName: "Fischer 1960" + }; + exports.fschr68 = { + a: 6378150.0, + rf: 298.3, + ellipseName: "Fischer 1968" + }; + exports.helmert = { + a: 6378200.0, + rf: 298.3, + ellipseName: "Helmert 1906" + }; + exports.hough = { + a: 6378270.0, + rf: 297.0, + ellipseName: "Hough" + }; + exports.intl = { + a: 6378388.0, + rf: 297.0, + ellipseName: "International 1909 (Hayford)" + }; + exports.kaula = { + a: 6378163.0, + rf: 298.24, + ellipseName: "Kaula 1961" + }; + exports.lerch = { + a: 6378139.0, + rf: 298.257, + ellipseName: "Lerch 1979" + }; + exports.mprts = { + a: 6397300.0, + rf: 191.0, + ellipseName: "Maupertius 1738" + }; + exports.new_intl = { + a: 6378157.5, + b: 6356772.2, + ellipseName: "New International 1967" + }; + exports.plessis = { + a: 6376523.0, + rf: 6355863.0, + ellipseName: "Plessis 1817 (France)" + }; + exports.krass = { + a: 6378245.0, + rf: 298.3, + ellipseName: "Krassovsky, 1942" + }; + exports.SEasia = { + a: 6378155.0, + b: 6356773.3205, + ellipseName: "Southeast Asia" + }; + exports.walbeck = { + a: 6376896.0, + b: 6355834.8467, + ellipseName: "Walbeck" + }; + exports.WGS60 = { + a: 6378165.0, + rf: 298.3, + ellipseName: "WGS 60" + }; + exports.WGS66 = { + a: 6378145.0, + rf: 298.25, + ellipseName: "WGS 66" + }; + exports.WGS7 = { + a: 6378135.0, + rf: 298.26, + ellipseName: "WGS 72" + }; + exports.WGS84 = { + a: 6378137.0, + rf: 298.257223563, + ellipseName: "WGS 84" + }; + exports.sphere = { + a: 6370997.0, + b: 6370997.0, + ellipseName: "Normal Sphere (r=6370997)" + }; + +/***/ }, +/* 170 */ +/***/ function(module, exports) { + + var HALF_PI = Math.PI/2; + var PJD_3PARAM = 1; + var PJD_7PARAM = 2; + var PJD_GRIDSHIFT = 3; + var PJD_WGS84 = 4; // WGS84 or equivalent + var PJD_NODATUM = 5; // WGS84 or equivalent + var SEC_TO_RAD = 4.84813681109535993589914102357e-6; + var AD_C = 1.0026000; + var COS_67P5 = 0.38268343236508977; + var datum = function(proj) { + if (!(this instanceof datum)) { + return new datum(proj); + } + this.datum_type = PJD_WGS84; //default setting + if (!proj) { + return; + } + if (proj.datumCode && proj.datumCode === 'none') { + this.datum_type = PJD_NODATUM; + } + + if (proj.datum_params) { + this.datum_params = proj.datum_params.map(parseFloat); + if (this.datum_params[0] !== 0 || this.datum_params[1] !== 0 || this.datum_params[2] !== 0) { + this.datum_type = PJD_3PARAM; + } + if (this.datum_params.length > 3) { + if (this.datum_params[3] !== 0 || this.datum_params[4] !== 0 || this.datum_params[5] !== 0 || this.datum_params[6] !== 0) { + this.datum_type = PJD_7PARAM; + this.datum_params[3] *= SEC_TO_RAD; + this.datum_params[4] *= SEC_TO_RAD; + this.datum_params[5] *= SEC_TO_RAD; + this.datum_params[6] = (this.datum_params[6] / 1000000.0) + 1.0; + } + } + } + + // DGR 2011-03-21 : nadgrids support + this.datum_type = proj.grids ? PJD_GRIDSHIFT : this.datum_type; + + this.a = proj.a; //datum object also uses these values + this.b = proj.b; + this.es = proj.es; + this.ep2 = proj.ep2; + if (this.datum_type === PJD_GRIDSHIFT) { + this.grids = proj.grids; + } + }; + datum.prototype = { + + + /****************************************************************/ + // cs_compare_datums() + // Returns TRUE if the two datums match, otherwise FALSE. + compare_datums: function(dest) { + if (this.datum_type !== dest.datum_type) { + return false; // false, datums are not equal + } + else if (this.a !== dest.a || Math.abs(this.es - dest.es) > 0.000000000050) { + // the tolerence for es is to ensure that GRS80 and WGS84 + // are considered identical + return false; + } + else if (this.datum_type === PJD_3PARAM) { + return (this.datum_params[0] === dest.datum_params[0] && this.datum_params[1] === dest.datum_params[1] && this.datum_params[2] === dest.datum_params[2]); + } + else if (this.datum_type === PJD_7PARAM) { + return (this.datum_params[0] === dest.datum_params[0] && this.datum_params[1] === dest.datum_params[1] && this.datum_params[2] === dest.datum_params[2] && this.datum_params[3] === dest.datum_params[3] && this.datum_params[4] === dest.datum_params[4] && this.datum_params[5] === dest.datum_params[5] && this.datum_params[6] === dest.datum_params[6]); + } + else if (this.datum_type === PJD_GRIDSHIFT || dest.datum_type === PJD_GRIDSHIFT) { + //alert("ERROR: Grid shift transformations are not implemented."); + //return false + //DGR 2012-07-29 lazy ... + return this.nadgrids === dest.nadgrids; + } + else { + return true; // datums are equal + } + }, // cs_compare_datums() + + /* + * The function Convert_Geodetic_To_Geocentric converts geodetic coordinates + * (latitude, longitude, and height) to geocentric coordinates (X, Y, Z), + * according to the current ellipsoid parameters. + * + * Latitude : Geodetic latitude in radians (input) + * Longitude : Geodetic longitude in radians (input) + * Height : Geodetic height, in meters (input) + * X : Calculated Geocentric X coordinate, in meters (output) + * Y : Calculated Geocentric Y coordinate, in meters (output) + * Z : Calculated Geocentric Z coordinate, in meters (output) + * + */ + geodetic_to_geocentric: function(p) { + var Longitude = p.x; + var Latitude = p.y; + var Height = p.z ? p.z : 0; //Z value not always supplied + var X; // output + var Y; + var Z; + + var Error_Code = 0; // GEOCENT_NO_ERROR; + var Rn; /* Earth radius at location */ + var Sin_Lat; /* Math.sin(Latitude) */ + var Sin2_Lat; /* Square of Math.sin(Latitude) */ + var Cos_Lat; /* Math.cos(Latitude) */ + + /* + ** Don't blow up if Latitude is just a little out of the value + ** range as it may just be a rounding issue. Also removed longitude + ** test, it should be wrapped by Math.cos() and Math.sin(). NFW for PROJ.4, Sep/2001. + */ + if (Latitude < -HALF_PI && Latitude > -1.001 * HALF_PI) { + Latitude = -HALF_PI; + } + else if (Latitude > HALF_PI && Latitude < 1.001 * HALF_PI) { + Latitude = HALF_PI; + } + else if ((Latitude < -HALF_PI) || (Latitude > HALF_PI)) { + /* Latitude out of range */ + //..reportError('geocent:lat out of range:' + Latitude); + return null; + } + + if (Longitude > Math.PI) { + Longitude -= (2 * Math.PI); + } + Sin_Lat = Math.sin(Latitude); + Cos_Lat = Math.cos(Latitude); + Sin2_Lat = Sin_Lat * Sin_Lat; + Rn = this.a / (Math.sqrt(1.0e0 - this.es * Sin2_Lat)); + X = (Rn + Height) * Cos_Lat * Math.cos(Longitude); + Y = (Rn + Height) * Cos_Lat * Math.sin(Longitude); + Z = ((Rn * (1 - this.es)) + Height) * Sin_Lat; + + p.x = X; + p.y = Y; + p.z = Z; + return Error_Code; + }, // cs_geodetic_to_geocentric() + + + geocentric_to_geodetic: function(p) { + /* local defintions and variables */ + /* end-criterium of loop, accuracy of sin(Latitude) */ + var genau = 1e-12; + var genau2 = (genau * genau); + var maxiter = 30; + + var P; /* distance between semi-minor axis and location */ + var RR; /* distance between center and location */ + var CT; /* sin of geocentric latitude */ + var ST; /* cos of geocentric latitude */ + var RX; + var RK; + var RN; /* Earth radius at location */ + var CPHI0; /* cos of start or old geodetic latitude in iterations */ + var SPHI0; /* sin of start or old geodetic latitude in iterations */ + var CPHI; /* cos of searched geodetic latitude */ + var SPHI; /* sin of searched geodetic latitude */ + var SDPHI; /* end-criterium: addition-theorem of sin(Latitude(iter)-Latitude(iter-1)) */ + var At_Pole; /* indicates location is in polar region */ + var iter; /* # of continous iteration, max. 30 is always enough (s.a.) */ + + var X = p.x; + var Y = p.y; + var Z = p.z ? p.z : 0.0; //Z value not always supplied + var Longitude; + var Latitude; + var Height; + + At_Pole = false; + P = Math.sqrt(X * X + Y * Y); + RR = Math.sqrt(X * X + Y * Y + Z * Z); + + /* special cases for latitude and longitude */ + if (P / this.a < genau) { + + /* special case, if P=0. (X=0., Y=0.) */ + At_Pole = true; + Longitude = 0.0; + + /* if (X,Y,Z)=(0.,0.,0.) then Height becomes semi-minor axis + * of ellipsoid (=center of mass), Latitude becomes PI/2 */ + if (RR / this.a < genau) { + Latitude = HALF_PI; + Height = -this.b; + return; + } + } + else { + /* ellipsoidal (geodetic) longitude + * interval: -PI < Longitude <= +PI */ + Longitude = Math.atan2(Y, X); + } + + /* -------------------------------------------------------------- + * Following iterative algorithm was developped by + * "Institut for Erdmessung", University of Hannover, July 1988. + * Internet: www.ife.uni-hannover.de + * Iterative computation of CPHI,SPHI and Height. + * Iteration of CPHI and SPHI to 10**-12 radian resp. + * 2*10**-7 arcsec. + * -------------------------------------------------------------- + */ + CT = Z / RR; + ST = P / RR; + RX = 1.0 / Math.sqrt(1.0 - this.es * (2.0 - this.es) * ST * ST); + CPHI0 = ST * (1.0 - this.es) * RX; + SPHI0 = CT * RX; + iter = 0; + + /* loop to find sin(Latitude) resp. Latitude + * until |sin(Latitude(iter)-Latitude(iter-1))| < genau */ + do { + iter++; + RN = this.a / Math.sqrt(1.0 - this.es * SPHI0 * SPHI0); + + /* ellipsoidal (geodetic) height */ + Height = P * CPHI0 + Z * SPHI0 - RN * (1.0 - this.es * SPHI0 * SPHI0); + + RK = this.es * RN / (RN + Height); + RX = 1.0 / Math.sqrt(1.0 - RK * (2.0 - RK) * ST * ST); + CPHI = ST * (1.0 - RK) * RX; + SPHI = CT * RX; + SDPHI = SPHI * CPHI0 - CPHI * SPHI0; + CPHI0 = CPHI; + SPHI0 = SPHI; + } + while (SDPHI * SDPHI > genau2 && iter < maxiter); + + /* ellipsoidal (geodetic) latitude */ + Latitude = Math.atan(SPHI / Math.abs(CPHI)); + + p.x = Longitude; + p.y = Latitude; + p.z = Height; + return p; + }, // cs_geocentric_to_geodetic() + + /** Convert_Geocentric_To_Geodetic + * The method used here is derived from 'An Improved Algorithm for + * Geocentric to Geodetic Coordinate Conversion', by Ralph Toms, Feb 1996 + */ + geocentric_to_geodetic_noniter: function(p) { + var X = p.x; + var Y = p.y; + var Z = p.z ? p.z : 0; //Z value not always supplied + var Longitude; + var Latitude; + var Height; + + var W; /* distance from Z axis */ + var W2; /* square of distance from Z axis */ + var T0; /* initial estimate of vertical component */ + var T1; /* corrected estimate of vertical component */ + var S0; /* initial estimate of horizontal component */ + var S1; /* corrected estimate of horizontal component */ + var Sin_B0; /* Math.sin(B0), B0 is estimate of Bowring aux variable */ + var Sin3_B0; /* cube of Math.sin(B0) */ + var Cos_B0; /* Math.cos(B0) */ + var Sin_p1; /* Math.sin(phi1), phi1 is estimated latitude */ + var Cos_p1; /* Math.cos(phi1) */ + var Rn; /* Earth radius at location */ + var Sum; /* numerator of Math.cos(phi1) */ + var At_Pole; /* indicates location is in polar region */ + + X = parseFloat(X); // cast from string to float + Y = parseFloat(Y); + Z = parseFloat(Z); + + At_Pole = false; + if (X !== 0.0) { + Longitude = Math.atan2(Y, X); + } + else { + if (Y > 0) { + Longitude = HALF_PI; + } + else if (Y < 0) { + Longitude = -HALF_PI; + } + else { + At_Pole = true; + Longitude = 0.0; + if (Z > 0.0) { /* north pole */ + Latitude = HALF_PI; + } + else if (Z < 0.0) { /* south pole */ + Latitude = -HALF_PI; + } + else { /* center of earth */ + Latitude = HALF_PI; + Height = -this.b; + return; + } + } + } + W2 = X * X + Y * Y; + W = Math.sqrt(W2); + T0 = Z * AD_C; + S0 = Math.sqrt(T0 * T0 + W2); + Sin_B0 = T0 / S0; + Cos_B0 = W / S0; + Sin3_B0 = Sin_B0 * Sin_B0 * Sin_B0; + T1 = Z + this.b * this.ep2 * Sin3_B0; + Sum = W - this.a * this.es * Cos_B0 * Cos_B0 * Cos_B0; + S1 = Math.sqrt(T1 * T1 + Sum * Sum); + Sin_p1 = T1 / S1; + Cos_p1 = Sum / S1; + Rn = this.a / Math.sqrt(1.0 - this.es * Sin_p1 * Sin_p1); + if (Cos_p1 >= COS_67P5) { + Height = W / Cos_p1 - Rn; + } + else if (Cos_p1 <= -COS_67P5) { + Height = W / -Cos_p1 - Rn; + } + else { + Height = Z / Sin_p1 + Rn * (this.es - 1.0); + } + if (At_Pole === false) { + Latitude = Math.atan(Sin_p1 / Cos_p1); + } + + p.x = Longitude; + p.y = Latitude; + p.z = Height; + return p; + }, // geocentric_to_geodetic_noniter() + + /****************************************************************/ + // pj_geocentic_to_wgs84( p ) + // p = point to transform in geocentric coordinates (x,y,z) + geocentric_to_wgs84: function(p) { + + if (this.datum_type === PJD_3PARAM) { + // if( x[io] === HUGE_VAL ) + // continue; + p.x += this.datum_params[0]; + p.y += this.datum_params[1]; + p.z += this.datum_params[2]; + + } + else if (this.datum_type === PJD_7PARAM) { + var Dx_BF = this.datum_params[0]; + var Dy_BF = this.datum_params[1]; + var Dz_BF = this.datum_params[2]; + var Rx_BF = this.datum_params[3]; + var Ry_BF = this.datum_params[4]; + var Rz_BF = this.datum_params[5]; + var M_BF = this.datum_params[6]; + // if( x[io] === HUGE_VAL ) + // continue; + var x_out = M_BF * (p.x - Rz_BF * p.y + Ry_BF * p.z) + Dx_BF; + var y_out = M_BF * (Rz_BF * p.x + p.y - Rx_BF * p.z) + Dy_BF; + var z_out = M_BF * (-Ry_BF * p.x + Rx_BF * p.y + p.z) + Dz_BF; + p.x = x_out; + p.y = y_out; + p.z = z_out; + } + }, // cs_geocentric_to_wgs84 + + /****************************************************************/ + // pj_geocentic_from_wgs84() + // coordinate system definition, + // point to transform in geocentric coordinates (x,y,z) + geocentric_from_wgs84: function(p) { + + if (this.datum_type === PJD_3PARAM) { + //if( x[io] === HUGE_VAL ) + // continue; + p.x -= this.datum_params[0]; + p.y -= this.datum_params[1]; + p.z -= this.datum_params[2]; + + } + else if (this.datum_type === PJD_7PARAM) { + var Dx_BF = this.datum_params[0]; + var Dy_BF = this.datum_params[1]; + var Dz_BF = this.datum_params[2]; + var Rx_BF = this.datum_params[3]; + var Ry_BF = this.datum_params[4]; + var Rz_BF = this.datum_params[5]; + var M_BF = this.datum_params[6]; + var x_tmp = (p.x - Dx_BF) / M_BF; + var y_tmp = (p.y - Dy_BF) / M_BF; + var z_tmp = (p.z - Dz_BF) / M_BF; + //if( x[io] === HUGE_VAL ) + // continue; + + p.x = x_tmp + Rz_BF * y_tmp - Ry_BF * z_tmp; + p.y = -Rz_BF * x_tmp + y_tmp + Rx_BF * z_tmp; + p.z = Ry_BF * x_tmp - Rx_BF * y_tmp + z_tmp; + } //cs_geocentric_from_wgs84() + } + }; + + /** point object, nothing fancy, just allows values to be + passed back and forth by reference rather than by value. + Other point classes may be used as long as they have + x and y properties, which will get modified in the transform method. + */ + module.exports = datum; + + +/***/ }, +/* 171 */ +/***/ function(module, exports, __webpack_require__) { + + var D2R = 0.01745329251994329577; + var R2D = 57.29577951308232088; + var PJD_3PARAM = 1; + var PJD_7PARAM = 2; + var datum_transform = __webpack_require__(172); + var adjust_axis = __webpack_require__(173); + var proj = __webpack_require__(150); + var toPoint = __webpack_require__(174); + module.exports = function transform(source, dest, point) { + var wgs84; + if (Array.isArray(point)) { + point = toPoint(point); + } + function checkNotWGS(source, dest) { + return ((source.datum.datum_type === PJD_3PARAM || source.datum.datum_type === PJD_7PARAM) && dest.datumCode !== "WGS84"); + } + + // Workaround for datum shifts towgs84, if either source or destination projection is not wgs84 + if (source.datum && dest.datum && (checkNotWGS(source, dest) || checkNotWGS(dest, source))) { + wgs84 = new proj('WGS84'); + transform(source, wgs84, point); + source = wgs84; + } + // DGR, 2010/11/12 + if (source.axis !== "enu") { + adjust_axis(source, false, point); + } + // Transform source points to long/lat, if they aren't already. + if (source.projName === "longlat") { + point.x *= D2R; // convert degrees to radians + point.y *= D2R; + } + else { + if (source.to_meter) { + point.x *= source.to_meter; + point.y *= source.to_meter; + } + source.inverse(point); // Convert Cartesian to longlat + } + // Adjust for the prime meridian if necessary + if (source.from_greenwich) { + point.x += source.from_greenwich; + } + + // Convert datums if needed, and if possible. + point = datum_transform(source.datum, dest.datum, point); + + // Adjust for the prime meridian if necessary + if (dest.from_greenwich) { + point.x -= dest.from_greenwich; + } + + if (dest.projName === "longlat") { + // convert radians to decimal degrees + point.x *= R2D; + point.y *= R2D; + } + else { // else project + dest.forward(point); + if (dest.to_meter) { + point.x /= dest.to_meter; + point.y /= dest.to_meter; + } + } + + // DGR, 2010/11/12 + if (dest.axis !== "enu") { + adjust_axis(dest, true, point); + } + + return point; + }; + +/***/ }, +/* 172 */ +/***/ function(module, exports) { + + var PJD_3PARAM = 1; + var PJD_7PARAM = 2; + var PJD_GRIDSHIFT = 3; + var PJD_NODATUM = 5; // WGS84 or equivalent + var SRS_WGS84_SEMIMAJOR = 6378137; // only used in grid shift transforms + var SRS_WGS84_ESQUARED = 0.006694379990141316; //DGR: 2012-07-29 + module.exports = function(source, dest, point) { + var wp, i, l; + + function checkParams(fallback) { + return (fallback === PJD_3PARAM || fallback === PJD_7PARAM); + } + // Short cut if the datums are identical. + if (source.compare_datums(dest)) { + return point; // in this case, zero is sucess, + // whereas cs_compare_datums returns 1 to indicate TRUE + // confusing, should fix this + } + + // Explicitly skip datum transform by setting 'datum=none' as parameter for either source or dest + if (source.datum_type === PJD_NODATUM || dest.datum_type === PJD_NODATUM) { + return point; + } + + //DGR: 2012-07-29 : add nadgrids support (begin) + var src_a = source.a; + var src_es = source.es; + + var dst_a = dest.a; + var dst_es = dest.es; + + var fallback = source.datum_type; + // If this datum requires grid shifts, then apply it to geodetic coordinates. + if (fallback === PJD_GRIDSHIFT) { + if (this.apply_gridshift(source, 0, point) === 0) { + source.a = SRS_WGS84_SEMIMAJOR; + source.es = SRS_WGS84_ESQUARED; + } + else { + // try 3 or 7 params transformation or nothing ? + if (!source.datum_params) { + source.a = src_a; + source.es = source.es; + return point; + } + wp = 1; + for (i = 0, l = source.datum_params.length; i < l; i++) { + wp *= source.datum_params[i]; + } + if (wp === 0) { + source.a = src_a; + source.es = source.es; + return point; + } + if (source.datum_params.length > 3) { + fallback = PJD_7PARAM; + } + else { + fallback = PJD_3PARAM; + } + } + } + if (dest.datum_type === PJD_GRIDSHIFT) { + dest.a = SRS_WGS84_SEMIMAJOR; + dest.es = SRS_WGS84_ESQUARED; + } + // Do we need to go through geocentric coordinates? + if (source.es !== dest.es || source.a !== dest.a || checkParams(fallback) || checkParams(dest.datum_type)) { + //DGR: 2012-07-29 : add nadgrids support (end) + // Convert to geocentric coordinates. + source.geodetic_to_geocentric(point); + // CHECK_RETURN; + // Convert between datums + if (checkParams(source.datum_type)) { + source.geocentric_to_wgs84(point); + // CHECK_RETURN; + } + if (checkParams(dest.datum_type)) { + dest.geocentric_from_wgs84(point); + // CHECK_RETURN; + } + // Convert back to geodetic coordinates + dest.geocentric_to_geodetic(point); + // CHECK_RETURN; + } + // Apply grid shift to destination if required + if (dest.datum_type === PJD_GRIDSHIFT) { + this.apply_gridshift(dest, 1, point); + // CHECK_RETURN; + } + + source.a = src_a; + source.es = src_es; + dest.a = dst_a; + dest.es = dst_es; + + return point; + }; + + + +/***/ }, +/* 173 */ +/***/ function(module, exports) { + + module.exports = function(crs, denorm, point) { + var xin = point.x, + yin = point.y, + zin = point.z || 0.0; + var v, t, i; + for (i = 0; i < 3; i++) { + if (denorm && i === 2 && point.z === undefined) { + continue; + } + if (i === 0) { + v = xin; + t = 'x'; + } + else if (i === 1) { + v = yin; + t = 'y'; + } + else { + v = zin; + t = 'z'; + } + switch (crs.axis[i]) { + case 'e': + point[t] = v; + break; + case 'w': + point[t] = -v; + break; + case 'n': + point[t] = v; + break; + case 's': + point[t] = -v; + break; + case 'u': + if (point[t] !== undefined) { + point.z = v; + } + break; + case 'd': + if (point[t] !== undefined) { + point.z = -v; + } + break; + default: + //console.log("ERROR: unknow axis ("+crs.axis[i]+") - check definition of "+crs.projName); + return null; + } + } + return point; + }; + + +/***/ }, +/* 174 */ +/***/ function(module, exports) { + + module.exports = function (array){ + var out = { + x: array[0], + y: array[1] + }; + if (array.length>2) { + out.z = array[2]; + } + if (array.length>3) { + out.m = array[3]; + } + return out; + }; + +/***/ }, +/* 175 */ +/***/ function(module, exports, __webpack_require__) { + + var mgrs = __webpack_require__(176); + + function Point(x, y, z) { + if (!(this instanceof Point)) { + return new Point(x, y, z); + } + if (Array.isArray(x)) { + this.x = x[0]; + this.y = x[1]; + this.z = x[2] || 0.0; + } else if(typeof x === 'object') { + this.x = x.x; + this.y = x.y; + this.z = x.z || 0.0; + } else if (typeof x === 'string' && typeof y === 'undefined') { + var coords = x.split(','); + this.x = parseFloat(coords[0], 10); + this.y = parseFloat(coords[1], 10); + this.z = parseFloat(coords[2], 10) || 0.0; + } else { + this.x = x; + this.y = y; + this.z = z || 0.0; + } + console.warn('proj4.Point will be removed in version 3, use proj4.toPoint'); + } + + Point.fromMGRS = function(mgrsStr) { + return new Point(mgrs.toPoint(mgrsStr)); + }; + Point.prototype.toMGRS = function(accuracy) { + return mgrs.forward([this.x, this.y], accuracy); + }; + module.exports = Point; + + +/***/ }, +/* 176 */ +/***/ function(module, exports) { + + + + + /** + * UTM zones are grouped, and assigned to one of a group of 6 + * sets. + * + * {int} @private + */ + var NUM_100K_SETS = 6; + + /** + * The column letters (for easting) of the lower left value, per + * set. + * + * {string} @private + */ + var SET_ORIGIN_COLUMN_LETTERS = 'AJSAJS'; + + /** + * The row letters (for northing) of the lower left value, per + * set. + * + * {string} @private + */ + var SET_ORIGIN_ROW_LETTERS = 'AFAFAF'; + + var A = 65; // A + var I = 73; // I + var O = 79; // O + var V = 86; // V + var Z = 90; // Z + + /** + * Conversion of lat/lon to MGRS. + * + * @param {object} ll Object literal with lat and lon properties on a + * WGS84 ellipsoid. + * @param {int} accuracy Accuracy in digits (5 for 1 m, 4 for 10 m, 3 for + * 100 m, 2 for 1000 m or 1 for 10000 m). Optional, default is 5. + * @return {string} the MGRS string for the given location and accuracy. + */ + exports.forward = function(ll, accuracy) { + accuracy = accuracy || 5; // default accuracy 1m + return encode(LLtoUTM({ + lat: ll[1], + lon: ll[0] + }), accuracy); + }; + + /** + * Conversion of MGRS to lat/lon. + * + * @param {string} mgrs MGRS string. + * @return {array} An array with left (longitude), bottom (latitude), right + * (longitude) and top (latitude) values in WGS84, representing the + * bounding box for the provided MGRS reference. + */ + exports.inverse = function(mgrs) { + var bbox = UTMtoLL(decode(mgrs.toUpperCase())); + if (bbox.lat && bbox.lon) { + return [bbox.lon, bbox.lat, bbox.lon, bbox.lat]; + } + return [bbox.left, bbox.bottom, bbox.right, bbox.top]; + }; + + exports.toPoint = function(mgrs) { + var bbox = UTMtoLL(decode(mgrs.toUpperCase())); + if (bbox.lat && bbox.lon) { + return [bbox.lon, bbox.lat]; + } + return [(bbox.left + bbox.right) / 2, (bbox.top + bbox.bottom) / 2]; + }; + /** + * Conversion from degrees to radians. + * + * @private + * @param {number} deg the angle in degrees. + * @return {number} the angle in radians. + */ + function degToRad(deg) { + return (deg * (Math.PI / 180.0)); + } + + /** + * Conversion from radians to degrees. + * + * @private + * @param {number} rad the angle in radians. + * @return {number} the angle in degrees. + */ + function radToDeg(rad) { + return (180.0 * (rad / Math.PI)); + } + + /** + * Converts a set of Longitude and Latitude co-ordinates to UTM + * using the WGS84 ellipsoid. + * + * @private + * @param {object} ll Object literal with lat and lon properties + * representing the WGS84 coordinate to be converted. + * @return {object} Object literal containing the UTM value with easting, + * northing, zoneNumber and zoneLetter properties, and an optional + * accuracy property in digits. Returns null if the conversion failed. + */ + function LLtoUTM(ll) { + var Lat = ll.lat; + var Long = ll.lon; + var a = 6378137.0; //ellip.radius; + var eccSquared = 0.00669438; //ellip.eccsq; + var k0 = 0.9996; + var LongOrigin; + var eccPrimeSquared; + var N, T, C, A, M; + var LatRad = degToRad(Lat); + var LongRad = degToRad(Long); + var LongOriginRad; + var ZoneNumber; + // (int) + ZoneNumber = Math.floor((Long + 180) / 6) + 1; + + //Make sure the longitude 180.00 is in Zone 60 + if (Long === 180) { + ZoneNumber = 60; + } + + // Special zone for Norway + if (Lat >= 56.0 && Lat < 64.0 && Long >= 3.0 && Long < 12.0) { + ZoneNumber = 32; + } + + // Special zones for Svalbard + if (Lat >= 72.0 && Lat < 84.0) { + if (Long >= 0.0 && Long < 9.0) { + ZoneNumber = 31; + } + else if (Long >= 9.0 && Long < 21.0) { + ZoneNumber = 33; + } + else if (Long >= 21.0 && Long < 33.0) { + ZoneNumber = 35; + } + else if (Long >= 33.0 && Long < 42.0) { + ZoneNumber = 37; + } + } + + LongOrigin = (ZoneNumber - 1) * 6 - 180 + 3; //+3 puts origin + // in middle of + // zone + LongOriginRad = degToRad(LongOrigin); + + eccPrimeSquared = (eccSquared) / (1 - eccSquared); + + N = a / Math.sqrt(1 - eccSquared * Math.sin(LatRad) * Math.sin(LatRad)); + T = Math.tan(LatRad) * Math.tan(LatRad); + C = eccPrimeSquared * Math.cos(LatRad) * Math.cos(LatRad); + A = Math.cos(LatRad) * (LongRad - LongOriginRad); + + M = a * ((1 - eccSquared / 4 - 3 * eccSquared * eccSquared / 64 - 5 * eccSquared * eccSquared * eccSquared / 256) * LatRad - (3 * eccSquared / 8 + 3 * eccSquared * eccSquared / 32 + 45 * eccSquared * eccSquared * eccSquared / 1024) * Math.sin(2 * LatRad) + (15 * eccSquared * eccSquared / 256 + 45 * eccSquared * eccSquared * eccSquared / 1024) * Math.sin(4 * LatRad) - (35 * eccSquared * eccSquared * eccSquared / 3072) * Math.sin(6 * LatRad)); + + var UTMEasting = (k0 * N * (A + (1 - T + C) * A * A * A / 6.0 + (5 - 18 * T + T * T + 72 * C - 58 * eccPrimeSquared) * A * A * A * A * A / 120.0) + 500000.0); + + var UTMNorthing = (k0 * (M + N * Math.tan(LatRad) * (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * A * A * A * A / 24.0 + (61 - 58 * T + T * T + 600 * C - 330 * eccPrimeSquared) * A * A * A * A * A * A / 720.0))); + if (Lat < 0.0) { + UTMNorthing += 10000000.0; //10000000 meter offset for + // southern hemisphere + } + + return { + northing: Math.round(UTMNorthing), + easting: Math.round(UTMEasting), + zoneNumber: ZoneNumber, + zoneLetter: getLetterDesignator(Lat) + }; + } + + /** + * Converts UTM coords to lat/long, using the WGS84 ellipsoid. This is a convenience + * class where the Zone can be specified as a single string eg."60N" which + * is then broken down into the ZoneNumber and ZoneLetter. + * + * @private + * @param {object} utm An object literal with northing, easting, zoneNumber + * and zoneLetter properties. If an optional accuracy property is + * provided (in meters), a bounding box will be returned instead of + * latitude and longitude. + * @return {object} An object literal containing either lat and lon values + * (if no accuracy was provided), or top, right, bottom and left values + * for the bounding box calculated according to the provided accuracy. + * Returns null if the conversion failed. + */ + function UTMtoLL(utm) { + + var UTMNorthing = utm.northing; + var UTMEasting = utm.easting; + var zoneLetter = utm.zoneLetter; + var zoneNumber = utm.zoneNumber; + // check the ZoneNummber is valid + if (zoneNumber < 0 || zoneNumber > 60) { + return null; + } + + var k0 = 0.9996; + var a = 6378137.0; //ellip.radius; + var eccSquared = 0.00669438; //ellip.eccsq; + var eccPrimeSquared; + var e1 = (1 - Math.sqrt(1 - eccSquared)) / (1 + Math.sqrt(1 - eccSquared)); + var N1, T1, C1, R1, D, M; + var LongOrigin; + var mu, phi1Rad; + + // remove 500,000 meter offset for longitude + var x = UTMEasting - 500000.0; + var y = UTMNorthing; + + // We must know somehow if we are in the Northern or Southern + // hemisphere, this is the only time we use the letter So even + // if the Zone letter isn't exactly correct it should indicate + // the hemisphere correctly + if (zoneLetter < 'N') { + y -= 10000000.0; // remove 10,000,000 meter offset used + // for southern hemisphere + } + + // There are 60 zones with zone 1 being at West -180 to -174 + LongOrigin = (zoneNumber - 1) * 6 - 180 + 3; // +3 puts origin + // in middle of + // zone + + eccPrimeSquared = (eccSquared) / (1 - eccSquared); + + M = y / k0; + mu = M / (a * (1 - eccSquared / 4 - 3 * eccSquared * eccSquared / 64 - 5 * eccSquared * eccSquared * eccSquared / 256)); + + phi1Rad = mu + (3 * e1 / 2 - 27 * e1 * e1 * e1 / 32) * Math.sin(2 * mu) + (21 * e1 * e1 / 16 - 55 * e1 * e1 * e1 * e1 / 32) * Math.sin(4 * mu) + (151 * e1 * e1 * e1 / 96) * Math.sin(6 * mu); + // double phi1 = ProjMath.radToDeg(phi1Rad); + + N1 = a / Math.sqrt(1 - eccSquared * Math.sin(phi1Rad) * Math.sin(phi1Rad)); + T1 = Math.tan(phi1Rad) * Math.tan(phi1Rad); + C1 = eccPrimeSquared * Math.cos(phi1Rad) * Math.cos(phi1Rad); + R1 = a * (1 - eccSquared) / Math.pow(1 - eccSquared * Math.sin(phi1Rad) * Math.sin(phi1Rad), 1.5); + D = x / (N1 * k0); + + var lat = phi1Rad - (N1 * Math.tan(phi1Rad) / R1) * (D * D / 2 - (5 + 3 * T1 + 10 * C1 - 4 * C1 * C1 - 9 * eccPrimeSquared) * D * D * D * D / 24 + (61 + 90 * T1 + 298 * C1 + 45 * T1 * T1 - 252 * eccPrimeSquared - 3 * C1 * C1) * D * D * D * D * D * D / 720); + lat = radToDeg(lat); + + var lon = (D - (1 + 2 * T1 + C1) * D * D * D / 6 + (5 - 2 * C1 + 28 * T1 - 3 * C1 * C1 + 8 * eccPrimeSquared + 24 * T1 * T1) * D * D * D * D * D / 120) / Math.cos(phi1Rad); + lon = LongOrigin + radToDeg(lon); + + var result; + if (utm.accuracy) { + var topRight = UTMtoLL({ + northing: utm.northing + utm.accuracy, + easting: utm.easting + utm.accuracy, + zoneLetter: utm.zoneLetter, + zoneNumber: utm.zoneNumber + }); + result = { + top: topRight.lat, + right: topRight.lon, + bottom: lat, + left: lon + }; + } + else { + result = { + lat: lat, + lon: lon + }; + } + return result; + } + + /** + * Calculates the MGRS letter designator for the given latitude. + * + * @private + * @param {number} lat The latitude in WGS84 to get the letter designator + * for. + * @return {char} The letter designator. + */ + function getLetterDesignator(lat) { + //This is here as an error flag to show that the Latitude is + //outside MGRS limits + var LetterDesignator = 'Z'; + + if ((84 >= lat) && (lat >= 72)) { + LetterDesignator = 'X'; + } + else if ((72 > lat) && (lat >= 64)) { + LetterDesignator = 'W'; + } + else if ((64 > lat) && (lat >= 56)) { + LetterDesignator = 'V'; + } + else if ((56 > lat) && (lat >= 48)) { + LetterDesignator = 'U'; + } + else if ((48 > lat) && (lat >= 40)) { + LetterDesignator = 'T'; + } + else if ((40 > lat) && (lat >= 32)) { + LetterDesignator = 'S'; + } + else if ((32 > lat) && (lat >= 24)) { + LetterDesignator = 'R'; + } + else if ((24 > lat) && (lat >= 16)) { + LetterDesignator = 'Q'; + } + else if ((16 > lat) && (lat >= 8)) { + LetterDesignator = 'P'; + } + else if ((8 > lat) && (lat >= 0)) { + LetterDesignator = 'N'; + } + else if ((0 > lat) && (lat >= -8)) { + LetterDesignator = 'M'; + } + else if ((-8 > lat) && (lat >= -16)) { + LetterDesignator = 'L'; + } + else if ((-16 > lat) && (lat >= -24)) { + LetterDesignator = 'K'; + } + else if ((-24 > lat) && (lat >= -32)) { + LetterDesignator = 'J'; + } + else if ((-32 > lat) && (lat >= -40)) { + LetterDesignator = 'H'; + } + else if ((-40 > lat) && (lat >= -48)) { + LetterDesignator = 'G'; + } + else if ((-48 > lat) && (lat >= -56)) { + LetterDesignator = 'F'; + } + else if ((-56 > lat) && (lat >= -64)) { + LetterDesignator = 'E'; + } + else if ((-64 > lat) && (lat >= -72)) { + LetterDesignator = 'D'; + } + else if ((-72 > lat) && (lat >= -80)) { + LetterDesignator = 'C'; + } + return LetterDesignator; + } + + /** + * Encodes a UTM location as MGRS string. + * + * @private + * @param {object} utm An object literal with easting, northing, + * zoneLetter, zoneNumber + * @param {number} accuracy Accuracy in digits (1-5). + * @return {string} MGRS string for the given UTM location. + */ + function encode(utm, accuracy) { + // prepend with leading zeroes + var seasting = "00000" + utm.easting, + snorthing = "00000" + utm.northing; + + return utm.zoneNumber + utm.zoneLetter + get100kID(utm.easting, utm.northing, utm.zoneNumber) + seasting.substr(seasting.length - 5, accuracy) + snorthing.substr(snorthing.length - 5, accuracy); + } + + /** + * Get the two letter 100k designator for a given UTM easting, + * northing and zone number value. + * + * @private + * @param {number} easting + * @param {number} northing + * @param {number} zoneNumber + * @return the two letter 100k designator for the given UTM location. + */ + function get100kID(easting, northing, zoneNumber) { + var setParm = get100kSetForZone(zoneNumber); + var setColumn = Math.floor(easting / 100000); + var setRow = Math.floor(northing / 100000) % 20; + return getLetter100kID(setColumn, setRow, setParm); + } + + /** + * Given a UTM zone number, figure out the MGRS 100K set it is in. + * + * @private + * @param {number} i An UTM zone number. + * @return {number} the 100k set the UTM zone is in. + */ + function get100kSetForZone(i) { + var setParm = i % NUM_100K_SETS; + if (setParm === 0) { + setParm = NUM_100K_SETS; + } + + return setParm; + } + + /** + * Get the two-letter MGRS 100k designator given information + * translated from the UTM northing, easting and zone number. + * + * @private + * @param {number} column the column index as it relates to the MGRS + * 100k set spreadsheet, created from the UTM easting. + * Values are 1-8. + * @param {number} row the row index as it relates to the MGRS 100k set + * spreadsheet, created from the UTM northing value. Values + * are from 0-19. + * @param {number} parm the set block, as it relates to the MGRS 100k set + * spreadsheet, created from the UTM zone. Values are from + * 1-60. + * @return two letter MGRS 100k code. + */ + function getLetter100kID(column, row, parm) { + // colOrigin and rowOrigin are the letters at the origin of the set + var index = parm - 1; + var colOrigin = SET_ORIGIN_COLUMN_LETTERS.charCodeAt(index); + var rowOrigin = SET_ORIGIN_ROW_LETTERS.charCodeAt(index); + + // colInt and rowInt are the letters to build to return + var colInt = colOrigin + column - 1; + var rowInt = rowOrigin + row; + var rollover = false; + + if (colInt > Z) { + colInt = colInt - Z + A - 1; + rollover = true; + } + + if (colInt === I || (colOrigin < I && colInt > I) || ((colInt > I || colOrigin < I) && rollover)) { + colInt++; + } + + if (colInt === O || (colOrigin < O && colInt > O) || ((colInt > O || colOrigin < O) && rollover)) { + colInt++; + + if (colInt === I) { + colInt++; + } + } + + if (colInt > Z) { + colInt = colInt - Z + A - 1; + } + + if (rowInt > V) { + rowInt = rowInt - V + A - 1; + rollover = true; + } + else { + rollover = false; + } + + if (((rowInt === I) || ((rowOrigin < I) && (rowInt > I))) || (((rowInt > I) || (rowOrigin < I)) && rollover)) { + rowInt++; + } + + if (((rowInt === O) || ((rowOrigin < O) && (rowInt > O))) || (((rowInt > O) || (rowOrigin < O)) && rollover)) { + rowInt++; + + if (rowInt === I) { + rowInt++; + } + } + + if (rowInt > V) { + rowInt = rowInt - V + A - 1; + } + + var twoLetter = String.fromCharCode(colInt) + String.fromCharCode(rowInt); + return twoLetter; + } + + /** + * Decode the UTM parameters from a MGRS string. + * + * @private + * @param {string} mgrsString an UPPERCASE coordinate string is expected. + * @return {object} An object literal with easting, northing, zoneLetter, + * zoneNumber and accuracy (in meters) properties. + */ + function decode(mgrsString) { + + if (mgrsString && mgrsString.length === 0) { + throw ("MGRSPoint coverting from nothing"); + } + + var length = mgrsString.length; + + var hunK = null; + var sb = ""; + var testChar; + var i = 0; + + // get Zone number + while (!(/[A-Z]/).test(testChar = mgrsString.charAt(i))) { + if (i >= 2) { + throw ("MGRSPoint bad conversion from: " + mgrsString); + } + sb += testChar; + i++; + } + + var zoneNumber = parseInt(sb, 10); + + if (i === 0 || i + 3 > length) { + // A good MGRS string has to be 4-5 digits long, + // ##AAA/#AAA at least. + throw ("MGRSPoint bad conversion from: " + mgrsString); + } + + var zoneLetter = mgrsString.charAt(i++); + + // Should we check the zone letter here? Why not. + if (zoneLetter <= 'A' || zoneLetter === 'B' || zoneLetter === 'Y' || zoneLetter >= 'Z' || zoneLetter === 'I' || zoneLetter === 'O') { + throw ("MGRSPoint zone letter " + zoneLetter + " not handled: " + mgrsString); + } + + hunK = mgrsString.substring(i, i += 2); + + var set = get100kSetForZone(zoneNumber); + + var east100k = getEastingFromChar(hunK.charAt(0), set); + var north100k = getNorthingFromChar(hunK.charAt(1), set); + + // We have a bug where the northing may be 2000000 too low. + // How + // do we know when to roll over? + + while (north100k < getMinNorthing(zoneLetter)) { + north100k += 2000000; + } + + // calculate the char index for easting/northing separator + var remainder = length - i; + + if (remainder % 2 !== 0) { + throw ("MGRSPoint has to have an even number \nof digits after the zone letter and two 100km letters - front \nhalf for easting meters, second half for \nnorthing meters" + mgrsString); + } + + var sep = remainder / 2; + + var sepEasting = 0.0; + var sepNorthing = 0.0; + var accuracyBonus, sepEastingString, sepNorthingString, easting, northing; + if (sep > 0) { + accuracyBonus = 100000.0 / Math.pow(10, sep); + sepEastingString = mgrsString.substring(i, i + sep); + sepEasting = parseFloat(sepEastingString) * accuracyBonus; + sepNorthingString = mgrsString.substring(i + sep); + sepNorthing = parseFloat(sepNorthingString) * accuracyBonus; + } + + easting = sepEasting + east100k; + northing = sepNorthing + north100k; + + return { + easting: easting, + northing: northing, + zoneLetter: zoneLetter, + zoneNumber: zoneNumber, + accuracy: accuracyBonus + }; + } + + /** + * Given the first letter from a two-letter MGRS 100k zone, and given the + * MGRS table set for the zone number, figure out the easting value that + * should be added to the other, secondary easting value. + * + * @private + * @param {char} e The first letter from a two-letter MGRS 100´k zone. + * @param {number} set The MGRS table set for the zone number. + * @return {number} The easting value for the given letter and set. + */ + function getEastingFromChar(e, set) { + // colOrigin is the letter at the origin of the set for the + // column + var curCol = SET_ORIGIN_COLUMN_LETTERS.charCodeAt(set - 1); + var eastingValue = 100000.0; + var rewindMarker = false; + + while (curCol !== e.charCodeAt(0)) { + curCol++; + if (curCol === I) { + curCol++; + } + if (curCol === O) { + curCol++; + } + if (curCol > Z) { + if (rewindMarker) { + throw ("Bad character: " + e); + } + curCol = A; + rewindMarker = true; + } + eastingValue += 100000.0; + } + + return eastingValue; + } + + /** + * Given the second letter from a two-letter MGRS 100k zone, and given the + * MGRS table set for the zone number, figure out the northing value that + * should be added to the other, secondary northing value. You have to + * remember that Northings are determined from the equator, and the vertical + * cycle of letters mean a 2000000 additional northing meters. This happens + * approx. every 18 degrees of latitude. This method does *NOT* count any + * additional northings. You have to figure out how many 2000000 meters need + * to be added for the zone letter of the MGRS coordinate. + * + * @private + * @param {char} n Second letter of the MGRS 100k zone + * @param {number} set The MGRS table set number, which is dependent on the + * UTM zone number. + * @return {number} The northing value for the given letter and set. + */ + function getNorthingFromChar(n, set) { + + if (n > 'V') { + throw ("MGRSPoint given invalid Northing " + n); + } + + // rowOrigin is the letter at the origin of the set for the + // column + var curRow = SET_ORIGIN_ROW_LETTERS.charCodeAt(set - 1); + var northingValue = 0.0; + var rewindMarker = false; + + while (curRow !== n.charCodeAt(0)) { + curRow++; + if (curRow === I) { + curRow++; + } + if (curRow === O) { + curRow++; + } + // fixing a bug making whole application hang in this loop + // when 'n' is a wrong character + if (curRow > V) { + if (rewindMarker) { // making sure that this loop ends + throw ("Bad character: " + n); + } + curRow = A; + rewindMarker = true; + } + northingValue += 100000.0; + } + + return northingValue; + } + + /** + * The function getMinNorthing returns the minimum northing value of a MGRS + * zone. + * + * Ported from Geotrans' c Lattitude_Band_Value structure table. + * + * @private + * @param {char} zoneLetter The MGRS zone to get the min northing for. + * @return {number} + */ + function getMinNorthing(zoneLetter) { + var northing; + switch (zoneLetter) { + case 'C': + northing = 1100000.0; + break; + case 'D': + northing = 2000000.0; + break; + case 'E': + northing = 2800000.0; + break; + case 'F': + northing = 3700000.0; + break; + case 'G': + northing = 4600000.0; + break; + case 'H': + northing = 5500000.0; + break; + case 'J': + northing = 6400000.0; + break; + case 'K': + northing = 7300000.0; + break; + case 'L': + northing = 8200000.0; + break; + case 'M': + northing = 9100000.0; + break; + case 'N': + northing = 0.0; + break; + case 'P': + northing = 800000.0; + break; + case 'Q': + northing = 1700000.0; + break; + case 'R': + northing = 2600000.0; + break; + case 'S': + northing = 3500000.0; + break; + case 'T': + northing = 4400000.0; + break; + case 'U': + northing = 5300000.0; + break; + case 'V': + northing = 6200000.0; + break; + case 'W': + northing = 7000000.0; + break; + case 'X': + northing = 7900000.0; + break; + default: + northing = -1.0; + } + if (northing >= 0.0) { + return northing; + } + else { + throw ("Invalid zone letter: " + zoneLetter); + } + + } + + +/***/ }, +/* 177 */ +/***/ function(module, exports) { + + module.exports = { + "_args": [ + [ + "proj4@^2.3.14", + "/Users/jbeezley/git/geojs2" + ] + ], + "_from": "proj4@>=2.3.14 <3.0.0", + "_id": "proj4@2.3.14", + "_inCache": true, + "_installable": true, + "_location": "/proj4", + "_nodeVersion": "4.2.6", + "_npmOperationalInternal": { + "host": "packages-13-west.internal.npmjs.com", + "tmp": "tmp/proj4-2.3.14.tgz_1457689264880_0.9409773757215589" + }, + "_npmUser": { + "email": "andreas.hocevar@gmail.com", + "name": "ahocevar" + }, + "_npmVersion": "2.14.12", + "_phantomChildren": {}, + "_requested": { + "name": "proj4", + "raw": "proj4@^2.3.14", + "rawSpec": "^2.3.14", + "scope": null, + "spec": ">=2.3.14 <3.0.0", + "type": "range" + }, + "_requiredBy": [ + "#DEV:/" + ], + "_resolved": "https://registry.npmjs.org/proj4/-/proj4-2.3.14.tgz", + "_shasum": "928906144388980c914c5a357fc493aba59a747a", + "_shrinkwrap": null, + "_spec": "proj4@^2.3.14", + "_where": "/Users/jbeezley/git/geojs2", + "author": "", + "bugs": { + "url": "https://github.com/proj4js/proj4js/issues" + }, + "contributors": [ + { + "email": "madair@dmsolutions.ca", + "name": "Mike Adair" + }, + { + "email": "rich@greenwoodmap.com", + "name": "Richard Greenwood" + }, + { + "email": "calvin.metcalf@gmail.com", + "name": "Calvin Metcalf" + }, + { + "name": "Richard Marsden", + "url": "http://www.winwaed.com" + }, + { + "name": "T. Mittan" + }, + { + "name": "D. Steinwand" + }, + { + "name": "S. Nelson" + } + ], + "dependencies": { + "mgrs": "~0.0.2" + }, + "description": "Proj4js is a JavaScript library to transform point coordinates from one coordinate system to another, including datum transformations.", + "devDependencies": { + "browserify": "~12.0.1", + "chai": "~1.8.1", + "curl": "git://github.com/cujojs/curl.git", + "grunt": "~0.4.2", + "grunt-browserify": "~4.0.1", + "grunt-cli": "~0.1.13", + "grunt-contrib-connect": "~0.6.0", + "grunt-contrib-jshint": "~0.8.0", + "grunt-contrib-uglify": "~0.11.1", + "grunt-mocha-phantomjs": "~0.4.0", + "istanbul": "~0.2.4", + "mocha": "~1.17.1", + "tin": "~0.4.0" + }, + "directories": { + "doc": "docs", + "test": "test" + }, + "dist": { + "shasum": "928906144388980c914c5a357fc493aba59a747a", + "tarball": "http://registry.npmjs.org/proj4/-/proj4-2.3.14.tgz" + }, + "gitHead": "7619c8a63df1eae5bad0b9ad31ca1d87b0549243", + "homepage": "https://github.com/proj4js/proj4js#readme", + "jam": { + "include": [ + "dist/proj4.js", + "README.md", + "AUTHORS", + "LICENSE.md" + ], + "main": "dist/proj4.js" + }, + "license": "MIT", + "main": "lib/index.js", + "maintainers": [ + { + "email": "calvin.metcalf@gmail.com", + "name": "cwmma" + }, + { + "email": "andreas.hocevar@gmail.com", + "name": "ahocevar" + } + ], + "name": "proj4", + "optionalDependencies": {}, + "readme": "ERROR: No README data found!", + "repository": { + "type": "git", + "url": "git://github.com/proj4js/proj4js.git" + }, + "scripts": { + "test": "./node_modules/istanbul/lib/cli.js test ./node_modules/mocha/bin/_mocha test/test.js" + }, + "version": "2.3.14" + }; + +/***/ }, +/* 178 */ +/***/ function(module, exports, __webpack_require__) { + + var projs = [ + __webpack_require__(179), + __webpack_require__(186), + __webpack_require__(187), + __webpack_require__(190), + __webpack_require__(191), + __webpack_require__(192), + __webpack_require__(193), + __webpack_require__(194), + __webpack_require__(195), + __webpack_require__(199), + __webpack_require__(201), + __webpack_require__(202), + __webpack_require__(203), + __webpack_require__(205), + __webpack_require__(206), + __webpack_require__(207), + __webpack_require__(208), + __webpack_require__(209), + __webpack_require__(213), + __webpack_require__(214), + __webpack_require__(215), + __webpack_require__(216) + ]; + module.exports = function(proj4){ + projs.forEach(function(proj){ + proj4.Proj.projections.add(proj); + }); + }; + +/***/ }, +/* 179 */ +/***/ function(module, exports, __webpack_require__) { + + var e0fn = __webpack_require__(180); + var e1fn = __webpack_require__(181); + var e2fn = __webpack_require__(182); + var e3fn = __webpack_require__(183); + var mlfn = __webpack_require__(184); + var adjust_lon = __webpack_require__(162); + var HALF_PI = Math.PI/2; + var EPSLN = 1.0e-10; + var sign = __webpack_require__(163); + var asinz = __webpack_require__(185); + + exports.init = function() { + this.e0 = e0fn(this.es); + this.e1 = e1fn(this.es); + this.e2 = e2fn(this.es); + this.e3 = e3fn(this.es); + this.ml0 = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); + }; + + /** + Transverse Mercator Forward - long/lat to x/y + long/lat in radians + */ + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + + var delta_lon = adjust_lon(lon - this.long0); + var con; + var x, y; + var sin_phi = Math.sin(lat); + var cos_phi = Math.cos(lat); + + if (this.sphere) { + var b = cos_phi * Math.sin(delta_lon); + if ((Math.abs(Math.abs(b) - 1)) < 0.0000000001) { + return (93); + } + else { + x = 0.5 * this.a * this.k0 * Math.log((1 + b) / (1 - b)); + con = Math.acos(cos_phi * Math.cos(delta_lon) / Math.sqrt(1 - b * b)); + if (lat < 0) { + con = -con; + } + y = this.a * this.k0 * (con - this.lat0); + } + } + else { + var al = cos_phi * delta_lon; + var als = Math.pow(al, 2); + var c = this.ep2 * Math.pow(cos_phi, 2); + var tq = Math.tan(lat); + var t = Math.pow(tq, 2); + con = 1 - this.es * Math.pow(sin_phi, 2); + var n = this.a / Math.sqrt(con); + var ml = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, lat); + + x = this.k0 * n * al * (1 + als / 6 * (1 - t + c + als / 20 * (5 - 18 * t + Math.pow(t, 2) + 72 * c - 58 * this.ep2))) + this.x0; + y = this.k0 * (ml - this.ml0 + n * tq * (als * (0.5 + als / 24 * (5 - t + 9 * c + 4 * Math.pow(c, 2) + als / 30 * (61 - 58 * t + Math.pow(t, 2) + 600 * c - 330 * this.ep2))))) + this.y0; + + } + p.x = x; + p.y = y; + return p; + }; + + /** + Transverse Mercator Inverse - x/y to long/lat + */ + exports.inverse = function(p) { + var con, phi; + var delta_phi; + var i; + var max_iter = 6; + var lat, lon; + + if (this.sphere) { + var f = Math.exp(p.x / (this.a * this.k0)); + var g = 0.5 * (f - 1 / f); + var temp = this.lat0 + p.y / (this.a * this.k0); + var h = Math.cos(temp); + con = Math.sqrt((1 - h * h) / (1 + g * g)); + lat = asinz(con); + if (temp < 0) { + lat = -lat; + } + if ((g === 0) && (h === 0)) { + lon = this.long0; + } + else { + lon = adjust_lon(Math.atan2(g, h) + this.long0); + } + } + else { // ellipsoidal form + var x = p.x - this.x0; + var y = p.y - this.y0; + + con = (this.ml0 + y / this.k0) / this.a; + phi = con; + for (i = 0; true; i++) { + delta_phi = ((con + this.e1 * Math.sin(2 * phi) - this.e2 * Math.sin(4 * phi) + this.e3 * Math.sin(6 * phi)) / this.e0) - phi; + phi += delta_phi; + if (Math.abs(delta_phi) <= EPSLN) { + break; + } + if (i >= max_iter) { + return (95); + } + } // for() + if (Math.abs(phi) < HALF_PI) { + var sin_phi = Math.sin(phi); + var cos_phi = Math.cos(phi); + var tan_phi = Math.tan(phi); + var c = this.ep2 * Math.pow(cos_phi, 2); + var cs = Math.pow(c, 2); + var t = Math.pow(tan_phi, 2); + var ts = Math.pow(t, 2); + con = 1 - this.es * Math.pow(sin_phi, 2); + var n = this.a / Math.sqrt(con); + var r = n * (1 - this.es) / con; + var d = x / (n * this.k0); + var ds = Math.pow(d, 2); + lat = phi - (n * tan_phi * ds / r) * (0.5 - ds / 24 * (5 + 3 * t + 10 * c - 4 * cs - 9 * this.ep2 - ds / 30 * (61 + 90 * t + 298 * c + 45 * ts - 252 * this.ep2 - 3 * cs))); + lon = adjust_lon(this.long0 + (d * (1 - ds / 6 * (1 + 2 * t + c - ds / 20 * (5 - 2 * c + 28 * t - 3 * cs + 8 * this.ep2 + 24 * ts))) / cos_phi)); + } + else { + lat = HALF_PI * sign(y); + lon = this.long0; + } + } + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["Transverse_Mercator", "Transverse Mercator", "tmerc"]; + + +/***/ }, +/* 180 */ +/***/ function(module, exports) { + + module.exports = function(x) { + return (1 - 0.25 * x * (1 + x / 16 * (3 + 1.25 * x))); + }; + +/***/ }, +/* 181 */ +/***/ function(module, exports) { + + module.exports = function(x) { + return (0.375 * x * (1 + 0.25 * x * (1 + 0.46875 * x))); + }; + +/***/ }, +/* 182 */ +/***/ function(module, exports) { + + module.exports = function(x) { + return (0.05859375 * x * x * (1 + 0.75 * x)); + }; + +/***/ }, +/* 183 */ +/***/ function(module, exports) { + + module.exports = function(x) { + return (x * x * x * (35 / 3072)); + }; + +/***/ }, +/* 184 */ +/***/ function(module, exports) { + + module.exports = function(e0, e1, e2, e3, phi) { + return (e0 * phi - e1 * Math.sin(2 * phi) + e2 * Math.sin(4 * phi) - e3 * Math.sin(6 * phi)); + }; + +/***/ }, +/* 185 */ +/***/ function(module, exports) { + + module.exports = function(x) { + if (Math.abs(x) > 1) { + x = (x > 1) ? 1 : -1; + } + return Math.asin(x); + }; + +/***/ }, +/* 186 */ +/***/ function(module, exports, __webpack_require__) { + + var D2R = 0.01745329251994329577; + var tmerc = __webpack_require__(179); + exports.dependsOn = 'tmerc'; + exports.init = function() { + if (!this.zone) { + return; + } + this.lat0 = 0; + this.long0 = ((6 * Math.abs(this.zone)) - 183) * D2R; + this.x0 = 500000; + this.y0 = this.utmSouth ? 10000000 : 0; + this.k0 = 0.9996; + + tmerc.init.apply(this); + this.forward = tmerc.forward; + this.inverse = tmerc.inverse; + }; + exports.names = ["Universal Transverse Mercator System", "utm"]; + + +/***/ }, +/* 187 */ +/***/ function(module, exports, __webpack_require__) { + + var gauss = __webpack_require__(188); + var adjust_lon = __webpack_require__(162); + exports.init = function() { + gauss.init.apply(this); + if (!this.rc) { + return; + } + this.sinc0 = Math.sin(this.phic0); + this.cosc0 = Math.cos(this.phic0); + this.R2 = 2 * this.rc; + if (!this.title) { + this.title = "Oblique Stereographic Alternative"; + } + }; + + exports.forward = function(p) { + var sinc, cosc, cosl, k; + p.x = adjust_lon(p.x - this.long0); + gauss.forward.apply(this, [p]); + sinc = Math.sin(p.y); + cosc = Math.cos(p.y); + cosl = Math.cos(p.x); + k = this.k0 * this.R2 / (1 + this.sinc0 * sinc + this.cosc0 * cosc * cosl); + p.x = k * cosc * Math.sin(p.x); + p.y = k * (this.cosc0 * sinc - this.sinc0 * cosc * cosl); + p.x = this.a * p.x + this.x0; + p.y = this.a * p.y + this.y0; + return p; + }; + + exports.inverse = function(p) { + var sinc, cosc, lon, lat, rho; + p.x = (p.x - this.x0) / this.a; + p.y = (p.y - this.y0) / this.a; + + p.x /= this.k0; + p.y /= this.k0; + if ((rho = Math.sqrt(p.x * p.x + p.y * p.y))) { + var c = 2 * Math.atan2(rho, this.R2); + sinc = Math.sin(c); + cosc = Math.cos(c); + lat = Math.asin(cosc * this.sinc0 + p.y * sinc * this.cosc0 / rho); + lon = Math.atan2(p.x * sinc, rho * this.cosc0 * cosc - p.y * this.sinc0 * sinc); + } + else { + lat = this.phic0; + lon = 0; + } + + p.x = lon; + p.y = lat; + gauss.inverse.apply(this, [p]); + p.x = adjust_lon(p.x + this.long0); + return p; + }; + + exports.names = ["Stereographic_North_Pole", "Oblique_Stereographic", "Polar_Stereographic", "sterea","Oblique Stereographic Alternative"]; + + +/***/ }, +/* 188 */ +/***/ function(module, exports, __webpack_require__) { + + var FORTPI = Math.PI/4; + var srat = __webpack_require__(189); + var HALF_PI = Math.PI/2; + var MAX_ITER = 20; + exports.init = function() { + var sphi = Math.sin(this.lat0); + var cphi = Math.cos(this.lat0); + cphi *= cphi; + this.rc = Math.sqrt(1 - this.es) / (1 - this.es * sphi * sphi); + this.C = Math.sqrt(1 + this.es * cphi * cphi / (1 - this.es)); + this.phic0 = Math.asin(sphi / this.C); + this.ratexp = 0.5 * this.C * this.e; + this.K = Math.tan(0.5 * this.phic0 + FORTPI) / (Math.pow(Math.tan(0.5 * this.lat0 + FORTPI), this.C) * srat(this.e * sphi, this.ratexp)); + }; + + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + + p.y = 2 * Math.atan(this.K * Math.pow(Math.tan(0.5 * lat + FORTPI), this.C) * srat(this.e * Math.sin(lat), this.ratexp)) - HALF_PI; + p.x = this.C * lon; + return p; + }; + + exports.inverse = function(p) { + var DEL_TOL = 1e-14; + var lon = p.x / this.C; + var lat = p.y; + var num = Math.pow(Math.tan(0.5 * lat + FORTPI) / this.K, 1 / this.C); + for (var i = MAX_ITER; i > 0; --i) { + lat = 2 * Math.atan(num * srat(this.e * Math.sin(p.y), - 0.5 * this.e)) - HALF_PI; + if (Math.abs(lat - p.y) < DEL_TOL) { + break; + } + p.y = lat; + } + /* convergence failed */ + if (!i) { + return null; + } + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["gauss"]; + + +/***/ }, +/* 189 */ +/***/ function(module, exports) { + + module.exports = function(esinp, exp) { + return (Math.pow((1 - esinp) / (1 + esinp), exp)); + }; + +/***/ }, +/* 190 */ +/***/ function(module, exports, __webpack_require__) { + + var HALF_PI = Math.PI/2; + var EPSLN = 1.0e-10; + var sign = __webpack_require__(163); + var msfnz = __webpack_require__(161); + var tsfnz = __webpack_require__(164); + var phi2z = __webpack_require__(165); + var adjust_lon = __webpack_require__(162); + exports.ssfn_ = function(phit, sinphi, eccen) { + sinphi *= eccen; + return (Math.tan(0.5 * (HALF_PI + phit)) * Math.pow((1 - sinphi) / (1 + sinphi), 0.5 * eccen)); + }; + + exports.init = function() { + this.coslat0 = Math.cos(this.lat0); + this.sinlat0 = Math.sin(this.lat0); + if (this.sphere) { + if (this.k0 === 1 && !isNaN(this.lat_ts) && Math.abs(this.coslat0) <= EPSLN) { + this.k0 = 0.5 * (1 + sign(this.lat0) * Math.sin(this.lat_ts)); + } + } + else { + if (Math.abs(this.coslat0) <= EPSLN) { + if (this.lat0 > 0) { + //North pole + //trace('stere:north pole'); + this.con = 1; + } + else { + //South pole + //trace('stere:south pole'); + this.con = -1; + } + } + this.cons = Math.sqrt(Math.pow(1 + this.e, 1 + this.e) * Math.pow(1 - this.e, 1 - this.e)); + if (this.k0 === 1 && !isNaN(this.lat_ts) && Math.abs(this.coslat0) <= EPSLN) { + this.k0 = 0.5 * this.cons * msfnz(this.e, Math.sin(this.lat_ts), Math.cos(this.lat_ts)) / tsfnz(this.e, this.con * this.lat_ts, this.con * Math.sin(this.lat_ts)); + } + this.ms1 = msfnz(this.e, this.sinlat0, this.coslat0); + this.X0 = 2 * Math.atan(this.ssfn_(this.lat0, this.sinlat0, this.e)) - HALF_PI; + this.cosX0 = Math.cos(this.X0); + this.sinX0 = Math.sin(this.X0); + } + }; + + // Stereographic forward equations--mapping lat,long to x,y + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + var sinlat = Math.sin(lat); + var coslat = Math.cos(lat); + var A, X, sinX, cosX, ts, rh; + var dlon = adjust_lon(lon - this.long0); + + if (Math.abs(Math.abs(lon - this.long0) - Math.PI) <= EPSLN && Math.abs(lat + this.lat0) <= EPSLN) { + //case of the origine point + //trace('stere:this is the origin point'); + p.x = NaN; + p.y = NaN; + return p; + } + if (this.sphere) { + //trace('stere:sphere case'); + A = 2 * this.k0 / (1 + this.sinlat0 * sinlat + this.coslat0 * coslat * Math.cos(dlon)); + p.x = this.a * A * coslat * Math.sin(dlon) + this.x0; + p.y = this.a * A * (this.coslat0 * sinlat - this.sinlat0 * coslat * Math.cos(dlon)) + this.y0; + return p; + } + else { + X = 2 * Math.atan(this.ssfn_(lat, sinlat, this.e)) - HALF_PI; + cosX = Math.cos(X); + sinX = Math.sin(X); + if (Math.abs(this.coslat0) <= EPSLN) { + ts = tsfnz(this.e, lat * this.con, this.con * sinlat); + rh = 2 * this.a * this.k0 * ts / this.cons; + p.x = this.x0 + rh * Math.sin(lon - this.long0); + p.y = this.y0 - this.con * rh * Math.cos(lon - this.long0); + //trace(p.toString()); + return p; + } + else if (Math.abs(this.sinlat0) < EPSLN) { + //Eq + //trace('stere:equateur'); + A = 2 * this.a * this.k0 / (1 + cosX * Math.cos(dlon)); + p.y = A * sinX; + } + else { + //other case + //trace('stere:normal case'); + A = 2 * this.a * this.k0 * this.ms1 / (this.cosX0 * (1 + this.sinX0 * sinX + this.cosX0 * cosX * Math.cos(dlon))); + p.y = A * (this.cosX0 * sinX - this.sinX0 * cosX * Math.cos(dlon)) + this.y0; + } + p.x = A * cosX * Math.sin(dlon) + this.x0; + } + //trace(p.toString()); + return p; + }; + + + //* Stereographic inverse equations--mapping x,y to lat/long + exports.inverse = function(p) { + p.x -= this.x0; + p.y -= this.y0; + var lon, lat, ts, ce, Chi; + var rh = Math.sqrt(p.x * p.x + p.y * p.y); + if (this.sphere) { + var c = 2 * Math.atan(rh / (0.5 * this.a * this.k0)); + lon = this.long0; + lat = this.lat0; + if (rh <= EPSLN) { + p.x = lon; + p.y = lat; + return p; + } + lat = Math.asin(Math.cos(c) * this.sinlat0 + p.y * Math.sin(c) * this.coslat0 / rh); + if (Math.abs(this.coslat0) < EPSLN) { + if (this.lat0 > 0) { + lon = adjust_lon(this.long0 + Math.atan2(p.x, - 1 * p.y)); + } + else { + lon = adjust_lon(this.long0 + Math.atan2(p.x, p.y)); + } + } + else { + lon = adjust_lon(this.long0 + Math.atan2(p.x * Math.sin(c), rh * this.coslat0 * Math.cos(c) - p.y * this.sinlat0 * Math.sin(c))); + } + p.x = lon; + p.y = lat; + return p; + } + else { + if (Math.abs(this.coslat0) <= EPSLN) { + if (rh <= EPSLN) { + lat = this.lat0; + lon = this.long0; + p.x = lon; + p.y = lat; + //trace(p.toString()); + return p; + } + p.x *= this.con; + p.y *= this.con; + ts = rh * this.cons / (2 * this.a * this.k0); + lat = this.con * phi2z(this.e, ts); + lon = this.con * adjust_lon(this.con * this.long0 + Math.atan2(p.x, - 1 * p.y)); + } + else { + ce = 2 * Math.atan(rh * this.cosX0 / (2 * this.a * this.k0 * this.ms1)); + lon = this.long0; + if (rh <= EPSLN) { + Chi = this.X0; + } + else { + Chi = Math.asin(Math.cos(ce) * this.sinX0 + p.y * Math.sin(ce) * this.cosX0 / rh); + lon = adjust_lon(this.long0 + Math.atan2(p.x * Math.sin(ce), rh * this.cosX0 * Math.cos(ce) - p.y * this.sinX0 * Math.sin(ce))); + } + lat = -1 * phi2z(this.e, Math.tan(0.5 * (HALF_PI + Chi))); + } + } + p.x = lon; + p.y = lat; + + //trace(p.toString()); + return p; + + }; + exports.names = ["stere", "Stereographic_South_Pole", "Polar Stereographic (variant B)"]; + + +/***/ }, +/* 191 */ +/***/ function(module, exports) { + + /* + references: + Formules et constantes pour le Calcul pour la + projection cylindrique conforme à axe oblique et pour la transformation entre + des systèmes de référence. + http://www.swisstopo.admin.ch/internet/swisstopo/fr/home/topics/survey/sys/refsys/switzerland.parsysrelated1.31216.downloadList.77004.DownloadFile.tmp/swissprojectionfr.pdf + */ + exports.init = function() { + var phy0 = this.lat0; + this.lambda0 = this.long0; + var sinPhy0 = Math.sin(phy0); + var semiMajorAxis = this.a; + var invF = this.rf; + var flattening = 1 / invF; + var e2 = 2 * flattening - Math.pow(flattening, 2); + var e = this.e = Math.sqrt(e2); + this.R = this.k0 * semiMajorAxis * Math.sqrt(1 - e2) / (1 - e2 * Math.pow(sinPhy0, 2)); + this.alpha = Math.sqrt(1 + e2 / (1 - e2) * Math.pow(Math.cos(phy0), 4)); + this.b0 = Math.asin(sinPhy0 / this.alpha); + var k1 = Math.log(Math.tan(Math.PI / 4 + this.b0 / 2)); + var k2 = Math.log(Math.tan(Math.PI / 4 + phy0 / 2)); + var k3 = Math.log((1 + e * sinPhy0) / (1 - e * sinPhy0)); + this.K = k1 - this.alpha * k2 + this.alpha * e / 2 * k3; + }; + + + exports.forward = function(p) { + var Sa1 = Math.log(Math.tan(Math.PI / 4 - p.y / 2)); + var Sa2 = this.e / 2 * Math.log((1 + this.e * Math.sin(p.y)) / (1 - this.e * Math.sin(p.y))); + var S = -this.alpha * (Sa1 + Sa2) + this.K; + + // spheric latitude + var b = 2 * (Math.atan(Math.exp(S)) - Math.PI / 4); + + // spheric longitude + var I = this.alpha * (p.x - this.lambda0); + + // psoeudo equatorial rotation + var rotI = Math.atan(Math.sin(I) / (Math.sin(this.b0) * Math.tan(b) + Math.cos(this.b0) * Math.cos(I))); + + var rotB = Math.asin(Math.cos(this.b0) * Math.sin(b) - Math.sin(this.b0) * Math.cos(b) * Math.cos(I)); + + p.y = this.R / 2 * Math.log((1 + Math.sin(rotB)) / (1 - Math.sin(rotB))) + this.y0; + p.x = this.R * rotI + this.x0; + return p; + }; + + exports.inverse = function(p) { + var Y = p.x - this.x0; + var X = p.y - this.y0; + + var rotI = Y / this.R; + var rotB = 2 * (Math.atan(Math.exp(X / this.R)) - Math.PI / 4); + + var b = Math.asin(Math.cos(this.b0) * Math.sin(rotB) + Math.sin(this.b0) * Math.cos(rotB) * Math.cos(rotI)); + var I = Math.atan(Math.sin(rotI) / (Math.cos(this.b0) * Math.cos(rotI) - Math.sin(this.b0) * Math.tan(rotB))); + + var lambda = this.lambda0 + I / this.alpha; + + var S = 0; + var phy = b; + var prevPhy = -1000; + var iteration = 0; + while (Math.abs(phy - prevPhy) > 0.0000001) { + if (++iteration > 20) { + //...reportError("omercFwdInfinity"); + return; + } + //S = Math.log(Math.tan(Math.PI / 4 + phy / 2)); + S = 1 / this.alpha * (Math.log(Math.tan(Math.PI / 4 + b / 2)) - this.K) + this.e * Math.log(Math.tan(Math.PI / 4 + Math.asin(this.e * Math.sin(phy)) / 2)); + prevPhy = phy; + phy = 2 * Math.atan(Math.exp(S)) - Math.PI / 2; + } + + p.x = lambda; + p.y = phy; + return p; + }; + + exports.names = ["somerc"]; + + +/***/ }, +/* 192 */ +/***/ function(module, exports, __webpack_require__) { + + var tsfnz = __webpack_require__(164); + var adjust_lon = __webpack_require__(162); + var phi2z = __webpack_require__(165); + var HALF_PI = Math.PI/2; + var FORTPI = Math.PI/4; + var EPSLN = 1.0e-10; + + /* Initialize the Oblique Mercator projection + ------------------------------------------*/ + exports.init = function() { + this.no_off = this.no_off || false; + this.no_rot = this.no_rot || false; + + if (isNaN(this.k0)) { + this.k0 = 1; + } + var sinlat = Math.sin(this.lat0); + var coslat = Math.cos(this.lat0); + var con = this.e * sinlat; + + this.bl = Math.sqrt(1 + this.es / (1 - this.es) * Math.pow(coslat, 4)); + this.al = this.a * this.bl * this.k0 * Math.sqrt(1 - this.es) / (1 - con * con); + var t0 = tsfnz(this.e, this.lat0, sinlat); + var dl = this.bl / coslat * Math.sqrt((1 - this.es) / (1 - con * con)); + if (dl * dl < 1) { + dl = 1; + } + var fl; + var gl; + if (!isNaN(this.longc)) { + //Central point and azimuth method + + if (this.lat0 >= 0) { + fl = dl + Math.sqrt(dl * dl - 1); + } + else { + fl = dl - Math.sqrt(dl * dl - 1); + } + this.el = fl * Math.pow(t0, this.bl); + gl = 0.5 * (fl - 1 / fl); + this.gamma0 = Math.asin(Math.sin(this.alpha) / dl); + this.long0 = this.longc - Math.asin(gl * Math.tan(this.gamma0)) / this.bl; + + } + else { + //2 points method + var t1 = tsfnz(this.e, this.lat1, Math.sin(this.lat1)); + var t2 = tsfnz(this.e, this.lat2, Math.sin(this.lat2)); + if (this.lat0 >= 0) { + this.el = (dl + Math.sqrt(dl * dl - 1)) * Math.pow(t0, this.bl); + } + else { + this.el = (dl - Math.sqrt(dl * dl - 1)) * Math.pow(t0, this.bl); + } + var hl = Math.pow(t1, this.bl); + var ll = Math.pow(t2, this.bl); + fl = this.el / hl; + gl = 0.5 * (fl - 1 / fl); + var jl = (this.el * this.el - ll * hl) / (this.el * this.el + ll * hl); + var pl = (ll - hl) / (ll + hl); + var dlon12 = adjust_lon(this.long1 - this.long2); + this.long0 = 0.5 * (this.long1 + this.long2) - Math.atan(jl * Math.tan(0.5 * this.bl * (dlon12)) / pl) / this.bl; + this.long0 = adjust_lon(this.long0); + var dlon10 = adjust_lon(this.long1 - this.long0); + this.gamma0 = Math.atan(Math.sin(this.bl * (dlon10)) / gl); + this.alpha = Math.asin(dl * Math.sin(this.gamma0)); + } + + if (this.no_off) { + this.uc = 0; + } + else { + if (this.lat0 >= 0) { + this.uc = this.al / this.bl * Math.atan2(Math.sqrt(dl * dl - 1), Math.cos(this.alpha)); + } + else { + this.uc = -1 * this.al / this.bl * Math.atan2(Math.sqrt(dl * dl - 1), Math.cos(this.alpha)); + } + } + + }; + + + /* Oblique Mercator forward equations--mapping lat,long to x,y + ----------------------------------------------------------*/ + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + var dlon = adjust_lon(lon - this.long0); + var us, vs; + var con; + if (Math.abs(Math.abs(lat) - HALF_PI) <= EPSLN) { + if (lat > 0) { + con = -1; + } + else { + con = 1; + } + vs = this.al / this.bl * Math.log(Math.tan(FORTPI + con * this.gamma0 * 0.5)); + us = -1 * con * HALF_PI * this.al / this.bl; + } + else { + var t = tsfnz(this.e, lat, Math.sin(lat)); + var ql = this.el / Math.pow(t, this.bl); + var sl = 0.5 * (ql - 1 / ql); + var tl = 0.5 * (ql + 1 / ql); + var vl = Math.sin(this.bl * (dlon)); + var ul = (sl * Math.sin(this.gamma0) - vl * Math.cos(this.gamma0)) / tl; + if (Math.abs(Math.abs(ul) - 1) <= EPSLN) { + vs = Number.POSITIVE_INFINITY; + } + else { + vs = 0.5 * this.al * Math.log((1 - ul) / (1 + ul)) / this.bl; + } + if (Math.abs(Math.cos(this.bl * (dlon))) <= EPSLN) { + us = this.al * this.bl * (dlon); + } + else { + us = this.al * Math.atan2(sl * Math.cos(this.gamma0) + vl * Math.sin(this.gamma0), Math.cos(this.bl * dlon)) / this.bl; + } + } + + if (this.no_rot) { + p.x = this.x0 + us; + p.y = this.y0 + vs; + } + else { + + us -= this.uc; + p.x = this.x0 + vs * Math.cos(this.alpha) + us * Math.sin(this.alpha); + p.y = this.y0 + us * Math.cos(this.alpha) - vs * Math.sin(this.alpha); + } + return p; + }; + + exports.inverse = function(p) { + var us, vs; + if (this.no_rot) { + vs = p.y - this.y0; + us = p.x - this.x0; + } + else { + vs = (p.x - this.x0) * Math.cos(this.alpha) - (p.y - this.y0) * Math.sin(this.alpha); + us = (p.y - this.y0) * Math.cos(this.alpha) + (p.x - this.x0) * Math.sin(this.alpha); + us += this.uc; + } + var qp = Math.exp(-1 * this.bl * vs / this.al); + var sp = 0.5 * (qp - 1 / qp); + var tp = 0.5 * (qp + 1 / qp); + var vp = Math.sin(this.bl * us / this.al); + var up = (vp * Math.cos(this.gamma0) + sp * Math.sin(this.gamma0)) / tp; + var ts = Math.pow(this.el / Math.sqrt((1 + up) / (1 - up)), 1 / this.bl); + if (Math.abs(up - 1) < EPSLN) { + p.x = this.long0; + p.y = HALF_PI; + } + else if (Math.abs(up + 1) < EPSLN) { + p.x = this.long0; + p.y = -1 * HALF_PI; + } + else { + p.y = phi2z(this.e, ts); + p.x = adjust_lon(this.long0 - Math.atan2(sp * Math.cos(this.gamma0) - vp * Math.sin(this.gamma0), Math.cos(this.bl * us / this.al)) / this.bl); + } + return p; + }; + + exports.names = ["Hotine_Oblique_Mercator", "Hotine Oblique Mercator", "Hotine_Oblique_Mercator_Azimuth_Natural_Origin", "Hotine_Oblique_Mercator_Azimuth_Center", "omerc"]; + +/***/ }, +/* 193 */ +/***/ function(module, exports, __webpack_require__) { + + var EPSLN = 1.0e-10; + var msfnz = __webpack_require__(161); + var tsfnz = __webpack_require__(164); + var HALF_PI = Math.PI/2; + var sign = __webpack_require__(163); + var adjust_lon = __webpack_require__(162); + var phi2z = __webpack_require__(165); + exports.init = function() { + + // array of: r_maj,r_min,lat1,lat2,c_lon,c_lat,false_east,false_north + //double c_lat; /* center latitude */ + //double c_lon; /* center longitude */ + //double lat1; /* first standard parallel */ + //double lat2; /* second standard parallel */ + //double r_maj; /* major axis */ + //double r_min; /* minor axis */ + //double false_east; /* x offset in meters */ + //double false_north; /* y offset in meters */ + + if (!this.lat2) { + this.lat2 = this.lat1; + } //if lat2 is not defined + if (!this.k0) { + this.k0 = 1; + } + this.x0 = this.x0 || 0; + this.y0 = this.y0 || 0; + // Standard Parallels cannot be equal and on opposite sides of the equator + if (Math.abs(this.lat1 + this.lat2) < EPSLN) { + return; + } + + var temp = this.b / this.a; + this.e = Math.sqrt(1 - temp * temp); + + var sin1 = Math.sin(this.lat1); + var cos1 = Math.cos(this.lat1); + var ms1 = msfnz(this.e, sin1, cos1); + var ts1 = tsfnz(this.e, this.lat1, sin1); + + var sin2 = Math.sin(this.lat2); + var cos2 = Math.cos(this.lat2); + var ms2 = msfnz(this.e, sin2, cos2); + var ts2 = tsfnz(this.e, this.lat2, sin2); + + var ts0 = tsfnz(this.e, this.lat0, Math.sin(this.lat0)); + + if (Math.abs(this.lat1 - this.lat2) > EPSLN) { + this.ns = Math.log(ms1 / ms2) / Math.log(ts1 / ts2); + } + else { + this.ns = sin1; + } + if (isNaN(this.ns)) { + this.ns = sin1; + } + this.f0 = ms1 / (this.ns * Math.pow(ts1, this.ns)); + this.rh = this.a * this.f0 * Math.pow(ts0, this.ns); + if (!this.title) { + this.title = "Lambert Conformal Conic"; + } + }; + + + // Lambert Conformal conic forward equations--mapping lat,long to x,y + // ----------------------------------------------------------------- + exports.forward = function(p) { + + var lon = p.x; + var lat = p.y; + + // singular cases : + if (Math.abs(2 * Math.abs(lat) - Math.PI) <= EPSLN) { + lat = sign(lat) * (HALF_PI - 2 * EPSLN); + } + + var con = Math.abs(Math.abs(lat) - HALF_PI); + var ts, rh1; + if (con > EPSLN) { + ts = tsfnz(this.e, lat, Math.sin(lat)); + rh1 = this.a * this.f0 * Math.pow(ts, this.ns); + } + else { + con = lat * this.ns; + if (con <= 0) { + return null; + } + rh1 = 0; + } + var theta = this.ns * adjust_lon(lon - this.long0); + p.x = this.k0 * (rh1 * Math.sin(theta)) + this.x0; + p.y = this.k0 * (this.rh - rh1 * Math.cos(theta)) + this.y0; + + return p; + }; + + // Lambert Conformal Conic inverse equations--mapping x,y to lat/long + // ----------------------------------------------------------------- + exports.inverse = function(p) { + + var rh1, con, ts; + var lat, lon; + var x = (p.x - this.x0) / this.k0; + var y = (this.rh - (p.y - this.y0) / this.k0); + if (this.ns > 0) { + rh1 = Math.sqrt(x * x + y * y); + con = 1; + } + else { + rh1 = -Math.sqrt(x * x + y * y); + con = -1; + } + var theta = 0; + if (rh1 !== 0) { + theta = Math.atan2((con * x), (con * y)); + } + if ((rh1 !== 0) || (this.ns > 0)) { + con = 1 / this.ns; + ts = Math.pow((rh1 / (this.a * this.f0)), con); + lat = phi2z(this.e, ts); + if (lat === -9999) { + return null; + } + } + else { + lat = -HALF_PI; + } + lon = adjust_lon(theta / this.ns + this.long0); + + p.x = lon; + p.y = lat; + return p; + }; + + exports.names = ["Lambert Tangential Conformal Conic Projection", "Lambert_Conformal_Conic", "Lambert_Conformal_Conic_2SP", "lcc"]; + + +/***/ }, +/* 194 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + exports.init = function() { + this.a = 6377397.155; + this.es = 0.006674372230614; + this.e = Math.sqrt(this.es); + if (!this.lat0) { + this.lat0 = 0.863937979737193; + } + if (!this.long0) { + this.long0 = 0.7417649320975901 - 0.308341501185665; + } + /* if scale not set default to 0.9999 */ + if (!this.k0) { + this.k0 = 0.9999; + } + this.s45 = 0.785398163397448; /* 45 */ + this.s90 = 2 * this.s45; + this.fi0 = this.lat0; + this.e2 = this.es; + this.e = Math.sqrt(this.e2); + this.alfa = Math.sqrt(1 + (this.e2 * Math.pow(Math.cos(this.fi0), 4)) / (1 - this.e2)); + this.uq = 1.04216856380474; + this.u0 = Math.asin(Math.sin(this.fi0) / this.alfa); + this.g = Math.pow((1 + this.e * Math.sin(this.fi0)) / (1 - this.e * Math.sin(this.fi0)), this.alfa * this.e / 2); + this.k = Math.tan(this.u0 / 2 + this.s45) / Math.pow(Math.tan(this.fi0 / 2 + this.s45), this.alfa) * this.g; + this.k1 = this.k0; + this.n0 = this.a * Math.sqrt(1 - this.e2) / (1 - this.e2 * Math.pow(Math.sin(this.fi0), 2)); + this.s0 = 1.37008346281555; + this.n = Math.sin(this.s0); + this.ro0 = this.k1 * this.n0 / Math.tan(this.s0); + this.ad = this.s90 - this.uq; + }; + + /* ellipsoid */ + /* calculate xy from lat/lon */ + /* Constants, identical to inverse transform function */ + exports.forward = function(p) { + var gfi, u, deltav, s, d, eps, ro; + var lon = p.x; + var lat = p.y; + var delta_lon = adjust_lon(lon - this.long0); + /* Transformation */ + gfi = Math.pow(((1 + this.e * Math.sin(lat)) / (1 - this.e * Math.sin(lat))), (this.alfa * this.e / 2)); + u = 2 * (Math.atan(this.k * Math.pow(Math.tan(lat / 2 + this.s45), this.alfa) / gfi) - this.s45); + deltav = -delta_lon * this.alfa; + s = Math.asin(Math.cos(this.ad) * Math.sin(u) + Math.sin(this.ad) * Math.cos(u) * Math.cos(deltav)); + d = Math.asin(Math.cos(u) * Math.sin(deltav) / Math.cos(s)); + eps = this.n * d; + ro = this.ro0 * Math.pow(Math.tan(this.s0 / 2 + this.s45), this.n) / Math.pow(Math.tan(s / 2 + this.s45), this.n); + p.y = ro * Math.cos(eps) / 1; + p.x = ro * Math.sin(eps) / 1; + + if (!this.czech) { + p.y *= -1; + p.x *= -1; + } + return (p); + }; + + /* calculate lat/lon from xy */ + exports.inverse = function(p) { + var u, deltav, s, d, eps, ro, fi1; + var ok; + + /* Transformation */ + /* revert y, x*/ + var tmp = p.x; + p.x = p.y; + p.y = tmp; + if (!this.czech) { + p.y *= -1; + p.x *= -1; + } + ro = Math.sqrt(p.x * p.x + p.y * p.y); + eps = Math.atan2(p.y, p.x); + d = eps / Math.sin(this.s0); + s = 2 * (Math.atan(Math.pow(this.ro0 / ro, 1 / this.n) * Math.tan(this.s0 / 2 + this.s45)) - this.s45); + u = Math.asin(Math.cos(this.ad) * Math.sin(s) - Math.sin(this.ad) * Math.cos(s) * Math.cos(d)); + deltav = Math.asin(Math.cos(s) * Math.sin(d) / Math.cos(u)); + p.x = this.long0 - deltav / this.alfa; + fi1 = u; + ok = 0; + var iter = 0; + do { + p.y = 2 * (Math.atan(Math.pow(this.k, - 1 / this.alfa) * Math.pow(Math.tan(u / 2 + this.s45), 1 / this.alfa) * Math.pow((1 + this.e * Math.sin(fi1)) / (1 - this.e * Math.sin(fi1)), this.e / 2)) - this.s45); + if (Math.abs(fi1 - p.y) < 0.0000000001) { + ok = 1; + } + fi1 = p.y; + iter += 1; + } while (ok === 0 && iter < 15); + if (iter >= 15) { + return null; + } + + return (p); + }; + exports.names = ["Krovak", "krovak"]; + + +/***/ }, +/* 195 */ +/***/ function(module, exports, __webpack_require__) { + + var mlfn = __webpack_require__(184); + var e0fn = __webpack_require__(180); + var e1fn = __webpack_require__(181); + var e2fn = __webpack_require__(182); + var e3fn = __webpack_require__(183); + var gN = __webpack_require__(196); + var adjust_lon = __webpack_require__(162); + var adjust_lat = __webpack_require__(197); + var imlfn = __webpack_require__(198); + var HALF_PI = Math.PI/2; + var EPSLN = 1.0e-10; + exports.init = function() { + if (!this.sphere) { + this.e0 = e0fn(this.es); + this.e1 = e1fn(this.es); + this.e2 = e2fn(this.es); + this.e3 = e3fn(this.es); + this.ml0 = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); + } + }; + + + + /* Cassini forward equations--mapping lat,long to x,y + -----------------------------------------------------------------------*/ + exports.forward = function(p) { + + /* Forward equations + -----------------*/ + var x, y; + var lam = p.x; + var phi = p.y; + lam = adjust_lon(lam - this.long0); + + if (this.sphere) { + x = this.a * Math.asin(Math.cos(phi) * Math.sin(lam)); + y = this.a * (Math.atan2(Math.tan(phi), Math.cos(lam)) - this.lat0); + } + else { + //ellipsoid + var sinphi = Math.sin(phi); + var cosphi = Math.cos(phi); + var nl = gN(this.a, this.e, sinphi); + var tl = Math.tan(phi) * Math.tan(phi); + var al = lam * Math.cos(phi); + var asq = al * al; + var cl = this.es * cosphi * cosphi / (1 - this.es); + var ml = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, phi); + + x = nl * al * (1 - asq * tl * (1 / 6 - (8 - tl + 8 * cl) * asq / 120)); + y = ml - this.ml0 + nl * sinphi / cosphi * asq * (0.5 + (5 - tl + 6 * cl) * asq / 24); + + + } + + p.x = x + this.x0; + p.y = y + this.y0; + return p; + }; + + /* Inverse equations + -----------------*/ + exports.inverse = function(p) { + p.x -= this.x0; + p.y -= this.y0; + var x = p.x / this.a; + var y = p.y / this.a; + var phi, lam; + + if (this.sphere) { + var dd = y + this.lat0; + phi = Math.asin(Math.sin(dd) * Math.cos(x)); + lam = Math.atan2(Math.tan(x), Math.cos(dd)); + } + else { + /* ellipsoid */ + var ml1 = this.ml0 / this.a + y; + var phi1 = imlfn(ml1, this.e0, this.e1, this.e2, this.e3); + if (Math.abs(Math.abs(phi1) - HALF_PI) <= EPSLN) { + p.x = this.long0; + p.y = HALF_PI; + if (y < 0) { + p.y *= -1; + } + return p; + } + var nl1 = gN(this.a, this.e, Math.sin(phi1)); + + var rl1 = nl1 * nl1 * nl1 / this.a / this.a * (1 - this.es); + var tl1 = Math.pow(Math.tan(phi1), 2); + var dl = x * this.a / nl1; + var dsq = dl * dl; + phi = phi1 - nl1 * Math.tan(phi1) / rl1 * dl * dl * (0.5 - (1 + 3 * tl1) * dl * dl / 24); + lam = dl * (1 - dsq * (tl1 / 3 + (1 + 3 * tl1) * tl1 * dsq / 15)) / Math.cos(phi1); + + } + + p.x = adjust_lon(lam + this.long0); + p.y = adjust_lat(phi); + return p; + + }; + exports.names = ["Cassini", "Cassini_Soldner", "cass"]; + +/***/ }, +/* 196 */ +/***/ function(module, exports) { + + module.exports = function(a, e, sinphi) { + var temp = e * sinphi; + return a / Math.sqrt(1 - temp * temp); + }; + +/***/ }, +/* 197 */ +/***/ function(module, exports, __webpack_require__) { + + var HALF_PI = Math.PI/2; + var sign = __webpack_require__(163); + + module.exports = function(x) { + return (Math.abs(x) < HALF_PI) ? x : (x - (sign(x) * Math.PI)); + }; + +/***/ }, +/* 198 */ +/***/ function(module, exports) { + + module.exports = function(ml, e0, e1, e2, e3) { + var phi; + var dphi; + + phi = ml / e0; + for (var i = 0; i < 15; i++) { + dphi = (ml - (e0 * phi - e1 * Math.sin(2 * phi) + e2 * Math.sin(4 * phi) - e3 * Math.sin(6 * phi))) / (e0 - 2 * e1 * Math.cos(2 * phi) + 4 * e2 * Math.cos(4 * phi) - 6 * e3 * Math.cos(6 * phi)); + phi += dphi; + if (Math.abs(dphi) <= 0.0000000001) { + return phi; + } + } + + //..reportError("IMLFN-CONV:Latitude failed to converge after 15 iterations"); + return NaN; + }; + +/***/ }, +/* 199 */ +/***/ function(module, exports, __webpack_require__) { + + var HALF_PI = Math.PI/2; + var FORTPI = Math.PI/4; + var EPSLN = 1.0e-10; + var qsfnz = __webpack_require__(200); + var adjust_lon = __webpack_require__(162); + /* + reference + "New Equal-Area Map Projections for Noncircular Regions", John P. Snyder, + The American Cartographer, Vol 15, No. 4, October 1988, pp. 341-355. + */ + + exports.S_POLE = 1; + exports.N_POLE = 2; + exports.EQUIT = 3; + exports.OBLIQ = 4; + + + /* Initialize the Lambert Azimuthal Equal Area projection + ------------------------------------------------------*/ + exports.init = function() { + var t = Math.abs(this.lat0); + if (Math.abs(t - HALF_PI) < EPSLN) { + this.mode = this.lat0 < 0 ? this.S_POLE : this.N_POLE; + } + else if (Math.abs(t) < EPSLN) { + this.mode = this.EQUIT; + } + else { + this.mode = this.OBLIQ; + } + if (this.es > 0) { + var sinphi; + + this.qp = qsfnz(this.e, 1); + this.mmf = 0.5 / (1 - this.es); + this.apa = this.authset(this.es); + switch (this.mode) { + case this.N_POLE: + this.dd = 1; + break; + case this.S_POLE: + this.dd = 1; + break; + case this.EQUIT: + this.rq = Math.sqrt(0.5 * this.qp); + this.dd = 1 / this.rq; + this.xmf = 1; + this.ymf = 0.5 * this.qp; + break; + case this.OBLIQ: + this.rq = Math.sqrt(0.5 * this.qp); + sinphi = Math.sin(this.lat0); + this.sinb1 = qsfnz(this.e, sinphi) / this.qp; + this.cosb1 = Math.sqrt(1 - this.sinb1 * this.sinb1); + this.dd = Math.cos(this.lat0) / (Math.sqrt(1 - this.es * sinphi * sinphi) * this.rq * this.cosb1); + this.ymf = (this.xmf = this.rq) / this.dd; + this.xmf *= this.dd; + break; + } + } + else { + if (this.mode === this.OBLIQ) { + this.sinph0 = Math.sin(this.lat0); + this.cosph0 = Math.cos(this.lat0); + } + } + }; + + /* Lambert Azimuthal Equal Area forward equations--mapping lat,long to x,y + -----------------------------------------------------------------------*/ + exports.forward = function(p) { + + /* Forward equations + -----------------*/ + var x, y, coslam, sinlam, sinphi, q, sinb, cosb, b, cosphi; + var lam = p.x; + var phi = p.y; + + lam = adjust_lon(lam - this.long0); + + if (this.sphere) { + sinphi = Math.sin(phi); + cosphi = Math.cos(phi); + coslam = Math.cos(lam); + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + y = (this.mode === this.EQUIT) ? 1 + cosphi * coslam : 1 + this.sinph0 * sinphi + this.cosph0 * cosphi * coslam; + if (y <= EPSLN) { + return null; + } + y = Math.sqrt(2 / y); + x = y * cosphi * Math.sin(lam); + y *= (this.mode === this.EQUIT) ? sinphi : this.cosph0 * sinphi - this.sinph0 * cosphi * coslam; + } + else if (this.mode === this.N_POLE || this.mode === this.S_POLE) { + if (this.mode === this.N_POLE) { + coslam = -coslam; + } + if (Math.abs(phi + this.phi0) < EPSLN) { + return null; + } + y = FORTPI - phi * 0.5; + y = 2 * ((this.mode === this.S_POLE) ? Math.cos(y) : Math.sin(y)); + x = y * Math.sin(lam); + y *= coslam; + } + } + else { + sinb = 0; + cosb = 0; + b = 0; + coslam = Math.cos(lam); + sinlam = Math.sin(lam); + sinphi = Math.sin(phi); + q = qsfnz(this.e, sinphi); + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + sinb = q / this.qp; + cosb = Math.sqrt(1 - sinb * sinb); + } + switch (this.mode) { + case this.OBLIQ: + b = 1 + this.sinb1 * sinb + this.cosb1 * cosb * coslam; + break; + case this.EQUIT: + b = 1 + cosb * coslam; + break; + case this.N_POLE: + b = HALF_PI + phi; + q = this.qp - q; + break; + case this.S_POLE: + b = phi - HALF_PI; + q = this.qp + q; + break; + } + if (Math.abs(b) < EPSLN) { + return null; + } + switch (this.mode) { + case this.OBLIQ: + case this.EQUIT: + b = Math.sqrt(2 / b); + if (this.mode === this.OBLIQ) { + y = this.ymf * b * (this.cosb1 * sinb - this.sinb1 * cosb * coslam); + } + else { + y = (b = Math.sqrt(2 / (1 + cosb * coslam))) * sinb * this.ymf; + } + x = this.xmf * b * cosb * sinlam; + break; + case this.N_POLE: + case this.S_POLE: + if (q >= 0) { + x = (b = Math.sqrt(q)) * sinlam; + y = coslam * ((this.mode === this.S_POLE) ? b : -b); + } + else { + x = y = 0; + } + break; + } + } + + p.x = this.a * x + this.x0; + p.y = this.a * y + this.y0; + return p; + }; + + /* Inverse equations + -----------------*/ + exports.inverse = function(p) { + p.x -= this.x0; + p.y -= this.y0; + var x = p.x / this.a; + var y = p.y / this.a; + var lam, phi, cCe, sCe, q, rho, ab; + + if (this.sphere) { + var cosz = 0, + rh, sinz = 0; + + rh = Math.sqrt(x * x + y * y); + phi = rh * 0.5; + if (phi > 1) { + return null; + } + phi = 2 * Math.asin(phi); + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + sinz = Math.sin(phi); + cosz = Math.cos(phi); + } + switch (this.mode) { + case this.EQUIT: + phi = (Math.abs(rh) <= EPSLN) ? 0 : Math.asin(y * sinz / rh); + x *= sinz; + y = cosz * rh; + break; + case this.OBLIQ: + phi = (Math.abs(rh) <= EPSLN) ? this.phi0 : Math.asin(cosz * this.sinph0 + y * sinz * this.cosph0 / rh); + x *= sinz * this.cosph0; + y = (cosz - Math.sin(phi) * this.sinph0) * rh; + break; + case this.N_POLE: + y = -y; + phi = HALF_PI - phi; + break; + case this.S_POLE: + phi -= HALF_PI; + break; + } + lam = (y === 0 && (this.mode === this.EQUIT || this.mode === this.OBLIQ)) ? 0 : Math.atan2(x, y); + } + else { + ab = 0; + if (this.mode === this.OBLIQ || this.mode === this.EQUIT) { + x /= this.dd; + y *= this.dd; + rho = Math.sqrt(x * x + y * y); + if (rho < EPSLN) { + p.x = 0; + p.y = this.phi0; + return p; + } + sCe = 2 * Math.asin(0.5 * rho / this.rq); + cCe = Math.cos(sCe); + x *= (sCe = Math.sin(sCe)); + if (this.mode === this.OBLIQ) { + ab = cCe * this.sinb1 + y * sCe * this.cosb1 / rho; + q = this.qp * ab; + y = rho * this.cosb1 * cCe - y * this.sinb1 * sCe; + } + else { + ab = y * sCe / rho; + q = this.qp * ab; + y = rho * cCe; + } + } + else if (this.mode === this.N_POLE || this.mode === this.S_POLE) { + if (this.mode === this.N_POLE) { + y = -y; + } + q = (x * x + y * y); + if (!q) { + p.x = 0; + p.y = this.phi0; + return p; + } + ab = 1 - q / this.qp; + if (this.mode === this.S_POLE) { + ab = -ab; + } + } + lam = Math.atan2(x, y); + phi = this.authlat(Math.asin(ab), this.apa); + } + + + p.x = adjust_lon(this.long0 + lam); + p.y = phi; + return p; + }; + + /* determine latitude from authalic latitude */ + exports.P00 = 0.33333333333333333333; + exports.P01 = 0.17222222222222222222; + exports.P02 = 0.10257936507936507936; + exports.P10 = 0.06388888888888888888; + exports.P11 = 0.06640211640211640211; + exports.P20 = 0.01641501294219154443; + + exports.authset = function(es) { + var t; + var APA = []; + APA[0] = es * this.P00; + t = es * es; + APA[0] += t * this.P01; + APA[1] = t * this.P10; + t *= es; + APA[0] += t * this.P02; + APA[1] += t * this.P11; + APA[2] = t * this.P20; + return APA; + }; + + exports.authlat = function(beta, APA) { + var t = beta + beta; + return (beta + APA[0] * Math.sin(t) + APA[1] * Math.sin(t + t) + APA[2] * Math.sin(t + t + t)); + }; + exports.names = ["Lambert Azimuthal Equal Area", "Lambert_Azimuthal_Equal_Area", "laea"]; + + +/***/ }, +/* 200 */ +/***/ function(module, exports) { + + module.exports = function(eccent, sinphi) { + var con; + if (eccent > 1.0e-7) { + con = eccent * sinphi; + return ((1 - eccent * eccent) * (sinphi / (1 - con * con) - (0.5 / eccent) * Math.log((1 - con) / (1 + con)))); + } + else { + return (2 * sinphi); + } + }; + +/***/ }, +/* 201 */ +/***/ function(module, exports, __webpack_require__) { + + var EPSLN = 1.0e-10; + var msfnz = __webpack_require__(161); + var qsfnz = __webpack_require__(200); + var adjust_lon = __webpack_require__(162); + var asinz = __webpack_require__(185); + exports.init = function() { + + if (Math.abs(this.lat1 + this.lat2) < EPSLN) { + return; + } + this.temp = this.b / this.a; + this.es = 1 - Math.pow(this.temp, 2); + this.e3 = Math.sqrt(this.es); + + this.sin_po = Math.sin(this.lat1); + this.cos_po = Math.cos(this.lat1); + this.t1 = this.sin_po; + this.con = this.sin_po; + this.ms1 = msfnz(this.e3, this.sin_po, this.cos_po); + this.qs1 = qsfnz(this.e3, this.sin_po, this.cos_po); + + this.sin_po = Math.sin(this.lat2); + this.cos_po = Math.cos(this.lat2); + this.t2 = this.sin_po; + this.ms2 = msfnz(this.e3, this.sin_po, this.cos_po); + this.qs2 = qsfnz(this.e3, this.sin_po, this.cos_po); + + this.sin_po = Math.sin(this.lat0); + this.cos_po = Math.cos(this.lat0); + this.t3 = this.sin_po; + this.qs0 = qsfnz(this.e3, this.sin_po, this.cos_po); + + if (Math.abs(this.lat1 - this.lat2) > EPSLN) { + this.ns0 = (this.ms1 * this.ms1 - this.ms2 * this.ms2) / (this.qs2 - this.qs1); + } + else { + this.ns0 = this.con; + } + this.c = this.ms1 * this.ms1 + this.ns0 * this.qs1; + this.rh = this.a * Math.sqrt(this.c - this.ns0 * this.qs0) / this.ns0; + }; + + /* Albers Conical Equal Area forward equations--mapping lat,long to x,y + -------------------------------------------------------------------*/ + exports.forward = function(p) { + + var lon = p.x; + var lat = p.y; + + this.sin_phi = Math.sin(lat); + this.cos_phi = Math.cos(lat); + + var qs = qsfnz(this.e3, this.sin_phi, this.cos_phi); + var rh1 = this.a * Math.sqrt(this.c - this.ns0 * qs) / this.ns0; + var theta = this.ns0 * adjust_lon(lon - this.long0); + var x = rh1 * Math.sin(theta) + this.x0; + var y = this.rh - rh1 * Math.cos(theta) + this.y0; + + p.x = x; + p.y = y; + return p; + }; + + + exports.inverse = function(p) { + var rh1, qs, con, theta, lon, lat; + + p.x -= this.x0; + p.y = this.rh - p.y + this.y0; + if (this.ns0 >= 0) { + rh1 = Math.sqrt(p.x * p.x + p.y * p.y); + con = 1; + } + else { + rh1 = -Math.sqrt(p.x * p.x + p.y * p.y); + con = -1; + } + theta = 0; + if (rh1 !== 0) { + theta = Math.atan2(con * p.x, con * p.y); + } + con = rh1 * this.ns0 / this.a; + if (this.sphere) { + lat = Math.asin((this.c - con * con) / (2 * this.ns0)); + } + else { + qs = (this.c - con * con) / this.ns0; + lat = this.phi1z(this.e3, qs); + } + + lon = adjust_lon(theta / this.ns0 + this.long0); + p.x = lon; + p.y = lat; + return p; + }; + + /* Function to compute phi1, the latitude for the inverse of the + Albers Conical Equal-Area projection. + -------------------------------------------*/ + exports.phi1z = function(eccent, qs) { + var sinphi, cosphi, con, com, dphi; + var phi = asinz(0.5 * qs); + if (eccent < EPSLN) { + return phi; + } + + var eccnts = eccent * eccent; + for (var i = 1; i <= 25; i++) { + sinphi = Math.sin(phi); + cosphi = Math.cos(phi); + con = eccent * sinphi; + com = 1 - con * con; + dphi = 0.5 * com * com / cosphi * (qs / (1 - eccnts) - sinphi / com + 0.5 / eccent * Math.log((1 - con) / (1 + con))); + phi = phi + dphi; + if (Math.abs(dphi) <= 1e-7) { + return phi; + } + } + return null; + }; + exports.names = ["Albers_Conic_Equal_Area", "Albers", "aea"]; + + +/***/ }, +/* 202 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + var EPSLN = 1.0e-10; + var asinz = __webpack_require__(185); + + /* + reference: + Wolfram Mathworld "Gnomonic Projection" + http://mathworld.wolfram.com/GnomonicProjection.html + Accessed: 12th November 2009 + */ + exports.init = function() { + + /* Place parameters in static storage for common use + -------------------------------------------------*/ + this.sin_p14 = Math.sin(this.lat0); + this.cos_p14 = Math.cos(this.lat0); + // Approximation for projecting points to the horizon (infinity) + this.infinity_dist = 1000 * this.a; + this.rc = 1; + }; + + + /* Gnomonic forward equations--mapping lat,long to x,y + ---------------------------------------------------*/ + exports.forward = function(p) { + var sinphi, cosphi; /* sin and cos value */ + var dlon; /* delta longitude value */ + var coslon; /* cos of longitude */ + var ksp; /* scale factor */ + var g; + var x, y; + var lon = p.x; + var lat = p.y; + /* Forward equations + -----------------*/ + dlon = adjust_lon(lon - this.long0); + + sinphi = Math.sin(lat); + cosphi = Math.cos(lat); + + coslon = Math.cos(dlon); + g = this.sin_p14 * sinphi + this.cos_p14 * cosphi * coslon; + ksp = 1; + if ((g > 0) || (Math.abs(g) <= EPSLN)) { + x = this.x0 + this.a * ksp * cosphi * Math.sin(dlon) / g; + y = this.y0 + this.a * ksp * (this.cos_p14 * sinphi - this.sin_p14 * cosphi * coslon) / g; + } + else { + + // Point is in the opposing hemisphere and is unprojectable + // We still need to return a reasonable point, so we project + // to infinity, on a bearing + // equivalent to the northern hemisphere equivalent + // This is a reasonable approximation for short shapes and lines that + // straddle the horizon. + + x = this.x0 + this.infinity_dist * cosphi * Math.sin(dlon); + y = this.y0 + this.infinity_dist * (this.cos_p14 * sinphi - this.sin_p14 * cosphi * coslon); + + } + p.x = x; + p.y = y; + return p; + }; + + + exports.inverse = function(p) { + var rh; /* Rho */ + var sinc, cosc; + var c; + var lon, lat; + + /* Inverse equations + -----------------*/ + p.x = (p.x - this.x0) / this.a; + p.y = (p.y - this.y0) / this.a; + + p.x /= this.k0; + p.y /= this.k0; + + if ((rh = Math.sqrt(p.x * p.x + p.y * p.y))) { + c = Math.atan2(rh, this.rc); + sinc = Math.sin(c); + cosc = Math.cos(c); + + lat = asinz(cosc * this.sin_p14 + (p.y * sinc * this.cos_p14) / rh); + lon = Math.atan2(p.x * sinc, rh * this.cos_p14 * cosc - p.y * this.sin_p14 * sinc); + lon = adjust_lon(this.long0 + lon); + } + else { + lat = this.phic0; + lon = 0; + } + + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["gnom"]; + + +/***/ }, +/* 203 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + var qsfnz = __webpack_require__(200); + var msfnz = __webpack_require__(161); + var iqsfnz = __webpack_require__(204); + /* + reference: + "Cartographic Projection Procedures for the UNIX Environment- + A User's Manual" by Gerald I. Evenden, + USGS Open File Report 90-284and Release 4 Interim Reports (2003) + */ + exports.init = function() { + //no-op + if (!this.sphere) { + this.k0 = msfnz(this.e, Math.sin(this.lat_ts), Math.cos(this.lat_ts)); + } + }; + + + /* Cylindrical Equal Area forward equations--mapping lat,long to x,y + ------------------------------------------------------------*/ + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + var x, y; + /* Forward equations + -----------------*/ + var dlon = adjust_lon(lon - this.long0); + if (this.sphere) { + x = this.x0 + this.a * dlon * Math.cos(this.lat_ts); + y = this.y0 + this.a * Math.sin(lat) / Math.cos(this.lat_ts); + } + else { + var qs = qsfnz(this.e, Math.sin(lat)); + x = this.x0 + this.a * this.k0 * dlon; + y = this.y0 + this.a * qs * 0.5 / this.k0; + } + + p.x = x; + p.y = y; + return p; + }; + + /* Cylindrical Equal Area inverse equations--mapping x,y to lat/long + ------------------------------------------------------------*/ + exports.inverse = function(p) { + p.x -= this.x0; + p.y -= this.y0; + var lon, lat; + + if (this.sphere) { + lon = adjust_lon(this.long0 + (p.x / this.a) / Math.cos(this.lat_ts)); + lat = Math.asin((p.y / this.a) * Math.cos(this.lat_ts)); + } + else { + lat = iqsfnz(this.e, 2 * p.y * this.k0 / this.a); + lon = adjust_lon(this.long0 + p.x / (this.a * this.k0)); + } + + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["cea"]; + + +/***/ }, +/* 204 */ +/***/ function(module, exports) { + + var HALF_PI = Math.PI/2; + + module.exports = function(eccent, q) { + var temp = 1 - (1 - eccent * eccent) / (2 * eccent) * Math.log((1 - eccent) / (1 + eccent)); + if (Math.abs(Math.abs(q) - temp) < 1.0E-6) { + if (q < 0) { + return (-1 * HALF_PI); + } + else { + return HALF_PI; + } + } + //var phi = 0.5* q/(1-eccent*eccent); + var phi = Math.asin(0.5 * q); + var dphi; + var sin_phi; + var cos_phi; + var con; + for (var i = 0; i < 30; i++) { + sin_phi = Math.sin(phi); + cos_phi = Math.cos(phi); + con = eccent * sin_phi; + dphi = Math.pow(1 - con * con, 2) / (2 * cos_phi) * (q / (1 - eccent * eccent) - sin_phi / (1 - con * con) + 0.5 / eccent * Math.log((1 - con) / (1 + con))); + phi += dphi; + if (Math.abs(dphi) <= 0.0000000001) { + return phi; + } + } + + //console.log("IQSFN-CONV:Latitude failed to converge after 30 iterations"); + return NaN; + }; + +/***/ }, +/* 205 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + var adjust_lat = __webpack_require__(197); + exports.init = function() { + + this.x0 = this.x0 || 0; + this.y0 = this.y0 || 0; + this.lat0 = this.lat0 || 0; + this.long0 = this.long0 || 0; + this.lat_ts = this.lat_ts || 0; + this.title = this.title || "Equidistant Cylindrical (Plate Carre)"; + + this.rc = Math.cos(this.lat_ts); + }; + + + // forward equations--mapping lat,long to x,y + // ----------------------------------------------------------------- + exports.forward = function(p) { + + var lon = p.x; + var lat = p.y; + + var dlon = adjust_lon(lon - this.long0); + var dlat = adjust_lat(lat - this.lat0); + p.x = this.x0 + (this.a * dlon * this.rc); + p.y = this.y0 + (this.a * dlat); + return p; + }; + + // inverse equations--mapping x,y to lat/long + // ----------------------------------------------------------------- + exports.inverse = function(p) { + + var x = p.x; + var y = p.y; + + p.x = adjust_lon(this.long0 + ((x - this.x0) / (this.a * this.rc))); + p.y = adjust_lat(this.lat0 + ((y - this.y0) / (this.a))); + return p; + }; + exports.names = ["Equirectangular", "Equidistant_Cylindrical", "eqc"]; + + +/***/ }, +/* 206 */ +/***/ function(module, exports, __webpack_require__) { + + var e0fn = __webpack_require__(180); + var e1fn = __webpack_require__(181); + var e2fn = __webpack_require__(182); + var e3fn = __webpack_require__(183); + var adjust_lon = __webpack_require__(162); + var adjust_lat = __webpack_require__(197); + var mlfn = __webpack_require__(184); + var EPSLN = 1.0e-10; + var gN = __webpack_require__(196); + var MAX_ITER = 20; + exports.init = function() { + /* Place parameters in static storage for common use + -------------------------------------------------*/ + this.temp = this.b / this.a; + this.es = 1 - Math.pow(this.temp, 2); // devait etre dans tmerc.js mais n y est pas donc je commente sinon retour de valeurs nulles + this.e = Math.sqrt(this.es); + this.e0 = e0fn(this.es); + this.e1 = e1fn(this.es); + this.e2 = e2fn(this.es); + this.e3 = e3fn(this.es); + this.ml0 = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); //si que des zeros le calcul ne se fait pas + }; + + + /* Polyconic forward equations--mapping lat,long to x,y + ---------------------------------------------------*/ + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + var x, y, el; + var dlon = adjust_lon(lon - this.long0); + el = dlon * Math.sin(lat); + if (this.sphere) { + if (Math.abs(lat) <= EPSLN) { + x = this.a * dlon; + y = -1 * this.a * this.lat0; + } + else { + x = this.a * Math.sin(el) / Math.tan(lat); + y = this.a * (adjust_lat(lat - this.lat0) + (1 - Math.cos(el)) / Math.tan(lat)); + } + } + else { + if (Math.abs(lat) <= EPSLN) { + x = this.a * dlon; + y = -1 * this.ml0; + } + else { + var nl = gN(this.a, this.e, Math.sin(lat)) / Math.tan(lat); + x = nl * Math.sin(el); + y = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, lat) - this.ml0 + nl * (1 - Math.cos(el)); + } + + } + p.x = x + this.x0; + p.y = y + this.y0; + return p; + }; + + + /* Inverse equations + -----------------*/ + exports.inverse = function(p) { + var lon, lat, x, y, i; + var al, bl; + var phi, dphi; + x = p.x - this.x0; + y = p.y - this.y0; + + if (this.sphere) { + if (Math.abs(y + this.a * this.lat0) <= EPSLN) { + lon = adjust_lon(x / this.a + this.long0); + lat = 0; + } + else { + al = this.lat0 + y / this.a; + bl = x * x / this.a / this.a + al * al; + phi = al; + var tanphi; + for (i = MAX_ITER; i; --i) { + tanphi = Math.tan(phi); + dphi = -1 * (al * (phi * tanphi + 1) - phi - 0.5 * (phi * phi + bl) * tanphi) / ((phi - al) / tanphi - 1); + phi += dphi; + if (Math.abs(dphi) <= EPSLN) { + lat = phi; + break; + } + } + lon = adjust_lon(this.long0 + (Math.asin(x * Math.tan(phi) / this.a)) / Math.sin(lat)); + } + } + else { + if (Math.abs(y + this.ml0) <= EPSLN) { + lat = 0; + lon = adjust_lon(this.long0 + x / this.a); + } + else { + + al = (this.ml0 + y) / this.a; + bl = x * x / this.a / this.a + al * al; + phi = al; + var cl, mln, mlnp, ma; + var con; + for (i = MAX_ITER; i; --i) { + con = this.e * Math.sin(phi); + cl = Math.sqrt(1 - con * con) * Math.tan(phi); + mln = this.a * mlfn(this.e0, this.e1, this.e2, this.e3, phi); + mlnp = this.e0 - 2 * this.e1 * Math.cos(2 * phi) + 4 * this.e2 * Math.cos(4 * phi) - 6 * this.e3 * Math.cos(6 * phi); + ma = mln / this.a; + dphi = (al * (cl * ma + 1) - ma - 0.5 * cl * (ma * ma + bl)) / (this.es * Math.sin(2 * phi) * (ma * ma + bl - 2 * al * ma) / (4 * cl) + (al - ma) * (cl * mlnp - 2 / Math.sin(2 * phi)) - mlnp); + phi -= dphi; + if (Math.abs(dphi) <= EPSLN) { + lat = phi; + break; + } + } + + //lat=phi4z(this.e,this.e0,this.e1,this.e2,this.e3,al,bl,0,0); + cl = Math.sqrt(1 - this.es * Math.pow(Math.sin(lat), 2)) * Math.tan(lat); + lon = adjust_lon(this.long0 + Math.asin(x * cl / this.a) / Math.sin(lat)); + } + } + + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["Polyconic", "poly"]; + +/***/ }, +/* 207 */ +/***/ function(module, exports) { + + var SEC_TO_RAD = 4.84813681109535993589914102357e-6; + /* + reference + Department of Land and Survey Technical Circular 1973/32 + http://www.linz.govt.nz/docs/miscellaneous/nz-map-definition.pdf + OSG Technical Report 4.1 + http://www.linz.govt.nz/docs/miscellaneous/nzmg.pdf + */ + + /** + * iterations: Number of iterations to refine inverse transform. + * 0 -> km accuracy + * 1 -> m accuracy -- suitable for most mapping applications + * 2 -> mm accuracy + */ + exports.iterations = 1; + + exports.init = function() { + this.A = []; + this.A[1] = 0.6399175073; + this.A[2] = -0.1358797613; + this.A[3] = 0.063294409; + this.A[4] = -0.02526853; + this.A[5] = 0.0117879; + this.A[6] = -0.0055161; + this.A[7] = 0.0026906; + this.A[8] = -0.001333; + this.A[9] = 0.00067; + this.A[10] = -0.00034; + + this.B_re = []; + this.B_im = []; + this.B_re[1] = 0.7557853228; + this.B_im[1] = 0; + this.B_re[2] = 0.249204646; + this.B_im[2] = 0.003371507; + this.B_re[3] = -0.001541739; + this.B_im[3] = 0.041058560; + this.B_re[4] = -0.10162907; + this.B_im[4] = 0.01727609; + this.B_re[5] = -0.26623489; + this.B_im[5] = -0.36249218; + this.B_re[6] = -0.6870983; + this.B_im[6] = -1.1651967; + + this.C_re = []; + this.C_im = []; + this.C_re[1] = 1.3231270439; + this.C_im[1] = 0; + this.C_re[2] = -0.577245789; + this.C_im[2] = -0.007809598; + this.C_re[3] = 0.508307513; + this.C_im[3] = -0.112208952; + this.C_re[4] = -0.15094762; + this.C_im[4] = 0.18200602; + this.C_re[5] = 1.01418179; + this.C_im[5] = 1.64497696; + this.C_re[6] = 1.9660549; + this.C_im[6] = 2.5127645; + + this.D = []; + this.D[1] = 1.5627014243; + this.D[2] = 0.5185406398; + this.D[3] = -0.03333098; + this.D[4] = -0.1052906; + this.D[5] = -0.0368594; + this.D[6] = 0.007317; + this.D[7] = 0.01220; + this.D[8] = 0.00394; + this.D[9] = -0.0013; + }; + + /** + New Zealand Map Grid Forward - long/lat to x/y + long/lat in radians + */ + exports.forward = function(p) { + var n; + var lon = p.x; + var lat = p.y; + + var delta_lat = lat - this.lat0; + var delta_lon = lon - this.long0; + + // 1. Calculate d_phi and d_psi ... // and d_lambda + // For this algorithm, delta_latitude is in seconds of arc x 10-5, so we need to scale to those units. Longitude is radians. + var d_phi = delta_lat / SEC_TO_RAD * 1E-5; + var d_lambda = delta_lon; + var d_phi_n = 1; // d_phi^0 + + var d_psi = 0; + for (n = 1; n <= 10; n++) { + d_phi_n = d_phi_n * d_phi; + d_psi = d_psi + this.A[n] * d_phi_n; + } + + // 2. Calculate theta + var th_re = d_psi; + var th_im = d_lambda; + + // 3. Calculate z + var th_n_re = 1; + var th_n_im = 0; // theta^0 + var th_n_re1; + var th_n_im1; + + var z_re = 0; + var z_im = 0; + for (n = 1; n <= 6; n++) { + th_n_re1 = th_n_re * th_re - th_n_im * th_im; + th_n_im1 = th_n_im * th_re + th_n_re * th_im; + th_n_re = th_n_re1; + th_n_im = th_n_im1; + z_re = z_re + this.B_re[n] * th_n_re - this.B_im[n] * th_n_im; + z_im = z_im + this.B_im[n] * th_n_re + this.B_re[n] * th_n_im; + } + + // 4. Calculate easting and northing + p.x = (z_im * this.a) + this.x0; + p.y = (z_re * this.a) + this.y0; + + return p; + }; + + + /** + New Zealand Map Grid Inverse - x/y to long/lat + */ + exports.inverse = function(p) { + var n; + var x = p.x; + var y = p.y; + + var delta_x = x - this.x0; + var delta_y = y - this.y0; + + // 1. Calculate z + var z_re = delta_y / this.a; + var z_im = delta_x / this.a; + + // 2a. Calculate theta - first approximation gives km accuracy + var z_n_re = 1; + var z_n_im = 0; // z^0 + var z_n_re1; + var z_n_im1; + + var th_re = 0; + var th_im = 0; + for (n = 1; n <= 6; n++) { + z_n_re1 = z_n_re * z_re - z_n_im * z_im; + z_n_im1 = z_n_im * z_re + z_n_re * z_im; + z_n_re = z_n_re1; + z_n_im = z_n_im1; + th_re = th_re + this.C_re[n] * z_n_re - this.C_im[n] * z_n_im; + th_im = th_im + this.C_im[n] * z_n_re + this.C_re[n] * z_n_im; + } + + // 2b. Iterate to refine the accuracy of the calculation + // 0 iterations gives km accuracy + // 1 iteration gives m accuracy -- good enough for most mapping applications + // 2 iterations bives mm accuracy + for (var i = 0; i < this.iterations; i++) { + var th_n_re = th_re; + var th_n_im = th_im; + var th_n_re1; + var th_n_im1; + + var num_re = z_re; + var num_im = z_im; + for (n = 2; n <= 6; n++) { + th_n_re1 = th_n_re * th_re - th_n_im * th_im; + th_n_im1 = th_n_im * th_re + th_n_re * th_im; + th_n_re = th_n_re1; + th_n_im = th_n_im1; + num_re = num_re + (n - 1) * (this.B_re[n] * th_n_re - this.B_im[n] * th_n_im); + num_im = num_im + (n - 1) * (this.B_im[n] * th_n_re + this.B_re[n] * th_n_im); + } + + th_n_re = 1; + th_n_im = 0; + var den_re = this.B_re[1]; + var den_im = this.B_im[1]; + for (n = 2; n <= 6; n++) { + th_n_re1 = th_n_re * th_re - th_n_im * th_im; + th_n_im1 = th_n_im * th_re + th_n_re * th_im; + th_n_re = th_n_re1; + th_n_im = th_n_im1; + den_re = den_re + n * (this.B_re[n] * th_n_re - this.B_im[n] * th_n_im); + den_im = den_im + n * (this.B_im[n] * th_n_re + this.B_re[n] * th_n_im); + } + + // Complex division + var den2 = den_re * den_re + den_im * den_im; + th_re = (num_re * den_re + num_im * den_im) / den2; + th_im = (num_im * den_re - num_re * den_im) / den2; + } + + // 3. Calculate d_phi ... // and d_lambda + var d_psi = th_re; + var d_lambda = th_im; + var d_psi_n = 1; // d_psi^0 + + var d_phi = 0; + for (n = 1; n <= 9; n++) { + d_psi_n = d_psi_n * d_psi; + d_phi = d_phi + this.D[n] * d_psi_n; + } + + // 4. Calculate latitude and longitude + // d_phi is calcuated in second of arc * 10^-5, so we need to scale back to radians. d_lambda is in radians. + var lat = this.lat0 + (d_phi * SEC_TO_RAD * 1E5); + var lon = this.long0 + d_lambda; + + p.x = lon; + p.y = lat; + + return p; + }; + exports.names = ["New_Zealand_Map_Grid", "nzmg"]; + +/***/ }, +/* 208 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + /* + reference + "New Equal-Area Map Projections for Noncircular Regions", John P. Snyder, + The American Cartographer, Vol 15, No. 4, October 1988, pp. 341-355. + */ + + + /* Initialize the Miller Cylindrical projection + -------------------------------------------*/ + exports.init = function() { + //no-op + }; + + + /* Miller Cylindrical forward equations--mapping lat,long to x,y + ------------------------------------------------------------*/ + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + /* Forward equations + -----------------*/ + var dlon = adjust_lon(lon - this.long0); + var x = this.x0 + this.a * dlon; + var y = this.y0 + this.a * Math.log(Math.tan((Math.PI / 4) + (lat / 2.5))) * 1.25; + + p.x = x; + p.y = y; + return p; + }; + + /* Miller Cylindrical inverse equations--mapping x,y to lat/long + ------------------------------------------------------------*/ + exports.inverse = function(p) { + p.x -= this.x0; + p.y -= this.y0; + + var lon = adjust_lon(this.long0 + p.x / this.a); + var lat = 2.5 * (Math.atan(Math.exp(0.8 * p.y / this.a)) - Math.PI / 4); + + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["Miller_Cylindrical", "mill"]; + + +/***/ }, +/* 209 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + var adjust_lat = __webpack_require__(197); + var pj_enfn = __webpack_require__(210); + var MAX_ITER = 20; + var pj_mlfn = __webpack_require__(211); + var pj_inv_mlfn = __webpack_require__(212); + var HALF_PI = Math.PI/2; + var EPSLN = 1.0e-10; + var asinz = __webpack_require__(185); + exports.init = function() { + /* Place parameters in static storage for common use + -------------------------------------------------*/ + + + if (!this.sphere) { + this.en = pj_enfn(this.es); + } + else { + this.n = 1; + this.m = 0; + this.es = 0; + this.C_y = Math.sqrt((this.m + 1) / this.n); + this.C_x = this.C_y / (this.m + 1); + } + + }; + + /* Sinusoidal forward equations--mapping lat,long to x,y + -----------------------------------------------------*/ + exports.forward = function(p) { + var x, y; + var lon = p.x; + var lat = p.y; + /* Forward equations + -----------------*/ + lon = adjust_lon(lon - this.long0); + + if (this.sphere) { + if (!this.m) { + lat = this.n !== 1 ? Math.asin(this.n * Math.sin(lat)) : lat; + } + else { + var k = this.n * Math.sin(lat); + for (var i = MAX_ITER; i; --i) { + var V = (this.m * lat + Math.sin(lat) - k) / (this.m + Math.cos(lat)); + lat -= V; + if (Math.abs(V) < EPSLN) { + break; + } + } + } + x = this.a * this.C_x * lon * (this.m + Math.cos(lat)); + y = this.a * this.C_y * lat; + + } + else { + + var s = Math.sin(lat); + var c = Math.cos(lat); + y = this.a * pj_mlfn(lat, s, c, this.en); + x = this.a * lon * c / Math.sqrt(1 - this.es * s * s); + } + + p.x = x; + p.y = y; + return p; + }; + + exports.inverse = function(p) { + var lat, temp, lon, s; + + p.x -= this.x0; + lon = p.x / this.a; + p.y -= this.y0; + lat = p.y / this.a; + + if (this.sphere) { + lat /= this.C_y; + lon = lon / (this.C_x * (this.m + Math.cos(lat))); + if (this.m) { + lat = asinz((this.m * lat + Math.sin(lat)) / this.n); + } + else if (this.n !== 1) { + lat = asinz(Math.sin(lat) / this.n); + } + lon = adjust_lon(lon + this.long0); + lat = adjust_lat(lat); + } + else { + lat = pj_inv_mlfn(p.y / this.a, this.es, this.en); + s = Math.abs(lat); + if (s < HALF_PI) { + s = Math.sin(lat); + temp = this.long0 + p.x * Math.sqrt(1 - this.es * s * s) / (this.a * Math.cos(lat)); + //temp = this.long0 + p.x / (this.a * Math.cos(lat)); + lon = adjust_lon(temp); + } + else if ((s - EPSLN) < HALF_PI) { + lon = this.long0; + } + } + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["Sinusoidal", "sinu"]; + +/***/ }, +/* 210 */ +/***/ function(module, exports) { + + var C00 = 1; + var C02 = 0.25; + var C04 = 0.046875; + var C06 = 0.01953125; + var C08 = 0.01068115234375; + var C22 = 0.75; + var C44 = 0.46875; + var C46 = 0.01302083333333333333; + var C48 = 0.00712076822916666666; + var C66 = 0.36458333333333333333; + var C68 = 0.00569661458333333333; + var C88 = 0.3076171875; + + module.exports = function(es) { + var en = []; + en[0] = C00 - es * (C02 + es * (C04 + es * (C06 + es * C08))); + en[1] = es * (C22 - es * (C04 + es * (C06 + es * C08))); + var t = es * es; + en[2] = t * (C44 - es * (C46 + es * C48)); + t *= es; + en[3] = t * (C66 - es * C68); + en[4] = t * es * C88; + return en; + }; + +/***/ }, +/* 211 */ +/***/ function(module, exports) { + + module.exports = function(phi, sphi, cphi, en) { + cphi *= sphi; + sphi *= sphi; + return (en[0] * phi - cphi * (en[1] + sphi * (en[2] + sphi * (en[3] + sphi * en[4])))); + }; + +/***/ }, +/* 212 */ +/***/ function(module, exports, __webpack_require__) { + + var pj_mlfn = __webpack_require__(211); + var EPSLN = 1.0e-10; + var MAX_ITER = 20; + module.exports = function(arg, es, en) { + var k = 1 / (1 - es); + var phi = arg; + for (var i = MAX_ITER; i; --i) { /* rarely goes over 2 iterations */ + var s = Math.sin(phi); + var t = 1 - es * s * s; + //t = this.pj_mlfn(phi, s, Math.cos(phi), en) - arg; + //phi -= t * (t * Math.sqrt(t)) * k; + t = (pj_mlfn(phi, s, Math.cos(phi), en) - arg) * (t * Math.sqrt(t)) * k; + phi -= t; + if (Math.abs(t) < EPSLN) { + return phi; + } + } + //..reportError("cass:pj_inv_mlfn: Convergence error"); + return phi; + }; + +/***/ }, +/* 213 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + var EPSLN = 1.0e-10; + exports.init = function() {}; + + /* Mollweide forward equations--mapping lat,long to x,y + ----------------------------------------------------*/ + exports.forward = function(p) { + + /* Forward equations + -----------------*/ + var lon = p.x; + var lat = p.y; + + var delta_lon = adjust_lon(lon - this.long0); + var theta = lat; + var con = Math.PI * Math.sin(lat); + + /* Iterate using the Newton-Raphson method to find theta + -----------------------------------------------------*/ + for (var i = 0; true; i++) { + var delta_theta = -(theta + Math.sin(theta) - con) / (1 + Math.cos(theta)); + theta += delta_theta; + if (Math.abs(delta_theta) < EPSLN) { + break; + } + } + theta /= 2; + + /* If the latitude is 90 deg, force the x coordinate to be "0 + false easting" + this is done here because of precision problems with "cos(theta)" + --------------------------------------------------------------------------*/ + if (Math.PI / 2 - Math.abs(lat) < EPSLN) { + delta_lon = 0; + } + var x = 0.900316316158 * this.a * delta_lon * Math.cos(theta) + this.x0; + var y = 1.4142135623731 * this.a * Math.sin(theta) + this.y0; + + p.x = x; + p.y = y; + return p; + }; + + exports.inverse = function(p) { + var theta; + var arg; + + /* Inverse equations + -----------------*/ + p.x -= this.x0; + p.y -= this.y0; + arg = p.y / (1.4142135623731 * this.a); + + /* Because of division by zero problems, 'arg' can not be 1. Therefore + a number very close to one is used instead. + -------------------------------------------------------------------*/ + if (Math.abs(arg) > 0.999999999999) { + arg = 0.999999999999; + } + theta = Math.asin(arg); + var lon = adjust_lon(this.long0 + (p.x / (0.900316316158 * this.a * Math.cos(theta)))); + if (lon < (-Math.PI)) { + lon = -Math.PI; + } + if (lon > Math.PI) { + lon = Math.PI; + } + arg = (2 * theta + Math.sin(2 * theta)) / Math.PI; + if (Math.abs(arg) > 1) { + arg = 1; + } + var lat = Math.asin(arg); + + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["Mollweide", "moll"]; + + +/***/ }, +/* 214 */ +/***/ function(module, exports, __webpack_require__) { + + var e0fn = __webpack_require__(180); + var e1fn = __webpack_require__(181); + var e2fn = __webpack_require__(182); + var e3fn = __webpack_require__(183); + var msfnz = __webpack_require__(161); + var mlfn = __webpack_require__(184); + var adjust_lon = __webpack_require__(162); + var adjust_lat = __webpack_require__(197); + var imlfn = __webpack_require__(198); + var EPSLN = 1.0e-10; + exports.init = function() { + + /* Place parameters in static storage for common use + -------------------------------------------------*/ + // Standard Parallels cannot be equal and on opposite sides of the equator + if (Math.abs(this.lat1 + this.lat2) < EPSLN) { + return; + } + this.lat2 = this.lat2 || this.lat1; + this.temp = this.b / this.a; + this.es = 1 - Math.pow(this.temp, 2); + this.e = Math.sqrt(this.es); + this.e0 = e0fn(this.es); + this.e1 = e1fn(this.es); + this.e2 = e2fn(this.es); + this.e3 = e3fn(this.es); + + this.sinphi = Math.sin(this.lat1); + this.cosphi = Math.cos(this.lat1); + + this.ms1 = msfnz(this.e, this.sinphi, this.cosphi); + this.ml1 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat1); + + if (Math.abs(this.lat1 - this.lat2) < EPSLN) { + this.ns = this.sinphi; + } + else { + this.sinphi = Math.sin(this.lat2); + this.cosphi = Math.cos(this.lat2); + this.ms2 = msfnz(this.e, this.sinphi, this.cosphi); + this.ml2 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat2); + this.ns = (this.ms1 - this.ms2) / (this.ml2 - this.ml1); + } + this.g = this.ml1 + this.ms1 / this.ns; + this.ml0 = mlfn(this.e0, this.e1, this.e2, this.e3, this.lat0); + this.rh = this.a * (this.g - this.ml0); + }; + + + /* Equidistant Conic forward equations--mapping lat,long to x,y + -----------------------------------------------------------*/ + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + var rh1; + + /* Forward equations + -----------------*/ + if (this.sphere) { + rh1 = this.a * (this.g - lat); + } + else { + var ml = mlfn(this.e0, this.e1, this.e2, this.e3, lat); + rh1 = this.a * (this.g - ml); + } + var theta = this.ns * adjust_lon(lon - this.long0); + var x = this.x0 + rh1 * Math.sin(theta); + var y = this.y0 + this.rh - rh1 * Math.cos(theta); + p.x = x; + p.y = y; + return p; + }; + + /* Inverse equations + -----------------*/ + exports.inverse = function(p) { + p.x -= this.x0; + p.y = this.rh - p.y + this.y0; + var con, rh1, lat, lon; + if (this.ns >= 0) { + rh1 = Math.sqrt(p.x * p.x + p.y * p.y); + con = 1; + } + else { + rh1 = -Math.sqrt(p.x * p.x + p.y * p.y); + con = -1; + } + var theta = 0; + if (rh1 !== 0) { + theta = Math.atan2(con * p.x, con * p.y); + } + + if (this.sphere) { + lon = adjust_lon(this.long0 + theta / this.ns); + lat = adjust_lat(this.g - rh1 / this.a); + p.x = lon; + p.y = lat; + return p; + } + else { + var ml = this.g - rh1 / this.a; + lat = imlfn(ml, this.e0, this.e1, this.e2, this.e3); + lon = adjust_lon(this.long0 + theta / this.ns); + p.x = lon; + p.y = lat; + return p; + } + + }; + exports.names = ["Equidistant_Conic", "eqdc"]; + + +/***/ }, +/* 215 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + var HALF_PI = Math.PI/2; + var EPSLN = 1.0e-10; + var asinz = __webpack_require__(185); + /* Initialize the Van Der Grinten projection + ----------------------------------------*/ + exports.init = function() { + //this.R = 6370997; //Radius of earth + this.R = this.a; + }; + + exports.forward = function(p) { + + var lon = p.x; + var lat = p.y; + + /* Forward equations + -----------------*/ + var dlon = adjust_lon(lon - this.long0); + var x, y; + + if (Math.abs(lat) <= EPSLN) { + x = this.x0 + this.R * dlon; + y = this.y0; + } + var theta = asinz(2 * Math.abs(lat / Math.PI)); + if ((Math.abs(dlon) <= EPSLN) || (Math.abs(Math.abs(lat) - HALF_PI) <= EPSLN)) { + x = this.x0; + if (lat >= 0) { + y = this.y0 + Math.PI * this.R * Math.tan(0.5 * theta); + } + else { + y = this.y0 + Math.PI * this.R * -Math.tan(0.5 * theta); + } + // return(OK); + } + var al = 0.5 * Math.abs((Math.PI / dlon) - (dlon / Math.PI)); + var asq = al * al; + var sinth = Math.sin(theta); + var costh = Math.cos(theta); + + var g = costh / (sinth + costh - 1); + var gsq = g * g; + var m = g * (2 / sinth - 1); + var msq = m * m; + var con = Math.PI * this.R * (al * (g - msq) + Math.sqrt(asq * (g - msq) * (g - msq) - (msq + asq) * (gsq - msq))) / (msq + asq); + if (dlon < 0) { + con = -con; + } + x = this.x0 + con; + //con = Math.abs(con / (Math.PI * this.R)); + var q = asq + g; + con = Math.PI * this.R * (m * q - al * Math.sqrt((msq + asq) * (asq + 1) - q * q)) / (msq + asq); + if (lat >= 0) { + //y = this.y0 + Math.PI * this.R * Math.sqrt(1 - con * con - 2 * al * con); + y = this.y0 + con; + } + else { + //y = this.y0 - Math.PI * this.R * Math.sqrt(1 - con * con - 2 * al * con); + y = this.y0 - con; + } + p.x = x; + p.y = y; + return p; + }; + + /* Van Der Grinten inverse equations--mapping x,y to lat/long + ---------------------------------------------------------*/ + exports.inverse = function(p) { + var lon, lat; + var xx, yy, xys, c1, c2, c3; + var a1; + var m1; + var con; + var th1; + var d; + + /* inverse equations + -----------------*/ + p.x -= this.x0; + p.y -= this.y0; + con = Math.PI * this.R; + xx = p.x / con; + yy = p.y / con; + xys = xx * xx + yy * yy; + c1 = -Math.abs(yy) * (1 + xys); + c2 = c1 - 2 * yy * yy + xx * xx; + c3 = -2 * c1 + 1 + 2 * yy * yy + xys * xys; + d = yy * yy / c3 + (2 * c2 * c2 * c2 / c3 / c3 / c3 - 9 * c1 * c2 / c3 / c3) / 27; + a1 = (c1 - c2 * c2 / 3 / c3) / c3; + m1 = 2 * Math.sqrt(-a1 / 3); + con = ((3 * d) / a1) / m1; + if (Math.abs(con) > 1) { + if (con >= 0) { + con = 1; + } + else { + con = -1; + } + } + th1 = Math.acos(con) / 3; + if (p.y >= 0) { + lat = (-m1 * Math.cos(th1 + Math.PI / 3) - c2 / 3 / c3) * Math.PI; + } + else { + lat = -(-m1 * Math.cos(th1 + Math.PI / 3) - c2 / 3 / c3) * Math.PI; + } + + if (Math.abs(xx) < EPSLN) { + lon = this.long0; + } + else { + lon = adjust_lon(this.long0 + Math.PI * (xys - 1 + Math.sqrt(1 + 2 * (xx * xx - yy * yy) + xys * xys)) / 2 / xx); + } + + p.x = lon; + p.y = lat; + return p; + }; + exports.names = ["Van_der_Grinten_I", "VanDerGrinten", "vandg"]; + +/***/ }, +/* 216 */ +/***/ function(module, exports, __webpack_require__) { + + var adjust_lon = __webpack_require__(162); + var HALF_PI = Math.PI/2; + var EPSLN = 1.0e-10; + var mlfn = __webpack_require__(184); + var e0fn = __webpack_require__(180); + var e1fn = __webpack_require__(181); + var e2fn = __webpack_require__(182); + var e3fn = __webpack_require__(183); + var gN = __webpack_require__(196); + var asinz = __webpack_require__(185); + var imlfn = __webpack_require__(198); + exports.init = function() { + this.sin_p12 = Math.sin(this.lat0); + this.cos_p12 = Math.cos(this.lat0); + }; + + exports.forward = function(p) { + var lon = p.x; + var lat = p.y; + var sinphi = Math.sin(p.y); + var cosphi = Math.cos(p.y); + var dlon = adjust_lon(lon - this.long0); + var e0, e1, e2, e3, Mlp, Ml, tanphi, Nl1, Nl, psi, Az, G, H, GH, Hs, c, kp, cos_c, s, s2, s3, s4, s5; + if (this.sphere) { + if (Math.abs(this.sin_p12 - 1) <= EPSLN) { + //North Pole case + p.x = this.x0 + this.a * (HALF_PI - lat) * Math.sin(dlon); + p.y = this.y0 - this.a * (HALF_PI - lat) * Math.cos(dlon); + return p; + } + else if (Math.abs(this.sin_p12 + 1) <= EPSLN) { + //South Pole case + p.x = this.x0 + this.a * (HALF_PI + lat) * Math.sin(dlon); + p.y = this.y0 + this.a * (HALF_PI + lat) * Math.cos(dlon); + return p; + } + else { + //default case + cos_c = this.sin_p12 * sinphi + this.cos_p12 * cosphi * Math.cos(dlon); + c = Math.acos(cos_c); + kp = c / Math.sin(c); + p.x = this.x0 + this.a * kp * cosphi * Math.sin(dlon); + p.y = this.y0 + this.a * kp * (this.cos_p12 * sinphi - this.sin_p12 * cosphi * Math.cos(dlon)); + return p; + } + } + else { + e0 = e0fn(this.es); + e1 = e1fn(this.es); + e2 = e2fn(this.es); + e3 = e3fn(this.es); + if (Math.abs(this.sin_p12 - 1) <= EPSLN) { + //North Pole case + Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); + Ml = this.a * mlfn(e0, e1, e2, e3, lat); + p.x = this.x0 + (Mlp - Ml) * Math.sin(dlon); + p.y = this.y0 - (Mlp - Ml) * Math.cos(dlon); + return p; + } + else if (Math.abs(this.sin_p12 + 1) <= EPSLN) { + //South Pole case + Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); + Ml = this.a * mlfn(e0, e1, e2, e3, lat); + p.x = this.x0 + (Mlp + Ml) * Math.sin(dlon); + p.y = this.y0 + (Mlp + Ml) * Math.cos(dlon); + return p; + } + else { + //Default case + tanphi = sinphi / cosphi; + Nl1 = gN(this.a, this.e, this.sin_p12); + Nl = gN(this.a, this.e, sinphi); + psi = Math.atan((1 - this.es) * tanphi + this.es * Nl1 * this.sin_p12 / (Nl * cosphi)); + Az = Math.atan2(Math.sin(dlon), this.cos_p12 * Math.tan(psi) - this.sin_p12 * Math.cos(dlon)); + if (Az === 0) { + s = Math.asin(this.cos_p12 * Math.sin(psi) - this.sin_p12 * Math.cos(psi)); + } + else if (Math.abs(Math.abs(Az) - Math.PI) <= EPSLN) { + s = -Math.asin(this.cos_p12 * Math.sin(psi) - this.sin_p12 * Math.cos(psi)); + } + else { + s = Math.asin(Math.sin(dlon) * Math.cos(psi) / Math.sin(Az)); + } + G = this.e * this.sin_p12 / Math.sqrt(1 - this.es); + H = this.e * this.cos_p12 * Math.cos(Az) / Math.sqrt(1 - this.es); + GH = G * H; + Hs = H * H; + s2 = s * s; + s3 = s2 * s; + s4 = s3 * s; + s5 = s4 * s; + c = Nl1 * s * (1 - s2 * Hs * (1 - Hs) / 6 + s3 / 8 * GH * (1 - 2 * Hs) + s4 / 120 * (Hs * (4 - 7 * Hs) - 3 * G * G * (1 - 7 * Hs)) - s5 / 48 * GH); + p.x = this.x0 + c * Math.sin(Az); + p.y = this.y0 + c * Math.cos(Az); + return p; + } + } + + + }; + + exports.inverse = function(p) { + p.x -= this.x0; + p.y -= this.y0; + var rh, z, sinz, cosz, lon, lat, con, e0, e1, e2, e3, Mlp, M, N1, psi, Az, cosAz, tmp, A, B, D, Ee, F; + if (this.sphere) { + rh = Math.sqrt(p.x * p.x + p.y * p.y); + if (rh > (2 * HALF_PI * this.a)) { + return; + } + z = rh / this.a; + + sinz = Math.sin(z); + cosz = Math.cos(z); + + lon = this.long0; + if (Math.abs(rh) <= EPSLN) { + lat = this.lat0; + } + else { + lat = asinz(cosz * this.sin_p12 + (p.y * sinz * this.cos_p12) / rh); + con = Math.abs(this.lat0) - HALF_PI; + if (Math.abs(con) <= EPSLN) { + if (this.lat0 >= 0) { + lon = adjust_lon(this.long0 + Math.atan2(p.x, - p.y)); + } + else { + lon = adjust_lon(this.long0 - Math.atan2(-p.x, p.y)); + } + } + else { + /*con = cosz - this.sin_p12 * Math.sin(lat); + if ((Math.abs(con) < EPSLN) && (Math.abs(p.x) < EPSLN)) { + //no-op, just keep the lon value as is + } else { + var temp = Math.atan2((p.x * sinz * this.cos_p12), (con * rh)); + lon = adjust_lon(this.long0 + Math.atan2((p.x * sinz * this.cos_p12), (con * rh))); + }*/ + lon = adjust_lon(this.long0 + Math.atan2(p.x * sinz, rh * this.cos_p12 * cosz - p.y * this.sin_p12 * sinz)); + } + } + + p.x = lon; + p.y = lat; + return p; + } + else { + e0 = e0fn(this.es); + e1 = e1fn(this.es); + e2 = e2fn(this.es); + e3 = e3fn(this.es); + if (Math.abs(this.sin_p12 - 1) <= EPSLN) { + //North pole case + Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); + rh = Math.sqrt(p.x * p.x + p.y * p.y); + M = Mlp - rh; + lat = imlfn(M / this.a, e0, e1, e2, e3); + lon = adjust_lon(this.long0 + Math.atan2(p.x, - 1 * p.y)); + p.x = lon; + p.y = lat; + return p; + } + else if (Math.abs(this.sin_p12 + 1) <= EPSLN) { + //South pole case + Mlp = this.a * mlfn(e0, e1, e2, e3, HALF_PI); + rh = Math.sqrt(p.x * p.x + p.y * p.y); + M = rh - Mlp; + + lat = imlfn(M / this.a, e0, e1, e2, e3); + lon = adjust_lon(this.long0 + Math.atan2(p.x, p.y)); + p.x = lon; + p.y = lat; + return p; + } + else { + //default case + rh = Math.sqrt(p.x * p.x + p.y * p.y); + Az = Math.atan2(p.x, p.y); + N1 = gN(this.a, this.e, this.sin_p12); + cosAz = Math.cos(Az); + tmp = this.e * this.cos_p12 * cosAz; + A = -tmp * tmp / (1 - this.es); + B = 3 * this.es * (1 - A) * this.sin_p12 * this.cos_p12 * cosAz / (1 - this.es); + D = rh / N1; + Ee = D - A * (1 + A) * Math.pow(D, 3) / 6 - B * (1 + 3 * A) * Math.pow(D, 4) / 24; + F = 1 - A * Ee * Ee / 2 - D * Ee * Ee * Ee / 6; + psi = Math.asin(this.sin_p12 * Math.cos(Ee) + this.cos_p12 * Math.sin(Ee) * cosAz); + lon = adjust_lon(this.long0 + Math.asin(Math.sin(Az) * Math.sin(Ee) / Math.cos(psi))); + lat = Math.atan((1 - this.es * F * this.sin_p12 / Math.sin(psi)) * Math.tan(psi) / (1 - this.es)); + p.x = lon; + p.y = lat; + return p; + } + } + + }; + exports.names = ["Azimuthal_Equidistant", "aeqd"]; + + +/***/ }, +/* 217 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var object = __webpack_require__(5); + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 geo.mapInteractor + * @extends geo.object + * @returns {geo.mapInteractor} + */ + ////////////////////////////////////////////////////////////////////////////// + var mapInteractor = function (args) { + 'use strict'; + if (!(this instanceof mapInteractor)) { + return new mapInteractor(args); + } + object.call(this); + + var $ = __webpack_require__(1); + var geo_event = __webpack_require__(125); + var throttle = __webpack_require__(120).throttle; + var debounce = __webpack_require__(120).debounce; + + var m_options = args || {}, + m_this = this, + m_mouse, + m_keyboard, + m_state, + m_queue, + $node, + m_selectionLayer = null, + m_selectionPlane = null, + m_paused = false, + m_clickMaybe = false, + m_callZoom = function () {}; + + // Helper method to decide if the current button/modifiers match a set of + // conditions. + // button: 'left' | 'right' | 'middle' + // modifiers: [ 'alt' | 'meta' | 'ctrl' | 'shift' ] + function eventMatch(button, modifiers) { + return (button === 'wheel' || m_mouse.buttons[button]) && + (!!m_mouse.modifiers.alt) === (!!modifiers.alt) && + (!!m_mouse.modifiers.meta) === (!!modifiers.meta) && + (!!m_mouse.modifiers.shift) === (!!modifiers.shift) && + (!!m_mouse.modifiers.ctrl) === (!!modifiers.ctrl); + } + + // Helper method to calculate the speed from a velocity + function calcSpeed(v) { + var x = v.x, y = v.y; + return Math.sqrt(x * x + y * y); + } + + // copy the options object with defaults + m_options = $.extend( + true, + {}, + { + throttle: 30, + discreteZoom: false, + panMoveButton: 'left', + panMoveModifiers: {}, + zoomMoveButton: 'right', + zoomMoveModifiers: {}, + rotateMoveButton: 'left', + rotateMoveModifiers: {'ctrl': true}, + panWheelEnabled: false, + panWheelModifiers: {}, + zoomWheelEnabled: true, + zoomWheelModifiers: {}, + rotateWheelEnabled: true, + rotateWheelModifiers: {'ctrl': true}, + wheelScaleX: 1, + wheelScaleY: 1, + zoomScale: 1, + rotateWheelScale: 6 * Math.PI / 180, + selectionButton: 'left', + selectionModifiers: {'shift': true}, + momentum: { + enabled: true, + maxSpeed: 2.5, + minSpeed: 0.01, + stopTime: 250, + drag: 0.01, + actions: ['pan', 'zoom'] + }, + spring: { + enabled: false, + springConstant: 0.00005 + }, + click: { + enabled: true, + buttons: {left: true, right: true, middle: true}, + duration: 0, + cancelOnMove: true + }, + zoomAnimation: { + enabled: true, + duration: 500, + ease: function (t) { return (2 - t) * t; } + } + }, + m_options + ); + + // options supported: + // { + // // throttle mouse events to at most this many milliseconds each (default 30) + // throttle: number + // + // // Clamp zoom events to discrete (integer) zoom levels. If a number is + // // provided then zoom events will be debounced (and accumulated) + // // with the given delay. The default debounce interval is 400 ms. + // discreteZoom: boolean | number > 0 + // + // // button that must be pressed to initiate a pan on mousedown + // panMoveButton: 'right' | 'left' | 'middle' + // + // // modifier keys that must be pressed to initiate a pan on mousemove + // panMoveModifiers: { 'ctrl' | 'alt' | 'meta' | 'shift' } + // + // // button that must be pressed to initiate a zoom on mousedown + // zoomMoveButton: 'right' | 'left' | 'middle' + // + // // modifier keys that must be pressed to initiate a zoom on mousemove + // zoomMoveModifiers: { 'ctrl' | 'alt' | 'meta' | 'shift' } + // + // // button that must be pressed to initiate a rotate on mousedown + // rotateMoveButton: 'right' | 'left' | 'middle' + // + // // modifier keys that must be pressed to initiate a rotate on mousemove + // rotateMoveModifiers: { 'ctrl' | 'alt' | 'meta' | 'shift' } + // + // // enable or disable panning with the mouse wheel + // panWheelEnabled: true | false + // + // // modifier keys that must be pressed to trigger a pan on wheel + // panWheelModifiers: {...} + // + // // enable or disable zooming with the mouse wheel + // zoomWheelEnabled: true | false + // + // // modifier keys that must be pressed to trigger a zoom on wheel + // zoomWheelModifiers: {...} + // + // // enable or disable rotation with the mouse wheel + // rotateWheelEnabled: true | false + // + // // modifier keys that must be pressed to trigger a rotate on wheel + // rotateWheelModifiers: {...} + // + // // wheel scale factor to change the magnitude of wheel interactions + // wheelScaleX: 1 + // wheelScaleY: 1 + // + // // zoom scale factor to change the magnitude of zoom move interactions + // zoomScale: 1 + // + // // scale factor to change the magnitude of wheel rotation interactions + // rotateWheelScale: 1 + // + // // button that must be pressed to enable drag selection + // selectionButton: 'right' | 'left' | 'middle' + // + // // keyboard modifiers that must be pressed to initiate a selection + // selectionModifiers: {...} + // + // // enable momentum when panning + // momentum: { + // enabled: true | false, + // maxSpeed: number, // don't allow animation to pan faster than this + // minSpeed: number, // stop animations if the speed is less than this + // stopTime: number, // if the mouse hasn't moved for this many + // // milliseconds, don't apply momentum + // drag: number, // drag coefficient + // actions: ['pan', 'zoom'] // actions on which to apply momentum + // } + // + // // enable spring clamping to screen edges to enforce clamping + // spring: { + // enabled: true | false, + // springConstant: number, + // } + // + // // enable animation for both discrete and continuous zoom + // zoomAnimation: { + // enabled: true | false, + // duration: number, // milliseconds + // ease: function // easing function + // } + // + // // 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: + /** + * General representation of rectangular bounds in world coordinates + * @typedef geo.geoBounds + * @type {object} + * @property {geo.geoPosition} upperLeft Upper left corner + * @property {geo.geoPosition} upperRight Upper right corner + * @property {geo.geoPosition} lowerLeft Lower left corner + * @property {geo.geoPosition} lowerRight Lower right corner + */ + + /** + * General representation of rectangular bounds in pixel coordinates + * @typedef geo.screenBounds + * @type {object} + * @property {geo.screenPosition} upperLeft Upper left corner + * @property {geo.screenPosition} upperRight Upper right corner + * @property {geo.screenPosition} lowerLeft Lower left corner + * @property {geo.screenPosition} lowerRight Lower right corner + */ + + /** + * General representation of a point on the screen. + * @typedef geo.screenPosition + * @type {object} + * @property {Number} x Horizontal coordinate in pixels + * @property {Number} y Vertical coordinate in pixels + */ + + /** + * General represention of a point on the earth. + * @typedef geo.geoPosition + * @type {object} + * @property {Number} x Horizontal coordinate in degrees longitude + * @property {Number} y Vertical coordinate in degrees latitude + */ + + /** + * The status of all mouse buttons. + * @typedef geo.mouseButtons + * @type {object} + * @property {true|false} left The left mouse button + * @property {true|false} right The right mouse button + * @property {true|false} middle The middle mouse button + */ + + /** + * The status of all modifier keys these are copied from the + * standard DOM events. + * @typedef geo.modifierKeys + * @type {object} + * @property {true|false} alt Event.alt + * @property {true|false} ctrl Event.ctrl + * @property {true|false} shift Event.shift + * @property {true|false} meta Event.meta + */ + + /** + * Provides information about the state of the mouse + * @typedef geo.mouseState + * @type {object} + * @property {geo.screenPosition} page Mouse location in pixel space + * @property {geo.geoPosition} map Mouse location in world space + * @property {geo.mouseButtons} buttons The current state of the mouse buttons + * @property {geo.modifierKeys} modifiers The current state of all modifier keys + * @property {Date} time The timestamp the event took place + * @property {Number} deltaTime The time in milliseconds since the last mouse event + * @property {geo.screenPosition} velocity The velocity of the mouse pointer + * in pixels per second + */ + + /** + * @typedef geo.brushSelection + * @type {object} + * @property {geo.screenBounds} display The selection bounds in pixel space + * @property {geo.geoBounds} gcs The selection bounds in world space + * @property {geo.mouseState} mouse The current mouse state + * @property {geo.mouseState} origin The mouse state at the start of the + * brush action + */ + + // default mouse object + m_mouse = { + page: { // mouse position relative to the page + x: 0, + y: 0 + }, + map: { // mouse position relative to the map + x: 0, + y: 0 + }, + // mouse button status + buttons: { + left: false, + right: false, + middle: false + }, + // keyboard modifier status + modifiers: { + alt: false, + ctrl: false, + shift: false, + meta: false + }, + // time the event was captured + time: new Date(), + // time elapsed since the last mouse event + deltaTime: 1, + // pixels/ms + velocity: { + x: 0, + y: 0 + } + }; + + // default keyboard object + // (keyboard events not implemented yet) + m_keyboard = { + }; + + // The interactor state determines what actions are taken in response to + // core browser events. + // + // i.e. + // { + // 'action': 'pan', // an ongoing pan event + // 'origin': {...}, // mouse object at the start of the action + // 'delta': {x: *, y: *} // mouse movement since action start + // // not including the current event + // } + // + // { + // 'action': 'zoom', // an ongoing zoom event + // ... + // } + // + // { + // 'action': 'rotate', // an ongoing rotate event + // 'origin': {...}, // mouse object at the start of the action + // 'delta': {x: *, y: *} // mouse movement since action start + // // not including the current event + // } + // + // { + // 'acton': 'select', + // 'origin': {...}, + // 'delta': {x: *, y: *} + // } + // + // { + // 'action': 'momentum', + // 'origin': {...}, + // 'handler': function () { }, // called in animation loop + // 'timer': animate loop timer + // } + m_state = {}; + + /** + * Store queued map navigation commands (due to throttling) here + * { + * kind: 'move' | 'wheel', // what kind of mouse action triggered this + * method: function () {}, // the throttled method + * scroll: {x: ..., y: ...} // accumulated scroll wheel deltas + * } + */ + m_queue = {}; + + //////////////////////////////////////////////////////////////////////////// + /** + * Connects events to a map. If the map is not set, then this does nothing. + * @returns {geo.mapInteractor} + */ + //////////////////////////////////////////////////////////////////////////// + this._connectEvents = function () { + if (!m_options.map) { + return m_this; + } + + // prevent double binding to dom elements + m_this._disconnectEvents(); + + // store the connected element + $node = $(m_options.map.node()); + + // set methods related to asyncronous event handling + m_this._handleMouseWheel = throttled_wheel(); + m_callZoom = debounced_zoom(); + + // add event handlers + $node.on('wheel.geojs', m_this._handleMouseWheel); + $node.on('mousemove.geojs', m_this._handleMouseMove); + $node.on('mousedown.geojs', m_this._handleMouseDown); + $node.on('mouseup.geojs', m_this._handleMouseUp); + // Disable dragging images and such + $node.on('dragstart', function () { return false; }); + if (m_options.panMoveButton === 'right' || + m_options.zoomMoveButton === 'right' || + m_options.rotateMoveButton === 'right' || + m_options.selectionButton === 'right') { + $node.on('contextmenu.geojs', function () { return false; }); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Disonnects events to a map. If the map is not set, then this does nothing. + * @returns {geo.mapInteractor} + */ + //////////////////////////////////////////////////////////////////////////// + this._disconnectEvents = function () { + if ($node) { + $node.off('.geojs'); + $node = null; + } + m_this._handleMouseWheel = function () {}; + m_callZoom = function () {}; + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Sets or gets map for this interactor, adds draw region layer if needed + * + * @param {geo.map} newMap optional + * @returns {geo.interactorStyle|geo.map} + */ + //////////////////////////////////////////////////////////////////////////// + this.map = function (val) { + if (val !== undefined) { + m_options.map = val; + m_this._connectEvents(); + return m_this; + } + return m_options.map; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Gets/sets the options object for the interactor. + * + * @param {object} opts optional + * @returns {geo.interactorStyle|object} + */ + //////////////////////////////////////////////////////////////////////////// + this.options = function (opts) { + if (opts === undefined) { + return $.extend({}, m_options); + } + $.extend(m_options, opts); + + // reset event handlers for new options + this._connectEvents(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Stores the current mouse position from an event + */ + //////////////////////////////////////////////////////////////////////////// + this._getMousePosition = function (evt) { + var offset = $node.offset(), dt, t; + + 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) { + // catch georeferencing problems and move on + // needed for handling the map before the base layer + // is attached + m_mouse.geo = null; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Stores the current mouse button + */ + //////////////////////////////////////////////////////////////////////////// + this._getMouseButton = function (evt) { + if (evt.which === 1) { + m_mouse.buttons.left = evt.type !== 'mouseup'; + } else if (evt.which === 3) { + m_mouse.buttons.right = evt.type !== 'mouseup'; + } else if (evt.which === 2) { + m_mouse.buttons.middle = evt.type !== 'mouseup'; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Stores the current keyboard modifiers + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Compute a selection information object. + * @private + * @returns {object} + */ + //////////////////////////////////////////////////////////////////////////// + this._getSelection = function () { + var origin = m_state.origin, + mouse = m_this.mouse(), + map = m_this.map(), + display = {}, gcs = {}; + + // TODO: clamp to map bounds + // Get the display coordinates + 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 + }; + + // Get the gcs coordinates + 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(); + + return { + display: display, + gcs: gcs, + mouse: mouse, + origin: $.extend({}, m_state.origin) + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Immediately cancel an ongoing action. + * + * @param {string?} action The action type, if null cancel any action + * @param {bool} keepQueue If truthy, keep the queue event if an action is + * canceled. + * @returns {bool} If an action was canceled + */ + //////////////////////////////////////////////////////////////////////////// + this.cancel = function (action, keepQueue) { + var out; + if (!action) { + out = !!m_state.action; + } else { + out = m_state.action === action; + } + if (out) { + // cancel any queued interaction events + if (!keepQueue) { + m_queue = {}; + } + m_state = {}; + } + return out; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle event when a mouse button is pressed + */ + //////////////////////////////////////////////////////////////////////////// + this._handleMouseDown = function (evt) { + var action = null; + + if (m_paused) { + return; + } + + m_this._getMousePosition(evt); + 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)) { + action = 'zoom'; + } else if (eventMatch(m_options.rotateMoveButton, m_options.rotateMoveModifiers)) { + action = 'rotate'; + } else if (eventMatch(m_options.selectionButton, m_options.selectionModifiers)) { + action = 'select'; + } + + // cancel transitions and momentum on click + m_this.map().transitionCancel('_handleMouseDown.' + action); + m_this.cancel('momentum'); + + m_mouse.velocity = { + x: 0, + y: 0 + }; + + if (action) { + // cancel any ongoing interaction queue + m_queue = { + kind: 'move' + }; + + // store the state object + m_state = { + action: action, + origin: $.extend(true, {}, m_mouse), + delta: {x: 0, y: 0} + }; + + if (action === 'select') { + // Make sure the old selection layer is gone. + if (m_selectionLayer) { + m_selectionLayer.clear(); + m_this.map().deleteLayer(m_selectionLayer); + m_selectionLayer = null; + } + // Create a feature layer and plane feature to show the selection bounds + m_selectionLayer = m_this.map().createLayer('feature', {renderer: 'd3'}); + m_selectionPlane = m_selectionLayer.createFeature('plane'); + m_selectionPlane.style({ + screenCoordinates: true, + fillOpacity: function () { return 0.25; } + }); + m_this.map().geoTrigger(geo_event.brushstart, m_this._getSelection()); + } + + // bind temporary handlers to document + if (m_options.throttle > 0) { + $(document).on( + 'mousemove.geojs', + throttle( + m_options.throttle, + m_this._handleMouseMoveDocument + ) + ); + } else { + $(document).on('mousemove.geojs', m_this._handleMouseMoveDocument); + } + $(document).on('mouseup.geojs', m_this._handleMouseUpDocument); + } + + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event + */ + //////////////////////////////////////////////////////////////////////////// + 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; + } + + m_this._getMousePosition(evt); + m_this._getMouseButton(evt); + m_this._getMouseModifiers(evt); + + if (m_clickMaybe) { + return; + } + + m_this.map().geoTrigger(geo_event.mousemove, m_this.mouse()); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse move event on the document (temporary bindings) + */ + //////////////////////////////////////////////////////////////////////////// + this._handleMouseMoveDocument = function (evt) { + var dx, dy, selectionObj; + + // If the map has been disconnected, we do nothing. + if (!m_this.map()) { + return; + } + + if (m_paused || m_queue.kind !== 'move') { + 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.'); + return; + } + + // calculate the delta from the origin point to avoid + // accumulation of floating point errors + 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; + + if (m_state.action === 'pan') { + m_this.map().pan({x: dx, y: dy}); + } else if (m_state.action === 'zoom') { + m_callZoom(-dy * m_options.zoomScale / 120, m_state); + } else if (m_state.action === 'rotate') { + var cx, cy; + if (m_state.origin.rotation === undefined) { + cx = m_state.origin.map.x - m_this.map().size().width / 2; + cy = m_state.origin.map.y - m_this.map().size().height / 2; + m_state.origin.rotation = m_this.map().rotation() - Math.atan2(cy, cx); + } + cx = m_mouse.map.x - m_this.map().size().width / 2; + cy = m_mouse.map.y - m_this.map().size().height / 2; + m_this.map().rotation(m_state.origin.rotation + Math.atan2(cy, cx)); + } else if (m_state.action === 'select') { + // Get the bounds of the current selection + selectionObj = m_this._getSelection(); + m_this.map().geoTrigger(geo_event.brush, selectionObj); + } + + // Prevent default to stop text selection in particular + evt.preventDefault(); + }; + + /** + * Use interactor options to modify the mouse velocity by momentum + * or spring equations depending on the current map state. + * @private + * @param {object} v Current velocity in pixels / ms + * @param {number} deltaT The time delta + * @returns {object} New velocity + */ + function modifyVelocity(v, deltaT) { + deltaT = deltaT <= 0 ? 30 : deltaT; + var sf = springForce(); + var speed = calcSpeed(v); + var vx = v.x / speed; + var vy = v.y / speed; + + speed = speed * Math.exp(-m_options.momentum.drag * deltaT); + + // |force| + |velocity| < c <- stopping condition + if (calcSpeed(sf) * deltaT + speed < m_options.momentum.minSpeed) { + return null; + } + + if (speed > 0) { + vx = vx * speed; + vy = vy * speed; + } else { + vx = 0; + vy = 0; + } + + return { + x: vx - sf.x * deltaT, + y: vy - sf.y * deltaT + }; + } + + /** + * Get the spring force for the current map bounds + * @private + * @returns {object} The spring force + */ + function springForce() { + var xplus, // force to the right + xminus, // force to the left + yplus, // force to the top + yminus; // force to the bottom + + if (!m_options.spring.enabled) { + return {x: 0, y: 0}; + } + // get screen coordinates of corners + var maxBounds = m_this.map().maxBounds(undefined, null); + var ul = m_this.map().gcsToDisplay({ + x: maxBounds.left, + y: maxBounds.top + }, null); + var lr = m_this.map().gcsToDisplay({ + x: maxBounds.right, + y: maxBounds.bottom + }, null); + + var c = m_options.spring.springConstant; + // Arg... map needs to expose the canvas size + var width = m_this.map().node().width(); + var height = m_this.map().node().height(); + + 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; + + return { + x: xplus - xminus, + y: yplus - yminus + }; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle event when a mouse button is unpressed on the document. + * Removes temporary bindings. + */ + //////////////////////////////////////////////////////////////////////////// + this._handleMouseUpDocument = function (evt) { + var selectionObj, oldAction; + + if (m_paused) { + return; + } + + // cancel queued interactions + m_queue = {}; + + m_clickMaybe = false; + m_this._getMouseButton(evt); + m_this._getMouseModifiers(evt); + + // unbind temporary handlers on document + $(document).off('.geojs'); + + if (m_mouse.buttons.right) { + evt.preventDefault(); + } + + if (m_state.action === 'select') { + 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); + } + + // reset the interactor state + oldAction = m_state.action; + m_state = {}; + + // if momentum is enabled, start the action here + if (m_options.momentum.enabled && + $.inArray(oldAction, m_options.momentum.actions) >= 0) { + + var t = (new Date()).valueOf(); + var dt = t - m_mouse.time + m_mouse.deltaTime; + if (t - m_mouse.time < m_options.momentum.stopTime) { + m_mouse.velocity.x = m_mouse.velocity.x * m_mouse.deltaTime / dt; + m_mouse.velocity.y = m_mouse.velocity.y * m_mouse.deltaTime / dt; + m_mouse.deltaTime = dt; + } else { + m_mouse.velocity.x = m_mouse.velocity.y = 0; + } + m_this.springBack(true, oldAction); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle event when a mouse button is unpressed + */ + //////////////////////////////////////////////////////////////////////////// + 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); + + // 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()); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Private wrapper around the map zoom method that is debounced to support + * discrete zoom interactions. + * @param {number} deltaZ The zoom increment + */ + //////////////////////////////////////////////////////////////////////////// + function debounced_zoom() { + var deltaZ = 0, delay = 400, direction, startZoom, targetZoom; + + function accum(dz, dir) { + var map = m_this.map(), zoom; + + direction = dir; + deltaZ += dz; + if (targetZoom === undefined) { + startZoom = targetZoom = map.zoom(); + } + targetZoom += dz; + + // Respond to debounced events when they add up to a change in the + // discrete zoom level. + if (map && Math.abs(deltaZ) >= 1 && m_options.discreteZoom && + !m_options.zoomAnimation.enabled) { + + zoom = Math.round(deltaZ + map.zoom()); + + // delta is what is left over from the zoom delta after the new zoom + // value + deltaZ = deltaZ + map.zoom() - zoom; + + map.zoom(zoom, direction); + } + + } + + function apply() { + var map = m_this.map(), zoom; + if (map) { + if (m_options.zoomAnimation.enabled) { + zoom = targetZoom; + if (m_options.discreteZoom) { + zoom = Math.round(zoom); + if (zoom === startZoom && targetZoom !== startZoom) { + zoom = startZoom + (targetZoom > startZoom ? 1 : -1); + } + } + map.transitionCancel('debounced_zoom.zoom'); + map.transition({ + zoom: zoom, + zoomOrigin: direction, + duration: m_options.zoomAnimation.duration, + ease: m_options.zoomAnimation.ease, + done: function (status) { + status = status || {}; + if (!status.next && (!status.cancel || + ('' + status.source).search(/\.zoom$/) < 0)) { + targetZoom = undefined; + } + /* If we were animating the zoom, if the zoom is continuous, just + * stop where we are. If using discrete zoom, we need to make + * sure we end up discrete. However, we don't want to do that if + * the next action is further zooming. */ + if (m_options.discreteZoom && status.cancel && + status.transition && status.transition.end && + ('' + status.source).search(/\.zoom$/) < 0) { + map.zoom(status.transition.end.zoom, + status.transition.end.zoomOrigin); + } + } + }); + } else { + zoom = deltaZ + map.zoom(); + if (m_options.discreteZoom) { + // round off the zoom to an integer and throw away the rest + zoom = Math.round(zoom); + } + map.zoom(zoom, direction); + } + } + deltaZ = 0; + } + + if (m_options.discreteZoom !== true && m_options.discreteZoom > 0) { + delay = m_options.discreteZoom; + } + if ((m_options.discreteZoom === true || m_options.discreteZoom > 0) && + !m_options.zoomAnimation.enabled) { + return debounce(delay, false, apply, accum); + } else { + return function (dz, dir) { + if (!dz && targetZoom === undefined) { + return; + } + accum(dz, dir); + apply(dz, dir); + }; + } + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Attaches wrapped methods for accumulating fast mouse wheel events and + * throttling map interactions. + * @private + */ + //////////////////////////////////////////////////////////////////////////// + function throttled_wheel() { + var my_queue = {}; + + function accum(evt) { + var dx, dy; + + if (m_paused) { + return; + } + + if (my_queue !== m_queue) { + my_queue = { + kind: 'wheel', + scroll: {x: 0, y: 0} + }; + m_queue = my_queue; + } + + evt.preventDefault(); + + // try to normalize deltas using the wheel event standard: + // https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent + evt.deltaFactor = 1; + if (evt.originalEvent.deltaMode === 1) { + // DOM_DELTA_LINE -- estimate line height + evt.deltaFactor = 40; + } else if (evt.originalEvent.deltaMode === 2) { + // DOM_DELTA_PAGE -- get window height + evt.deltaFactor = $(window).height(); + } + + // prevent NaN's on legacy browsers + dx = evt.originalEvent.deltaX || 0; + dy = evt.originalEvent.deltaY || 0; + + // scale according to the options + dx = dx * m_options.wheelScaleX * evt.deltaFactor / 120; + dy = dy * m_options.wheelScaleY * evt.deltaFactor / 120; + + my_queue.scroll.x += dx; + my_queue.scroll.y += dy; + } + + function wheel(evt) { + var zoomFactor, action; + + // If the current queue doesn't match the queue passed in as an argument, + // assume it was cancelled and do nothing. + if (my_queue !== m_queue) { + return; + } + + // perform the map navigation event + m_this._getMouseModifiers(evt); + if (m_options.panWheelEnabled && + eventMatch('wheel', m_options.panWheelModifiers)) { + action = 'pan'; + } else if (m_options.zoomWheelEnabled && + eventMatch('wheel', m_options.zoomWheelModifiers)) { + action = 'zoom'; + } else if (m_options.rotateWheelEnabled && + eventMatch('wheel', m_options.rotateWheelModifiers)) { + action = 'rotate'; + } + if (action) { + // if we were moving because of momentum or a transition, cancel it and + // recompute where the mouse action is occuring. + var recompute = m_this.map().transitionCancel('wheel.' + action); + recompute |= m_this.cancel('momentum', true); + if (recompute) { + m_mouse.geo = m_this.map().displayToGcs(m_mouse.map); + } + switch (action) { + case 'pan': + m_this.map().pan({ + x: m_queue.scroll.x, + y: m_queue.scroll.y + }); + break; + case 'zoom': + zoomFactor = -m_queue.scroll.y; + m_callZoom(zoomFactor, m_mouse); + break; + case 'rotate': + m_this.map().rotation( + m_this.map().rotation() + + m_queue.scroll.y * m_options.rotateWheelScale, + m_mouse); + break; + } + } + + // reset the queue + m_queue = {}; + } + + if (m_options.throttle > 0) { + return throttle(m_options.throttle, false, wheel, accum); + } else { + return function (evt) { + accum(evt); + wheel(evt); + }; + } + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle mouse wheel event. (Defined inside _connectEvents). + */ + //////////////////////////////////////////////////////////////////////////// + this._handleMouseWheel = function () {}; + + //////////////////////////////////////////////////////////////////////////// + /** + * Start up a spring back action when the map bounds are out of range. + * Not to be user callable. + * @todo Move this and momentum handling to the map class + * @protected + * + */ + //////////////////////////////////////////////////////////////////////////// + this.springBack = function (initialVelocity, origAction) { + if (m_state.action === 'momentum') { + return; + } + if (!initialVelocity) { + m_mouse.velocity = { + x: 0, + y: 0 + }; + } + m_state.origAction = origAction; + m_state.action = 'momentum'; + m_state.origin = m_this.mouse(); + m_state.start = new Date(); + m_state.handler = function () { + var v, s, last, dt; + + // Not sure the correct way to do this. We need the delta t for the + // next time step... Maybe use a better interpolator and the time + // parameter from requestAnimationFrame. + dt = Math.min(m_mouse.deltaTime, 30); + if (m_state.action !== 'momentum' || + !m_this.map() || + m_this.map().transition()) { + // cancel if a new action was performed + return; + } + + last = m_state.start.valueOf(); + m_state.start = new Date(); + + v = modifyVelocity(m_mouse.velocity, m_state.start - last); + + // stop panning when the speed is below the threshold + if (!v) { + m_state = {}; + return; + } + + s = calcSpeed(v); + if (s > m_options.momentum.maxSpeed) { + s = m_options.momentum.maxSpeed / s; + v.x = v.x * s; + v.y = v.y * s; + } + + if (!isFinite(v.x) || !isFinite(v.y)) { + v.x = 0; + v.y = 0; + } + m_mouse.velocity.x = v.x; + m_mouse.velocity.y = v.y; + + switch (m_state.origAction) { + case 'zoom': + var dy = m_mouse.velocity.y * dt; + m_callZoom(-dy * m_options.zoomScale / 120, m_state); + break; + default: + m_this.map().pan({ + x: m_mouse.velocity.x * dt, + y: m_mouse.velocity.y * dt + }); + break; + } + + if (m_state.handler) { + window.requestAnimationFrame(m_state.handler); + } + }; + if (m_state.handler) { + window.requestAnimationFrame(m_state.handler); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle double click event + */ + //////////////////////////////////////////////////////////////////////////// + this._handleDoubleClick = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Public method that unbinds all events + */ + //////////////////////////////////////////////////////////////////////////// + this.destroy = function () { + m_this._disconnectEvents(); + m_this.map(null); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get current mouse information + */ + //////////////////////////////////////////////////////////////////////////// + this.mouse = function () { + return $.extend(true, {}, m_mouse); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get current keyboard information + */ + //////////////////////////////////////////////////////////////////////////// + this.keyboard = function () { + return $.extend(true, {}, m_keyboard); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the current interactor state + */ + //////////////////////////////////////////////////////////////////////////// + this.state = function () { + 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. + * + * The options for creating the events are as follows, not all + * options are required for every event type. :: + * + * options = { + * page: {x, y} // position on the page + * map: {x, y} // position on the map (overrides page) + * button: 'left' | 'right' | 'middle' + * modifiers: [ 'alt' | 'ctrl' | 'meta' | 'shift' ] + * wheelDelta: {x, y} + * } + * + * @param {string} type Event type 'mousemove', 'mousedown', 'mouseup', ... + * @param {object} options + * @returns {mapInteractor} + */ + //////////////////////////////////////////////////////////////////////////// + this.simulateEvent = function (type, options) { + var evt, page, offset, which; + + if (!m_this.map()) { + return m_this; + } + + page = options.page || {}; + + if (options.map) { + offset = $node.offset(); + page.x = options.map.x + offset.left; + page.y = options.map.y + offset.top; + } + + if (options.button === 'left') { + which = 1; + } else if (options.button === 'right') { + which = 3; + } else if (options.button === 'middle') { + 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, + preventDefault: function () {}, + stopPropagation: function () {}, + stopImmediatePropagation: function () {} + } + } + ); + $node.trigger(evt); + }; + this._connectEvents(); + return this; + }; + + inherit(mapInteractor, object); + module.exports = mapInteractor; + + +/***/ }, +/* 218 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerLayer = __webpack_require__(131).registerLayer; + var layer = __webpack_require__(130); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class uiLayer + * + * @class geo.gui.uiLayer + * @extends {geo.layer} + * @returns {geo.gui.uiLayer} + */ + ////////////////////////////////////////////////////////////////////////////// + var uiLayer = function (arg) { + 'use strict'; + + var createWidget = __webpack_require__(131).createWidget; + + // The widget stays fixed on the screen. + arg.renderer = 'dom'; + arg.sticky = false; + + if (!(this instanceof uiLayer)) { + return new uiLayer(arg); + } + layer.call(this, arg); + + var m_this = this, + s_exit = this._exit; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create a new ui control + * + * @returns {geo.gui.Widget} Will return a new control widget + */ + //////////////////////////////////////////////////////////////////////////// + this.createWidget = function (widgetName, arg) { + var newWidget = createWidget(widgetName, m_this, arg); + + // We only want top level widgets to be a child of the uiLayer + if (!(arg && 'parent' in arg)) { + m_this.addChild(newWidget); + } + + newWidget._init(arg); + m_this.modified(); + return newWidget; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Delete a ui control + */ + //////////////////////////////////////////////////////////////////////////// + this.deleteWidget = function (widget) { + widget._exit(); + m_this.removeChild(widget); + m_this.modified(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Free memory and destroy the layer. + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.children().forEach(function (child) { + m_this.deleteWidget(child); + }); + s_exit(); + }; + }; + + inherit(uiLayer, layer); + + registerLayer('ui', uiLayer); + module.exports = uiLayer; + + +/***/ }, +/* 219 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = (function () { + 'use strict'; + + var $ = __webpack_require__(1); + var inherit = __webpack_require__(4); + var tileLayer = __webpack_require__(220); + var registry = __webpack_require__(131); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of osmLayer + * + * @class geo.osmLayer + * @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). + */ + ////////////////////////////////////////////////////////////////////////////// + var osmLayer = function (arg) { + + var imageTile = __webpack_require__(142); + + if (!(this instanceof osmLayer)) { + return new osmLayer(arg); + } + if (arg.mapOpacity !== undefined && arg.opacity === undefined) { + arg.opacity = arg.mapOpacity; + } + tileLayer.call(this, arg); + + /* mapOpacity is just another name for the layer opacity. */ + this.mapOpacity = this.opacity; + + /** + * 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 imageTile({ + index: index, + size: {x: this._options.tileWidth, y: this._options.tileHeight}, + queue: this._queue, + url: this._options.url(urlParams.x, urlParams.y, urlParams.level || 0, + this._options.subdomains) + }); + }.bind(this); + }; + + /** + * This object contains the default options used to initialize the osmLayer. + */ + osmLayer.defaults = $.extend({}, tileLayer.defaults, { + 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' + }); + + inherit(osmLayer, tileLayer); + registry.registerLayer('osm', osmLayer); + return osmLayer; + })(); + + +/***/ }, +/* 220 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = (function () { + 'use strict'; + + var inherit = __webpack_require__(4); + var featureLayer = __webpack_require__(137); + + /** + * Standard modulo operator where the output is in [0, b) for all inputs. + * @private + */ + function modulo(a, b) { + return ((a % b) + b) % b; + } + + /** + * 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. + */ + function m_getTileSubdomain(x, y, subdomains) { + return subdomains[modulo(x + y, subdomains.length)]; + } + + /** + * 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); + }; + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 geo.tileLayer + * @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 {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} + */ + ////////////////////////////////////////////////////////////////////////////// + var tileLayer = function (options) { + + var $ = __webpack_require__(1); + var geo_event = __webpack_require__(125); + var transform = __webpack_require__(147); + var tileCache = __webpack_require__(221); + var fetchQueue = __webpack_require__(138); + var adjustLayerForRenderer = __webpack_require__(131).adjustLayerForRenderer; + var Tile = __webpack_require__(143); + + if (!(this instanceof tileLayer)) { + return new tileLayer(options); + } + 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 ? 600 : 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); + } + + var s_init = this._init, + s_exit = this._exit, + m_lastTileSet = [], + m_exited; + + // copy the options into a private variable + this._options = $.extend(true, {}, options); + + // set the layer attribution text + this.attribution(options.attribution); + + // initialize the object that keeps track of actively drawn tiles + this._activeTiles = {}; + + // 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 = tileCache({size: options.cacheSize}); + + // initialize the tile fetch queue + this._queue = fetchQueue({ + // this should probably be 6 * subdomains.length if subdomains are used + size: 6, + // if track is the same as the cache size, then neither processing time + // nor memory will be wasted. Larger values will use more memory, + // smaller values will do needless computations. + track: options.cacheSize, + needed: function (tile) { + return tile === this.cache.get(tile.toString(), true); + }.bind(this) + }); + + var m_tileOffsetValues = {}; + + /** + * 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; + }}); + + /** + * 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 + }}); + + /** + * 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; + }; + + /** + * 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); + if (isNaN(point.x)) { point.x = 0; } + if (isNaN(point.y)) { point.y = 0; } + var to = this._tileOffset(level); + if (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; + }; + + /** + * 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 : Tile({ + index: indexOrTile, + size: {x: this._options.tileWidth, y: this._options.tileHeight}, + url: '' + })); + var to = this._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 = transform.transformCoordinates(gcs, map.gcs(), coord); + } + return { + left: coord[0].x, + top: coord[0].y, + right: coord[1].x, + bottom: coord[1].y + }; + }; + + /** + * 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 Tile({ + index: index, + size: {x: this._options.tileWidth, y: this._options.tileHeight}, + queue: this._queue, + 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 + * @param {boolean} delayPurge If true, don't purge tiles from the cache + * @returns {geo.tile} + */ + this._getTileCached = function (index, source, delayPurge) { + var tile = this.cache.get(this._tileHash(index)); + if (tile === null) { + tile = this._getTile(index, source); + this.cache.add(tile, this.remove.bind(this), delayPurge); + } + 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) { + var corners = [ + this.tileAtPoint({x: bounds.left, y: bounds.top}, level), + this.tileAtPoint({x: bounds.right, y: bounds.top}, level), + this.tileAtPoint({x: bounds.left, y: bounds.bottom}, level), + this.tileAtPoint({x: bounds.right, y: bounds.bottom}, level) + ]; + return { + start: { + x: Math.min(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.min(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + }, + end: { + x: Math.max(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.max(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + } + }; + }; + + /** + * 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 + * @param {boolean} onlyIfChanged If the set of tiles have not changed + * (even if their desired order has), return undefined instead of an + * array of tiles. + * @returns {geo.tile[]} An array of tile objects + */ + this._getTiles = function (maxLevel, bounds, sorted, onlyIfChanged) { + var i, j, tiles = [], index, nTilesLevel, + start, end, indexRange, source, center, changed = false, old, level, + minLevel = (this._options.keepLower ? this._options.minLevel : + maxLevel); + if (maxLevel < minLevel) { + maxLevel = minLevel; + } + + /* Generate a list of the tiles that we want to create. This is done + * before sorting, because we want to actually generate the tiles in + * the sort order. */ + 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); + + if (!this._options.wrapX) { + start.x = Math.min(Math.max(start.x, 0), nTilesLevel.x - 1); + end.x = Math.min(Math.max(end.x, 0), nTilesLevel.x - 1); + if (level === minLevel && this._options.keepLower) { + start.x = 0; + end.x = nTilesLevel.x - 1; + } + } + if (!this._options.wrapY) { + start.y = Math.min(Math.max(start.y, 0), nTilesLevel.y - 1); + end.y = Math.min(Math.max(end.y, 0), nTilesLevel.y - 1); + if (level === minLevel && this._options.keepLower) { + start.y = 0; + end.y = nTilesLevel.y - 1; + } + } + /* If we are reprojecting tiles, we need a check to not use all levels + * if the number of tiles is excessive. */ + if (this._options.gcs && this._options.gcs !== this.map().gcs() && + level !== minLevel && + (end.x + 1 - start.x) * (end.y + 1 - start.y) > + (this.map().size().width * this.map().size().height / + this._options.tileWidth / this._options.tileHeight) * 16) { + break; + } + + // loop over the tile range + for (i = start.x; i <= end.x; i += 1) { + for (j = start.y; j <= end.y; j += 1) { + index = {level: level, x: i, y: j}; + source = {level: level, x: i, y: j}; + if (this._options.wrapX) { + source.x = modulo(source.x, nTilesLevel.x); + } + if (this._options.wrapY) { + source.y = modulo(source.y, nTilesLevel.y); + } + if (this.isValid(source)) { + if (onlyIfChanged && tiles.length < m_lastTileSet.length) { + old = m_lastTileSet[tiles.length]; + changed = changed || (index.level !== old.level || + index.x !== old.x || index.y !== old.y); + } + tiles.push({index: index, source: source}); + } + } + } + } + + if (onlyIfChanged) { + if (!changed && tiles.length === m_lastTileSet.length) { + return; + } + m_lastTileSet.splice(0, m_lastTileSet.length); + $.each(tiles, function (idx, tile) { + m_lastTileSet.push(tile.index); + }); + } + + if (sorted) { + center = { + x: (start.x + end.x) / 2, + y: (start.y + end.y) / 2, + level: maxLevel, + bottomLevel: maxLevel + }; + var numTiles = Math.max(end.x - start.x, end.y - start.y) + 1; + for (; numTiles >= 1; numTiles /= 2) { + center.bottomLevel -= 1; + } + tiles.sort(this._loadMetric(center)); + /* If we are using a fetch queue, start a new batch */ + if (this._queue) { + this._queue.batch(true); + } + } + if (this.cache.size < tiles.length) { + console.log('Increasing cache size to ' + tiles.length); + this.cache.size = tiles.length; + } + /* Actually get the tiles. */ + for (i = 0; i < tiles.length; i += 1) { + tiles[i] = this._getTileCached(tiles[i].index, tiles[i].source, true); + } + this.cache.purge(this.remove.bind(this)); + return tiles; + }; + + /** + * 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 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; + + a = a.index || a; + b = b.index || b; + // shortcut if zoom level differs + if (a.level !== b.level) { + if (center.bottomLevel && ((a.level >= center.bottomLevel) !== + (b.level >= center.bottomLevel))) { + return a.level >= center.bottomLevel ? -1 : 1; + } + return a.level - b.level; + } + + /* compute the center coordinates relative to a.level. Since we really + * care about the center of the tiles, use an offset */ + scale = Math.pow(2, a.level - center.level); + cx = (center.x + 0.5) * scale - 0.5; + cy = (center.y + 0.5) * scale - 0.5; + + // calculate distances to the center squared + 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; + + // return negative if a < b, or positive if a > b + return a0 - b0; + }; + }; + + /** + * 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: coord.x * s, + y: coord.y * s + }; + }; + + /** + * 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 + }; + }; + + /** + * Draw the given tile on the active canvas. + * @param {geo.tile} tile The tile to draw + */ + this.drawTile = function (tile) { + var hash = tile.toString(); + + 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); + } + + // add the tile to the active cache + this._activeTiles[hash] = tile; + }; + + /** + * 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.'); + } + + // 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 - parseInt(div.attr('offsetx') || 0, 10)) + 'px', + top: (bounds.top - parseInt(div.attr('offsety') || 0, 10)) + 'px' + }); + + // 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.toString()); + 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 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 { + /* 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); + } + $(tile.image).remove(); + } + }; + + /** + * 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); + }; + + /** + * 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}), + ur = this.displayToLevel({x: size.width, y: 0}), + ll = this.displayToLevel({x: 0, y: size.height}), + lr = this.displayToLevel({x: size.width, y: size.height}); + return { + level: zoom, + scale: scale, + left: Math.min(ul.x, ur.x, ll.x, lr.x), + right: Math.max(ul.x, ur.x, ll.x, lr.x), + top: Math.min(ul.y, ur.y, ll.y, lr.y), + bottom: Math.max(ul.y, ur.y, ll.y, lr.y) + }; + }; + + /** + * 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. + * @param {object} bounds view bounds. If not specified, this is + * obtained from _getViewBounds(). + */ + this._purge = function (zoom, doneLoading, bounds) { + var tile, hash; + + // Don't purge tiles in an active update + if (this._updating) { + return; + } + + // get the view bounds + if (!bounds) { + bounds = this._getViewBounds(); + } + + for (hash in this._activeTiles) { + + tile = this._activeTiles[hash]; + if (this._canPurge(tile, bounds, zoom, doneLoading)) { + this.remove(tile); + } + } + return this; + }; + + /** + * Remove all active tiles from the canvas. + * @returns {geo.tile[]} The array of tiles removed + */ + this.clear = function () { + var tiles = [], tile; + + // ignoring the warning here because this is a privately + // controlled object with simple keys + for (tile in this._activeTiles) { + tiles.push(this.remove(tile)); + } + + // clear out the tile coverage tree + this._tileTree = {}; + + m_lastTileSet = []; + + return tiles; + }; + + /** + * 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(); + }; + + /** + * 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 + }; + }; + + /** + * 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) { + // these need to always use the *layer* unitsPerPixel, or possibly + // convert tile space using a transform + var map = this.map(), + unit = map.unitsPerPixel(zoom === undefined ? map.zoom() : zoom); + return { + x: pt.x * unit, + y: this._topDown() * pt.y * unit + }; + }; + + /** + * Return a factor for invertin the y units as appropriate. + * @return {number} + */ + this._topDown = function () { + return this._options.topDown ? 1 : -1; + }; + + /** + * 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) { + if (!this.canvas()) { + return; + } + var node = this.canvas() + .find('div[data-tile-layer=' + level.toFixed() + ']').get(0); + if (!node) { + node = $( + '
' + ).css({ + 'transform-origin': '0px 0px', + 'line-height': 0, + 'font-size': 0 + }).get(0); + this.canvas().append(node); + } + return node; + }; + + /** + * Set sublayer transforms to align them with the given zoom level. + * @param {number} level The target zoom level + * @param {object} view The view bounds. The top and left are used to + * adjust the offset of tile layers. + * @return {object} the x and y offsets for the current level. + */ + this._updateSubLayers = function (level, view) { + var canvas = this.canvas(), + lastlevel = parseInt(canvas.attr('lastlevel'), 10), + lastx = parseInt(canvas.attr('lastoffsetx') || 0, 10), + lasty = parseInt(canvas.attr('lastoffsety') || 0, 10); + if (lastlevel === level && Math.abs(lastx - view.left) < 65536 && + Math.abs(lasty - view.top) < 65536) { + return {x: lastx, y: lasty}; + } + var map = this.map(), + to = this._tileOffset(level), + x = parseInt((view.left + view.right - map.size().width) / 2 + to.x, 10), + y = parseInt((view.top + view.bottom - map.size().height) / 2 + to.y, 10); + canvas.find('.geo-tile-layer').each(function (idx, el) { + var $el = $(el), + layer = parseInt($el.data('tileLayer'), 10); + $el.css( + 'transform', + 'scale(' + Math.pow(2, level - layer) + ')' + ); + var layerx = parseInt(x / Math.pow(2, level - layer), 10), + layery = parseInt(y / Math.pow(2, level - layer), 10), + dx = layerx - parseInt($el.attr('offsetx') || 0, 10), + dy = layery - parseInt($el.attr('offsety') || 0, 10); + $el.attr({offsetx: layerx, offsety: layery}); + $el.find('.geo-tile-container').each(function (tileidx, tileel) { + $(tileel).css({ + left: (parseInt($(tileel).css('left'), 10) - dx) + 'px', + top: (parseInt($(tileel).css('top'), 10) - dy) + 'px' + }); + }); + }); + canvas.attr({lastoffsetx: x, lastoffsety: y, lastlevel: level}); + return {x: x, y: y}; + }; + + /** + * Update the view according to the map/camera. + * @returns {this} Chainable + */ + this._update = function (evt) { + /* Ignore zoom and rotate events, as they are ALWAYS followed by a pan + * event */ + if (evt && evt.event && (evt.event.event === geo_event.zoom || + evt.event.event === geo_event.rotate)) { + return; + } + var map = this.map(), + bounds = map.bounds(undefined, null), + tiles; + + if (this._updateSubLayers) { + var mapZoom = map.zoom(), + zoom = this._options.tileRounding(mapZoom), + view = this._getViewBounds(); + // Update the transform for the local layer coordinates + var offset = this._updateSubLayers(zoom, view) || {x: 0, y: 0}; + + var to = this._tileOffset(zoom); + if (this.renderer() === null) { + var scale = Math.pow(2, mapZoom - zoom), + rotation = map.rotation(), + rx = -to.x + -(view.left + view.right) / 2 + offset.x, + ry = -to.y + -(view.bottom + view.top) / 2 + offset.y, + dx = (rx + map.size().width / 2) * scale, + dy = (ry + map.size().height / 2) * scale; + + this.canvas().css({ + 'transform-origin': '' + + -rx + 'px ' + + -ry + 'px' + }); + var transform = 'translate(' + dx + 'px' + ',' + dy + 'px' + ')' + + 'scale(' + scale + ')'; + if (rotation) { + transform += 'rotate(' + (rotation * 180 / Math.PI) + 'deg)'; + } + this.canvas().css('transform', transform); + } + /* 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, + offsetx: offset.x, + offsety: offset.y, + rotation: map.rotation() + }); + } + + tiles = this._getTiles( + zoom, bounds, true, true + ); + + if (tiles === undefined) { + return; + } + + // reset the tile coverage tree + this._tileTree = {}; + + tiles.forEach(function (tile) { + if (tile.fetched()) { + /* if we have already fetched the tile, we know we can just draw it, + * as the bounds won't have changed since the call to _getTiles. */ + this.drawTile(tile); + + // mark the tile as covered + this._setTileTree(tile); + } else { + if (!tile._queued) { + tile.then(function () { + if (m_exited) { + /* If we have disconnected the renderer, do nothing. This + * happens when the layer is being deleted. */ + return; + } + 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), + view = this._getViewBounds(); + if (this._canPurge(tile, view, zoom)) { + this.remove(tile); + return; + } + + this.drawTile(tile); + + // mark the tile as covered + this._setTileTree(tile); + }.bind(this)); + + this.addPromise(tile); + tile._queued = true; + } else { + /* If we are using a fetch queue, tell the queue so this tile can + * be reprioritized. */ + var pos = this._queue ? this._queue.get(tile) : -1; + if (pos >= 0) { + this._queue.add(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 () { + var map = this.map(), + mapZoom = map.zoom(), + zoom = this._options.tileRounding(mapZoom); + 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) { + if (this._options.keepLower) { + return; + } + 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]; + } + + // 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._tileOffset(tile.index.level); + var scale = 1; + if (tile.index.level !== bounds.level) { + scale = Math.pow(2, (bounds.level || 0) - (tile.index.level || 0)); + } + 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 && + tile.index.level !== this._options.minLevel) { + return true; + } + if (tile.index.level === this._options.minLevel && + !this._options.wrapX && !this._options.wrapY) { + return false; + } + } else { + /* 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; + }; + + /** + * 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, this._options.gcs || 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; + }; + + /** + * Get or set the subdomains used for templating. + * + * @param {string|list} [subdomains] A comma-separated list, a string of + * single character subdomains, or a list. + * @returns {string|list|this} + */ + this.subdomains = function (subdomains) { + if (subdomains === undefined) { + return this._options.subdomains; + } + if (subdomains) { + if ($.type(subdomains) === 'string') { + if (subdomains.indexOf(',') >= 0) { + subdomains = subdomains.split(','); + } else { + subdomains = subdomains.split(''); + } + } + this._options.subdomains = subdomains; + this.reset(); + this.map().draw(); + } + return this; + }; + + /** + * Return a value from the tileOffset function, caching it for different + * levels. + * + * @param {Number} level the level to pass to the tileOffset function. + * @returns {Object} a tile offset object with x and y properties. + */ + this._tileOffset = function (level) { + if (m_tileOffsetValues[level] === undefined) { + m_tileOffsetValues[level] = this._options.tileOffset(level); + } + return m_tileOffsetValues[level]; + }; + + /** + * 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; + }; + + /** + * Clean up the layer. + */ + this._exit = function () { + this.reset(); + // call super method + s_exit.apply(this, arguments); + m_exited = true; + return this; + }; + + adjustLayerForRenderer('tile', this); + + return this; + }; + + /** + * This object contains the default options used to initialize the tileLayer. + */ + tileLayer.defaults = { + minLevel: 0, + maxLevel: 18, + tileOverlap: 0, + tileWidth: 256, + tileHeight: 256, + wrapX: true, + wrapY: false, + url: null, + subdomains: 'abc', + 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(tileLayer, featureLayer); + return tileLayer; + })(); + + +/***/ }, +/* 221 */ +/***/ function(module, exports) { + + module.exports = (function () { + 'use strict'; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 geo.tileCache + * + * @param {object?} [options] A configuratoin object for the cache + * @param {number} [options.size=64] The maximum number of tiles to store + */ + ////////////////////////////////////////////////////////////////////////////// + var tileCache = function (options) { + if (!(this instanceof tileCache)) { + return new tileCache(options); + } + options = options || {}; + this._size = options.size || 64; + + /** + * 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; + } + }); + + /** + * 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; + } + + // Remove the tile from the access queue + this._atime.splice(this._access(hash), 1); + + // Remove the tile from the cache + delete this._cache[hash]; + return true; + }; + + /** + * 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; + }; + + /** + * 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 + * @param {boolean} noMove if true, don't move the tile to the front of the + * access queue. + * @returns {geo.tile|null} + */ + this.get = function (hash, noMove) { + hash = typeof hash === 'string' ? hash : hash.toString(); + if (!(hash in this._cache)) { + return null; + } + + if (!noMove) { + this._atime.splice(this._access(hash), 1); + this._atime.unshift(hash); + } + return this._cache[hash]; + }; + + /** + * Add a tile to the cache. + * @param {geo.tile} tile + * @param {function} removeFunc if specified and tiles must be purged from + * the cache, call this function on each tile before purging. + * @param {boolean} noPurge if true, don't purge tiles. + */ + this.add = function (tile, removeFunc, noPurge) { + // 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); + + if (!noPurge) { + this.purge(removeFunc); + } + }; + + /** + * Purge tiles from the cache if it is full. + * @param {function} removeFunc if specified and tiles must be purged from + * the cache, call this function on each tile before purging. + */ + this.purge = function (removeFunc) { + var hash; + while (this._atime.length > this.size) { + hash = this._atime.pop(); + var tile = this._cache[hash]; + if (removeFunc) { + removeFunc(tile); + } + delete this._cache[hash]; + } + }; + + this.clear(); + return this; + }; + return tileCache; + })(); + + +/***/ }, +/* 222 */ +/***/ function(module, exports, __webpack_require__) { + + var $ = __webpack_require__(1); + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class pathFeature + * + * @class geo.pathFeature + * @extends geo.feature + * @returns {geo.pathFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var pathFeature = function (arg) { + 'use strict'; + if (!(this instanceof pathFeature)) { + return new pathFeature(arg); + } + arg = arg || {}; + feature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + m_position = arg.position === undefined ? [] : arg.position, + s_init = this._init; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set positions + * + * @returns {geo.pathFeature} + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + 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 + ); + + m_this.style(defaultStyle); + + if (m_position) { + m_this.dataTime().modified(); + } + }; + + this._init(arg); + return this; + }; + + inherit(pathFeature, feature); + module.exports = pathFeature; + + +/***/ }, +/* 223 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var polygonFeature = __webpack_require__(224); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class planeFeature + * + * @class geo.planeFeature + * @extends geo.polygonFeature + * @returns {geo.planeFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var planeFeature = function (arg) { + 'use strict'; + if (!(this instanceof planeFeature)) { + return new planeFeature(arg); + } + arg = 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; + + 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 = arg.drawOnAsyncResourceLoad === undefined ? + true : false, + s_init = this._init; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 { + m_drawOnAsyncResourceLoad = val; + return m_this; + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + } + m_this.style(style); + }; + + this._init(arg); + return this; + }; + + inherit(planeFeature, polygonFeature); + module.exports = planeFeature; + + +/***/ }, +/* 224 */ +/***/ function(module, exports, __webpack_require__) { + + var $ = __webpack_require__(1); + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class polygonFeature + * + * @class geo.polygonFeature + * @extends geo.feature + * @returns {geo.polygonFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var polygonFeature = function (arg) { + 'use strict'; + if (!(this instanceof polygonFeature)) { + return new polygonFeature(arg); + } + arg = arg || {}; + feature.call(this, arg); + + var util = __webpack_require__(120); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + 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; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Override the parent data method to keep track of changes to the + * internal coordinates. + */ + //////////////////////////////////////////////////////////////////////////// + this.data = function (arg) { + var ret = s_data(arg); + if (arg !== undefined) { + getCoordinates(); + } + 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 + */ + //////////////////////////////////////////////////////////////////////////// + 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 + }; + }); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set polygon accessor + * + * @returns {geo.pointFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.polygon = function (val) { + if (val === undefined) { + return m_polygon; + } else { + m_polygon = val; + m_this.dataTime().modified(); + m_this.modified(); + getCoordinates(); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set position accessor + * + * @returns {geo.pointFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.position = function (val) { + if (val === undefined) { + return m_position; + } else { + m_position = val; + m_this.dataTime().modified(); + m_this.modified(); + getCoordinates(); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 = util.pointInPolygon( + coordinate, + coord.outer, + coord.inner + ); + if (inside) { + indices.push(i); + found.push(data[i]); + } + }); + return { + index: indices, + found: found + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + + var defaultStyle = $.extend( + {}, + { + 'fillColor': { r: 0.0, g: 0.5, b: 0.5 }, + 'fillOpacity': 1.0 + }, + arg.style === undefined ? {} : arg.style + ); + + m_this.style(defaultStyle); + + if (m_position) { + m_this.dataTime().modified(); + } + }; + + this._init(arg); + return this; + }; + + inherit(polygonFeature, feature); + module.exports = polygonFeature; + + +/***/ }, +/* 225 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class pointFeature + * + * @class geo.pointFeature + * @param {object} arg Options object + * @param {boolean} arg.clustering Enable point clustering + * @extends geo.feature + * @returns {geo.pointFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var pointFeature = function (arg) { + 'use strict'; + if (!(this instanceof pointFeature)) { + return new pointFeature(arg); + } + arg = arg || {}; + feature.call(this, arg); + + var $ = __webpack_require__(1); + var timestamp = __webpack_require__(129); + var ClusterGroup = __webpack_require__(124); + var geo_event = __webpack_require__(125); + var util = __webpack_require__(120); + var wigglemaps = __webpack_require__(226); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + m_rangeTree = null, + m_rangeTreeTime = 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 clustering option + * + * @returns {geo.pointFeature|boolean} + */ + //////////////////////////////////////////////////////////////////////////// + this.clustering = function (val) { + if (val === undefined) { + 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(); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Generate the clustering tree from positions. This might be async in the + * future. + */ + //////////////////////////////////////////////////////////////////////////// + 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; + + // generate the cluster tree from the raw data + var position = m_this.position(); + m_clusterTree = new 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 = 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()); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.pointFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.position = function (val) { + if (val === undefined) { + return m_this.style('position'); + } else { + val = 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update the current range tree object. Should be called whenever the + * data changes. + */ + //////////////////////////////////////////////////////////////////////////// + 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 wigglemaps.RangeTree(pts); + m_rangeTreeTime.modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.pointSearch = function (p) { + var min, max, data, idx = [], box, found = [], ifound = [], map, pt, + corners, + stroke = m_this.style.get('stroke'), + strokeWidth = m_this.style.get('strokeWidth'), + radius = m_this.style.get('radius'); + + if (!m_this.selectionAPI()) { + return []; + } + + data = m_this.data(); + if (!data || !data.length) { + return { + found: [], + index: [] + }; + } + + map = m_this.layer().map(); + pt = map.gcsToDisplay(p); + // check all corners to make sure we handle rotations + corners = [ + map.displayToGcs({x: pt.x - m_maxRadius, y: pt.y - m_maxRadius}), + map.displayToGcs({x: pt.x + m_maxRadius, y: pt.y - m_maxRadius}), + map.displayToGcs({x: pt.x - m_maxRadius, y: pt.y + m_maxRadius}), + map.displayToGcs({x: pt.x + m_maxRadius, y: pt.y + m_maxRadius}) + ]; + min = { + x: Math.min(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.min(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + }; + max = { + x: Math.max(corners[0].x, corners[1].x, corners[2].x, corners[3].x), + y: Math.max(corners[0].y, corners[1].y, corners[2].y, corners[3].y) + }; + + // Find points inside the bounding box + box = new wigglemaps.Box( + wigglemaps.vect(min.x, min.y), + wigglemaps.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, rad2; + + rad = radius(data[i], i); + rad += stroke(data[i], i) ? strokeWidth(data[i], i) : 0; + rad2 = rad * rad; + p = map.gcsToDisplay(p); + dx = p.x - pt.x; + dy = p.y - pt.y; + if (dx * dx + dy * dy <= rad2) { + found.push(d); + ifound.push(i); + } + }); + + return { + data: found, + index: ifound + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Returns an array of datum indices that are contained in the given box. + */ + //////////////////////////////////////////////////////////////////////////// + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Overloaded data method that updates the internal range tree on write. + */ + //////////////////////////////////////////////////////////////////////////// + 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: :: + * + * { + * min: { + * x: value, + * y: value + * }, + * max: { + * x: value, + * y: value + * } + * } + * + * @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); + + return { + min: { + x: pt.x - radius, + y: pt.y - radius + }, + max: { + x: pt.x + radius, + y: pt.y + radius + } + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + 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 + ); + + 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; + }; + + /** + * 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} + */ + + /** + * 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} + */ + pointFeature.create = function (layer, renderer, spec) { + 'use strict'; + + spec.type = 'point'; + return feature.create(layer, spec); + }; + + inherit(pointFeature, feature); + module.exports = pointFeature; + + +/***/ }, +/* 226 */ +/***/ function(module, exports) { + + ////////////////////////////////////////////////////////////////////////////// + /** + * @license + * Includes several support classes adapted from wigglemaps. + * + * https://github.com/dotskapes/wigglemaps + * + * Copyright 2013 Preston and Krejci (dotSkapes Virtual Lab) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + ////////////////////////////////////////////////////////////////////////////// + + (function () { + 'use strict'; + + var RangeNode = function (elem, start, end, current) { + this.data = elem[current]; + this.left = null; + this.right = null; + if (start !== current) + this.left = new RangeNode(elem, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); + if (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 is populated as needed */ + this.search = rangeNodeSearch; + }; + + var rangeNodeSearch = function (result, box) { + var m_this = this; + + var xrange = function (b) { + return (b.x_in(m_this.elem[m_this.start]) && + b.x_in(m_this.elem[m_this.end])); + }; + + var yrange = function (b, start, end) { + return (b.y_in(m_this.subtree[start]) && + b.y_in(m_this.subtree[end])); + }; + + var subquery = function (result, box, start, end, current) { + if (yrange(box, start, end)) { + for (var i = start; i <= end; i ++) { + result.push(m_this.subtree[i]); + } + return; + } + if (box.y_in(m_this.subtree[current])) + result.push(m_this.subtree[current]); + if (box.y_left(m_this.subtree[current])){ + if (current !== end) + subquery(result, box, current + 1, end, parseInt((end + (current + 1)) / 2, 10)); + } else if (box.x_right(m_this.subtree[current])) { + if (current !== start) + subquery(result, box, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); + } else { + if (current !== end) + subquery(result, box, current + 1, end, parseInt((end + (current + 1)) / 2, 10)); + if (current !== start) + subquery(result, box, start, current - 1, parseInt((start + (current - 1)) / 2, 10)); + } + }; + + if (xrange(box)) { + if (!this.subtree) { + this.subtree = this.elem.slice(this.start, this.end + 1); + this.subtree.sort(function (a, b) { + return a.y - b.y; + }); + } + subquery(result, box, 0, this.subtree.length - 1, parseInt((this.subtree.length - 1) / 2, 10)); + return; + } else { + if (box.contains(this.data)) + result.push(this.data); + if (box.x_left(this.data)) { + if (this.right) + this.right.search(result, box); + } else if (box.x_right(this.data)) { + if (this.left) + this.left.search(result, box); + } else { + if (this.left) + this.left.search(result, box); + if (this.right) + this.right.search(result, box); + } + } + }; + + var RangeTree = function (elem) { + elem.sort(function (a, b) { + return a.x - b.x; + }); + if (elem.length > 0) + this.root = new RangeNode(elem, 0, elem.length - 1, parseInt((elem.length - 1) / 2, 10)); + else + this.root = null; + + this.search = function (_box) { + if (!this.root) + return []; + //var box = new Box (min, max); + var box = _box.clone (); + var result = []; + this.root.search (result, box); + return result; + }; + }; + + var 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; i < 4; i ++) { + for (var j = 0; j < 4; j ++) { + if (vect.intersects (this.vertex (i), this.vertex ((i + 1) % 4), + box.vertex (j), box.vertex ((j + 1) % 4))) + return true; + } + } + if (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))) + return true; + if (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))) + return true; + return false; + }; + + 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); + }; + }; + + // A basic vector type. Supports standard 2D vector operations + var Vector2D = function (x, y) { + this.x = x; + this.y = y; + + this.add = function (v) { + this.x += v.x; + this.y += v.y; + return this; + }; + this.sub = function (v) { + this.x -= v.x; + this.y -= v.y; + return this; + }; + this.scale = function (s) { + this.x *= s; + this.y *= s; + return this; + }; + this.length = function () { + return Math.sqrt (this.x * this.x + this.y * this.y); + }; + this.normalize = function () { + var scale = this.length (); + if (scale === 0) + return this; + this.x /= scale; + this.y /= scale; + return this; + }; + this.div = function (v) { + this.x /= v.x; + this.y /= v.y; + return this; + }; + this.floor = function () { + this.x = Math.floor (this.x); + this.y = Math.floor (this.y); + return this; + }; + this.zero = function (tol) { + tol = tol || 0; + return (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); + var sin = Math.sin (omega); + xp = cos * this.x - sin * this.y; + yp = sin * this.x + cos * this.y; + this.x = xp; + this.y = yp; + return this; + }; + this.clone = function () { + return new Vector2D (this.x, this.y); + }; + + this.array = function () { + return [this.x, this.y]; + }; + }; + + // A shortcut for the vector constructor + function vect (x, y) { + return new Vector2D (x, y); + } + + // Shorthand operations for vectors for operations that make new vectors + + 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) { + if (!tol) + tol = 0; + var v1 = vect.sub (b, a); + var v2 = vect.sub (c, a); + return (vect.cross (v1, v2) >= -tol); + }; + + vect.intersects = function (a, b, c, d, tol) { + if (!tol) + tol = 0; + return (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 (denom === 0) + return Infinity; + + var num_s = a.x * (d.y - c.y) + + c.x * (a.y - d.y) + + d.x * (c.y - a.y); + var s = num_s / denom; + + var num_t = -(a.x * (c.y - b.y) + + b.x * (a.y - c.y) + + c.x * (b.y - a.y)); + var 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 (denom === 0) + return Infinity; + + var num_s = a.x * (d.y - c.y) + + c.x * (a.y - d.y) + + d.x * (c.y - a.y); + var s = num_s / denom; + + /*var num_t = -(a.x * (c.y - b.y) + + b.x * (a.y - c.y) + + c.x * (b.y - a.y)); + var t = num_t / denom;*/ + + var dir = vect.sub (b, a); + dir.scale (s); + return vect.add (a, dir); + }; + + vect.rotate = function (v, omega) { + var cos = Math.cos (omega); + var 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 (); + }; + + module.exports = { + Box: Box, + vect: vect, + RangeTree: RangeTree + }; + }()); + + +/***/ }, +/* 227 */ +/***/ function(module, exports, __webpack_require__) { + + var $ = __webpack_require__(1); + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class quadFeature + * + * @class geo.quadFeature + * @param {Object} arg Options object + * @extends geo.feature + * @param {Object|string|Function} [color] Color for quads without images. + * Default is white ({r: 1, g: 1, b: 1}). + * @param {number|Function} [opacity=1] Opacity for quad + * @param {number|Function} [depth=0] Default z-coordinate for positions that + * don't explicitly specify one. + * @param {boolean|Function} [drawOnAsyncResourceLoaded=true] Redraw quads + * when images are loaded after initial render. + * @param {Image|string|Function} [image] Image for each data item. If + * undefined or null, the quad is a solid color. Default is (data).image. + * @param {Object|string|Function} [previewColor=null] If specified, a color to + * show on image quads while waiting for the image to load. + * @param {Image|string|Function} [previewImage=null] If specified, an image to + * show on image quads while waiting for the quad-specific image to load. + * This will only be shown if it is already loaded. + * @param {Object|Function} [position] Position of the quad. Default is + * (data). The position is an Object which specifies the corners of the + * quad: ll, lr, ur, ul. At least two opposite corners must be specified. + * The corners do not have to physically correspond to the order specified, + * but rather correspond to that part of an image (if there is one). If a + * corner is unspecified, it will use the x coordinate from one adjacent + * corner, the y coordinate from the other adjacent corner, and the average + * z value of those two corners. For instance, if ul is unspecified, it is + * {x: ll.x, y: ur.y}. Note that each quad is rendered as a pair of + * triangles: (ll, lr, ul) and (ur, ul, lr). Nothing special is done for + * quads that are not convex or quads that have substantially different + * transformations for those two triangles. + * @param {boolean} [cacheQuads=true] If true, a set of internal information is + * stored on each data item in the _cachedQuad attribute. If this is false, + * the data item is not altered. If the data (positions, opacity, etc,) of + * individual quads will change, set this to false or delete the _cachedQuad + * attribute of the data item. + * @returns {geo.quadFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var quadFeature = function (arg) { + 'use strict'; + + var transform = __webpack_require__(147); + var util = __webpack_require__(120); + + if (!(this instanceof quadFeature)) { + return new quadFeature(arg); + } + arg = arg || {}; + feature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + m_cacheQuads, + m_images = [], + m_quads; + + /** + * Track a list of object->object mappings. The mappings are kept in a list. + * This marks all known mappings as unused. If they are not marked used + * before _objectListEnd is called, that function will remove them. + * + * @param {array} list the list of mappings. + */ + this._objectListStart = function (list) { + $.each(list, function (idx, item) { + item.used = false; + }); + }; + + /** + * Get the value from a list of object->object mappings. If the key object + * is not present, return undefined. If found, the entry is marked as being + * in use. + * + * @param {array} list the list of mappings. + * @param {object} entry the key to search for. + * @returns {object} the associated object or undefined. + */ + this._objectListGet = function (list, entry) { + for (var i = 0; i < list.length; i += 1) { + if (list[i].entry === entry) { + list[i].used = true; + return list[i].value; + } + } + return undefined; + }; + + /** + * Add a new object to a list of object->object mappings. The key object + * should not exist, or this will create a duplicated. The new entry is + * marked as being in use. + * + * @param {array} list the list of mappings. + * @param {object} entry the key to add. + * @param {object} value the value to store with the entry. + */ + this._objectListAdd = function (list, entry, value) { + list.push({entry: entry, value: value, used: true}); + }; + + /** + * Remove all unused entries from a list of object->object mappings. + * + * @param {array} list the list of mappings. + */ + this._objectListEnd = function (list) { + for (var i = list.length - 1; i >= 0; i -= 1) { + if (!list[i].used) { + list.splice(i, 1); + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Point search 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(), i, + poly1 = [{}, {}, {}, {}], poly2 = [{}, {}, {}, {}], + map = m_this.layer().map(), + order1 = [0, 1, 2, 0], order2 = [1, 2, 3, 1]; + coordinate = transform.transformCoordinates( + map.ingcs(), map.gcs(), [coordinate])[0]; + if (!m_quads) { + this._generateQuads(); + } + $.each([m_quads.clrQuads, m_quads.imgQuads], function (idx, quadList) { + quadList.forEach(function (quad, idx) { + for (i = 0; i < order1.length; i += 1) { + poly1[i].x = quad.pos[order1[i] * 3]; + poly1[i].y = quad.pos[order1[i] * 3 + 1]; + poly1[i].z = quad.pos[order1[i] * 3 + 2]; + poly2[i].x = quad.pos[order2[i] * 3]; + poly2[i].y = quad.pos[order2[i] * 3 + 1]; + poly2[i].z = quad.pos[order2[i] * 3 + 2]; + } + if (util.pointInPolygon(coordinate, poly1) || + util.pointInPolygon(coordinate, poly2)) { + indices.push(quad.idx); + found.push(data[quad.idx]); + } + }); + }); + return { + index: indices, + found: found + }; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set position + * + * @returns {geo.quadFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.position = function (val) { + if (val === undefined) { + return m_this.style('position'); + } else { + m_this.style('position', util.ensureFunction(val)); + m_this.dataTime().modified(); + m_this.modified(); + } + return m_this; + }; + + /** + * Given a data item and its index, fetch its position and ensure we have + * compelte information for the quad. This generates missing corners and z + * values. + * + * @param {function} posFunc a function to call to get the position of a data + * item. It is passed (d, i). + * @param {function} depthFunc a function to call to get the z-value of a + * data item. It is passed (d, i). + * @param d a data item. Used to fetch position and possibly depth. + * @param i the index within the data. Used to fetch position and possibly + * depth. + * @returns {Object|undefined} either an object with all four corners, or + * undefined if no such object can be generated. The coordinates have been + * converted to map coordinates. + */ + this._positionToQuad = function (posFunc, depthFunc, d, i) { + var initPos = posFunc.call(m_this, d, i); + if ((!initPos.ll || !initPos.ur) && (!initPos.ul || !initPos.lr)) { + return; + } + var gcs = m_this.gcs(), + map_gcs = m_this.layer().map().gcs(), + pos = {}; + $.each(['ll', 'lr', 'ul', 'ur'], function (idx, key) { + if (initPos[key] !== undefined) { + pos[key] = {}; + if (initPos[key].x === undefined) { + pos[key] = [initPos[key][0], initPos[key][1], initPos[key][2]]; + } else { + pos[key] = [initPos[key].x, initPos[key].y, initPos[key].z]; + } + if (pos[key][2] === undefined) { + pos[key][2] = depthFunc.call(m_this, d, i); + } + if (gcs !== map_gcs) { + pos[key] = transform.transformCoordinates( + gcs, map_gcs, pos[key]); + } + } + }); + pos.ll = pos.ll || [pos.ul[0], pos.lr[1], (pos.ul[2] + pos.lr[2]) / 2]; + pos.lr = pos.lr || [pos.ur[0], pos.ll[1], (pos.ur[2] + pos.ll[2]) / 2]; + pos.ur = pos.ur || [pos.lr[0], pos.ul[1], (pos.lr[2] + pos.ul[2]) / 2]; + pos.ul = pos.ul || [pos.ll[0], pos.ur[1], (pos.ll[2] + pos.ur[2]) / 2]; + return pos; + }; + + /** + * Convert the current data set to a pair of arrays, one of quads that are + * solid color and one of qudas that have an image. All quads are objects + * with pos (a 12 value array containing 4 three-dimensional position + * coordinates), and opacity. Color quads also have a color. Image quads + * may have an image element, if the image is loaded. If it isn't, this + * element will be missing. For preview images, the image quad will have a + * reference to the preview element that may later be removed. If a preview + * color is used, the quad will be in both lists, but may be removed from the + * color quad list once the image is loaded. + * + * The value for origin is one of an ll corner from one of the quads with the + * smallest sum of diagonals. The assumption is that, if using the origin to + * improve precision, the smallest quads are the ones most in need of this + * benefit. + * + * @returns {Object} An object with clrQuads and imgQuads, each of which is + * an array, and origin, which is a triplet that is guaranteed to be one of + * the quads corners for a quad with the smallest sum of diagonal lengths. + */ + this._generateQuads = function () { + var posFunc = m_this.position(), + imgFunc = util.ensureFunction(m_this.style('image')), + colorFunc = util.ensureFunction(m_this.style('color')), + depthFunc = util.ensureFunction(m_this.style('depth')), + opacityFunc = util.ensureFunction(m_this.style('opacity')), + loadedFunc = util.ensureFunction(m_this.style( + 'drawOnAsyncResourceLoaded')), + previewColorFunc = util.ensureFunction(m_this.style( + 'previewColor')), + previewImageFunc = util.ensureFunction(m_this.style( + 'previewImage')), + data = m_this.data(), + clrQuads = [], imgQuads = [], + origin = [0, 0, 0], origindiag2, diag2; + /* Keep track of images that we are using. This prevents creating + * additional Image elemnts for repeated urls. */ + m_this._objectListStart(m_images); + $.each(data, function (i, d) { + if (d._cachedQuad) { + diag2 = d._cachedQuad.diag2; + if (origindiag2 === undefined || (d._cachedQuad.diag2 && + d._cachedQuad.diag2 < origindiag2)) { + origin = d._cachedQuad.ll; + origindiag2 = d._cachedQuad.diag2; + } + if (d._cachedQuad.clrquad) { + clrQuads.push(d._cachedQuad.clrquad); + } + if (d._cachedQuad.imgquad) { + imgQuads.push(d._cachedQuad.imgquad); + } + return; + } + var quad, reload, image, prev_onload, + pos, img, opacity, previewColor, previewImage, quadinfo = {}; + + pos = m_this._positionToQuad(posFunc, depthFunc, d, i); + opacity = opacityFunc.call(m_this, d, i); + if (pos === undefined || !opacity) { + return; + } + diag2 = Math.pow(pos.ll[0] - pos.ur[0], 2) + Math.pow(pos.ll[1] - + pos.ur[1], 2) + Math.pow(pos.ll[2] - pos.ur[0], 2) + Math.pow( + pos.lr[0] - pos.ur[0], 2) + Math.pow(pos.lr[1] - pos.ur[1], 2) + + Math.pow(pos.lr[2] - pos.ur[0], 2); + quadinfo.diag2 = diag2; + quadinfo.ll = pos.ll; + if (origindiag2 === undefined || (diag2 && diag2 < origindiag2)) { + origin = pos.ll; + origindiag2 = diag2; + } + pos = [pos.ll[0], pos.ll[1], pos.ll[2], + pos.lr[0], pos.lr[1], pos.lr[2], + pos.ul[0], pos.ul[1], pos.ul[2], + pos.ur[0], pos.ur[1], pos.ur[2]]; + img = imgFunc.call(m_this, d, i); + if (!img) { + quad = { + idx: i, + pos: pos, + opacity: opacity, + color: util.convertColor(colorFunc.call(m_this, d, i)) + }; + clrQuads.push(quad); + quadinfo.clrquad = quad; + } else { + image = m_this._objectListGet(m_images, img); + if (image === undefined) { + if (img instanceof Image) { + image = img; + } else { + image = new Image(); + image.src = img; + } + m_this._objectListAdd(m_images, img, image); + } + quad = { + idx: i, + pos: pos, + opacity: opacity + }; + if (image.complete && image.naturalWidth && image.naturalHeight) { + quad.image = image; + } else { + previewColor = undefined; + previewImage = previewImageFunc.call(m_this, d, i); + if (previewImage && previewImage instanceof Image && + previewImage.complete && previewImage.naturalWidth && + previewImage.naturalHeight) { + quad.image = previewImage; + } else { + previewColor = previewColorFunc.call(m_this, d, i); + if (previewColor === null) { + previewColor = undefined; + } + if (previewColor !== undefined) { + quad.color = util.convertColor(previewColor); + clrQuads.push(quad); + quadinfo.keep = false; + } + } + reload = loadedFunc.call(m_this, d, i); + if (reload) { + prev_onload = image.onload; + image.onload = function () { + if (previewColor !== undefined) { + if ($.inArray(quad, clrQuads) >= 0) { + clrQuads.splice($.inArray(quad, clrQuads), 1); + } + } + quad.image = image; + m_this.dataTime().modified(); + m_this.modified(); + m_this._update(); + m_this.layer().draw(); + if (prev_onload) { + return prev_onload.apply(this, arguments); + } + }; + } else if (previewColor === undefined && !quad.image) { + return; + } + } + imgQuads.push(quad); + quadinfo.imgquad = quad; + } + if (m_cacheQuads !== false && quadinfo.keep !== false) { + d._cachedQuad = quadinfo; + } + }); + m_this._objectListEnd(m_images); + m_quads = {clrQuads: clrQuads, imgQuads: imgQuads, origin: origin}; + return m_quads; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + arg = arg || {}; + s_init.call(m_this, arg); + + m_cacheQuads = (arg.cacheQuads !== false); + + var style = $.extend( + {}, + { + color: { r: 1.0, g: 1, b: 1 }, + opacity: 1, + depth: 0, + drawOnAsyncResourceLoaded: true, + previewColor: null, + previewImage: null, + image: function (d) { return d.image; }, + position: function (d) { return d; } + }, + arg.style === undefined ? {} : arg.style + ); + + if (arg.position !== undefined) { + style.position = util.ensureFunction(arg.position); + } + m_this.style(style); + m_this.dataTime().modified(); + }; + + return m_this; + }; + + /** + * Object specification for a quad feature. + * + * @extends geo.feature.spec // need to make a jsdoc plugin for this to work + * @typedef geo.quadFeature.spec + * @type {object} + */ + + /** + * Create a quadFeature from an object. + * @see {@link geo.feature.create} + * @param {geo.layer} layer The layer to add the feature to + * @param {geo.quadFeature.spec} spec The object specification + * @returns {geo.quadFeature|null} + */ + quadFeature.create = function (layer, spec) { + 'use strict'; + + spec = spec || {}; + spec.type = 'quad'; + return feature.create(layer, spec); + }; + + inherit(quadFeature, feature); + module.exports = quadFeature; + + +/***/ }, +/* 228 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var feature = __webpack_require__(127); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class vectorFeature + * + * @class + * @extends geo.feature + * @returns {geo.vectorFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var vectorFeature = function (arg) { + 'use strict'; + if (!(this instanceof vectorFeature)) { + return new vectorFeature(arg); + } + + var $ = __webpack_require__(1); + + arg = arg || {}; + feature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + s_style = this.style; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 { + s_style('delta', val); + m_this.dataTime().modified(); + m_this.modified(); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + + var defaultStyle = $.extend( + {}, + { + strokeColor: 'black', + strokeWidth: 2.0, + strokeOpacity: 1.0, + 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 (arg.origin !== undefined) { + defaultStyle.origin = arg.origin; + } + + m_this.style(defaultStyle); + m_this.dataTime().modified(); + }; + }; + + inherit(vectorFeature, feature); + module.exports = vectorFeature; + + +/***/ }, +/* 229 */ +/***/ function(module, exports, __webpack_require__) { + + module.exports = ("0.9.0"); + + +/***/ }, +/* 230 */ +/***/ function(module, exports, __webpack_require__) { + + var geo_event = __webpack_require__(125); + geo_event.d3 = { + rescale: __webpack_require__(231) + }; + + /** + * @namespace geo.d3 + */ + module.exports = { + graphFeature: __webpack_require__(232), + lineFeature: __webpack_require__(233), + object: __webpack_require__(234), + pathFeature: __webpack_require__(236), + planeFeature: __webpack_require__(237), + pointFeature: __webpack_require__(238), + renderer: __webpack_require__(239), + tileLayer: __webpack_require__(240), + uniqueID: __webpack_require__(235), + vectorFeature: __webpack_require__(241) + }; + + +/***/ }, +/* 231 */ +/***/ function(module, exports) { + + module.exports = 'geo_d3_rescale'; + + +/***/ }, +/* 232 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var graphFeature = __webpack_require__(141); + + /** + * @class geo.d3.graphFeature + * @extends geo.graphFeature + */ + var d3_graphFeature = function (arg) { + 'use strict'; + + var m_this = this; + + if (!(this instanceof d3_graphFeature)) { + return new d3_graphFeature(arg); + } + graphFeature.call(this, arg); + + //////////////////////////////////////////////////////////////////////////// + /** + * 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()); + }); + return selection; + }; + + return this; + }; + + inherit(d3_graphFeature, graphFeature); + + registerFeature('d3', 'graph', d3_graphFeature); + + module.exports = d3_graphFeature; + + +/***/ }, +/* 233 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var lineFeature = __webpack_require__(145); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class lineFeature + * + * @class geo.d3.lineFeature + * @extends geo.lineFeature + * @extends geo.d3.object + * @returns {geo.d3.lineFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var d3_lineFeature = function (arg) { + 'use strict'; + if (!(this instanceof d3_lineFeature)) { + return new d3_lineFeature(arg); + } + + var d3 = __webpack_require__(132); + var object = __webpack_require__(234); + var timestamp = __webpack_require__(129); + var util = __webpack_require__(120); + + arg = arg || {}; + lineFeature.call(this, arg); + object.call(this); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + m_buildTime = timestamp(), + s_update = this._update; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + 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; }); + + 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 (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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + 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(d3_lineFeature, lineFeature); + + registerFeature('d3', 'line', d3_lineFeature); + + module.exports = d3_lineFeature; + + +/***/ }, +/* 234 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var sceneObject = __webpack_require__(128); + + ////////////////////////////////////////////////////////////////////////////// + /** + * D3 specific subclass of object which adds an id property for d3 selections + * on groups of objects by class id. + * @class geo.d3.object + * @extends geo.sceneObject + */ + ////////////////////////////////////////////////////////////////////////////// + + var d3_object = function (arg) { + 'use strict'; + + var object = __webpack_require__(5); + var uniqueID = __webpack_require__(235); + + // this is used to extend other geojs classes, so only generate + // a new object when that is not the case... like if this === window + if (!(this instanceof object)) { + return new d3_object(arg); + } + sceneObject.call(this); + + var m_id = 'd3-' + uniqueID(), + s_exit = this._exit, + m_this = this, + s_draw = this.draw; + + this._d3id = function () { + return m_id; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Returns a d3 selection for the feature elements + */ + //////////////////////////////////////////////////////////////////////////// + this.select = function () { + return m_this.renderer().select(m_this._d3id()); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Redraw the object. + */ + //////////////////////////////////////////////////////////////////////////// + this.draw = function () { + m_this._update(); + s_draw(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Removes the element from the svg and the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.renderer()._removeFeature(m_this._d3id()); + s_exit(); + }; + + return this; + }; + + inherit(d3_object, sceneObject); + module.exports = d3_object; + + +/***/ }, +/* 235 */ +/***/ function(module, exports) { + + var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz', + strLength = 8; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get a random string to use as a div ID + * @function geo.d3.uniqueID + * @returns {string} + */ + ////////////////////////////////////////////////////////////////////////////// + var uniqueID = function () { + var strArray = [], + i; + strArray.length = strLength; + for (i = 0; i < strLength; i += 1) { + strArray[i] = chars.charAt(Math.floor(Math.random() * chars.length)); + } + return strArray.join(''); + }; + + module.exports = uniqueID; + + +/***/ }, +/* 236 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var pathFeature = __webpack_require__(222); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class pathFeature + * + * @class geo.d3.pathFeature + * @extends geo.pathFeature + * @extends geo.d3.object + * @returns {geo.d3.pathFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var d3_pathFeature = function (arg) { + 'use strict'; + if (!(this instanceof d3_pathFeature)) { + return new d3_pathFeature(arg); + } + + var $ = __webpack_require__(1); + var d3 = __webpack_require__(132); + var object = __webpack_require__(234); + var timestamp = __webpack_require__(129); + + arg = arg || {}; + pathFeature.call(this, arg); + object.call(this); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + m_buildTime = timestamp(), + s_update = this._update, + m_style = {}; + + m_style.style = {}; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + var data = m_this.data() || [], + s_style = m_this.style(), + 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_this.featureGcsToDisplay(src), + target: m_this.featureGcsToDisplay(trg) + }); + } + }); + m_style.data = tmp; + m_style.attributes = { + d: diag + }; + + 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); + + m_this.renderer()._drawFeatures(m_style); + + m_buildTime.modified(); + m_this.updateTime().modified(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + s_update.call(m_this); + + if (m_this.dataTime().getMTime() >= m_buildTime.getMTime()) { + m_this._build(); + } + + return m_this; + }; + + this._init(arg); + return this; + }; + + inherit(d3_pathFeature, pathFeature); + + registerFeature('d3', 'path', d3_pathFeature); + + module.exports = d3_pathFeature; + + +/***/ }, +/* 237 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var planeFeature = __webpack_require__(223); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a plane feature given a lower left corner point + * and and upper right corner point + * + * @class geo.d3.planeFeature + * @extends geo.planeFeature + * @param lowerleft + * @param upperright + * @returns {geo.d3.planeFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var d3_planeFeature = function (arg) { + 'use strict'; + if (!(this instanceof d3_planeFeature)) { + return new d3_planeFeature(arg); + } + + var object = __webpack_require__(234); + var timestamp = __webpack_require__(129); + + planeFeature.call(this, arg); + object.call(this); + + var m_this = this, + m_style = {}, + s_update = this._update, + s_init = this._init, + m_buildTime = timestamp(); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Normalize a coordinate as an object {x: ..., y: ...} + * + * @private + * @returns {Object} + */ + ////////////////////////////////////////////////////////////////////////////// + function normalize(pt) { + if (Array.isArray(pt)) { + return { + x: pt[0], + y: pt[1] + }; + } + return pt; + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * Build the feature object and pass to the renderer for drawing. + * + * @private + * @returns {geo.d3.planeFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + 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); + } + */ + 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 { + m_style.append = 'rect'; + } + 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; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Redraw the plane feature if necessary. + * + * @private + * @returns {geo.d3.planeFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + this._update = function () { + s_update.call(m_this); + + if (m_this.dataTime().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(); + return this; + }; + + inherit(d3_planeFeature, planeFeature); + + registerFeature('d3', 'plane', d3_planeFeature); + module.exports = d3_planeFeature; + + +/***/ }, +/* 238 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var pointFeature = __webpack_require__(225); + + ////////////////////////////////////////////////////////////////////////////// + /** + * + * Create a new instance of pointFeature + * + * @class geo.d3.pointFeature + * @extends geo.pointFeature + * @extends geo.d3.object + * @returns {geo.d3.pointFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var d3_pointFeature = function (arg) { + 'use strict'; + if (!(this instanceof d3_pointFeature)) { + return new d3_pointFeature(arg); + } + + var d3_object = __webpack_require__(234); + var timestamp = __webpack_require__(129); + + arg = arg || {}; + pointFeature.call(this, arg); + d3_object.call(this); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + s_update = this._update, + m_buildTime = timestamp(), + m_style = {}; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + 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_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']; + + // 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 + */ + //////////////////////////////////////////////////////////////////////////// + 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(d3_pointFeature, pointFeature); + + // Now register it + registerFeature('d3', 'point', d3_pointFeature); + + module.exports = d3_pointFeature; + + +/***/ }, +/* 239 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerRenderer = __webpack_require__(131).registerRenderer; + var renderer = __webpack_require__(136); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class d3Renderer + * + * @class geo.d3.renderer + * @extends geo.renderer + * @returns {geo.d3.d3Renderer} + */ + ////////////////////////////////////////////////////////////////////////////// + var d3Renderer = function (arg) { + 'use strict'; + + var d3 = __webpack_require__(132); + var object = __webpack_require__(234); + var util = __webpack_require__(120); + var geo_event = __webpack_require__(125); + var d3Rescale = __webpack_require__(231); + + if (!(this instanceof d3Renderer)) { + return new d3Renderer(arg); + } + renderer.call(this, arg); + + var s_exit = this._exit; + + 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_diagonal = null, + m_scale = 1, + m_transform = {dx: 0, dy: 0, rx: 0, ry: 0, rotation: 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 = 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 = util.ensureFunction(f); + return function () { + return m_this.layer().map().worldToDisplay(f.apply(m_this, arguments)); + }; + }; + + this._convertScale = function (f) { + f = 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) { + 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] && + !styles.hasOwnProperty('strokeColor')) { + 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 = map.size().width, + height = map.size().height; + + m_width = width; + m_height = height; + if (!m_width || !m_height) { + throw 'Map layer has size 0'; + } + m_diagonal = Math.pow(width * width + height * height, 0.5); + m_corners = { + upperLeft: map.displayToGcs({'x': 0, 'y': 0}, null), + lowerRight: map.displayToGcs({'x': width, 'y': height}, null), + center: map.displayToGcs({'x': width / 2, 'y': height / 2}, null) + }; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Set the translation, scale, and zoom for the current view. + * @note rotation not yet supported + * @private + */ + //////////////////////////////////////////////////////////////////////////// + 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), + center = map.gcsToDisplay(m_corners.center, null), + group = getGroup(), + canvas = m_this.canvas(), + dx, dy, scale, rotation, rx, ry; + + if (canvas.attr('scale') !== null) { + scale = parseFloat(canvas.attr('scale') || 1); + rx = (parseFloat(canvas.attr('dx') || 0) + + parseFloat(canvas.attr('offsetx') || 0)); + ry = (parseFloat(canvas.attr('dy') || 0) + + parseFloat(canvas.attr('offsety') || 0)); + rotation = parseFloat(canvas.attr('rotation') || 0); + dx = scale * rx + map.size().width / 2; + dy = scale * ry + map.size().height / 2; + } else { + scale = Math.sqrt( + Math.pow(lowerRight.y - upperLeft.y, 2) + + Math.pow(lowerRight.x - upperLeft.x, 2)) / m_diagonal; + // calculate the translation + rotation = map.rotation(); + rx = -m_width / 2; + ry = -m_height / 2; + dx = scale * rx + center.x; + dy = scale * ry + center.y; + } + + // set the group transform property + var transform = 'matrix(' + [scale, 0, 0, scale, dx, dy].join() + ')'; + if (rotation) { + transform += ' rotate(' + [ + rotation * 180 / Math.PI, -rx, -ry].join() + ')'; + } + group.attr('transform', transform); + + // set internal variables + m_scale = scale; + m_transform.dx = dx; + m_transform.dy = dy; + m_transform.rx = rx; + m_transform.ry = ry; + m_transform.rotation = rotation; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Convert from screen pixel coordinates to the local coordinate system + * in the SVG group element taking into account the transform. + * @private + */ + //////////////////////////////////////////////////////////////////////////// + this.baseToLocal = function (pt) { + pt = { + x: (pt.x - m_transform.dx) / m_scale, + y: (pt.y - m_transform.dy) / m_scale + }; + if (m_transform.rotation) { + var sinr = Math.sin(-m_transform.rotation), + cosr = Math.cos(-m_transform.rotation); + var x = pt.x + m_transform.rx, y = pt.y + m_transform.ry; + pt = { + x: x * cosr - y * sinr - m_transform.rx, + y: x * sinr + y * cosr - m_transform.ry + }; + } + return pt; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Convert from the local coordinate system in the SVG group element + * to screen pixel coordinates. + * @private + */ + //////////////////////////////////////////////////////////////////////////// + this.localToBase = function (pt) { + if (m_transform.rotation) { + var sinr = Math.sin(m_transform.rotation), + cosr = Math.cos(m_transform.rotation); + var x = pt.x + m_transform.rx, y = pt.y + m_transform.ry; + pt = { + x: x * cosr - y * sinr - m_transform.rx, + y: x * sinr + y * cosr - m_transform.ry + }; + } + pt = { + x: pt.x * m_scale + m_transform.dx, + y: pt.y * m_scale + m_transform.dy + }; + return pt; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + if (!m_this.canvas()) { + var canvas; + arg.widget = arg.widget || false; + + 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'); + } + + // 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'); + + if (!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()); + + if (!arg.widget) { + canvas.attr('class', 'group-' + m_this._d3id()); + + m_this.canvas(canvas); + } else { + m_this.canvas(m_svg); + } + } + m_this._setTransform(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get API used by the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.api = function () { + return 'd3'; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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.scaleFactor = function () { + return m_scale; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle resize event + */ + //////////////////////////////////////////////////////////////////////////// + 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 noop for geo.d3.object api. + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Exit + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_features = {}; + m_this.canvas().remove(); + m_svg.remove(); + m_svg = undefined; + m_defs.remove(); + m_defs = undefined; + s_exit(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get the definitions dom element for the layer + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + this._definitions = function () { + return m_defs; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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. + * parentId: If set, the group ID of the parent element. + * } + */ + //////////////////////////////////////////////////////////////////////////// + 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Returns a d3 selection for the given feature id. + */ + //////////////////////////////////////////////////////////////////////////// + this.select = function (id, parentId) { + return getGroup(parentId).selectAll('.' + id); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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. + */ + //////////////////////////////////////////////////////////////////////////// + this.draw = function () { + }; + + // connect to pan event + this.layer().geoOn(geo_event.pan, m_this._setTransform); + + // connect to rotate event + this.layer().geoOn(geo_event.rotate, m_this._setTransform); + + // connect to zoom event + this.layer().geoOn(geo_event.zoom, function () { + m_this._setTransform(); + m_this.__render(); + m_this.layer().geoTrigger(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(d3Renderer, renderer); + + registerRenderer('d3', d3Renderer); + + (function () { + 'use strict'; + + /** + * Report if the d3 renderer is supported. This is just a check if d3 is + * available. + * + * @returns {boolean} true if available. + */ + d3Renderer.supported = function () { + return !!__webpack_require__.m[/*require.resolve*/(132)]; // eslint-disable-line + }; + + /** + * If the d3 renderer is not supported, supply the name of a renderer that + * should be used instead. This asks for the null renderer. + * + * @returns null for the null renderer. + */ + d3Renderer.fallback = function () { + return null; + }; + })(); + + module.exports = d3Renderer; + + +/***/ }, +/* 240 */ +/***/ function(module, exports, __webpack_require__) { + + var registerLayerAdjustment = __webpack_require__(131).registerLayerAdjustment; + + var d3_tileLayer = function () { + 'use strict'; + var m_this = this, + s_update = this._update, + s_init = this._init, + $ = __webpack_require__(1), + uniqueID = __webpack_require__(235); + + this._drawTile = function (tile) { + var bounds = m_this._tileBounds(tile), + parentNode = m_this._getSubLayer(tile.index.level), + offsetx = parseInt(parentNode.attr('offsetx') || 0, 10), + offsety = parseInt(parentNode.attr('offsety') || 0, 10); + tile.feature = m_this.createFeature( + 'plane', {drawOnAsyncResourceLoad: true}) + .origin([bounds.left - offsetx, bounds.top - offsety]) + .upperLeft([bounds.left - offsetx, bounds.top - offsety]) + .lowerRight([bounds.right - offsetx, bounds.bottom - offsety]) + .style({ + image: tile._url, + opacity: 1, + reference: tile.toString(), + parentId: parentNode.attr('data-tile-layer-id') + }); + /* Don't respond to geo events */ + tile.feature.geoTrigger = undefined; + tile.feature._update(); + m_this.draw(); + }; + + /** + * 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 = m_this.canvas().select( + 'g[data-tile-layer="' + level.toFixed() + '"]'); + if (node.empty()) { + node = m_this.canvas().append('g'); + var id = 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; + }; + + /** + * Set sublayer transforms to align them with the given zoom level. + * @param {number} level The target zoom level + * @param {object} view The view bounds. The top and left are used to + * adjust the offset of tile layers. + * @return {object} the x and y offsets for the current level. + */ + this._updateSubLayers = function (level, view) { + var canvas = m_this.canvas(), + lastlevel = parseInt(canvas.attr('lastlevel'), 10), + lastx = parseInt(canvas.attr('lastoffsetx') || 0, 10), + lasty = parseInt(canvas.attr('lastoffsety') || 0, 10); + if (lastlevel === level && Math.abs(lastx - view.left) < 65536 && + Math.abs(lasty - view.top) < 65536) { + return {x: lastx, y: lasty}; + } + var to = this._tileOffset(level), + x = parseInt(view.left, 10) + to.x, + y = parseInt(view.top, 10) + to.y; + var tileCache = m_this.cache._cache; + $.each(canvas.selectAll('.geo-tile-layer')[0], function (idx, el) { + var layer = parseInt($(el).attr('data-tile-layer'), 10), + scale = Math.pow(2, level - layer); + el = m_this._getSubLayer(layer); + el.attr('transform', 'matrix(' + [scale, 0, 0, scale, 0, 0].join() + ')'); + /* x and y are the upper left of our view. This is the zero-point for + * offsets at the current level. Other tile layers' offsets are scaled + * by appropriate factors of 2. We need to shift the tiles of each + * layer by the appropriate amount (computed as dx and dy). */ + var layerx = parseInt(x / Math.pow(2, level - layer), 10), + layery = parseInt(y / Math.pow(2, level - layer), 10), + dx = layerx - parseInt(el.attr('offsetx') || 0, 10), + dy = layery - parseInt(el.attr('offsety') || 0, 10); + el.attr({offsetx: layerx, offsety: layery}); + /* We have to update the values stored in the tile features, too, + * otherwise when d3 regenerates these features, the offsets will be + * wrong. */ + $.each(tileCache, function (idx, tile) { + if (tile._index.level === layer && tile.feature) { + var f = tile.feature, + o = f.origin(), ul = f.upperLeft(), lr = f.lowerRight(); + f.origin([o[0] - dx, o[1] - dy, o[2]]); + f.upperLeft([ul[0] - dx, ul[1] - dy, ul[2]]); + f.lowerRight([lr[0] - dx, lr[1] - dy, lr[2]]); + f._update(); + } + }); + }); + canvas.attr({lastoffsetx: x, lastoffsety: y, lastlevel: level}); + return {x: x, y: y}; + }; + + /* 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; + + s_init.apply(m_this, arguments); + for (sublayer = 0; sublayer <= m_this._options.maxLevel; sublayer += 1) { + m_this._getSubLayer(sublayer); + } + }; + + /* 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(); + } + }; + }; + + registerLayerAdjustment('d3', 'tile', d3_tileLayer); + module.exports = d3_tileLayer; + + +/***/ }, +/* 241 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var vectorFeature = __webpack_require__(228); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of vectorFeature + * + * @class geo.d3.vectorFeature + * @extends geo.vectorFeature + * @extends geo.d3.object + * @returns {geo.d3.vectorFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var d3_vectorFeature = function (arg) { + 'use strict'; + if (!(this instanceof d3_vectorFeature)) { + return new d3_vectorFeature(arg); + } + + var object = __webpack_require__(234); + var timestamp = __webpack_require__(129); + var d3 = __webpack_require__(132); + + arg = arg || {}; + vectorFeature.call(this, arg); + object.call(this); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_init = this._init, + s_exit = this._exit, + s_update = this._update, + m_buildTime = timestamp(), + m_style = {}; + + //////////////////////////////////////////////////////////////////////////// + /** + * Generate a unique ID for a marker definition + * @private + * @param {object} d Unused datum (for d3 compat) + * @param {number} i The marker index + * @param {string} position The marker's vector position (head or tail) + */ + //////////////////////////////////////////////////////////////////////////// + function markerID(d, i, position) { + return m_this._d3id() + '_marker_' + i + '_' + position; + } + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 + * @param {function} originStyle The marker style for the vector head + * @param {function} endStyle The marker style for the vector tail + */ + //////////////////////////////////////////////////////////////////////////// + function updateMarkers(data, stroke, opacity, originStyle, endStyle) { + + var markerConfigs = { + 'arrow': { + attrs: {'class': 'geo-vector-arrow geo-vector-marker', 'viewBox': '0 0 10 10', 'refX': '1', 'refY': '5', 'markerHeight': '5', 'markerWidth': '5', 'orient': 'auto'}, + path: 'M 0 0 L 10 5 L 0 10 z' + }, + 'point': { + attrs: {'class': 'geo-vector-point geo-vector-marker', 'viewBox': '0 0 12 12', 'refX': '6', 'refY': '6', 'markerHeight': '8', 'markerWidth': '8', 'orient': 'auto'}, + path: 'M 6 3 A 3 3 0 1 1 5.99999 3 Z' + }, + 'bar': { + attrs: {'class': 'geo-vector-bar geo-vector-marker', 'viewBox': '0 0 10 10', 'refX': '0', 'refY': '5', 'markerHeight': '6', 'markerWidth': '6', 'orient': 'auto'}, + path: 'M 0 0 L 2 0 L 2 10 L 0 10 z' + }, + 'wedge': { + attrs: {'class': 'geo-vector-wedge geo-vector-marker', 'viewBox': '0 0 10 10', 'refX': '10', 'refY': '5', 'markerHeight': '5', 'markerWidth': '5', 'orient': 'auto'}, + path: 'M 0 0 L 1 0 L 10 5 L 1 10 L 0 10 L 9 5 L 0 0' + } + }; + + //this allows for multiple VectorFeatures in a layer + var markerGroup = m_this.renderer()._definitions() + .selectAll('g.marker-group#' + m_this._d3id()) + .data(data.length ? [1] : []); + + markerGroup + .enter() + .append('g') + .attr('id', m_this._d3id) + .attr('class', 'marker-group'); + + markerGroup.exit().remove(); + + var markers = data.reduce(function (markers, d, i) { + var head = markerConfigs[endStyle(d, i)]; + var tail = markerConfigs[originStyle(d, i)]; + if (head) { + markers.push({ + data: d, + dataIndex: i, + head: true + }); + } + if (tail) { + markers.push({ + data: d, + dataIndex: i, + head: false + }); + } + return markers; + }, []); + + var sel = markerGroup + .selectAll('marker.geo-vector-marker') + .data(markers); + + sel.enter() + .append('marker') + .append('path'); + + var renderer = m_this.renderer(); + + sel + .each(function (d) { + var marker = d3.select(this); + var markerData = d.head ? markerConfigs[endStyle(d.data, d.dataIndex)] : markerConfigs[originStyle(d.data, d.dataIndex)]; + Object.keys(markerData.attrs).map(function (attrName) { + marker.attr(attrName, markerData.attrs[attrName]); + }); + }) + .attr('id', function (d) { + return markerID(d.data, d.dataIndex, d.head ? 'head' : 'tail'); + }) + .style('stroke', function (d) { + return renderer._convertColor(stroke)(d.data, d.dataIndex); + }) + .style('fill', function (d) { + return renderer._convertColor(stroke)(d.data, d.dataIndex); + }) + .style('opacity', function (d) { + return opacity(d.data, d.dataIndex); + }) + .select('path') + .attr('d', function (d) { + return d.head ? markerConfigs[endStyle(d.data, d.dataIndex)].path : markerConfigs[originStyle(d.data, d.dataIndex)].path; + }); + + sel.exit().remove(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + s_init.call(m_this, arg); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + 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; + + // 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; + } + + 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-start': function (d, i) { + return 'url(#' + markerID(d, i, 'tail') + ')'; + }, + 'marker-end': function (d, i) { + return 'url(#' + markerID(d, i, 'head') + ')'; + } + }; + m_style.style = { + stroke: function () { return true; }, + strokeColor: s_style.strokeColor, + strokeWidth: s_style.strokeWidth, + strokeOpacity: s_style.strokeOpacity, + originStyle: s_style.originStyle, + endStyle: s_style.endStyle + }; + m_style.classes = ['d3VectorFeature']; + + // Add markers to the defition list + updateMarkers(data, s_style.strokeColor, s_style.strokeOpacity, s_style.originStyle, s_style.endStyle); + + // 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 + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + 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, + m_style.style.originStyle, + m_style.style.endStyle + ); + } + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Exit + * @protected + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + s_exit.call(m_this); + m_style = {}; + updateMarkers([], null, null, null, null); + }; + + this._init(arg); + return this; + }; + + inherit(d3_vectorFeature, vectorFeature); + + // Now register it + registerFeature('d3', 'vector', d3_vectorFeature); + module.exports = d3_vectorFeature; + + +/***/ }, +/* 242 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * @namespace geo.gl + */ + module.exports = { + choroplethFeature: __webpack_require__(243), + contourFeature: __webpack_require__(244), + ellipsoid: __webpack_require__(245), + geomFeature: __webpack_require__(246), + lineFeature: __webpack_require__(247), + planeFeature: __webpack_require__(248), + pointFeature: __webpack_require__(249), + polygonFeature: __webpack_require__(250), + quadFeature: __webpack_require__(252), + tileLayer: __webpack_require__(253), + vglRenderer: __webpack_require__(254) + }; + + +/***/ }, +/* 243 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var choroplethFeature = __webpack_require__(126); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of choroplethFeature + * + * @class geo.gl.choroplethFeature + * @extends geo.choroplethFeature + * @returns {geo.gl.choroplethFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_choroplethFeature = function (arg) { + 'use strict'; + + if (!(this instanceof gl_choroplethFeature)) { + return new gl_choroplethFeature(arg); + } + arg = arg || {}; + 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(); + } + m_this.updateTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy Polygon Sub-Features + */ + //////////////////////////////////////////////////////////////////////////// + this._wipePolygons = function () { + if (m_gl_polygons) { + m_gl_polygons.map(function (polygon) { + return polygon._exit(); + }); + } + m_gl_polygons = null; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this._wipePolygons(); + s_exit(); + }; + + this._init(arg); + return this; + }; + + inherit(gl_choroplethFeature, choroplethFeature); + + // Now register it + registerFeature('vgl', 'choropleth', gl_choroplethFeature); + + module.exports = gl_choroplethFeature; + + +/***/ }, +/* 244 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var contourFeature = __webpack_require__(134); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of contourFeature + * + * @class geo.gl.contourFeature + * @extends geo.contourFeature + * @returns {geo.gl.contourFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_contourFeature = function (arg) { + 'use strict'; + + if (!(this instanceof gl_contourFeature)) { + return new gl_contourFeature(arg); + } + arg = arg || {}; + contourFeature.call(this, arg); + + var vgl = __webpack_require__(6); + var transform = __webpack_require__(147); + var util = __webpack_require__(120); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + 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 = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, + s_init = this._init, + s_update = this._update; + + 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)', + '{', + /* Don't use z values; something is rotten in one of our matrices */ + ' 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); + shader.setShaderSource(vertexShaderSource); + return 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); + shader.setShaderSource(fragmentShaderSource); + return shader; + } + + /* Create the contours. This calls the base class to generate the geometry, + * color map, and other parameters. The generated geoemtry is then loaded + * into the various gl uniforms and buffers. + */ + function createGLContours() { + var contour = m_this.createContours(), + numPts = contour.elements.length, + colorTable = [], + i, i3, j, j3, + posBuf, opacityBuf, valueBuf, indicesBuf, + geom = m_mapper.geometryData(); + + 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); + for (i = 0; i < contour.colorMap.length; i += 1) { + colorTable.push(contour.colorMap[i].r * 255); + colorTable.push(contour.colorMap[i].g * 255); + colorTable.push(contour.colorMap[i].b * 255); + colorTable.push(contour.colorMap[i].a * 255); + } + m_texture.setColorTable(colorTable); + contour.pos = transform.transformCoordinates( + m_this.gcs(), m_this.layer().map().gcs(), contour.pos, 3); + posBuf = util.getGeomBuffer(geom, 'pos', numPts * 3); + opacityBuf = util.getGeomBuffer(geom, 'opacity', numPts); + valueBuf = util.getGeomBuffer(geom, 'value', numPts); + for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { + j = contour.elements[i]; + j3 = j * 3; + 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(); + if (!(indicesBuf instanceof Uint16Array) || indicesBuf.length !== numPts) { + indicesBuf = new Uint16Array(numPts); + geom.primitive(0).setIndices(indicesBuf); + } + geom.boundsDirty(true); + m_mapper.modified(); + m_mapper.boundsDirtyTimestamp().modified(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + 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); + /* steps is always an integer, but it is more efficient if we use a float + */ + 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + if (m_actor) { + m_this.renderer().contextRenderer().removeActor(m_actor); + } + + createGLContours(); + + m_this.renderer().contextRenderer().addActor(m_actor); + m_this.buildTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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._build(); + } + + m_actor.setVisible(m_this.visible()); + m_actor.material().setBinNumber(m_this.bin()); + m_this.updateTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.renderer().contextRenderer().removeActor(m_actor); + s_exit(); + }; + + this._init(arg); + return this; + }; + + inherit(gl_contourFeature, contourFeature); + + // Now register it + registerFeature('vgl', 'contour', gl_contourFeature); + + module.exports = gl_contourFeature; + + +/***/ }, +/* 245 */ +/***/ function(module, exports, __webpack_require__) { + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 + * + * @class geo.gl.ellipsoid + * @param {Number} [x=0] Radius in X direction + * @param {Number} [y=0] Radius in Y direction + * @param {Number} [z=0] Radius in Z direction + * + * @returns {geo.gl.ellipsoid} + */ + ////////////////////////////////////////////////////////////////////////////// + var ellipsoid = function (x, y, z) { + 'use strict'; + if (!(this instanceof ellipsoid)) { + return new ellipsoid(x, y, z); + } + + var vgl = __webpack_require__(6); + var util = __webpack_require__(120); + var vec3 = __webpack_require__(58); + + 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'); + } + + var m_this = this, + m_radii = [x, y, z], + m_radiiSquared = [x * x, y * y, z * z], + m_minimumRadius = Math.min(x, y, z), + m_maximumRadius = Math.max(x, y, z); + + //////////////////////////////////////////////////////////////////////////// + /** + * Return radii of ellipsoid + */ + //////////////////////////////////////////////////////////////////////////// + this.radii = function () { + return m_radii; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return squared radii of the ellipsoid + */ + //////////////////////////////////////////////////////////////////////////// + this.radiiSquared = function () { + return m_radiiSquared; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return maximum radius of the ellipsoid + * + * @return {Number} The maximum radius of the ellipsoid + */ + //////////////////////////////////////////////////////////////////////////// + this.maximumRadius = function () { + return m_maximumRadius; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return minimum radius of the ellipsoid + * + * @return {Number} The maximum radius of the ellipsoid + */ + //////////////////////////////////////////////////////////////////////////// + this.minimumRadius = function () { + return m_minimumRadius; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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 + * + * @return {vec3} + * + * @exception {DeveloperError} cartographic is required. + */ + //////////////////////////////////////////////////////////////////////////// + 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 = util.vec3AsArray(); + + result[0] = cosLatitude * Math.cos(lon); + result[1] = cosLatitude * Math.sin(lon); + result[2] = Math.sin(lat); + + vec3.normalize(result, result); + return result; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Converts the provided geographic latitude, longitude, + * and height to WGS84 coordinate system + * + * @param {Number} lat Latitude in radians + * @param {Number} lon Longitude in radians + * @param {Number} elev Elevation + * @return {vec3} Position in the WGS84 coordinate system + */ + //////////////////////////////////////////////////////////////////////////// + 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 = util.vec3AsArray(), + gamma = Math.sqrt(vec3.dot(n, k)), + result = util.vec3AsArray(); + + vec3.multiply(k, m_radiiSquared, n); + vec3.scale(k, k, 1 / gamma); + vec3.scale(n, n, elev); + vec3.add(result, n, k); + return result; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Converts the provided geographic latitude, longitude, + * and height to WGS84 coordinate system + * + * @param {vgl.geometryData} geom + */ + //////////////////////////////////////////////////////////////////////////// + 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 = util.vec3AsArray(), + result = util.vec3AsArray(); + + 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]; + } + }; + + return m_this; + }; + + module.exports = ellipsoid; + + +/***/ }, +/* 246 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var geomFeature = __webpack_require__(140); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of geomFeature + * + * @class geo.gl.geomFeature + * @extends geo.geomFeature + * @param {vgl.geometryData} arg + * @returns {geo.gl.geomFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_geomFeature = function (arg) { + 'use strict'; + if (!(this instanceof gl_geomFeature)) { + return new gl_geomFeature(arg); + } + + var vgl = __webpack_require__(6); + + arg = arg || {}; + geomFeature.call(this, arg); + + // Initialize + var m_this = this, + m_geom = arg.geom || null, + m_actor = vgl.actor(), + m_mapper = vgl.mapper(), + m_material = null, + m_scalar = null, + m_color = arg.color || [1.0, 1.0, 1.0], + m_buildTime = null, + m_noOfPrimitives = 0; + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + var style = m_this.style(); + + // Vertex color gets the preference + if (m_geom !== null) { + m_scalar = m_geom.sourceData(vgl.vertexAttributeKeys.Scalar); + m_color = m_geom.sourceData(vgl.vertexAttributeKeys.Color); + m_mapper.setGeometryData(m_geom); + } + + m_this.setMapper(m_mapper); + + if (style.point_sprites !== undefined && style.point_sprites && + style.point_sprites_image !== undefined && + style.point_sprites_image !== null && + m_noOfPrimitives === 1 && + m_geom.primitive(0).primitiveType() === vgl.GL.POINTS) { + m_material = vgl.utils.createPointSpritesMaterial( + style.point_sprites_image); + } else if (m_scalar) { + if (m_color instanceof vgl.lookupTable) { + m_color.updateRange(m_scalar.scalarRange()); + m_material = vgl.utils.createColorMappedMaterial(m_color); + } else { + m_color = vgl.lookupTable(); + m_color.updateRange(m_scalar.scalarRange()); + m_material = vgl.utils.createColorMappedMaterial(m_color); + } + } else if (m_color) { + m_material = vgl.utils.createColorMaterial(); + } else { + m_material = vgl.utils.createSolidColorMaterial(); + } + m_actor.setMaterial(m_material); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + * + * @private + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + if (m_buildTime && + m_buildTime.getMTime() < m_this.getMTime()) { + if (m_color instanceof vgl.lookupTable) { + vgl.utils.updateColorMappedMaterial(m_this.material(), + m_this.style.color); + }/* else { + // TODO + }*/ + } else { + m_buildTime = vgl.timestamp(); + m_buildTime.modified(); + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get/Set geometry + * + * @returns {geo.gl.geomFeature} + */ + //////////////////////////////////////////////////////////////////////////// + this.geometry = function (val) { + if (val === undefined) { + return m_geom; + } else { + m_geom = val; + m_this.modified(); + return m_this; + } + }; + + return this; + }; + + inherit(gl_geomFeature, geomFeature); + module.exports = gl_geomFeature; + + +/***/ }, +/* 247 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var lineFeature = __webpack_require__(145); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of lineFeature + * + * @class geo.gl.lineFeature + * @extends geo.lineFeature + * @returns {geo.gl.lineFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_lineFeature = function (arg) { + 'use strict'; + if (!(this instanceof gl_lineFeature)) { + return new gl_lineFeature(arg); + } + arg = arg || {}; + lineFeature.call(this, arg); + + var vgl = __webpack_require__(6); + var transform = __webpack_require__(147); + var util = __webpack_require__(120); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_exit = this._exit, + m_actor = null, + m_mapper = null, + m_material = null, + m_pixelWidthUnif = null, + m_aspectUniform = null, + m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, + s_init = this._init, + s_update = this._update; + + 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 any vertex has been deliberately set to a negative opacity, + * skip doing computations on it. */ + ' 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); + shader.setShaderSource(vertexShaderSource); + return 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); + shader.setShaderSource(fragmentShaderSource); + return shader; + } + + function createGLLines() { + var data = m_this.data(), + i, j, k, v, + numSegments = 0, len, + lineItem, lineItemData, + vert = [{}, {}], vertTemp, + pos, posIdx3, + 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(), + posBuf, nextBuf, prevBuf, offsetBuf, indicesBuf, + strokeWidthBuf, strokeColorBuf, strokeOpacityBuf, + dest, dest3, + geom = m_mapper.geometryData(); + + for (i = 0; i < data.length; i += 1) { + lineItem = m_this.line()(data[i], i); + numSegments += lineItem.length - 1; + for (j = 0; j < lineItem.length; j += 1) { + pos = posFunc(lineItem[j], j, lineItem, i); + position.push(pos.x); + position.push(pos.y); + position.push(pos.z || 0.0); + } + } + + position = transform.transformCoordinates( + m_this.gcs(), m_this.layer().map().gcs(), + position, 3); + + len = numSegments * order.length; + posBuf = util.getGeomBuffer(geom, 'pos', len * 3); + nextBuf = util.getGeomBuffer(geom, 'next', len * 3); + prevBuf = util.getGeomBuffer(geom, 'prev', len * 3); + offsetBuf = util.getGeomBuffer(geom, 'offset', len); + strokeWidthBuf = util.getGeomBuffer(geom, 'strokeWidth', len); + strokeColorBuf = util.getGeomBuffer(geom, 'strokeColor', len * 3); + strokeOpacityBuf = util.getGeomBuffer(geom, 'strokeOpacity', len); + indicesBuf = geom.primitive(0).indices(); + if (!(indicesBuf instanceof Uint16Array) || indicesBuf.length !== len) { + indicesBuf = new Uint16Array(len); + geom.primitive(0).setIndices(indicesBuf); + } + + for (i = posIdx3 = dest = dest3 = 0; i < data.length; i += 1) { + lineItem = m_this.line()(data[i], i); + for (j = 0; j < lineItem.length; j += 1, posIdx3 += 3) { + lineItemData = lineItem[j]; + /* swap entries in vert so that vert[0] is the first vertex, and + * vert[1] will be reused for the second vertex */ + if (j) { + vertTemp = vert[0]; + vert[0] = vert[1]; + vert[1] = vertTemp; + } + vert[1].pos = posIdx3; + vert[1].prev = posIdx3 - (j ? 3 : 0); + vert[1].next = posIdx3 + (j + 1 < lineItem.length ? 3 : 0); + vert[1].strokeWidth = strkWidthFunc(lineItemData, j, lineItem, i); + vert[1].strokeColor = strkColorFunc(lineItemData, j, lineItem, i); + vert[1].strokeOpacity = strkOpacityFunc(lineItemData, j, lineItem, i); + if (j) { + for (k = 0; k < order.length; k += 1, dest += 1, dest3 += 3) { + v = vert[order[k][0]]; + posBuf[dest3] = position[v.pos]; + posBuf[dest3 + 1] = position[v.pos + 1]; + posBuf[dest3 + 2] = position[v.pos + 2]; + prevBuf[dest3] = position[v.prev]; + prevBuf[dest3 + 1] = position[v.prev + 1]; + prevBuf[dest3 + 2] = position[v.prev + 2]; + nextBuf[dest3] = position[v.next]; + nextBuf[dest3 + 1] = position[v.next + 1]; + nextBuf[dest3 + 2] = position[v.next + 2]; + offsetBuf[dest] = order[k][1]; + /* We can ignore the indicies (they will all be zero) */ + strokeWidthBuf[dest] = v.strokeWidth; + strokeColorBuf[dest3] = v.strokeColor.r; + strokeColorBuf[dest3 + 1] = v.strokeColor.g; + strokeColorBuf[dest3 + 2] = v.strokeColor.b; + strokeOpacityBuf[dest] = v.strokeOpacity; + } + } + } + } + + geom.boundsDirty(true); + m_mapper.modified(); + m_mapper.boundsDirtyTimestamp().modified(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the arrangement of vertices used for each line segment. + * + * @returns {Number} + */ + //////////////////////////////////////////////////////////////////////////// + this.featureVertices = function () { + return [[0, 1], [1, -1], [0, -1], [0, 1], [1, 1], [1, -1]]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the number of vertices used for each line segment. + * + * @returns {Number} + */ + //////////////////////////////////////////////////////////////////////////// + this.verticesPerFeature = function () { + return m_this.featureVertices().length; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function (arg) { + var prog = vgl.shaderProgram(), + vs = createVertexShader(), + fs = createFragmentShader(), + // Vertex attributes + posAttr = vgl.vertexAttribute('pos'), + prvAttr = vgl.vertexAttribute('prev'), + nxtAttr = vgl.vertexAttribute('next'), + offAttr = vgl.vertexAttribute('offset'), + strkWidthAttr = vgl.vertexAttribute('strokeWidth'), + strkColorAttr = vgl.vertexAttribute('strokeColor'), + strkOpacityAttr = vgl.vertexAttribute('strokeOpacity'), + // Shader uniforms + mviUnif = new vgl.modelViewUniform('modelViewMatrix'), + prjUnif = new vgl.projectionUniform('projectionMatrix'), + geom = vgl.geometryData(), + // Sources + posData = vgl.sourceDataP3fv({'name': 'pos'}), + prvPosData = vgl.sourceDataAnyfv( + 3, vgl.vertexAttributeKeysIndexed.Four, {'name': 'prev'}), + nxtPosData = vgl.sourceDataAnyfv( + 3, vgl.vertexAttributeKeysIndexed.Five, {'name': 'next'}), + offPosData = vgl.sourceDataAnyfv( + 1, vgl.vertexAttributeKeysIndexed.Six, {'name': 'offset'}), + strkWidthData = vgl.sourceDataAnyfv( + 1, vgl.vertexAttributeKeysIndexed.One, {'name': 'strokeWidth'}), + strkColorData = vgl.sourceDataAnyfv( + 3, vgl.vertexAttributeKeysIndexed.Two, {'name': 'strokeColor'}), + strkOpacityData = vgl.sourceDataAnyfv( + 1, vgl.vertexAttributeKeysIndexed.Three, + {'name': 'strokeOpacity'}), + // Primitive indices + triangles = vgl.triangles(); + + m_pixelWidthUnif = new vgl.floatUniform('pixelWidth', + 1.0 / 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_material = vgl.material(); + m_mapper = vgl.mapper({dynamicDraw: m_dynamicDraw}); + + prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); + prog.addVertexAttribute(strkWidthAttr, vgl.vertexAttributeKeysIndexed.One); + prog.addVertexAttribute(strkColorAttr, vgl.vertexAttributeKeysIndexed.Two); + prog.addVertexAttribute(strkOpacityAttr, vgl.vertexAttributeKeysIndexed.Three); + prog.addVertexAttribute(prvAttr, vgl.vertexAttributeKeysIndexed.Four); + prog.addVertexAttribute(nxtAttr, vgl.vertexAttributeKeysIndexed.Five); + prog.addVertexAttribute(offAttr, vgl.vertexAttributeKeysIndexed.Six); + + prog.addUniform(mviUnif); + prog.addUniform(prjUnif); + prog.addUniform(m_pixelWidthUnif); + prog.addUniform(m_aspectUniform); + + prog.addShader(fs); + prog.addShader(vs); + + m_material.addAttribute(prog); + m_material.addAttribute(vgl.blend()); + + m_actor = vgl.actor(); + m_actor.setMaterial(m_material); + m_actor.setMapper(m_mapper); + + geom.addSource(posData); + geom.addSource(prvPosData); + geom.addSource(nxtPosData); + geom.addSource(strkWidthData); + geom.addSource(strkColorData); + geom.addSource(strkOpacityData); + geom.addSource(offPosData); + geom.addPrimitive(triangles); + m_mapper.setGeometryData(geom); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return list of actors + * + * @returns {vgl.actor[]} + */ + //////////////////////////////////////////////////////////////////////////// + this.actors = function () { + if (!m_actor) { + return []; + } + return [m_actor]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + if (m_actor) { + m_this.renderer().contextRenderer().removeActor(m_actor); + } + + createGLLines(); + + m_this.renderer().contextRenderer().addActor(m_actor); + m_this.buildTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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._build(); + } + + m_pixelWidthUnif.set(1.0 / 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(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.renderer().contextRenderer().removeActor(m_actor); + s_exit(); + }; + + this._init(arg); + return this; + }; + + inherit(gl_lineFeature, lineFeature); + + // Now register it + registerFeature('vgl', 'line', gl_lineFeature); + + module.exports = gl_lineFeature; + + +/***/ }, +/* 248 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var planeFeature = __webpack_require__(223); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a plane feature given a lower left corner point + * and and upper right corner point + * @class geo.gl.planeFeature + * @extends geo.planeFeature + * @param lowerleft + * @param upperright + * @returns {geo.gl.planeFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_planeFeature = function (arg) { + 'use strict'; + if (!(this instanceof gl_planeFeature)) { + return new gl_planeFeature(arg); + } + planeFeature.call(this, arg); + + var transform = __webpack_require__(147); + var vgl = __webpack_require__(6); + + var m_this = this, + s_exit = this._exit, + m_actor = null, + m_onloadCallback = arg.onload === undefined ? null : arg.onload; + + //////////////////////////////////////////////////////////////////////////// + /** + * Gets the coordinates for this plane + * + * @returns {Array} [[origin], [upper left] [lower right]] + */ + //////////////////////////////////////////////////////////////////////////// + this.coords = function () { + return [m_this.origin(), m_this.upperLeft(), m_this.lowerRight()]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build this feature + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + var or = m_this.origin(), + ul = m_this.upperLeft(), + lr = m_this.lowerRight(), + /// img could be a source or an Image + img = m_this.style().image, + image = null, + texture = null, + gcs = m_this.gcs(), + map_gcs = m_this.layer().map().gcs(); + + if (gcs !== map_gcs) { + or = transform.transformCoordinates(gcs, map_gcs, or); + ul = transform.transformCoordinates(gcs, map_gcs, ul); + lr = transform.transformCoordinates(gcs, map_gcs, lr); + } + + m_this.buildTime().modified(); + + if (m_actor) { + m_this.renderer().contextRenderer().removeActor(m_actor); + } + + if (img && img instanceof Image) { + image = img; + } else if (img) { + image = new Image(); + image.src = img; + } + + if (!image) { + m_actor = vgl.utils.createPlane(or[0], or[1], or[2], + ul[0], ul[1], ul[2], + lr[0], lr[1], lr[2]); + + m_actor.material().shaderProgram().uniform('opacity').set( + m_this.style().opacity !== undefined ? m_this.style().opacity : 1); + + m_this.renderer().contextRenderer().addActor(m_actor); + + } else { + m_actor = vgl.utils.createTexturePlane(or[0], or[1], or[2], + lr[0], lr[1], lr[2], + ul[0], ul[1], ul[2], true); + + m_actor.material().shaderProgram().uniform('opacity').set( + m_this.style().opacity !== undefined ? m_this.style().opacity : 1); + + texture = vgl.texture(); + m_this.visible(false); + + m_this.renderer().contextRenderer().addActor(m_actor); + + /* An image is already loaded if .complete is true and .naturalWidth + * and .naturalHeight are defined and non-zero (not falsy seems to be + * sufficient). */ + if (image.complete && image.naturalWidth && image.naturalHeight) { + texture.setImage(image); + m_actor.material().addAttribute(texture); + /// NOTE Currently we assume that we want to show the feature as + /// soon as the image gets loaded. However, there might be a case + /// where we want to lock down the visibility. We will deal with that + /// later. + m_this.visible(true); + + if (m_onloadCallback) { + m_onloadCallback.call(m_this); + } + } else { + image.onload = function () { + texture.setImage(image); + m_actor.material().addAttribute(texture); + /// NOTE Currently we assume that we want to show the feature as + /// soon as the image gets loaded. However, there might be a case + /// where we want to lock down the visibility. We will deal with that + /// later. + m_this.visible(true); + + if (m_onloadCallback) { + m_onloadCallback.call(m_this); + } + + if (m_this.drawOnAsyncResourceLoad()) { + m_this._update(); + m_this.layer().draw(); + } + }; + } + } + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime()) { + m_this._build(); + } + + if (m_this.updateTime().getMTime() <= m_this.getMTime()) { + m_actor.setVisible(m_this.visible()); + m_actor.material().setBinNumber(m_this.bin()); + m_actor.material().shaderProgram().uniform('opacity').set( + m_this.style().opacity !== undefined ? m_this.style().opacity : 1); + } + + m_this.updateTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.renderer().contextRenderer().removeActor(m_actor); + s_exit(); + }; + + return this; + }; + + inherit(gl_planeFeature, planeFeature); + + // Now register it + registerFeature('vgl', 'plane', gl_planeFeature); + + module.exports = gl_planeFeature; + + +/***/ }, +/* 249 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var pointFeature = __webpack_require__(225); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of pointFeature + * + * @class geo.gl.pointFeature + * @extends geo.pointFeature + * @returns {geo.gl.pointFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_pointFeature = function (arg) { + 'use strict'; + if (!(this instanceof gl_pointFeature)) { + return new gl_pointFeature(arg); + } + arg = arg || {}; + pointFeature.call(this, arg); + + var vgl = __webpack_require__(6); + var transform = __webpack_require__(147); + var util = __webpack_require__(120); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + var m_this = this, + s_exit = this._exit, + m_actor = null, + m_mapper = null, + m_pixelWidthUniform = null, + m_aspectUniform = null, + m_dynamicDraw = arg.dynamicDraw === undefined ? false : arg.dynamicDraw, + m_primitiveShape = 'sprite', // arg can change this, below + s_init = this._init, + s_update = this._update, + vertexShaderSource = null, + fragmentShaderSource = null; + + if (arg.primitiveShape === 'triangle' || + arg.primitiveShape === 'square' || + arg.primitiveShape === 'sprite') { + 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;' + ]; + + if (m_primitiveShape !== 'sprite') { + 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 the point has no visible pixels, skip doing computations on it. */ + ' 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;' + ]); + + if (m_primitiveShape === 'sprite') { + vertexShaderSource.push.apply(vertexShaderSource, [ + ' gl_Position = (projectionMatrix * modelViewMatrix * vec4(pos, 1.0)).xyzw;', + ' gl_PointSize = 2.0 * (rad + strokeWidthVar); ', + '}' + ]); + } else { + 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;' + ]; + + if (m_primitiveShape !== 'sprite') { + 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;' + ]); + + if (m_primitiveShape === 'sprite') { + fragmentShaderSource.push( + ' float rad = 2.0 * length (gl_PointCoord - vec2(0.5));'); + } else { + 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'); + + function createVertexShader() { + var shader = new vgl.shader(vgl.GL.VERTEX_SHADER); + shader.setShaderSource(vertexShaderSource); + return shader; + } + + function createFragmentShader() { + var shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); + shader.setShaderSource(fragmentShaderSource); + return shader; + } + + function pointPolygon(x, y, w, h) { + var verts; + switch (m_primitiveShape) { + case 'triangle': + /* Use an equilateral triangle. While this has 30% more area than a + * square, the reduction in vertices should help more than the + * processing the additional fragments. */ + verts = [ + x, y - h * 2, + x - w * Math.sqrt(3.0), y + h, + x + w * Math.sqrt(3.0), y + h + ]; + break; + case 'sprite': + /* Point sprite uses only one vertex per point. */ + verts = [x, y]; + break; + default: // "square" + /* Use a surrounding square split diagonally into two triangles. */ + verts = [ + x - w, y + h, + x - w, y - h, + x + w, y + h, + x - w, y - h, + x + w, y - h, + x + w, y + h + ]; + break; + } + return verts; + } + + function createGLPoints() { + // unit and associated data is not used when drawing sprite + var i, j, numPts = m_this.data().length, + unit = pointPolygon(0, 0, 1, 1), + position = new Array(numPts * 3), posBuf, posVal, posFunc, + unitBuf, indices, + radius, radiusVal, radFunc, + stroke, strokeVal, strokeFunc, + strokeWidth, strokeWidthVal, strokeWidthFunc, + strokeOpacity, strokeOpacityVal, strokeOpacityFunc, + strokeColor, strokeColorVal, strokeColorFunc, + fill, fillVal, fillFunc, + fillOpacity, fillOpacityVal, fillOpacityFunc, + fillColor, fillColorVal, fillColorFunc, + vpf = m_this.verticesPerFeature(), + data = m_this.data(), + item, ivpf, ivpf3, iunit, i3, + geom = m_mapper.geometryData(), nonzeroZ; + + posFunc = m_this.position(); + radFunc = m_this.style.get('radius'); + strokeFunc = m_this.style.get('stroke'); + strokeWidthFunc = m_this.style.get('strokeWidth'); + strokeOpacityFunc = 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'); + + /* It is more efficient to do a transform on a single array rather than on + * an array of arrays or an array of objects. */ + for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { + posVal = posFunc(data[i]); + position[i3] = posVal.x; + position[i3 + 1] = posVal.y; + position[i3 + 2] = posVal.z || 0; + nonzeroZ = nonzeroZ || position[i3 + 2]; + } + position = transform.transformCoordinates( + m_this.gcs(), m_this.layer().map().gcs(), + position, 3); + /* Some transforms modify the z-coordinate. If we started with all zero z + * coordinates, don't modify them. This could be changed if the + * z-coordinate space of the gl cube is scaled appropriately. */ + if (!nonzeroZ && m_this.gcs() !== m_this.layer().map().gcs()) { + for (i = i3 = 0; i < numPts; i += 1, i3 += 3) { + position[i3 + 2] = 0; + } + } + + posBuf = util.getGeomBuffer(geom, 'pos', vpf * numPts * 3); + + if (m_primitiveShape !== 'sprite') { + unitBuf = util.getGeomBuffer(geom, 'unit', vpf * numPts * 2); + } + + radius = util.getGeomBuffer(geom, 'rad', vpf * numPts); + stroke = util.getGeomBuffer(geom, 'stroke', vpf * numPts); + strokeWidth = util.getGeomBuffer(geom, 'strokeWidth', vpf * numPts); + strokeOpacity = util.getGeomBuffer(geom, 'strokeOpacity', vpf * numPts); + strokeColor = util.getGeomBuffer(geom, 'strokeColor', vpf * numPts * 3); + fill = util.getGeomBuffer(geom, 'fill', vpf * numPts); + fillOpacity = util.getGeomBuffer(geom, 'fillOpacity', vpf * numPts); + fillColor = util.getGeomBuffer(geom, 'fillColor', vpf * numPts * 3); + indices = geom.primitive(0).indices(); + if (!(indices instanceof Uint16Array) || indices.length !== vpf * numPts) { + indices = new Uint16Array(vpf * numPts); + geom.primitive(0).setIndices(indices); + } + + for (i = ivpf = ivpf3 = iunit = i3 = 0; i < numPts; i += 1, i3 += 3) { + item = data[i]; + if (m_primitiveShape !== 'sprite') { + for (j = 0; j < unit.length; j += 1, iunit += 1) { + unitBuf[iunit] = unit[j]; + } + } + /* We can ignore the indicies (they will all be zero) */ + radiusVal = radFunc(item); + strokeVal = strokeFunc(item) ? 1.0 : 0.0; + strokeWidthVal = strokeWidthFunc(item); + strokeOpacityVal = strokeOpacityFunc(item); + strokeColorVal = strokeColorFunc(item); + fillVal = fillFunc(item) ? 1.0 : 0.0; + fillOpacityVal = fillOpacityFunc(item); + fillColorVal = fillColorFunc(item); + for (j = 0; j < vpf; 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(true); + m_mapper.modified(); + m_mapper.boundsDirtyTimestamp().modified(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Return list of actors + * + * @returns {vgl.actor[]} + */ + //////////////////////////////////////////////////////////////////////////// + this.actors = function () { + if (!m_actor) { + return []; + } + return [m_actor]; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the number of vertices used for each point. + * + * @returns {Number} + */ + //////////////////////////////////////////////////////////////////////////// + this.verticesPerFeature = function () { + var unit = pointPolygon(0, 0, 1, 1); + return unit.length / 2; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + 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(); + + if (m_primitiveShape === 'sprite') { + primitive = new vgl.points(); + } + + m_pixelWidthUniform = new vgl.floatUniform('pixelWidth', + 2.0 / 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}); + + // TODO: Right now this is ugly but we will fix it. + prog.addVertexAttribute(posAttr, vgl.vertexAttributeKeys.Position); + if (m_primitiveShape !== 'sprite') { + 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + + if (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(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + + s_update.call(m_this); + + // For now build if the data or style changes. In the future we may + // we able to partially update the data using dynamic gl buffers. + if (m_this.dataTime().getMTime() >= m_this.buildTime().getMTime() || + m_this.updateTime().getMTime() < m_this.getMTime()) { + m_this._build(); + } + + // Update uniforms + m_pixelWidthUniform.set(2.0 / 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(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.renderer().contextRenderer().removeActor(m_actor); + s_exit(); + }; + + m_this._init(); + return this; + }; + + inherit(gl_pointFeature, pointFeature); + + // Now register it + registerFeature('vgl', 'point', gl_pointFeature); + + module.exports = gl_pointFeature; + + +/***/ }, +/* 250 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var polygonFeature = __webpack_require__(224); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of polygonFeature + * + * @class geo.gl.polygonFeature + * @extends geo.polygonFeature + * @returns {geo.gl.polygonFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_polygonFeature = function (arg) { + 'use strict'; + if (!(this instanceof gl_polygonFeature)) { + return new gl_polygonFeature(arg); + } + arg = arg || {}; + polygonFeature.call(this, arg); + + var vgl = __webpack_require__(6); + var Triangulator = __webpack_require__(251).Triangulator; + var transform = __webpack_require__(147); + + //////////////////////////////////////////////////////////////////////////// + /** + * @private + */ + //////////////////////////////////////////////////////////////////////////// + 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; + + function createVertexShader() { + var vertexShaderSource = [ + 'attribute vec3 pos;', + 'attribute vec3 fillColor;', + 'attribute float fillOpacity;', + 'uniform mat4 modelViewMatrix;', + 'uniform mat4 projectionMatrix;', + 'uniform float pixelWidth;', + 'varying vec3 fillColorVar;', + 'varying float fillOpacityVar;', + + 'void main(void)', + '{', + ' vec4 clipPos = projectionMatrix * modelViewMatrix * vec4(pos.xyz, 1);', + ' if (clipPos.w != 0.0) {', + ' clipPos = clipPos/clipPos.w;', + ' }', + ' fillColorVar = fillColor;', + ' fillOpacityVar = fillOpacity;', + ' gl_Position = clipPos;', + '}' + ].join('\n'), + shader = new vgl.shader(vgl.GL.VERTEX_SHADER); + shader.setShaderSource(vertexShaderSource); + return shader; + } + + function createFragmentShader() { + var fragmentShaderSource = [ + '#ifdef GL_ES', + ' precision highp float;', + '#endif', + 'varying vec3 fillColorVar;', + 'varying float fillOpacityVar;', + 'void main () {', + ' gl_FragColor = vec4 (fillColorVar, fillOpacityVar);', + '}' + ].join('\n'), + shader = new vgl.shader(vgl.GL.FRAGMENT_SHADER); + shader.setShaderSource(fragmentShaderSource); + return shader; + } + + function createGLPolygons() { + var i = null, + numPts = null, + start = null, + itemIndex = 0, + polygonItemCoordIndex = 0, + position = [], + fillColor = [], + fillOpacity = [], + fillColorNew = [], + fillOpacityNew = [], + posFunc = null, + fillColorFunc = null, + polygonItem = null, + fillOpacityFunc = null, + buffers = vgl.DataBuffers(1024), + sourcePositions = vgl.sourceDataP3fv(), + sourceFillColor = + vgl.sourceDataAnyfv(3, vgl.vertexAttributeKeysIndexed.Two), + sourceFillOpacity = + vgl.sourceDataAnyfv(1, vgl.vertexAttributeKeysIndexed.Three), + trianglePrimitive = vgl.triangles(), + geom = vgl.geometryData(), + polygon = null, + holes = null, + extRing = null, + extIndex = 0, + extLength = null, + intIndex = 0, + posInstance = null, + triangulator = new Triangulator(), + triangList = null, + newTriangList = null, + fillColorInstance = null; + + posFunc = m_this.position(); + fillColorFunc = m_this.style.get('fillColor'); + fillOpacityFunc = m_this.style.get('fillOpacity'); + + m_this.data().forEach(function (item) { + polygon = m_this.polygon()(item, itemIndex); + polygonItem = polygon.outer || []; + holes = polygon.inner || []; + polygonItemCoordIndex = 0; + extRing = []; + extIndex = 0; + extLength = polygonItem.length - 1; + extRing[0] = []; + intIndex = 0; + + polygonItem.forEach(function (extRingCoords) { + if (extIndex !== extLength) { + //extRing = extRing.concat(extRingCoords); + posInstance = posFunc(extRingCoords, + polygonItemCoordIndex, + item, itemIndex); + extRing[0].push({ + x: posInstance.x, y: posInstance.y, i: fillColor.length + }); + + fillColorInstance = fillColorFunc(extRingCoords, + polygonItemCoordIndex, + item, itemIndex); + fillColor.push([fillColorInstance.r, + fillColorInstance.g, + fillColorInstance.b]); + fillOpacity.push(fillOpacityFunc(extRingCoords, + polygonItemCoordIndex, + item, + itemIndex)); + polygonItemCoordIndex += 1; + } + extIndex += 1; + }); + + polygonItemCoordIndex = 0; + holes.forEach(function (hole) { + extRing[intIndex + 1] = []; + hole.forEach(function (intRingCoords) { + posInstance = posFunc(intRingCoords, polygonItemCoordIndex, + item, itemIndex); + extRing[intIndex + 1].push({ + x: posInstance.x, y: posInstance.y, i: fillColor.length + }); + fillColorInstance = fillColorFunc(intRingCoords, + polygonItemCoordIndex, + item, itemIndex); + fillColor.push([fillColorInstance.r, + fillColorInstance.g, + fillColorInstance.b]); + fillOpacity.push(fillOpacityFunc(intRingCoords, + polygonItemCoordIndex, + item, itemIndex)); + polygonItemCoordIndex += 1; + }); + intIndex += 1; + }); + + //console.log("extRing ", extRing); + //console.log("result", PolyK.Triangulate(extRing)); + triangList = triangulator.triangulate_polygon(extRing); + newTriangList = []; + + triangList.forEach(function (newIndices) { + Array.prototype.push.apply(newTriangList, newIndices); + }); + + for (i = 1; i < extRing.length; i += 1) { + extRing[0] = extRing[0].concat(extRing[i]); + } + + newTriangList.forEach(function (polygonIndex) { + var polygonItemCoords = extRing[0][polygonIndex]; + position.push([polygonItemCoords.x, + polygonItemCoords.y, + polygonItemCoords.z || 0.0]); + fillColorNew.push(fillColor[polygonItemCoords.i]); + fillOpacityNew.push(fillOpacity[polygonItemCoords.i]); + }); + + itemIndex += 1; + }); + + position = transform.transformCoordinates( + m_this.gcs(), m_this.layer().map().gcs(), + position, 3); + + buffers.create('pos', 3); + buffers.create('indices', 1); + buffers.create('fillColor', 3); + buffers.create('fillOpacity', 1); + + numPts = position.length; + + start = buffers.alloc(numPts); + + //console.log("numPts ", numPts); + for (i = 0; i < numPts; 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); + } + + //console.log(buffers.get('fillColor')); + sourcePositions.pushBack(buffers.get('pos')); + geom.addSource(sourcePositions); + + sourceFillColor.pushBack(buffers.get('fillColor')); + geom.addSource(sourceFillColor); + + sourceFillOpacity.pushBack(buffers.get('fillOpacity')); + geom.addSource(sourceFillOpacity); + + //console.log(buffers.get('indices')); + trianglePrimitive.setIndices(buffers.get('indices')); + geom.addPrimitive(trianglePrimitive); + + m_mapper.setGeometryData(geom); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + 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); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build + * + * @override + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + if (m_actor) { + m_this.renderer().contextRenderer().removeActor(m_actor); + } + + createGLPolygons(); + + m_this.renderer().contextRenderer().addActor(m_actor); + m_this.buildTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * 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._build(); + } + + m_actor.setVisible(m_this.visible()); + m_actor.material().setBinNumber(m_this.bin()); + m_this.updateTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.renderer().contextRenderer().removeActor(m_actor); + s_exit(); + }; + + this._init(arg); + return this; + }; + + inherit(gl_polygonFeature, polygonFeature); + + // Now register it + registerFeature('vgl', 'polygon', gl_polygonFeature); + module.exports = gl_polygonFeature; + + +/***/ }, +/* 251 */ +/***/ function(module, exports, __webpack_require__) { + + // pnltri.js / raw.github.com/jahting/pnltri.js/master/LICENSE + + var self = self || {}; + + /** + * @author jahting / http://www.ameco.tv/ + * + * (Simple) Polygon Near-Linear Triangulation + * with fast ear-clipping for polygons without holes + * + */ + + var PNLTRI = { REVISION: '2.1.1' }; + + // ##### Global Constants ##### + + + // ##### Global Variables ##### + + + /** + * @author jahting / http://www.ameco.tv/ + */ + + PNLTRI.Math = { + + random: Math.random, // function to use for random number generation + + // generate random ordering in place: + // Fisher-Yates shuffle + array_shuffle: function( inoutArray ) { + for (var i = inoutArray.length - 1; i > 0; i-- ) { + var j = Math.floor( PNLTRI.Math.random() * (i+1) ); + var tmp = inoutArray[i]; + inoutArray[i] = inoutArray[j]; + inoutArray[j] = tmp; + } + return inoutArray; + }, + + + // like compare (<=>) + // yA > yB resp. xA > xB: 1, equal: 0, otherwise: -1 + compare_pts_yx: function ( inPtA, inPtB ) { + var deltaY = inPtA.y - inPtB.y; + if ( deltaY < PNLTRI.Math.EPSILON_N ) { + return -1; + } else if ( deltaY > PNLTRI.Math.EPSILON_P ) { + return 1; + } else { + var deltaX = inPtA.x - inPtB.x; + if ( deltaX < PNLTRI.Math.EPSILON_N ) { + return -1; + } else if ( deltaX > PNLTRI.Math.EPSILON_P ) { + return 1; + } else { + return 0; + } + } + }, + + + ptsCrossProd: function ( inPtVertex, inPtFrom, inPtTo ) { + // two vectors: ( v0: inPtVertex -> inPtFrom ), ( v1: inPtVertex -> inPtTo ) + // CROSS_SINE: sin(theta) * len(v0) * len(v1) + return ( inPtFrom.x - inPtVertex.x ) * ( inPtTo.y - inPtVertex.y ) - + ( inPtFrom.y - inPtVertex.y ) * ( inPtTo.x - inPtVertex.x ); + // <=> crossProd( inPtFrom-inPtVertex, inPtTo-inPtVertex ) + // == 0: colinear (angle == 0 or 180 deg == PI rad) + // > 0: v1 lies left of v0, CCW angle from v0 to v1 is convex ( < 180 deg ) + // < 0: v1 lies right of v0, CW angle from v0 to v1 is convex ( < 180 deg ) + }, + + }; + + // precision of floating point arithmetic + // PNLTRI.Math.EPSILON_P = Math.pow(2,-32); // ~ 0.0000000001 + PNLTRI.Math.EPSILON_P = Math.pow(2,-43); // ~ 0.0000000000001 + PNLTRI.Math.EPSILON_N = -PNLTRI.Math.EPSILON_P; + + // Problem with EPSILON-compares: + // - especially when there is a x-coordinate ordering on equal y-coordinates + // => either NO EPSILON-compares on y-coordinates, since almost equal y + // can have very different x - so they are not nearly close + // or EPSILON must be bigger: Solution so far. + /** + * @author jahting / http://www.ameco.tv/ + */ + + /** @constructor */ + PNLTRI.PolygonData = function ( inPolygonChainList ) { + + // list of polygon vertices + // .x, .y: coordinates + this.vertices = []; + + // list of polygon segments, original polygons ane holes + // and additional ones added during the subdivision into + // uni-y-monotone polygons (s. this.monoSubPolyChains) + // doubly linked by: snext, sprev + this.segments = []; + this.diagonals = []; + + // for the ORIGINAL polygon chains + this.idNextPolyChain = 0; + // for each original chain: lies the polygon inside to the left? + // "true": winding order is CCW for a contour or CW for a hole + // "false": winding order is CW for a contour or CCW for a hole + this.PolyLeftArr = []; + + // indices into this.segments: at least one for each monoton chain for the polygon + // these subdivide the polygon into uni-y-monotone polygons, that is + // polygons that have only one segment between ymax and ymin on one side + // and the other side has monotone increasing y from ymin to ymax + // the monoSubPolyChains are doubly linked by: mnext, mprev + this.monoSubPolyChains = []; + + // list of triangles: each 3 indices into this.vertices + this.triangles = []; + + // initialize optional polygon chains + if ( inPolygonChainList ) { + for (var i=0, j=inPolygonChainList.length; i start point entry in vertices + vTo: inVertexTo, // -> end point entry in vertices + // upward segment? (i.e. vTo > vFrom) !!! only valid for sprev,snext NOT for mprev,mnext !!! + upward: ( PNLTRI.Math.compare_pts_yx(inVertexTo, inVertexFrom) == 1 ), + // doubly linked list of original polygon chains (not the monoChains !) + sprev: null, // previous segment + snext: null, // next segment + // + // for performance reasons: + // initialization of all fields added later + // + // for trapezoids + rootFrom: null, // root of partial tree where vFrom is located + rootTo: null, // root of partial tree where vTo is located + is_inserted: false, // already inserted into QueryStructure ? + // for assigning depth: trapezoids + trLeft: null, // one trapezoid bordering on the left of this segment + trRight: null, // one trapezoid bordering on the right of this segment + // for monochains + mprev: null, // doubly linked list for monotone chains (sub-polygons) + mnext: null, + marked: false, // already visited during unique monoChain identification ? + }; + }, + + appendSegmentEntry: function ( inSegment ) { // private + this.segments.push( inSegment ); + return inSegment; + }, + + + appendDiagonalsEntry: function ( inDiagonal ) { // <<<<< public + this.diagonals.push( inDiagonal ); + return inDiagonal; + }, + + + addVertexChain: function ( inRawPointList ) { // private + + function verts_equal( inVert1, inVert2 ) { + return ( ( Math.abs(inVert1.x - inVert2.x) < PNLTRI.Math.EPSILON_P ) && + ( Math.abs(inVert1.y - inVert2.y) < PNLTRI.Math.EPSILON_P ) ); + } + + function verts_colinear_chain( inVert1, inVert2, inVert3 ) { + if ( Math.abs( PNLTRI.Math.ptsCrossProd( inVert2, inVert1, inVert3 ) ) > PNLTRI.Math.EPSILON_P ) return false; + // only real sequences, not direction reversals + var low, middle, high; + if ( Math.abs( inVert1.y - inVert2.y ) < PNLTRI.Math.EPSILON_P ) { + // horizontal line + middle = inVert2.x; + if ( inVert1.x < inVert3.x ) { + low = inVert1.x; + high = inVert3.x; + } else { + low = inVert3.x; + high = inVert1.x; + } + } else { + middle = inVert2.y; + if ( inVert1.y < inVert3.y ) { + low = inVert1.y; + high = inVert3.y; + } else { + low = inVert3.y; + high = inVert1.y; + } + } + return ( ( ( low - middle ) < PNLTRI.Math.EPSILON_P ) && ( ( middle - high ) < PNLTRI.Math.EPSILON_P ) ); + } + + var newVertices = []; + var newVertex, acceptVertex, lastIdx; + for ( var i=0; i < inRawPointList.length; i++ ) { + newVertex = this.appendVertexEntry( inRawPointList[i].x, inRawPointList[i].y ); + // suppresses zero-length segments + acceptVertex = true; + lastIdx = newVertices.length-1; + if ( lastIdx >= 0 ) { + if ( verts_equal( newVertex, newVertices[lastIdx] ) ) { + acceptVertex = false; + } else if ( lastIdx > 0 ) { + if ( verts_colinear_chain( newVertices[lastIdx-1], newVertices[lastIdx], newVertex ) ) { + newVertices.pop(); + } + } + } + if ( acceptVertex ) newVertices.push( newVertex ); + } + // compare last vertices to first: suppresses zero-length and co-linear segments + lastIdx = newVertices.length - 1; + if ( ( lastIdx > 0 ) && + verts_equal( newVertices[lastIdx], newVertices[0] ) ) { + newVertices.pop(); + lastIdx--; + } + if ( lastIdx > 1 ) { + if ( verts_colinear_chain( newVertices[lastIdx-1], newVertices[lastIdx], newVertices[0] ) ) { + newVertices.pop(); + lastIdx--; + } + if ( ( lastIdx > 1 ) && + verts_colinear_chain( newVertices[lastIdx], newVertices[0], newVertices[1] ) ) { + newVertices.shift(); + } + } + + return newVertices; + }, + + + addPolygonChain: function ( inRawPointList ) { // <<<<<< public + + // vertices + var newVertices = this.addVertexChain( inRawPointList ); + if ( newVertices.length < 3 ) { + console.log( "Polygon has < 3 vertices!", newVertices ); + return 0; + } + + // segments + var saveSegListLength = this.segments.length; + // + var segment, firstSeg, prevSeg; + for ( var i=0; i < newVertices.length-1; i++ ) { + segment = this.createSegmentEntry( newVertices[i], newVertices[i+1] ); + if (prevSeg) { + segment.sprev = prevSeg; + prevSeg.snext = segment; + } else { + firstSeg = segment; + } + prevSeg = segment; + this.appendSegmentEntry( segment ); + } + // close polygon + segment = this.createSegmentEntry( newVertices[newVertices.length-1], newVertices[0] ); + segment.sprev = prevSeg; + prevSeg.snext = segment; + this.appendSegmentEntry( segment ); + firstSeg.sprev = segment; + segment.snext = firstSeg; + + this.PolyLeftArr[this.idNextPolyChain++] = true; + return this.segments.length - saveSegListLength; + }, + + + /* Monotone Polygon Chains */ + + // Generate the uni-y-monotone sub-polygons from + // the trapezoidation of the polygon. + + create_mono_chains: function () { // <<<<<< public + var newMono, newMonoTo, toFirstOutSeg, fromRevSeg; + for ( var i = 0, j = this.segments.length; i < j; i++) { + newMono = this.segments[i]; + if ( this.PolyLeftArr[newMono.chainId] ) { + // preserve winding order + newMonoTo = newMono.vTo; // target of segment + newMono.mprev = newMono.sprev; // doubly linked list for monotone chains (sub-polygons) + newMono.mnext = newMono.snext; + } else { + // reverse winding order + newMonoTo = newMono.vFrom; + newMono = newMono.snext; + newMono.mprev = newMono.snext; + newMono.mnext = newMono.sprev; + } + if ( fromRevSeg = newMono.vFrom.lastInDiag ) { // assignment ! + fromRevSeg.mnext = newMono; + newMono.mprev = fromRevSeg; + newMono.vFrom.lastInDiag = null; // cleanup + } + if ( toFirstOutSeg = newMonoTo.firstOutDiag ) { // assignment ! + toFirstOutSeg.mprev = newMono; + newMono.mnext = toFirstOutSeg; + newMonoTo.firstOutDiag = null; // cleanup + } + } + }, + + // For each monotone polygon, find the ymax (to determine the two + // y-monotone chains) and skip duplicate monotone polygons + + unique_monotone_chains_max: function () { // <<<<<< public + + function find_monotone_chain_max( frontMono ) { + var frontPt, firstPt, ymaxPt; + + var monoPosmax = frontMono; + firstPt = ymaxPt = frontMono.vFrom; + + frontMono.marked = true; + frontMono = frontMono.mnext; + while ( frontPt = frontMono.vFrom ) { // assignment ! + if (frontMono.marked) { + if ( frontPt == firstPt ) break; // mono chain completed + console.log("ERR unique_monotone: segment in two chains", firstPt, frontMono ); + return null; + } else { + /* if ( frontPt == firstPt ) { // check for robustness + console.log("ERR unique_monotone: point double", firstPt, frontMono ); + } */ + frontMono.marked = true; + } + if ( PNLTRI.Math.compare_pts_yx( frontPt, ymaxPt ) == 1 ) { + ymaxPt = frontPt; + monoPosmax = frontMono; + } + frontMono = frontMono.mnext; + } + return monoPosmax; + } + + var frontMono, monoPosmax; + + // assumes attribute "marked" is NOT yet "true" for any mono chain segment + this.monoSubPolyChains = []; + // loop through all original segments + for ( var i = 0, j = this.segments.length; i < j; i++ ) { + frontMono = this.segments[i]; + if ( frontMono.marked ) continue; // already in a processed mono chain + monoPosmax = find_monotone_chain_max( frontMono ); + if ( monoPosmax ) this.monoSubPolyChains.push( monoPosmax ); + } + // loop through all additional segments (diagonals) // TODO: Testcase for mono chain without original segments !!! + /* for ( var i = 0, j = this.diagonals.length; i < j; i++ ) { + frontMono = this.diagonals[i]; + if ( frontMono.marked ) continue; // already in a processed mono chain + monoPosmax = find_monotone_chain_max( frontMono ); + if ( monoPosmax ) this.monoSubPolyChains.push( monoPosmax ); + } */ + return this.monoSubPolyChains; + }, + + + /* Triangles */ + + clearTriangles: function () { + this.triangles = []; + }, + + addTriangle: function ( inVert1, inVert2, inVert3 ) { + this.triangles.push( [ inVert1.id, inVert2.id, inVert3.id ] ); + }, + + }; + + /** + * Simple Polygon Triangulation by Ear Clipping + * + * description of technique employed: + * http://www.siggraph.org/education/materials/HyperGraph/scanline/outprims/polygon1.htm + * + * This code is a quick port of code written in C++ which was submitted to + * flipcode.com by John W. Ratcliff // July 22, 2000 + * See original code and more information here: + * http://www.flipcode.com/archives/Efficient_Polygon_Triangulation.shtml + * + * ported to actionscript by Zevan Rosser + * http://actionsnippet.com/?p=1462 + * + * ported to javascript by Joshua Koo + * http://www.lab4games.net/zz85/blog + * + * adapted to doubly linked list by Juergen Ahting + * http://www.ameco.tv + * + */ + + /** @constructor */ + PNLTRI.EarClipTriangulator = function ( inPolygonData ) { + + this.polyData = inPolygonData; + + }; + + + PNLTRI.EarClipTriangulator.prototype = { + + constructor: PNLTRI.EarClipTriangulator, + + + // triangulates first doubly linked segment list in this.polyData + // algorithm uses ear-clipping and runs in O(n^2) time + + triangulate_polygon_no_holes: function () { + + function isEarAt( vertex ) { + + var prevX = vertex.mprev.vFrom.x; + var prevY = vertex.mprev.vFrom.y; + + var vertX = vertex.vFrom.x; + var vertY = vertex.vFrom.y; + + var nextX = vertex.mnext.vFrom.x; + var nextY = vertex.mnext.vFrom.y; + + var vnX = nextX - vertX, vnY = nextY - vertY; + var npX = prevX - nextX, npY = prevY - nextY; + var pvX = vertX - prevX, pvY = vertY - prevY; + + // concave angle at vertex -> not an ear to cut off + if ( PNLTRI.Math.EPSILON_P > ( ( pvX * vnY ) - ( vnX * pvY ) ) ) return false; + + // check whether any other point lieas within the triangle abc + var vStop = vertex.mprev.mprev; + var vOther = vertex.mnext; + while ( vOther != vStop ) { + vOther = vOther.mnext; + var otherX = vOther.vFrom.x; + var otherY = vOther.vFrom.y; + + var poX = otherX - prevX, poY = otherY - prevY; + // just in case there are several vertices with the same coordinate + if ( ( poX === 0 ) && ( poY === 0 ) ) continue; // vOther == vertex.mprev + var voX = otherX - vertX, voY = otherY - vertY; + if ( ( voX === 0 ) && ( voY === 0 ) ) continue; // vOther == vertex + var noX = otherX - nextX, noY = otherY - nextY; + if ( ( noX === 0 ) && ( noY === 0 ) ) continue; // vOther == vertex.mnext + + // if vOther is inside triangle abc -> not an ear to cut off + if ( ( ( vnX * voY - vnY * voX ) >= PNLTRI.Math.EPSILON_N ) && + ( ( pvX * poY - pvY * poX ) >= PNLTRI.Math.EPSILON_N ) && + ( ( npX * noY - npY * noX ) >= PNLTRI.Math.EPSILON_N ) ) return false; + } + return true; + + } + + var myPolyData = this.polyData; + var startSeg = myPolyData.getFirstSegment(); + + // create a counter-clockwise ordered doubly linked list (monoChain links) + + var cursor = startSeg; + if ( myPolyData.isClockWise( startSeg ) ) { + do { // reverses chain order + cursor.mprev = cursor.snext; + cursor.mnext = cursor.sprev; + cursor = cursor.sprev; + } while ( cursor != startSeg ); + myPolyData.set_PolyLeft_wrong(0); + } else { + do { + cursor.mprev = cursor.sprev; + cursor.mnext = cursor.snext; + cursor = cursor.snext; + } while ( cursor != startSeg ); + } + + // remove all vertices except 2, creating 1 triangle every time + + var vertex = startSeg; + var fullLoop = vertex; // prevent infinite loop on "defective" polygons + + while ( vertex.mnext != vertex.mprev ) { + if ( isEarAt( vertex ) ) { + // found a triangle ear to cut off + this.polyData.addTriangle( vertex.mprev.vFrom, vertex.vFrom, vertex.mnext.vFrom ); + // remove vertex from the remaining chain + vertex.mprev.mnext = vertex.mnext; + vertex.mnext.mprev = vertex.mprev; + vertex = vertex.mnext; + fullLoop = vertex; // reset error detection + } else { + vertex = vertex.mnext; + // loop?: probably non-simple polygon -> stop with error + if ( vertex == fullLoop ) return false; + } + } + + return true; + + }, + + /* // takes one element of a double linked segment list + // works on array of vertices + + triangulate_polygon_no_holes: function () { + var startSeg = this.polyData.getFirstSegment(); + + function vertList( inStartSeg ) { + var verts = []; + // we want a counter-clockwise polygon in verts + var doubleArea = 0.0; + var cursor = inStartSeg; + var p,q; + var idx = 0; + do { + p = cursor.sprev.vFrom; + q = cursor.vFrom; + doubleArea += p.x * q.y - q.x * p.y; + verts[idx++] = q; + cursor = cursor.snext; + } while ( cursor != inStartSeg ); + if ( doubleArea < 0.0 ) { + verts = verts.reverse(); + var tmp = verts.pop(); + verts.unshift( tmp ); + } + return verts; + } + + function snip( verts, u, v, w, n ) { + + var ax = verts[ u ].x; + var ay = verts[ u ].y; + + var bx = verts[ v ].x; + var by = verts[ v ].y; + + var cx = verts[ w ].x; + var cy = verts[ w ].y; + + if ( PNLTRI.Math.EPSILON_P > ( ( bx - ax ) * ( cy - ay ) - ( by - ay ) * ( cx - ax ) ) ) return false; + + var aX, aY, bX, bY, cX, cY; + + aX = cx - bx; aY = cy - by; + bX = ax - cx; bY = ay - cy; + cX = bx - ax; cY = by - ay; + + var p, px, py; + + var apx, apy, bpx, bpy, cpx, cpy; + var cCROSSap, bCROSScp, aCROSSbp; + + for ( p = 0; p < n; p ++ ) { + + px = verts[ p ].x + py = verts[ p ].y + + apx = px - ax; apy = py - ay; + if ( ( apx == 0 ) && ( apy == 0 ) ) continue; + bpx = px - bx; bpy = py - by; + if ( ( bpx == 0 ) && ( bpy == 0 ) ) continue; + cpx = px - cx; cpy = py - cy; + if ( ( cpx == 0 ) && ( cpy == 0 ) ) continue; + + // see if p is inside triangle abc + + aCROSSbp = aX * bpy - aY * bpx; + cCROSSap = cX * apy - cY * apx; + bCROSScp = bX * cpy - bY * cpx; + + if ( ( aCROSSbp >= PNLTRI.Math.EPSILON_N ) && + ( bCROSScp >= PNLTRI.Math.EPSILON_N ) && + ( cCROSSap >= PNLTRI.Math.EPSILON_N ) ) return false; + + } + + return true; + + }; + + var result = []; + + var verts = vertList( startSeg ); + + var n = verts.length; + var nv = n; + + var u, v, w; + + // remove nv - 2 vertices, creating 1 triangle every time + + var count = 2 * nv; // error detection + + for ( v = nv - 1; nv > 2; ) { + + // if we loop, it is probably a non-simple polygon + + if ( ( count -- ) <= 0 ) return false; + + // three consecutive vertices in current polygon, + + u = v; if ( nv <= u ) u = 0; // previous + v = u + 1; if ( nv <= v ) v = 0; // new v + w = v + 1; if ( nv <= w ) w = 0; // next + + if ( snip( verts, u, v, w, nv ) ) { + + // output Triangle + + this.polyData.addTriangle( verts[ u ], verts[ v ], verts[ w ] ); + + // remove v from the remaining polygon + + var s, t; + + for ( s = v, t = v + 1; t < nv; s++, t++ ) { + + verts[ s ] = verts[ t ]; + + } + + nv --; + + v --; + if ( v < 0 ) v = nv-1; + + // reset error detection counter + + count = 2 * nv; + + } + + } + + return true; + + }, */ + + }; + + /** + * @author jahting / http://www.ameco.tv/ + * + * Algorithm to create the trapezoidation of a polygon with holes + * according to Seidel's algorithm [Sei91] + */ + + /** @constructor */ + PNLTRI.Trapezoid = function ( inHigh, inLow, inLeft, inRight ) { + + this.vHigh = inHigh ? inHigh : { x: Number.POSITIVE_INFINITY, y: Number.POSITIVE_INFINITY }; + this.vLow = inLow ? inLow : { x: Number.NEGATIVE_INFINITY, y: Number.NEGATIVE_INFINITY }; + + this.lseg = inLeft; + this.rseg = inRight; + + // this.uL = null; // -> Trapezoid: upper left neighbor + // this.uR = null; // -> Trapezoid: upper right neighbor + // this.dL = null; // -> Trapezoid: lower left neighbor + // this.dR = null; // -> Trapezoid: lower right neighbor + + // this.sink = null; // link to corresponding SINK-Node in QueryStructure + + // this.usave = null; // temp: uL/uR, preserved for next step + // this.uleft = null; // temp: from uL? (true) or uR (false) + + this.depth = -1; // no depth assigned yet + + this.monoDone = false; // monotonization: done with trying to split this trapezoid ? + + }; + + PNLTRI.Trapezoid.prototype = { + + constructor: PNLTRI.Trapezoid, + + clone: function () { + var newTrap = new PNLTRI.Trapezoid( this.vHigh, this.vLow, this.lseg, this.rseg ); + + newTrap.uL = this.uL; + newTrap.uR = this.uR; + + newTrap.dL = this.dL; + newTrap.dR = this.dR; + + newTrap.sink = this.sink; + + return newTrap; + }, + + + splitOffLower: function ( inSplitPt ) { + var trLower = this.clone(); // new lower trapezoid + + this.vLow = trLower.vHigh = inSplitPt; + + // L/R unknown, anyway changed later + this.dL = trLower; // setBelow + trLower.uL = this; // setAbove + this.dR = trLower.uR = null; + + // setAbove + if ( trLower.dL ) trLower.dL.uL = trLower; // dL always connects to uL + if ( trLower.dR ) trLower.dR.uR = trLower; // dR always connects to uR + + return trLower; + }, + + }; + + + /*============================================================================== + * + *============================================================================*/ + + // PNLTRI.qsCounter = 0; + + /** @constructor */ + PNLTRI.QsNode = function ( inTrapezoid ) { + // this.qsId = PNLTRI.qsCounter++; // Debug only + // Only SINK-nodes are created directly. + // The others originate from splitting trapezoids + // - by a horizontal line: SINK-Node -> Y-Node + // - by a segment: SINK-Node -> X-Node + this.trap = inTrapezoid; + inTrapezoid.sink = this; + }; + + PNLTRI.QsNode.prototype = { + + constructor: PNLTRI.QsNode, + + }; + + /*============================================================================== + * + *============================================================================*/ + + /** @constructor */ + PNLTRI.QueryStructure = function ( inPolygonData ) { + // initialise the query structure and trapezoid list + var initialTrap = new PNLTRI.Trapezoid( null, null, null, null ); + this.trapArray = []; + this.appendTrapEntry( initialTrap ); + + // PNLTRI.qsCounter = 0; + this.root = new PNLTRI.QsNode( initialTrap ); + + if ( inPolygonData ) { + /* + * adds and initializes specific attributes for all segments + * // -> QueryStructure: roots of partial tree where vertex is located + * rootFrom, rootTo: for vFrom, vTo + * // marker + * is_inserted: already inserted into QueryStructure ? + */ + var segListArray = inPolygonData.getSegments(); + for ( var i = 0; i < segListArray.length; i++ ) { + segListArray[i].rootFrom = segListArray[i].rootTo = this.root; + segListArray[i].is_inserted = false; + } + } + }; + + PNLTRI.QueryStructure.prototype = { + + constructor: PNLTRI.QueryStructure, + + getRoot: function () { + return this.root; + }, + + + appendTrapEntry: function ( inTrapezoid ) { + inTrapezoid.trapID = this.trapArray.length; // for Debug + this.trapArray.push( inTrapezoid ); + }, + cloneTrap: function ( inTrapezoid ) { + var trap = inTrapezoid.clone(); + this.appendTrapEntry( trap ); + return trap; + }, + + + splitNodeAtPoint: function ( inNode, inPoint, inReturnUpper ) { + // inNode: SINK-Node with trapezoid containing inPoint + var trUpper = inNode.trap; // trUpper: trapezoid includes the point + if (trUpper.vHigh == inPoint) return inNode; // (ERROR) inPoint is already inserted + if (trUpper.vLow == inPoint) return inNode; // (ERROR) inPoint is already inserted + var trLower = trUpper.splitOffLower( inPoint ); // trLower: new lower trapezoid + this.appendTrapEntry( trLower ); + + // SINK-Node -> Y-Node + inNode.yval = inPoint; + inNode.trap = null; + + inNode.right = new PNLTRI.QsNode( trUpper ); // Upper trapezoid sink + inNode.left = new PNLTRI.QsNode( trLower ); // Lower trapezoid sink + + return inReturnUpper ? trUpper.sink : trLower.sink; + }, + + + /* + * Mathematics & Geometry helper methods + */ + + fpEqual: function ( inNum0, inNum1 ) { + return Math.abs( inNum0 - inNum1 ) < PNLTRI.Math.EPSILON_P; + }, + + + // Checks, whether the vertex inPt is to the left of line segment inSeg. + // Returns: + // >0: inPt is left of inSeg, + // <0: inPt is right of inSeg, + // =0: inPt is co-linear with inSeg + // + // ATTENTION: always viewed from -y, not as if moving along the segment chain !! + + is_left_of: function ( inSeg, inPt, inBetweenY ) { + var retVal; + var dXfrom = inSeg.vFrom.x - inPt.x; + var dXto = inSeg.vTo.x - inPt.x; + var dYfromZero = this.fpEqual( inSeg.vFrom.y, inPt.y ); + if ( this.fpEqual( inSeg.vTo.y, inPt.y ) ) { + if ( dYfromZero ) return 0; // all points on a horizontal line + retVal = dXto; + } else if ( dYfromZero ) { + retVal = dXfrom; + /* } else if ( inBetweenY && ( dXfrom * dXto > 0 ) ) { + // both x-coordinates of inSeg are on the same side of inPt + if ( Math.abs( dXto ) >= PNLTRI.Math.EPSILON_P ) return dXto; + retVal = dXfrom; */ + } else { + if ( inSeg.upward ) { + return PNLTRI.Math.ptsCrossProd( inSeg.vFrom, inSeg.vTo, inPt ); + } else { + return PNLTRI.Math.ptsCrossProd( inSeg.vTo, inSeg.vFrom, inPt ); + } + } + if ( Math.abs( retVal ) < PNLTRI.Math.EPSILON_P ) return 0; + return retVal; + }, + + + /* + * Query structure main methods + */ + + // This method finds the Nodes in the QueryStructure corresponding + // to the trapezoids that contain the endpoints of inSegment, + // starting from Nodes rootFrom/rootTo and replacing them with the results. + + segNodes: function ( inSegment ) { + this.ptNode( inSegment, true ); + this.ptNode( inSegment, false ); + }, + + // TODO: may need to prevent infinite loop in case of messed up + // trapezoid structure (s. test_add_segment_special_6) + + ptNode: function ( inSegment, inUseFrom ) { + var ptMain, ptOther, qsNode; + if ( inUseFrom ) { + ptMain = inSegment.vFrom; + ptOther = inSegment.vTo; // used if ptMain is not sufficient + qsNode = inSegment.rootFrom; + } else { + ptMain = inSegment.vTo; + ptOther = inSegment.vFrom; + qsNode = inSegment.rootTo; + } + var compPt, compRes; + var isInSegmentShorter; + + while ( qsNode ) { + if ( qsNode.yval ) { // Y-Node: horizontal line + // 4 times as often as X-Node + qsNode = ( PNLTRI.Math.compare_pts_yx( ( ( ptMain == qsNode.yval ) ? // is the point already inserted ? + ptOther : ptMain ), qsNode.yval ) == -1 ) ? + qsNode.left : qsNode.right; // below : above + } else if ( qsNode.seg ) { // X-Node: segment (~vertical line) + // 0.8 to 1.5 times as often as SINK-Node + if ( ( ptMain == qsNode.seg.vFrom ) || + ( ptMain == qsNode.seg.vTo ) ) { + // the point is already inserted + if ( this.fpEqual( ptMain.y, ptOther.y ) ) { + // horizontal segment + if ( !this.fpEqual( qsNode.seg.vFrom.y, qsNode.seg.vTo.y ) ) { + qsNode = ( ptOther.x < ptMain.x ) ? qsNode.left : qsNode.right; // left : right + } else { // co-linear horizontal reversal: test_add_segment_special_7 + if ( ptMain == qsNode.seg.vFrom ) { + // connected at qsNode.seg.vFrom + // console.log("ptNode: co-linear horizontal reversal, connected at qsNode.seg.vFrom", inUseFrom, inSegment, qsNode ) + isInSegmentShorter = inSegment.upward ? + ( ptOther.x >= qsNode.seg.vTo.x ) : + ( ptOther.x < qsNode.seg.vTo.x ); + qsNode = ( isInSegmentShorter ? + inSegment.sprev.upward : + qsNode.seg.snext.upward ) ? qsNode.right : qsNode.left; // above : below + } else { + // connected at qsNode.seg.vTo + // console.log("ptNode: co-linear horizontal reversal, connected at qsNode.seg.vTo", inUseFrom, inSegment, qsNode ); + isInSegmentShorter = inSegment.upward ? + ( ptOther.x < qsNode.seg.vFrom.x ) : + ( ptOther.x >= qsNode.seg.vFrom.x ); + qsNode = ( isInSegmentShorter ? + inSegment.snext.upward : + qsNode.seg.sprev.upward ) ? qsNode.left : qsNode.right; // below : above + } + } + continue; + } else { + compRes = this.is_left_of( qsNode.seg, ptOther, false ); + if ( compRes === 0 ) { + // co-linear reversal (not horizontal) + // a co-linear continuation would not reach this point + // since the previous Y-node comparison would have led to a sink instead + // console.log("ptNode: co-linear, going back on previous segment", ptMain, ptOther, qsNode ); + // now as we have two consecutive co-linear segments we have to avoid a cross-over + // for this we need the far point on the "next" segment to the SHORTER of our two + // segments to avoid that "next" segment to cross the longer of our two segments + if ( ptMain == qsNode.seg.vFrom ) { + // connected at qsNode.seg.vFrom + // console.log("ptNode: co-linear, going back on previous segment, connected at qsNode.seg.vFrom", ptMain, ptOther, qsNode ); + isInSegmentShorter = inSegment.upward ? + ( ptOther.y >= qsNode.seg.vTo.y ) : + ( ptOther.y < qsNode.seg.vTo.y ); + compRes = isInSegmentShorter ? + this.is_left_of( qsNode.seg, inSegment.sprev.vFrom, false ) : + -this.is_left_of( qsNode.seg, qsNode.seg.snext.vTo, false ); + } else { + // connected at qsNode.seg.vTo + // console.log("ptNode: co-linear, going back on previous segment, connected at qsNode.seg.vTo", ptMain, ptOther, qsNode ); + isInSegmentShorter = inSegment.upward ? + ( ptOther.y < qsNode.seg.vFrom.y ) : + ( ptOther.y >= qsNode.seg.vFrom.y ); + compRes = isInSegmentShorter ? + this.is_left_of( qsNode.seg, inSegment.snext.vTo, false ) : + -this.is_left_of( qsNode.seg, qsNode.seg.sprev.vFrom, false ); + } + } + } + } else { + /* if ( ( PNLTRI.Math.compare_pts_yx( ptMain, qsNode.seg.vFrom ) * // TODO: Testcase + PNLTRI.Math.compare_pts_yx( ptMain, qsNode.seg.vTo ) + ) == 0 ) { + console.log("ptNode: Pts too close together#2: ", ptMain, qsNode.seg ); + } */ + compRes = this.is_left_of( qsNode.seg, ptMain, true ); + if ( compRes === 0 ) { + // touching: ptMain lies on qsNode.seg but is none of its endpoints + // should happen quite seldom + compRes = this.is_left_of( qsNode.seg, ptOther, false ); + if ( compRes === 0 ) { + // co-linear: inSegment and qsNode.seg + // includes case with ptOther connected to qsNode.seg + var tmpPtOther = inUseFrom ? inSegment.sprev.vFrom : inSegment.snext.vTo; + compRes = this.is_left_of( qsNode.seg, tmpPtOther, false ); + } + } + } + if ( compRes > 0 ) { + qsNode = qsNode.left; + } else if ( compRes < 0 ) { + qsNode = qsNode.right; + } else { + // ??? TODO - not reached with current tests + // possible at all ? + return qsNode; + // qsNode = qsNode.left; // left + // qsNode = qsNode.right; // right + } + } else { // SINK-Node: trapezoid area + // least often + if ( !qsNode.trap ) { console.log("ptNode: unknown type", qsNode); } + if ( inUseFrom ) { inSegment.rootFrom = qsNode; } + else { inSegment.rootTo = qsNode; } + return qsNode; + } + } // end while - should not exit here + }, + + + // Add a new segment into the trapezoidation and update QueryStructure and Trapezoids + // 1) locates the two endpoints of the segment in the QueryStructure and inserts them + // 2) goes from the high-end trapezoid down to the low-end trapezoid + // changing all the trapezoids in between. + // Except for the high-end and low-end no new trapezoids are created. + // For all in between either: + // - the existing trapezoid is restricted to the left of the new segment + // and on the right side the trapezoid from above is extended downwards + // - or the other way round: + // the existing trapezoid is restricted to the right of the new segment + // and on the left side the trapezoid from above is extended downwards + + add_segment: function ( inSegment ) { + var scope = this; + + // functions handling the relationship to the upper neighbors (uL, uR) + // of trNewLeft and trNewRight + + function fresh_seg_or_upward_cusp() { + // trCurrent has at most 1 upper neighbor + // and should also have at least 1, since the high-point trapezoid + // has been split off another one, which is now above + var trUpper = trCurrent.uL || trCurrent.uR; + + // trNewLeft and trNewRight CANNOT have been extended from above + if ( trUpper.dL && trUpper.dR ) { + // upward cusp: top forms a triangle + + // ATTENTION: the decision whether trNewLeft or trNewRight is the + // triangle trapezoid formed by the two segments has already been taken + // when selecting trCurrent as the left or right lower neighbor to trUpper !! + + if ( trCurrent == trUpper.dL ) { + // *** Case: FUC_UC_LEFT; prev: ---- + // console.log( "fresh_seg_or_upward_cusp: upward cusp, new seg to the left!" ); + // upper + // -------*------- + // + \ + // NL + \ + // + NR \ + // + \ + trNewRight.uL = null; // setAbove; trNewRight.uR, trNewLeft unchanged + trUpper.dL = trNewLeft; // setBelow; dR: unchanged, NEVER null + } else { + // *** Case: FUC_UC_RIGHT; prev: ---- + // console.log( "fresh_seg_or_upward_cusp: upward cusp, new seg from the right!" ); + // upper + // -------*------- + // / + + // / + NR + // / NL + + // / + + trNewLeft.uR = null; // setAbove; trNewLeft.uL, trNewRight unchanged + trUpper.dR = trNewRight; // setBelow; dL: unchanged, NEVER null + } + } else { + // *** Case: FUC_FS; prev: "splitOffLower" + // console.log( "fresh_seg_or_upward_cusp: fresh segment, high adjacent segment still missing" ); + // upper + // -------*------- + // + + // NL + + // + NR + // + + trNewRight.uL = null; // setAbove; trNewLeft unchanged, set by "splitOffLower" + trNewRight.uR = trUpper; + trUpper.dR = trNewRight; // setBelow; trUpper.dL unchanged, set by "splitOffLower" + } + } + + function continue_chain_from_above() { + // trCurrent has at least 2 upper neighbors + if ( trCurrent.usave ) { + // 3 upper neighbors (part II) + if ( trCurrent.uleft ) { + // *** Case: CC_3UN_LEFT; prev: 1B_3UN_LEFT + // console.log( "continue_chain_from_above: 3 upper neighbors (part II): u0a, u0b, uR(usave)" ); + // => left gets one, right gets two of the upper neighbors + // !! trNewRight cannot have been extended from above + // and trNewLeft must have been !! + // + / + // C.uL + C.uR / C.usave + // - - - -+----*---------- + // NL + NR + trNewRight.uL = trCurrent.uR; // setAbove + trNewRight.uR = trCurrent.usave; + trNewRight.uL.dL = trNewRight; // setBelow; trNewRight.uL.dR == null, unchanged + trNewRight.uR.dR = trNewRight; // setBelow; trNewRight.uR.dL == null, unchanged + } else { + // *** Case: CC_3UN_RIGHT; prev: 1B_3UN_RIGHT + // console.log( "continue_chain_from_above: 3 upper neighbors (part II): uL(usave), u1a, u1b" ); + // => left gets two, right gets one of the upper neighbors + // !! trNewLeft cannot have been extended from above + // and trNewRight must have been !! + // \ + + // C.usave \ C.uL + C.uR + // ---------*----+- - - - + // NL + NR + trNewLeft.uR = trCurrent.uL; // setAbove; first uR !!! + trNewLeft.uL = trCurrent.usave; + trNewLeft.uL.dL = trNewLeft; // setBelow; dR == null, unchanged + trNewLeft.uR.dR = trNewLeft; // setBelow; dL == null, unchanged + } + trNewLeft.usave = trNewRight.usave = null; + } else if ( trCurrent.vHigh == trFirst.vHigh ) { // && meetsHighAdjSeg ??? TODO + // *** Case: CC_2UN_CONN; prev: ---- + // console.log( "continue_chain_from_above: 2 upper neighbors, fresh seg, continues high adjacent seg" ); + // !! trNewLeft and trNewRight cannot have been extended from above !! + // C.uL / C.uR + // -------*--------- + // NL + NR + trNewRight.uR.dR = trNewRight; // setBelow; dL == null, unchanged + trNewLeft.uR = trNewRight.uL = null; // setAbove; trNewLeft.uL, trNewRight.uR unchanged + } else { + // *** Case: CC_2UN; prev: 1B_1UN_CONT, 2B_NOCON_RIGHT/LEFT, 2B_TOUCH_RIGHT/LEFT, 2B_COLIN_RIGHT/LEFT + // console.log( "continue_chain_from_above: simple case, 2 upper neighbors (no usave, not fresh seg)" ); + // !! trNewLeft XOR trNewRight will have been extended from above !! + // C.uL + C.uR + // -------+--------- + // NL + NR + if ( trNewRight == trCurrent ) { // trNewLeft has been extended from above + // setAbove + trNewRight.uL = trNewRight.uR; + trNewRight.uR = null; + // setBelow; dR: unchanged, is NOT always null (prev: 2B_NOCON_LEFT, 2B_TOUCH_LEFT, 2B_COLIN_LEFT) + trNewRight.uL.dL = trNewRight; + } else { // trNewRight has been extended from above + trNewLeft.uR = trNewLeft.uL; // setAbove; first uR !!! + trNewLeft.uL = null; + } + } + } + + // functions handling the relationship to the lower neighbors (dL, dR) + // of trNewLeft and trNewRight + // trNewLeft or trNewRight MIGHT have been extended from above + // !! in that case dL and dR are different from trCurrent and MUST be set here !! + + function only_one_trap_below( inTrNext ) { + + if ( trCurrent.vLow == trLast.vLow ) { + // final part of segment + + if ( meetsLowAdjSeg ) { + // downward cusp: bottom forms a triangle + + // ATTENTION: the decision whether trNewLeft and trNewRight are to the + // left or right of the already inserted segment the new one meets here + // has already been taken when selecting trLast to the left or right + // of that already inserted segment !! + + if ( trCurrent.dL ) { + // *** Case: 1B_DC_LEFT; next: ---- + // console.log( "only_one_trap_below: downward cusp, new seg from the left!" ); + // + / + // + NR / + // NL + / + // + / + // -------*------- + // C.dL = next + + // setAbove + inTrNext.uL = trNewLeft; // uR: unchanged, NEVER null + // setBelow part 1 + trNewLeft.dL = inTrNext; + trNewRight.dR = null; + } else { + // *** Case: 1B_DC_RIGHT; next: ---- + // console.log( "only_one_trap_below: downward cusp, new seg to the right!" ); + // \ + + // \ NL + + // \ + NR + // \ + + // -------*------- + // C.dR = next + + // setAbove + inTrNext.uR = trNewRight; // uL: unchanged, NEVER null + // setBelow part 1 + trNewLeft.dL = null; + trNewRight.dR = inTrNext; + } + } else { + // *** Case: 1B_1UN_END; next: ---- + // console.log( "only_one_trap_below: simple case, new seg ends here, low adjacent seg still missing" ); + // + + // NL + NR + // + + // ------*------- + // next + + // setAbove + inTrNext.uL = trNewLeft; // trNewLeft must + inTrNext.uR = trNewRight; // must + // setBelow part 1 + trNewLeft.dL = trNewRight.dR = inTrNext; // Error + // trNewRight.dR = inTrNext; + } + // setBelow part 2 + trNewLeft.dR = trNewRight.dL = null; + } else { + // NOT final part of segment + + if ( inTrNext.uL && inTrNext.uR ) { + // inTrNext has two upper neighbors + // => a segment ends on the upper Y-line of inTrNext + // => inTrNext has temporarily 3 upper neighbors + // => marks whether the new segment cuts through + // uL or uR of inTrNext and saves the other in .usave + if ( inTrNext.uL == trCurrent ) { + // *** Case: 1B_3UN_LEFT; next: CC_3UN_LEFT + // console.log( "only_one_trap_below: inTrNext has 3 upper neighbors (part I): u0a, u0b, uR(usave)" ); + // + / + // NL + NR / + // + / + // - - - -+--*---- + // + + // next + // if ( inTrNext.uR != trNewRight ) { // for robustness TODO: prevent + inTrNext.usave = inTrNext.uR; + inTrNext.uleft = true; + // trNewLeft: L/R undefined, will be extended down and changed anyway + // } else { + // ERROR: should not happen + // console.log( "ERR add_segment: Trapezoid Loop right", inTrNext, trCurrent, trNewLeft, trNewRight, inSegment, this ); + // } + } else { + // *** Case: 1B_3UN_RIGHT; next: CC_3UN_RIGHT + // console.log( "only_one_trap_below: inTrNext has 3 upper neighbors (part I): uL(usave), u1a, u1b" ); + // \ + + // \ NL + NR + // \ + + // ---*---+- - - - + // + + // next + // if ( inTrNext.uL != trNewLeft ) { // for robustness TODO: prevent + inTrNext.usave = inTrNext.uL; + inTrNext.uleft = false; + // trNewRight: L/R undefined, will be extended down and changed anyway + // } else { + // ERROR: should not happen + // console.log( "ERR add_segment: Trapezoid Loop left", inTrNext, trCurrent, trNewLeft, trNewRight, inSegment, this ); + // } + } + //} else { + // *** Case: 1B_1UN_CONT; next: CC_2UN + // console.log( "only_one_trap_below: simple case, new seg continues down" ); + // + + // NL + NR + // + + // ------+------- + // + + // next + + // L/R for one side undefined, which one is not fixed + // but that one will be extended down and changed anyway + // for the other side, vLow must lie at the opposite end + // thus both are set accordingly + } + // setAbove + inTrNext.uL = trNewLeft; + inTrNext.uR = trNewRight; + // setBelow + trNewLeft.dR = trNewRight.dL = inTrNext; + trNewLeft.dL = trNewRight.dR = null; + } + } + + function two_trap_below() { + // Find out which one (dL,dR) is intersected by this segment and + // continue down that one + var trNext; + if ( ( trCurrent.vLow == trLast.vLow ) && meetsLowAdjSeg ) { // meetsLowAdjSeg necessary? TODO + // *** Case: 2B_CON_END; next: ---- + // console.log( "two_trap_below: finished, meets low adjacent segment" ); + // + + // NL + NR + // + + // ------*------- + // \ C.dR + // C.dL \ + + // setAbove + trCurrent.dL.uL = trNewLeft; + trCurrent.dR.uR = trNewRight; + // setBelow; sequence of assignments essential, just in case: trCurrent == trNewLeft + trNewLeft.dL = trCurrent.dL; + trNewRight.dR = trCurrent.dR; + trNewLeft.dR = trNewRight.dL = null; + + trNext = null; // segment finished + } else { + // setAbove part 1 + trCurrent.dL.uL = trNewLeft; + trCurrent.dR.uR = trNewRight; + + var goDownRight; + // passes left or right of an already inserted NOT connected segment + // trCurrent.vLow: high-end of existing segment + var compRes = scope.is_left_of( inSegment, trCurrent.vLow, true ); + if ( compRes > 0 ) { // trCurrent.vLow is left of inSegment + // *** Case: 2B_NOCON_RIGHT; next: CC_2UN + // console.log( "two_trap_below: (intersecting dR)" ); + // + + // NL + NR + // + + // ---*---+- - - - + // \ + + // C.dL \ C.dR + goDownRight = true; + } else if ( compRes < 0 ) { // trCurrent.vLow is right of inSegment + // *** Case: 2B_NOCON_LEFT; next: CC_2UN + // console.log( "two_trap_below: (intersecting dL)" ); + // + + // NL + NR + // + + // - - -+---*------- + // + \ C.dR + // C.dL \ + goDownRight = false; + } else { // trCurrent.vLow lies ON inSegment + var vLowSeg = trCurrent.dL.rseg; + var directionIsUp = vLowSeg.upward; + var otherPt = directionIsUp ? vLowSeg.vFrom : vLowSeg.vTo; + compRes = scope.is_left_of( inSegment, otherPt, false ); + if ( compRes > 0 ) { // otherPt is left of inSegment + // *** Case: 2B_TOUCH_RIGHT; next: CC_2UN + // console.log( "two_trap_below: vLow ON new segment, touching from right" ); + // + + // NL + NR + // + + // -------*- - - - + // / + + // C.dL / C.dR + goDownRight = true; // like intersecting dR + } else if ( compRes < 0 ) { // otherPt is right of inSegment + // *** Case: 2B_TOUCH_LEFT; next: CC_2UN + // console.log( "two_trap_below: vLow ON new segment, touching from left" ); + // + + // NL + NR + // + + // - - -*------- + // + \ C.dR + // C.dL \ + goDownRight = false; // like intersecting dL + } else { // otherPt lies ON inSegment + vLowSeg = directionIsUp ? vLowSeg.snext : vLowSeg.sprev; // other segment with trCurrent.vLow + otherPt = directionIsUp ? vLowSeg.vTo : vLowSeg.vFrom; + compRes = scope.is_left_of( inSegment, otherPt, false ); + if ( compRes > 0 ) { // otherPt is left of inSegment + // *** Case: 2B_COLIN_RIGHT; next: CC_2UN + // console.log( "two_trap_below: vLow ON new segment, touching from right" ); + // + + // NL + NR + // -------*- - - - + // C.dL \+ C.dR + // \+ + goDownRight = true; // like intersecting dR + // } else if ( compRes == 0 ) { // NOT POSSIBLE, since 3 points on a line is prevented during input of polychains + // goDownRight = true; // like intersecting dR + } else { // otherPt is right of inSegment + // *** Case: 2B_COLIN_LEFT; next: CC_2UN + // console.log( "two_trap_below: vLow ON new segment, touching from left" ); + // + + // NL + NR + // - - - -*------- + // C.dL +/ C.dR + // +/ + goDownRight = false; // TODO: for test_add_segment_special_4 -> like intersecting dL + } + } + } + if ( goDownRight ) { + trNext = trCurrent.dR; + // setAbove part 2 + trCurrent.dR.uL = trNewLeft; + // setBelow part 1 + trNewLeft.dL = trCurrent.dL; + trNewRight.dR = null; // L/R undefined, will be extended down and changed anyway + } else { + trNext = trCurrent.dL; + // setAbove part 2 + trCurrent.dL.uR = trNewRight; + // setBelow part 1 + trNewRight.dR = trCurrent.dR; + trNewLeft.dL = null; // L/R undefined, will be extended down and changed anyway + } + // setBelow part 2 + trNewLeft.dR = trNewRight.dL = trNext; + } + + return trNext; + } + + // + // main function body + // + + /* if ( ( inSegment.sprev.vTo != inSegment.vFrom ) || ( inSegment.vTo != inSegment.snext.vFrom ) ) { + console.log( "add_segment: inconsistent point order of adjacent segments: ", + inSegment.sprev.vTo, inSegment.vFrom, inSegment.vTo, inSegment.snext.vFrom ); + return; + } */ + + // Find the top-most and bottom-most intersecting trapezoids -> rootXXX + this.segNodes( inSegment ); + + var segLowVert , segLowNode, meetsLowAdjSeg; // y-min vertex + var segHighVert, segHighNode, meetsHighAdjSeg; // y-max vertex + + if ( inSegment.upward ) { + segLowVert = inSegment.vFrom; + segHighVert = inSegment.vTo; + segLowNode = inSegment.rootFrom; + segHighNode = inSegment.rootTo; + // was lower point already inserted earlier? => segments meet at their ends + meetsLowAdjSeg = inSegment.sprev.is_inserted; + // was higher point already inserted earlier? => segments meet at their ends + meetsHighAdjSeg = inSegment.snext.is_inserted; + } else { + segLowVert = inSegment.vTo; + segHighVert = inSegment.vFrom; + segLowNode = inSegment.rootTo; + segHighNode = inSegment.rootFrom; + meetsLowAdjSeg = inSegment.snext.is_inserted; + meetsHighAdjSeg = inSegment.sprev.is_inserted; + } + + // insert higher vertex into QueryStructure + if ( !meetsHighAdjSeg ) { + // higher vertex not yet inserted => split trapezoid horizontally + var tmpNode = this.splitNodeAtPoint( segHighNode, segHighVert, false ); + // move segLowNode to new (lower) trapezoid, if it was the one which was just split + if ( segHighNode == segLowNode ) segLowNode = tmpNode; + segHighNode = tmpNode; + } + var trFirst = segHighNode.trap; // top-most trapezoid for this segment + + // check for robustness // TODO: prevent + if ( !trFirst.uL && !trFirst.uR ) { + console.log("ERR add_segment: missing trFirst.uX: ", trFirst ); + return; + } + if ( trFirst.vHigh != segHighVert ) { + console.log("ERR add_segment: trFirstHigh != segHigh: ", trFirst ); + return; + } + + // insert lower vertex into QueryStructure + if ( !meetsLowAdjSeg ) { + // lower vertex not yet inserted => split trapezoid horizontally + segLowNode = this.splitNodeAtPoint( segLowNode, segLowVert, true ); + } + var trLast = segLowNode.trap; // bottom-most trapezoid for this segment + + // + // Thread the segment into the query "tree" from top to bottom. + // All the trapezoids which are intersected by inSegment are "split" into two. + // For each the SINK-QsNode is converted into an X-Node and + // new sinks for the new partial trapezoids are added. + // In fact a real split only happens at the top and/or bottom end of the segment + // since at every y-line seperating two trapezoids is traverses it + // cuts off the "beam" from the y-vertex on one side, so that at that side + // the trapezoid from above can be extended down. + // + + var trCurrent = trFirst; + + var trNewLeft, trNewRight, trPrevLeft, trPrevRight; + + var counter = this.trapArray.length + 2; // just to prevent infinite loop + var trNext; + while ( trCurrent ) { + if ( --counter < 0 ) { + console.log( "ERR add_segment: infinite loop", trCurrent, inSegment, this ); + return; + } + if ( !trCurrent.dL && !trCurrent.dR ) { + // ERROR: no successors, cannot arise if data is correct + console.log( "ERR add_segment: missing successors", trCurrent, inSegment, this ); + return; + } + + var qs_trCurrent = trCurrent.sink; + // SINK-Node -> X-Node + qs_trCurrent.seg = inSegment; + qs_trCurrent.trap = null; + // + // successive trapezoids bordered by the same segments are merged + // by extending the trPrevRight or trPrevLeft down + // and redirecting the parent X-Node to the extended sink + // !!! destroys tree structure since several nodes now point to the same SINK-Node !!! + // TODO: maybe it's not a problem; + // merging of X-Nodes is no option, since they are used as "rootFrom/rootTo" ! + // + if ( trPrevRight && ( trPrevRight.rseg == trCurrent.rseg ) ) { + // console.log( "add_segment: extending right predecessor down!", trPrevRight ); + trNewLeft = trCurrent; + trNewRight = trPrevRight; + trNewRight.vLow = trCurrent.vLow; + // redirect parent X-Node to extended sink + qs_trCurrent.left = new PNLTRI.QsNode( trNewLeft ); // trCurrent -> left SINK-Node + qs_trCurrent.right = trPrevRight.sink; // deforms tree by multiple links to trPrevRight.sink + } else if ( trPrevLeft && ( trPrevLeft.lseg == trCurrent.lseg ) ) { + // console.log( "add_segment: extending left predecessor down!", trPrevLeft ); + trNewRight = trCurrent; + trNewLeft = trPrevLeft; + trNewLeft.vLow = trCurrent.vLow; + // redirect parent X-Node to extended sink + qs_trCurrent.left = trPrevLeft.sink; // deforms tree by multiple links to trPrevLeft.sink + qs_trCurrent.right = new PNLTRI.QsNode( trNewRight ); // trCurrent -> right SINK-Node + } else { + trNewLeft = trCurrent; + trNewRight = this.cloneTrap(trCurrent); + qs_trCurrent.left = new PNLTRI.QsNode( trNewLeft ); // trCurrent -> left SINK-Node + qs_trCurrent.right = new PNLTRI.QsNode( trNewRight ); // new clone -> right SINK-Node + } + + // handle neighbors above + if ( trCurrent.uL && trCurrent.uR ) { + continue_chain_from_above(); + } else { + fresh_seg_or_upward_cusp(); + } + + // handle neighbors below + if ( trCurrent.dL && trCurrent.dR ) { + trNext = two_trap_below(); + } else { + if ( trCurrent.dL ) { + // console.log( "add_segment: only_one_trap_below! (dL)" ); + trNext = trCurrent.dL; + } else { + // console.log( "add_segment: only_one_trap_below! (dR)" ); + trNext = trCurrent.dR; + } + only_one_trap_below( trNext ); + } + + if ( trNewLeft.rseg ) trNewLeft.rseg.trLeft = trNewRight; + if ( trNewRight.lseg ) trNewRight.lseg.trRight = trNewLeft; + trNewLeft.rseg = trNewRight.lseg = inSegment; + inSegment.trLeft = trNewLeft; + inSegment.trRight = trNewRight; + + // further loop-step down ? + if ( trCurrent.vLow != trLast.vLow ) { + trPrevLeft = trNewLeft; + trPrevRight = trNewRight; + + trCurrent = trNext; + } else { + trCurrent = null; + } + } // end while + + inSegment.is_inserted = true; + // console.log( "add_segment: ###### DONE ######" ); + }, + + // Assigns a depth to all trapezoids; + // 0: outside, 1: main polygon, 2: holes, 3:polygons in holes, ... + // Checks segment orientation and marks those polygon chains for reversal + // where the polygon inside lies to their right (contour in CW, holes in CCW) + assignDepths: function ( inPolyData ) { + var thisDepth = [ this.trapArray[0] ]; + var nextDepth = []; + + var thisTrap, borderSeg, curDepth = 0; + do { + // rseg should exactely go upward on trapezoids inside the polygon (odd depth) + var expectedRsegUpward = ( ( curDepth % 2 ) == 1 ); + while ( thisTrap = thisDepth.pop() ) { // assignment ! + if ( thisTrap.depth != -1 ) continue; + thisTrap.depth = curDepth; + // + if ( thisTrap.uL ) thisDepth.push( thisTrap.uL ); + if ( thisTrap.uR ) thisDepth.push( thisTrap.uR ); + if ( thisTrap.dL ) thisDepth.push( thisTrap.dL ); + if ( thisTrap.dR ) thisDepth.push( thisTrap.dR ); + // + if ( ( borderSeg = thisTrap.lseg ) && ( borderSeg.trLeft.depth == -1 ) ) // assignment ! + nextDepth.push( borderSeg.trLeft ); + if ( borderSeg = thisTrap.rseg ) { // assignment ! + if ( borderSeg.trRight.depth == -1 ) + nextDepth.push( borderSeg.trRight ); + if ( borderSeg.upward != expectedRsegUpward ) + inPolyData.set_PolyLeft_wrong( borderSeg.chainId ); + } + } + thisDepth = nextDepth; nextDepth = []; + curDepth++; + } while ( thisDepth.length > 0 ); + }, + + // creates the visibility map: + // for each vertex the list of all vertices in CW order which are directly + // visible through neighboring trapezoids and thus can be connected by a diagonal + + create_visibility_map: function ( inPolygonData ) { + // positional slots for neighboring trapezoid-diagonals + var DIAG_UL = 0, DIAG_UM = 1, DIAG_ULR = 2, DIAG_UR = 3; + var DIAG_DR = 4, DIAG_DM = 5, DIAG_DLR = 6, DIAG_DL = 7; + + var i, j; + var nbVertices = inPolygonData.nbVertices(); + + // initialize arrays for neighboring trapezoid-diagonals and vertices + var myVisibleDiagonals = new Array(nbVertices); + for ( i = 0; i < nbVertices; i++ ) { + myVisibleDiagonals[i] = new Array(DIAG_DL+1); + } + // create the list of neighboring trapezoid-diagonals + // put into their positional slots + var myExternalNeighbors = new Array(nbVertices); + for ( i = 0, j = this.trapArray.length; i < j; i++ ) { + var curTrap = this.trapArray[i]; + var highPos = curTrap.uL ? + ( curTrap.uR ? DIAG_DM : DIAG_DL ) : + ( curTrap.uR ? DIAG_DR : DIAG_DLR ); + var lowPos = curTrap.dL ? + ( curTrap.dR ? DIAG_UM : DIAG_UL ) : + ( curTrap.dR ? DIAG_UR : DIAG_ULR ); + + if ( ( curTrap.depth % 2 ) == 1 ) { // inside ? + if ( ( highPos == DIAG_DM ) || ( lowPos == DIAG_UM ) || + ( ( highPos == DIAG_DL ) && ( lowPos == DIAG_UR ) ) || + ( ( highPos == DIAG_DR ) && ( lowPos == DIAG_UL ) ) ) { + var lhDiag = inPolygonData.appendDiagonalsEntry( { + vFrom: curTrap.vLow, vTo: curTrap.vHigh, + mprev: null, mnext: null, marked: false } ); + var hlDiag = inPolygonData.appendDiagonalsEntry( { + vFrom: curTrap.vHigh, vTo: curTrap.vLow, revDiag: lhDiag, + mprev: null, mnext: null, marked: false } ); + lhDiag.revDiag = hlDiag; + myVisibleDiagonals[ curTrap.vLow.id][ lowPos] = lhDiag; + myVisibleDiagonals[curTrap.vHigh.id][highPos] = hlDiag; + } + } else { // outside, hole + if ( curTrap.vHigh.id !== null ) myExternalNeighbors[curTrap.vHigh.id] = highPos; + if ( curTrap.vLow.id !== null ) myExternalNeighbors[ curTrap.vLow.id] = lowPos; + } + } + // create the list of outgoing diagonals in the right order (CW) + // from the ordered list of neighboring trapezoid-diagonals + // - starting from an external one + // and connect those incoming to + var curDiag, curDiags, firstElem, fromVertex, lastIncoming; + for ( i = 0; i < nbVertices; i++ ) { + curDiags = myVisibleDiagonals[i]; + firstElem = myExternalNeighbors[i]; + if ( firstElem == null ) continue; // eg. skipped vertices (zero length, co-linear // NOT: === ! + j = firstElem; + lastIncoming = null; + do { + if ( j++ > DIAG_DL ) j = DIAG_UL; // circular positional list + if ( curDiag = curDiags[j] ) { + if ( lastIncoming ) { + curDiag.mprev = lastIncoming; + lastIncoming.mnext = curDiag; + } else { + fromVertex = curDiag.vFrom; + fromVertex.firstOutDiag = curDiag; + } + lastIncoming = curDiag.revDiag; + } + } while ( j != firstElem ); + if ( lastIncoming ) fromVertex.lastInDiag = lastIncoming; + } + }, + + + }; + + + /*============================================================================== + * + *============================================================================*/ + + /** @constructor */ + PNLTRI.Trapezoider = function ( inPolygonData ) { + + this.polyData = inPolygonData; + this.queryStructure = new PNLTRI.QueryStructure( this.polyData ); + + }; + + PNLTRI.Trapezoider.prototype = { + + constructor: PNLTRI.Trapezoider, + + + /* + * Mathematics helper methods + */ + + optimise_randomlist: function ( inOutSegListArray ) { + // makes sure that the first N segments are one from each of the N polygon chains + var mainIdx = 0; + var helpIdx = this.polyData.nbPolyChains(); + if ( helpIdx == 1 ) return; + var chainMarker = new Array(helpIdx); + var oldSegListArray = inOutSegListArray.concat(); + for (var i=0; i 1 ) ? Math.floor( nbSegs / logstar ) : nbSegs; + + // Core: adds next partition of the segments + for (; current < partEnd; current++ ) { myQs.add_segment( randSegListArray[current] ); } + // console.log( nbSegs, current ); + + // To speed up the segment insertion into the trapezoidation + // the endponts of those segments not yet inserted + // are repeatedly pre-located, + // thus their final location-query can start at the top of the + // appropriate sub-tree instead of the root of the whole + // query structure. + // + for (i = current; i < nbSegs; i++) { this.queryStructure.segNodes( randSegListArray[i] ); } + } + + myQs.assignDepths( this.polyData ); + // cleanup to support garbage collection + for (i = 0; i < nbSegs; i++) { randSegListArray[i].trLeft = randSegListArray[i].trRight = null; } + }, + + // Creates a visibility map: + // for each vertex the list of all vertices in CW order which are directly + // visible through neighboring trapezoids and thus can be connected by a diagonal + + create_visibility_map: function () { + return this.queryStructure.create_visibility_map( this.polyData ); + }, + + }; + + /** + * @author jahting / http://www.ameco.tv/ + * + * Algorithm to split a polygon into uni-y-monotone sub-polygons + * + * 1) creates a trapezoidation of the main polygon according to Seidel's + * algorithm [Sei91] + * 2) uses diagonals of the trapezoids as additional segments + * to split the main polygon into uni-y-monotone sub-polygons + */ + + /** @constructor */ + PNLTRI.MonoSplitter = function ( inPolygonData ) { + + this.polyData = inPolygonData; + + this.trapezoider = null; + + }; + + + PNLTRI.MonoSplitter.prototype = { + + constructor: PNLTRI.MonoSplitter, + + + monotonate_trapezoids: function () { // <<<<<<<<<< public + // Trapezoidation + this.trapezoider = new PNLTRI.Trapezoider( this.polyData ); + // => one triangular trapezoid which lies inside the polygon + this.trapezoider.trapezoide_polygon(); + + // create segments for diagonals + this.trapezoider.create_visibility_map(); + // create mono chains by inserting diagonals + this.polyData.create_mono_chains(); + + // create UNIQUE monotone sub-polygons + this.polyData.unique_monotone_chains_max(); + }, + + }; + + /** + * @author jahting / http://www.ameco.tv/ + * + * Algorithm to triangulate uni-y-monotone polygons [FoM84] + * + * expects list of doubly linked monoChains, with Y-max as first vertex + */ + + + /** @constructor */ + PNLTRI.MonoTriangulator = function ( inPolygonData ) { + + this.polyData = inPolygonData; + + }; + + + PNLTRI.MonoTriangulator.prototype = { + + constructor: PNLTRI.MonoTriangulator, + + + // Pass each uni-y-monotone polygon with start at Y-max for greedy triangulation. + + triangulate_all_polygons: function () { // <<<<<<<<<< public + var normedMonoChains = this.polyData.getMonoSubPolys(); + this.polyData.clearTriangles(); + for ( var i=0; i monoPosmin is next to monoPosmax (left or right) + var monoPosmax = normedMonoChains[i]; + var prevMono = monoPosmax.mprev; + var nextMono = monoPosmax.mnext; + + if ( nextMono.mnext == prevMono ) { // already a triangle + this.polyData.addTriangle( monoPosmax.vFrom, nextMono.vFrom, prevMono.vFrom ); + } else { // triangulate the polygon + this.triangulate_monotone_polygon( monoPosmax ); + } + } + }, + + // algorithm to triangulate an uni-y-monotone polygon in O(n) time.[FoM84] + + triangulate_monotone_polygon: function ( monoPosmax ) { // private + var scope = this; + + function error_cleanup() { + // Error in algorithm OR polygon is not uni-y-monotone + console.log( "ERR uni-y-monotone: only concave angles left", vertBackLog ); + // push all "wrong" triangles => loop ends + while (vertBackLogIdx > 1) { + vertBackLogIdx--; + scope.polyData.addTriangle( vertBackLog[vertBackLogIdx-1], + vertBackLog[vertBackLogIdx], + vertBackLog[vertBackLogIdx+1] ); + } + } + + // + // Decisive for this algorithm to work correctly is to make sure + // the polygon stays uni-y-monotone when cutting off ears, i.e. + // to make sure the top-most and bottom-most vertices are removed last + // Usually this is done by handling the LHS-case ("LeftHandSide is a single segment") + // and the RHS-case ("RightHandSide segment is a single segment") + // differently by starting at the bottom for LHS and at the top for RHS. + // This is not necessary. It can be seen easily, that starting + // from the vertex next to top handles both cases correctly. + // + + var frontMono = monoPosmax.mnext; // == LHS: YminPoint; RHS: YmaxPoint.mnext + var endVert = monoPosmax.vFrom; + + var vertBackLog = [ frontMono.vFrom ]; + var vertBackLogIdx = 0; + + frontMono = frontMono.mnext; + var frontVert = frontMono.vFrom; + + // check for robustness // TODO + if (frontVert == endVert) return; // Error: only 2 vertices + + while ( (frontVert != endVert) || (vertBackLogIdx > 1) ) { + if ( vertBackLogIdx > 0 ) { + // vertBackLog is not empty + var insideAngleCCW = PNLTRI.Math.ptsCrossProd( vertBackLog[vertBackLogIdx], frontVert, vertBackLog[vertBackLogIdx-1] ); + if ( Math.abs(insideAngleCCW) <= PNLTRI.Math.EPSILON_P ) { + // co-linear + if ( (frontVert == endVert) || // all remaining triangles are co-linear (180 degree) + ( PNLTRI.Math.compare_pts_yx( vertBackLog[vertBackLogIdx], frontVert ) == // co-linear-reversal + PNLTRI.Math.compare_pts_yx( vertBackLog[vertBackLogIdx], vertBackLog[vertBackLogIdx-1] ) ) ) { + insideAngleCCW = 1; // => create triangle + } + } + if ( insideAngleCCW > 0 ) { + // convex corner: cut if off + this.polyData.addTriangle( vertBackLog[vertBackLogIdx-1], vertBackLog[vertBackLogIdx], frontVert ); + vertBackLogIdx--; + } else { + // non-convex: add frontVert to the vertBackLog + vertBackLog[++vertBackLogIdx] = frontVert; + if (frontVert == endVert) error_cleanup(); // should never happen !! + else { + frontMono = frontMono.mnext; + frontVert = frontMono.vFrom; + } + } + } else { + // vertBackLog contains only start vertex: + // add frontVert to the vertBackLog and advance frontVert + vertBackLog[++vertBackLogIdx] = frontVert; + frontMono = frontMono.mnext; + frontVert = frontMono.vFrom; + } + } + // reached the last vertex. Add in the triangle formed + this.polyData.addTriangle( vertBackLog[vertBackLogIdx - 1], vertBackLog[vertBackLogIdx], frontVert ); + }, + + }; + + /** + * @author jahting / http://www.ameco.tv/ + */ + + /******************************************************************************* + * + * Triangulator for Simple Polygons with Holes + * + * polygon with holes: + * - one closed contour polygon chain + * - zero or more closed hole polygon chains + * + * polygon chain (closed): + * - Array of vertex Objects with attributes "x" and "y" + * - representing the sequence of line segments + * - closing line segment (last->first vertex) is implied + * - line segments are non-zero length and non-crossing + * + * "global vertex index": + * - vertex number resulting from concatenation all polygon chains (starts with 0) + * + * + * Parameters (will not be changed): + * inPolygonChains: + * - Array of polygon chains + * + * Results (are a fresh copy): + * triangulate_polygon: + * - Array of Triangles ( Array of 3 "global vertex index" values ) + * + ******************************************************************************/ + + /** @constructor */ + PNLTRI.Triangulator = function () { + + this.lastPolyData = null; // for Debug purposes only + + }; + + + PNLTRI.Triangulator.prototype = { + + constructor: PNLTRI.Triangulator, + + + clear_lastData: function () { // save memory after Debug + this.lastPolyData = null; + }, + + // for the polygon data AFTER triangulation + // returns an Array of flags, one flag for each polygon chain: + // lies the inside of the polygon to the left? + // "true" implies CCW for contours and CW for holes + get_PolyLeftArr: function () { + if ( this.lastPolyData ) return this.lastPolyData.get_PolyLeftArr(); + return null; + }, + + + triangulate_polygon: function ( inPolygonChains, inForceTrapezoidation ) { + + // collected conditions for selecting EarClipTriangulator over Seidel's algorithm + function is_basic_polygon() { + if (inForceTrapezoidation) return false; + return ( myPolygonData.nbPolyChains() == 1 ); + } + + + this.clear_lastData(); + if ( ( !inPolygonChains ) || ( inPolygonChains.length === 0 ) ) return []; + // + // initializes general polygon data structure + // + var myPolygonData = new PNLTRI.PolygonData( inPolygonChains ); + // + var basicPolygon = is_basic_polygon(); + var myTriangulator; + if ( basicPolygon ) { + // + // triangulates single polygon without holes + // + myTriangulator = new PNLTRI.EarClipTriangulator( myPolygonData ); + basicPolygon = myTriangulator.triangulate_polygon_no_holes(); + } + if ( !basicPolygon ) { + // + // splits polygon into uni-y-monotone sub-polygons + // + var myMonoSplitter = new PNLTRI.MonoSplitter( myPolygonData ); + myMonoSplitter.monotonate_trapezoids(); + // + // triangulates all uni-y-monotone sub-polygons + // + myTriangulator = new PNLTRI.MonoTriangulator( myPolygonData ); + myTriangulator.triangulate_all_polygons(); + } + // + this.lastPolyData = myPolygonData; + return myPolygonData.getTriangles(); // copy of triangle list + } + + + }; + + // Export the PNLTRI object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + + if (true) { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = PNLTRI; + } + exports.PNLTRI = PNLTRI; + } else { + this['PNLTRI'] = PNLTRI; + } + + +/***/ }, +/* 252 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var quadFeature = __webpack_require__(227); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class quadFeature + * + * @class geo.gl.quadFeature + * @param {Object} arg Options object + * @extends geo.quadFeature + * @returns {geo.gl.quadFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var gl_quadFeature = function (arg) { + 'use strict'; + if (!(this instanceof gl_quadFeature)) { + return new gl_quadFeature(arg); + } + quadFeature.call(this, arg); + + var $ = __webpack_require__(1); + var vgl = __webpack_require__(6); + + var m_this = this, + s_exit = this._exit, + s_init = this._init, + s_update = this._update, + m_modelViewUniform, + m_actor_image, m_actor_color, m_glBuffers = {}, m_imgposbuf, + m_clrposbuf, m_clrModelViewUniform, + m_glCompileTimestamp = vgl.timestamp(), + m_glColorCompileTimestamp = vgl.timestamp(), + m_quads; + var vertexShaderImageSource = [ + 'attribute vec3 vertexPosition;', + 'attribute vec3 textureCoord;', + 'uniform mat4 modelViewMatrix;', + 'uniform mat4 projectionMatrix;', + 'varying highp vec3 iTextureCoord;', + 'void main(void) {', + ' gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);', + ' iTextureCoord = textureCoord;', + '}'].join('\n'); + var vertexShaderColorSource = [ + 'attribute vec3 vertexPosition;', + 'uniform vec3 vertexColor;', + 'uniform mat4 modelViewMatrix;', + 'uniform mat4 projectionMatrix;', + 'varying mediump vec3 iVertexColor;', + 'varying highp vec3 iTextureCoord;', + 'void main(void) {', + ' gl_Position = projectionMatrix * modelViewMatrix * vec4(vertexPosition, 1.0);', + ' iVertexColor = vertexColor;', + '}'].join('\n'); + + /** + * Allocate buffers that we need to control for image quads. This mimics + * the actions from vgl.mapper to some degree. + * + * @private + */ + function setupDrawObjects(renderState) { + var context = renderState.m_context, + newbuf = false; + + if (m_quads.imgQuads.length) { + if (!m_imgposbuf || m_imgposbuf.length < m_quads.imgQuads.length * 12 || + !m_glBuffers.imgQuadsPosition) { + if (m_glBuffers.imgQuadsPosition) { + context.deleteBuffer(m_glBuffers.imgQuadsPosition); + } + m_glBuffers.imgQuadsPosition = context.createBuffer(); + m_imgposbuf = new Float32Array(Math.max( + 128, m_quads.imgQuads.length * 2) * 12); + newbuf = true; + } + $.each(m_quads.imgQuads, function (idx, quad) { + for (var i = 0; i < 12; i += 1) { + m_imgposbuf[idx * 12 + i] = quad.pos[i] - m_quads.origin[i % 3]; + } + }); + context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); + if (newbuf) { + context.bufferData(vgl.GL.ARRAY_BUFFER, m_imgposbuf, vgl.GL.DYNAMIC_DRAW); + } else { + context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, m_imgposbuf); + } + } + m_glCompileTimestamp.modified(); + } + + /** + * Allocate buffers that we need to control for color quads. This mimics + * the actions from vgl.mapper to some degree. + * + * @private + */ + function setupColorDrawObjects(renderState) { + var context = renderState.m_context, + newbuf = false; + + if (m_quads.clrQuads.length) { + if (!m_clrposbuf || m_clrposbuf.length < m_quads.clrQuads.length * 12 || + !m_glBuffers.clrQuadsPosition) { + if (m_glBuffers.imgQuadsPosition) { + context.deleteBuffer(m_glBuffers.clrQuadsPosition); + } + m_glBuffers.clrQuadsPosition = context.createBuffer(); + m_clrposbuf = new Float32Array(Math.max( + 128, m_quads.clrQuads.length * 2) * 12); + newbuf = true; + } + $.each(m_quads.clrQuads, function (idx, quad) { + for (var i = 0; i < 12; i += 1) { + m_clrposbuf[idx * 12 + i] = quad.pos[i] - m_quads.origin[i % 3]; + } + }); + context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition); + if (newbuf) { + context.bufferData(vgl.GL.ARRAY_BUFFER, m_clrposbuf, vgl.GL.DYNAMIC_DRAW); + } else { + context.bufferSubData(vgl.GL.ARRAY_BUFFER, 0, m_clrposbuf); + } + } + m_glColorCompileTimestamp.modified(); + } + + //////////////////////////////////////////////////////////////////////////// + /** + * Build this feature + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + var mapper, mat, prog, srctex, geom; + + if (!m_this.position()) { + return; + } + m_quads = this._generateQuads(); + /* Create an actor to render image quads */ + if (m_quads.imgQuads.length && !m_actor_image) { + m_this.visible(false); + mapper = new vgl.mapper({dynamicDraw: true}); + m_actor_image = new vgl.actor(); + /* This is similar to vgl.utils.createTextureMaterial */ + m_actor_image.setMapper(mapper); + mat = new vgl.material(); + prog = new vgl.shaderProgram(); + prog.addVertexAttribute(new vgl.vertexAttribute('vertexPosition'), + vgl.vertexAttributeKeys.Position); + prog.addVertexAttribute(new vgl.vertexAttribute('textureCoord'), + vgl.vertexAttributeKeys.TextureCoordinate); + m_modelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', + m_quads.origin); + prog.addUniform(m_modelViewUniform); + prog.addUniform(new vgl.projectionUniform('projectionMatrix')); + prog.addUniform(new vgl.floatUniform('opacity', 1.0)); + prog.addShader(vgl.getCachedShader( + vgl.GL.VERTEX_SHADER, vgl.GL, vertexShaderImageSource)); + prog.addShader(vgl.utils.createRgbaTextureFragmentShader(vgl.GL)); + mat.addAttribute(prog); + mat.addAttribute(new vgl.blend()); + /* This is similar to vgl.planeSource */ + geom = new vgl.geometryData(); + m_imgposbuf = undefined; + srctex = new vgl.sourceDataT2fv(); + srctex.pushBack([0, 0, 1, 0, 0, 1, 1, 1]); + geom.addSource(srctex); + /* We deliberately do not add a primitive to our geometry -- we take care + * of that ourselves. */ + + mapper.setGeometryData(geom); + m_actor_image.setMaterial(mat); + + mapper.s_render = mapper.render; + mapper.render = m_this._renderImageQuads; + m_this.renderer().contextRenderer().addActor(m_actor_image); + m_this.visible(true); + } + /* Create an actor to render color quads */ + if (m_quads.clrQuads.length && !m_actor_color) { + m_this.visible(false); + mapper = new vgl.mapper({dynamicDraw: true}); + m_actor_color = new vgl.actor(); + /* This is similar to vgl.utils.createTextureMaterial */ + m_actor_color.setMapper(mapper); + mat = new vgl.material(); + prog = new vgl.shaderProgram(); + prog.addVertexAttribute(new vgl.vertexAttribute('vertexPosition'), + vgl.vertexAttributeKeys.Position); + m_clrModelViewUniform = new vgl.modelViewOriginUniform('modelViewMatrix', + m_quads.origin); + prog.addUniform(m_clrModelViewUniform); + prog.addUniform(new vgl.projectionUniform('projectionMatrix')); + prog.addUniform(new vgl.floatUniform('opacity', 1.0)); + prog.addUniform(new vgl.uniform(vgl.GL.FLOAT_VEC3, 'vertexColor')); + prog.addShader(vgl.getCachedShader( + vgl.GL.VERTEX_SHADER, vgl.GL, vertexShaderColorSource)); + prog.addShader(vgl.utils.createFragmentShader(vgl.GL)); + mat.addAttribute(prog); + mat.addAttribute(new vgl.blend()); + /* This is similar to vgl.planeSource */ + geom = new vgl.geometryData(); + m_clrposbuf = undefined; + /* We deliberately do not add a primitive to our geometry -- we take care + * of that ourselves. */ + + mapper.setGeometryData(geom); + m_actor_color.setMaterial(mat); + + mapper.s_render = mapper.render; + mapper.render = m_this._renderColorQuads; + m_this.renderer().contextRenderer().addActor(m_actor_color); + m_this.visible(true); + } + if (m_modelViewUniform) { + m_modelViewUniform.setOrigin(m_quads.origin); + } + if (m_clrModelViewUniform) { + m_clrModelViewUniform.setOrigin(m_quads.origin); + } + m_this.buildTime().modified(); + }; + + /** + * Check all of the image quads. If any do not have the correct texture, + * update them. */ + this._updateTextures = function () { + var texture; + + $.each(m_quads.imgQuads, function (idx, quad) { + if (!quad.image) { + return; + } + if (quad.image._texture) { + quad.texture = quad.image._texture; + } else { + texture = new vgl.texture(); + texture.setImage(quad.image); + quad.texture = quad.image._texture = texture; + } + }); + }; + + /** + * Render all of the color quads using a single mapper. + * + * @param renderState: the render state used for the render. + */ + this._renderColorQuads = function (renderState) { + if (!m_quads.clrQuads.length) { + return; + } + var mapper = this; + if (mapper.getMTime() > m_glColorCompileTimestamp.getMTime() || + m_this.dataTime().getMTime() > m_glColorCompileTimestamp.getMTime() || + renderState.m_contextChanged || !m_clrposbuf || + m_quads.clrQuads.length * 12 > m_clrposbuf.length) { + setupColorDrawObjects(renderState); + } + mapper.s_render(renderState); + + var context = renderState.m_context, opacity = 1, color; + + context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition); + $.each(m_quads.clrQuads, function (idx, quad) { + if (quad.opacity !== opacity) { + opacity = quad.opacity; + context.uniform1fv(renderState.m_material.shaderProgram( + ).uniformLocation('opacity'), new Float32Array([opacity])); + } + if (!color || color.r !== quad.color.r || color.g !== quad.color.g || + color.b !== quad.color.b) { + color = quad.color; + context.uniform3fv(renderState.m_material.shaderProgram( + ).uniformLocation('vertexColor'), new Float32Array([ + color.r, color.g, color.b])); + } + + context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.clrQuadsPosition); + context.vertexAttribPointer(vgl.vertexAttributeKeys.Position, 3, + vgl.GL.FLOAT, false, 12, idx * 12 * 4); + context.enableVertexAttribArray(vgl.vertexAttributeKeys.Position); + + context.drawArrays(vgl.GL.TRIANGLE_STRIP, 0, 4); + }); + context.bindBuffer(vgl.GL.ARRAY_BUFFER, null); + }; + + /** + * Render all of the image quads using a single mapper. + * + * @param renderState: the render state used for the render. + */ + this._renderImageQuads = function (renderState) { + if (!m_quads.imgQuads.length) { + return; + } + var mapper = this; + if (mapper.getMTime() > m_glCompileTimestamp.getMTime() || + m_this.dataTime().getMTime() > m_glCompileTimestamp.getMTime() || + renderState.m_contextChanged || !m_imgposbuf || + m_quads.imgQuads.length * 12 > m_imgposbuf.length) { + setupDrawObjects(renderState); + } + mapper.s_render(renderState); + + var context = renderState.m_context, opacity = 1; + + m_this._updateTextures(); + + context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); + $.each(m_quads.imgQuads, function (idx, quad) { + if (!quad.image) { + return; + } + quad.texture.bind(renderState); + + if (quad.opacity !== opacity) { + opacity = quad.opacity; + context.uniform1fv(renderState.m_material.shaderProgram( + ).uniformLocation('opacity'), new Float32Array([opacity])); + } + + context.bindBuffer(vgl.GL.ARRAY_BUFFER, m_glBuffers.imgQuadsPosition); + context.vertexAttribPointer(vgl.vertexAttributeKeys.Position, 3, + vgl.GL.FLOAT, false, 12, idx * 12 * 4); + context.enableVertexAttribArray(vgl.vertexAttributeKeys.Position); + + context.drawArrays(vgl.GL.TRIANGLE_STRIP, 0, 4); + quad.texture.undoBind(renderState); + }); + context.bindBuffer(vgl.GL.ARRAY_BUFFER, null); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + s_update.call(m_this); + if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime() || + m_this.updateTime().getMTime() < m_this.getMTime()) { + m_this._build(); + } + if (m_actor_color) { + m_actor_color.setVisible(m_this.visible()); + m_actor_color.material().setBinNumber(m_this.bin()); + } + if (m_actor_image) { + m_actor_image.setVisible(m_this.visible()); + m_actor_image.material().setBinNumber(m_this.bin()); + } + m_this.updateTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + s_init.call(m_this, arg); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + if (m_actor_image) { + m_this.renderer().contextRenderer().removeActor(m_actor_image); + m_actor_image = null; + } + if (m_actor_color) { + m_this.renderer().contextRenderer().removeActor(m_actor_color); + m_actor_color = null; + } + s_exit.call(m_this); + }; + + m_this._init(arg); + return this; + }; + + inherit(gl_quadFeature, quadFeature); + + // Now register it + registerFeature('vgl', 'quad', gl_quadFeature); + module.exports = gl_quadFeature; + + +/***/ }, +/* 253 */ +/***/ function(module, exports, __webpack_require__) { + + var registerLayerAdjustment = __webpack_require__(131).registerLayerAdjustment; + + var gl_tileLayer = function () { + 'use strict'; + var m_this = this, + s_init = this._init, + s_exit = this._exit, + m_quadFeature, + m_nextTileId = 0, + m_tiles = []; + + /* Add a tile to the list of quads */ + this._drawTile = function (tile) { + if (!m_quadFeature) { + return; + } + var bounds = this._tileBounds(tile), + level = tile.index.level || 0, + to = this._tileOffset(level), + quad = {}; + quad.ul = this.fromLocal(this.fromLevel({ + x: bounds.left - to.x, y: bounds.top - to.y + }, level), 0); + quad.ll = this.fromLocal(this.fromLevel({ + x: bounds.left - to.x, y: bounds.bottom - to.y + }, level), 0); + quad.ur = this.fromLocal(this.fromLevel({ + x: bounds.right - to.x, y: bounds.top - to.y + }, level), 0); + quad.lr = this.fromLocal(this.fromLevel({ + x: bounds.right - to.x, y: bounds.bottom - to.y + }, level), 0); + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + m_nextTileId += 1; + quad.id = m_nextTileId; + tile.quadId = quad.id; + quad.image = tile.image; + m_tiles.push(quad); + m_quadFeature.data(m_tiles); + m_quadFeature._update(); + m_this.draw(); + }; + + /* Remove the tile feature. */ + this._remove = function (tile) { + if (tile.quadId !== undefined && m_quadFeature) { + for (var i = 0; i < m_tiles.length; i += 1) { + if (m_tiles[i].id === tile.quadId) { + m_tiles.splice(i, 1); + break; + } + } + m_quadFeature.data(m_tiles); + m_quadFeature._update(); + m_this.draw(); + } + }; + + /** + * Clean up the layer. + */ + this._exit = function () { + m_this.deleteFeature(m_quadFeature); + m_quadFeature = null; + m_tiles = []; + s_exit.apply(m_this, arguments); + }; + + /** + * Initialize after the layer is added to the map. + */ + this._init = function () { + s_init.apply(m_this, arguments); + m_quadFeature = this.createFeature('quad', { + previewColor: m_this._options.previewColor, + previewImage: m_this._options.previewImage + }); + m_quadFeature.geoTrigger = undefined; + m_quadFeature.gcs(m_this._options.gcs || m_this.map().gcs()); + m_quadFeature.data(m_tiles); + m_quadFeature._update(); + }; + + /* These functions don't need to do anything. */ + this._getSubLayer = function () {}; + this._updateSubLayer = undefined; + }; + + registerLayerAdjustment('vgl', 'tile', gl_tileLayer); + + module.exports = gl_tileLayer; + + +/***/ }, +/* 254 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerRenderer = __webpack_require__(131).registerRenderer; + var renderer = __webpack_require__(136); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class vglRenderer + * + * @class geo.gl.vglRenderer + * @extends geo.renderer + * @param canvas + * @returns {geo.gl.vglRenderer} + */ + ////////////////////////////////////////////////////////////////////////////// + var vglRenderer = function (arg) { + 'use strict'; + + if (!(this instanceof vglRenderer)) { + return new vglRenderer(arg); + } + arg = arg || {}; + renderer.call(this, arg); + + var $ = __webpack_require__(1); + var vgl = __webpack_require__(6); + var mat4 = __webpack_require__(8); + var util = __webpack_require__(120); + var geo_event = __webpack_require__(125); + + var m_this = this, + m_contextRenderer = null, + m_viewer = null, + m_width = 0, + m_height = 0, + m_renderAnimFrameRef = null, + m_lastZoom, + s_init = this._init, + s_exit = this._exit; + + /// TODO: Move this API to the base class + //////////////////////////////////////////////////////////////////////////// + /** + * Return width of the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.width = function () { + return m_width; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return height of the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.height = function () { + return m_height; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get context specific renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.contextRenderer = function () { + return m_contextRenderer; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get API used by the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.api = function () { + return 'vgl'; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + 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(false); + + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle resize event + */ + //////////////////////////////////////////////////////////////////////////// + this._resize = function (x, y, w, h) { + 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(); + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render. This actually schedules rendering for the next animation frame. + */ + //////////////////////////////////////////////////////////////////////////// + this._render = function () { + if (m_renderAnimFrameRef === null) { + m_renderAnimFrameRef = window.requestAnimationFrame(this._renderFrame); + } + return m_this; + }; + + /** + * This clears the render timer and actually renders. + */ + this._renderFrame = function () { + m_renderAnimFrameRef = null; + m_viewer.render(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Exit + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.canvas().remove(); + m_viewer.exit(); + s_exit(); + }; + + this._updateRendererCamera = function () { + var renderWindow = m_viewer.renderWindow(), + map = m_this.layer().map(), + camera = map.camera(), + rotation = map.rotation() || 0, + 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(util.mat4AsArray(), 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(util.mat4AsArray(), proj, + [0, 0, camera.constructor.bounds.far]); + /* Check if the rotation is a multiple of 90 */ + var basis = Math.PI / 2, + angle = rotation % basis, // move to range (-pi/2, pi/2) + ortho = (Math.min(Math.abs(angle), Math.abs(angle - basis)) < 0.00001); + renderWindow.renderers().forEach(function (renderer) { + var cam = renderer.camera(); + if (util.compareArrays(view, cam.viewMatrix()) && + util.compareArrays(proj, cam.projectionMatrix()) && + m_lastZoom === map.zoom()) { + return; + } + m_lastZoom = map.zoom(); + cam.setViewMatrix(view, true); + 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 || !ortho || + (parseFloat(m_lastZoom.toFixed(6)) !== + parseFloat(m_lastZoom.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 rotation event + m_this.layer().geoOn(geo_event.rotate, 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(); + + 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(); + } + }); + + return this; + }; + + inherit(vglRenderer, renderer); + + registerRenderer('vgl', vglRenderer); + + (function () { + 'use strict'; + + var checkedWebGL; + + /** + * Report if the vgl renderer is supported. This is just a check if webGL is + * supported and available. + * + * @returns {boolean} true if available. + */ + vglRenderer.supported = function () { + if (checkedWebGL === undefined) { + /* This is extracted from what Modernizr uses. */ + var canvas, ctx, exts; // eslint-disable-line no-unused-vars + try { + canvas = document.createElement('canvas'); + ctx = (canvas.getContext('webgl') || + canvas.getContext('experimental-webgl')); + exts = ctx.getSupportedExtensions(); + checkedWebGL = true; + } catch (e) { + console.error('No webGL support'); + checkedWebGL = false; + } + canvas = undefined; + ctx = undefined; + exts = undefined; + } + return checkedWebGL; + }; + + /** + * If the vgl renderer is not supported, supply the name of a renderer that + * should be used instead. This asks for the null renderer. + * + * @returns null for the null renderer. + */ + vglRenderer.fallback = function () { + return null; + }; + + })(); + + module.exports = vglRenderer; + + +/***/ }, +/* 255 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * @namespace geo.canvas + */ + module.exports = { + canvasRenderer: __webpack_require__(256), + quadFeature: __webpack_require__(257), + tileLayer: __webpack_require__(258) + }; + + +/***/ }, +/* 256 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerRenderer = __webpack_require__(131).registerRenderer; + var renderer = __webpack_require__(136); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class canvasRenderer + * + * @class geo.canvas.renderer + * @extends geo.renderer + * @param canvas + * @returns {geo.canvas.canvasRenderer} + */ + ////////////////////////////////////////////////////////////////////////////// + var canvasRenderer = function (arg) { + 'use strict'; + + var $ = __webpack_require__(1); + + if (!(this instanceof canvasRenderer)) { + return new canvasRenderer(arg); + } + arg = arg || {}; + renderer.call(this, arg); + + var m_this = this, + m_renderAnimFrameRef = null, + s_init = this._init, + s_exit = this._exit; + + //////////////////////////////////////////////////////////////////////////// + /** + * Get API used by the renderer + */ + //////////////////////////////////////////////////////////////////////////// + this.api = function () { + return 'canvas'; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + if (m_this.initialized()) { + return m_this; + } + + s_init.call(m_this); + + var canvas = $(document.createElement('canvas')); + m_this.context2d = canvas[0].getContext('2d'); + + canvas.attr('class', 'canvas-canvas'); + $(m_this.layer().node().get(0)).append(canvas); + + 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; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Handle resize event + */ + //////////////////////////////////////////////////////////////////////////// + this._resize = function (x, y, w, h) { + m_this.canvas().attr('width', w); + m_this.canvas().attr('height', h); + m_this._render(); + + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Render + */ + //////////////////////////////////////////////////////////////////////////// + this._render = function () { + if (m_renderAnimFrameRef === null) { + m_renderAnimFrameRef = window.requestAnimationFrame(function () { + m_renderAnimFrameRef = null; + + var layer = m_this.layer(), + map = layer.map(), + camera = map.camera(), + viewport = camera._viewport; + // Clear the canvas. + m_this.context2d.setTransform(1, 0, 0, 1, 0, 0); + m_this.context2d.clearRect(0, 0, viewport.width, viewport.height); + + var features = layer.features(); + for (var i = 0; i < features.length; i += 1) { + features[i]._renderOnCanvas(m_this.context2d, map); + } + }); + } + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Exit + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + m_this.canvas().remove(); + s_exit(); + }; + + return this; + }; + + inherit(canvasRenderer, renderer); + + registerRenderer('canvas', canvasRenderer); + + (function () { + 'use strict'; + + var checkedCanvas; + + /** + * Report if the canvas renderer is supported. + * + * @returns {boolean} true if available. + */ + canvasRenderer.supported = function () { + if (checkedCanvas === undefined) { + /* This is extracted from what Modernizr uses. */ + var canvas; // eslint-disable-line no-unused-vars + try { + canvas = document.createElement('canvas'); + checkedCanvas = !!(canvas.getContext && canvas.getContext('2d')); + } catch (e) { + checkedCanvas = false; + } + canvas = undefined; + } + return checkedCanvas; + }; + + /** + * If the canvas renderer is not supported, supply the name of a renderer that + * should be used instead. This asks for the null renderer. + * + * @returns null for the null renderer. + */ + canvasRenderer.fallback = function () { + return null; + }; + })(); + + module.exports = canvasRenderer; + + +/***/ }, +/* 257 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var registerFeature = __webpack_require__(131).registerFeature; + var quadFeature = __webpack_require__(227); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class quadFeature + * + * @class geo.canvas.quadFeature + * @param {Object} arg Options object + * @extends geo.quadFeature + * @returns {geo.canvas.quadFeature} + */ + ////////////////////////////////////////////////////////////////////////////// + var canvas_quadFeature = function (arg) { + 'use strict'; + + if (!(this instanceof canvas_quadFeature)) { + return new canvas_quadFeature(arg); + } + quadFeature.call(this, arg); + + var $ = __webpack_require__(1); + + var m_this = this, + s_exit = this._exit, + s_init = this._init, + s_update = this._update, + m_quads; + + //////////////////////////////////////////////////////////////////////////// + /** + * Build this feature + */ + //////////////////////////////////////////////////////////////////////////// + this._build = function () { + if (!m_this.position()) { + return; + } + m_quads = this._generateQuads(); + + if (m_quads.imgQuads) { + m_quads.imgQuads.sort(function (a, b) { + return a.pos[2] - b.pos[2]; + }); + } + m_this.buildTime().modified(); + }; + + /** + * Render all of the color quads using a single mapper. + * + * @param renderState: the render state used for the render. + */ + this._renderColorQuads = function (renderState) { + // .... + // Not implemented yet. + }; + + /** + * Render all of the image quads using a single mapper. + * + * @param renderState: the render state used for the render. + */ + this._renderImageQuads = function (context2d, map) { + if (!m_quads.imgQuads.length) { + return; + } + + var oldAlpha = context2d.globalAlpha; + var opacity = oldAlpha; + $.each(m_quads.imgQuads, function (idx, quad) { + if (!quad.image) { + return; + } + var w = quad.image.width, + h = quad.image.height; + // Canvas transform is affine, so quad has to be a parallelogram + // Also, canvas has no way to render z. + var p0 = map.gcsToDisplay({x:quad.pos[0], y:quad.pos[1]}, null), + p3 = map.gcsToDisplay({x:quad.pos[9], y:quad.pos[10]}, null), + p2 = map.gcsToDisplay({x:quad.pos[6], y:quad.pos[7]}, null); + context2d.setTransform((p3.x - p2.x) / w, (p3.y - p2.y) / h, + (p0.x - p2.x) / w, (p0.y - p2.y) / h, + p2.x, p2.y); + if (quad.opacity !== opacity) { + opacity = quad.opacity; + context2d.globalAlpha = opacity; + } + context2d.drawImage(quad.image, 0, 0); + }); + if (opacity !== oldAlpha) { + context2d.globalAlpha = oldAlpha; + } + }; + + this._renderOnCanvas = function (context, map) { + this._renderImageQuads(context, map); + this._renderColorQuads(context, map); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Update + */ + //////////////////////////////////////////////////////////////////////////// + this._update = function () { + s_update.call(m_this); + if (m_this.buildTime().getMTime() <= m_this.dataTime().getMTime() || + m_this.updateTime().getMTime() < m_this.getMTime()) { + m_this._build(); + } + + m_this.updateTime().modified(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Initialize + */ + //////////////////////////////////////////////////////////////////////////// + this._init = function () { + s_init.call(m_this, arg); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Destroy + */ + //////////////////////////////////////////////////////////////////////////// + this._exit = function () { + + s_exit.call(m_this); + }; + + m_this._init(arg); + return this; + }; + + inherit(canvas_quadFeature, quadFeature); + + // Now register it + registerFeature('canvas', 'quad', canvas_quadFeature); + module.exports = canvas_quadFeature; + + +/***/ }, +/* 258 */ +/***/ function(module, exports, __webpack_require__) { + + var registerLayerAdjustment = __webpack_require__(131).registerLayerAdjustment; + + var canvas_tileLayer = function () { + 'use strict'; + var m_this = this, + s_init = this._init, + s_exit = this._exit, + m_quadFeature, + m_nextTileId = 0, + m_tiles = []; + + /* Add a tile to the list of quads */ + this._drawTile = function (tile) { + if (!m_quadFeature) { + return; + } + var bounds = this._tileBounds(tile), + level = tile.index.level || 0, + to = this._tileOffset(level), + quad = {}; + quad.ul = this.fromLocal(this.fromLevel({ + x: bounds.left - to.x, y: bounds.top - to.y + }, level), 0); + quad.ll = this.fromLocal(this.fromLevel({ + x: bounds.left - to.x, y: bounds.bottom - to.y + }, level), 0); + quad.ur = this.fromLocal(this.fromLevel({ + x: bounds.right - to.x, y: bounds.top - to.y + }, level), 0); + quad.lr = this.fromLocal(this.fromLevel({ + x: bounds.right - to.x, y: bounds.bottom - to.y + }, level), 0); + quad.ul.z = quad.ll.z = quad.ur.z = quad.lr.z = level * 1e-5; + m_nextTileId += 1; + quad.id = m_nextTileId; + tile.quadId = quad.id; + quad.image = tile.image; + m_tiles.push(quad); + m_quadFeature.data(m_tiles); + m_quadFeature._update(); + m_this.draw(); + }; + + /* Remove the tile feature. */ + this._remove = function (tile) { + if (tile.quadId !== undefined && m_quadFeature) { + for (var i = 0; i < m_tiles.length; i += 1) { + if (m_tiles[i].id === tile.quadId) { + m_tiles.splice(i, 1); + break; + } + } + m_quadFeature.data(m_tiles); + m_quadFeature._update(); + m_this.draw(); + } + }; + + /** + * Clean up the layer. + */ + this._exit = function () { + m_this.deleteFeature(m_quadFeature); + m_quadFeature = null; + m_tiles = []; + s_exit.apply(m_this, arguments); + }; + + /** + * Initialize after the layer is added to the map. + */ + this._init = function () { + s_init.apply(m_this, arguments); + m_quadFeature = this.createFeature('quad', { + previewColor: m_this._options.previewColor, + previewImage: m_this._options.previewImage + }); + m_quadFeature.geoTrigger = undefined; + m_quadFeature.gcs(m_this._options.gcs || m_this.map().gcs()); + m_quadFeature.data(m_tiles); + m_quadFeature._update(); + }; + + /* These functions don't need to do anything. */ + this._getSubLayer = function () {}; + this._updateSubLayer = undefined; + }; + + registerLayerAdjustment('canvas', 'tile', canvas_tileLayer); + module.exports = canvas_tileLayer; + + +/***/ }, +/* 259 */ +/***/ function(module, exports, __webpack_require__) { + + /** + * @namespace geo.gui + */ + module.exports = { + domWidget: __webpack_require__(260), + legendWidget: __webpack_require__(262), + sliderWidget: __webpack_require__(264), + svgWidget: __webpack_require__(263), + uiLayer: __webpack_require__(218), + widget: __webpack_require__(261) + }; + + +/***/ }, +/* 260 */ +/***/ function(module, exports, __webpack_require__) { + + var widget = __webpack_require__(261); + var inherit = __webpack_require__(4); + var registerWidget = __webpack_require__(131).registerWidget; + + var domWidget = function (arg) { + 'use strict'; + if (!(this instanceof domWidget)) { + return new domWidget(arg); + } + + 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(domWidget, widget); + + registerWidget('dom', 'dom', domWidget); + module.exports = domWidget; + + +/***/ }, +/* 261 */ +/***/ function(module, exports, __webpack_require__) { + + var inherit = __webpack_require__(4); + var sceneObject = __webpack_require__(128); + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class widget + * + * @class geo.gui.widget + * @extends {geo.sceneObject} + * @returns {geo.gui.widget} + */ + ////////////////////////////////////////////////////////////////////////////// + var widget = function (arg) { + 'use strict'; + if (!(this instanceof widget)) { + return new widget(arg); + } + sceneObject.call(this, arg); + + var geo_event = __webpack_require__(125); + var createFeature = __webpack_require__(131).createFeature; + + var m_this = this, + s_exit = this._exit, + 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 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(); + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Create feature give a name + * + * @returns {geo.Feature} Will return a new feature + */ + //////////////////////////////////////////////////////////////////////////// + this._createFeature = function (featureName, arg) { + + var newFeature = createFeature( + featureName, m_this, m_this.renderer(), arg); + + m_this.addChild(newFeature); + m_this.modified(); + return newFeature; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Delete feature + */ + //////////////////////////////////////////////////////////////////////////// + this._deleteFeature = function (feature) { + m_this.removeChild(feature); + feature._exit(); + return m_this; + }; + + //////////////////////////////////////////////////////////////////////////// + /** + * Return the layer associated with this widget. + */ + //////////////////////////////////////////////////////////////////////////// + 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 (pos) { + if (pos !== undefined) { + arg.position = pos; + this.reposition(); + return this; + } + 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(widget, sceneObject); + module.exports = widget; + + +/***/ }, +/* 262 */ +/***/ function(module, exports, __webpack_require__) { + + var svgWidget = __webpack_require__(263); + var inherit = __webpack_require__(4); + var registerWidget = __webpack_require__(131).registerWidget; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class legendWidget + * + * @class geo.gui.legendWidget + * @extends geo.gui.svgWidget + * @returns {geo.gui.legendWidget} + */ + ////////////////////////////////////////////////////////////////////////////// + var legendWidget = function (arg) { + 'use strict'; + if (!(this instanceof legendWidget)) { + return new legendWidget(arg); + } + svgWidget.call(this, arg); + + var d3 = __webpack_require__(132); + var geo_event = __webpack_require__(125); + + /** @private */ + var m_this = this, + m_categories = [], + m_top = null, + m_group = null, + m_border = null, + m_spacing = 20, // distance in pixels between lines + m_padding = 12, // padding in pixels inside the border + s_createCanvas = this._createCanvas, + s_appendChild = this._appendChild; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get or set the category array associated with + * the legend. Each element of this array is + * an object: :: + * { + * name: string, + * style: object, + * type: 'point' | 'line' | ... + * } + * + * The style property can contain the following feature styles: + * * fill: bool + * * fillColor: object | string + * * fillOpacity: number + * * stroke: bool + * * strokeColor: object | string + * * strokeWidth: number + * * strokeOpacity: number + * + * The type controls how the element is displayed, point as a circle, + * line as a line segment. Any other value will display as a rounded + * rectangle. + * + * @param {object[]?} categories The categories to display + */ + ////////////////////////////////////////////////////////////////////////////// + this.categories = function (arg) { + if (arg === undefined) { + return m_categories.slice(); + } + m_categories = arg.slice().map(function (d) { + if (d.type === 'line') { + d.style.fill = false; + d.style.stroke = true; + } + return d; + }); + m_this.draw(); + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get the widget's size + * @return {{width: number, height: number}} The size in pixels + */ + ////////////////////////////////////////////////////////////////////////////// + this.size = function () { + var width = 1, height; + var test = d3.select(m_this.canvas()).append('text') + .style('opacity', 1e-6); + + 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); + return { + width: width + 50, + height: height + }; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Redraw the legend + */ + ////////////////////////////////////////////////////////////////////////////// + this.draw = function () { + + 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'; + } + }) + .style('fill-opacity', function (d) { + if (d.style.fillOpacity === undefined) { + return 1; + } + return d.style.fillOpacity; + }) + .style('stroke', function (d) { + if (d.style.stroke || d.style.stroke === undefined) { + return d.style.strokeColor; + } else { + return 'none'; + } + }) + .style('stroke-opacity', function (d) { + if (d.style.strokeOpacity === undefined) { + return 1; + } + return d.style.strokeOpacity; + }) + .style('stroke-width', function (d) { + if (d.style.strokeWidth === undefined) { + return 1.5; + } + return d.style.strokeWidth; + }); + } + + m_border.attr('height', m_this.size().height + 2 * m_padding) + .style('display', null); + + var scale = m_this._scale(); + + var labels = m_group.selectAll('g.geo-label') + .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) + ')'; + }); + + 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) + ); + + applyColor(g.filter(function (d) { + 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) + ); + + g.append('text') + .attr('x', '50px') + .attr('y', 0) + .attr('dy', '0.3em') + .text(function (d) { + return d.name; + }); + + m_this.reposition(); + + return m_this; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Get scales for the x and y axis for the current size. + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + 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]) + }; + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Private initialization. Creates the widget's DOM container and internal + * variables. + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + this._init = function () { + // 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(); + } + + 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': 0.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', 0.75); + }); + + m_this.reposition(); + }; + + this.geoOn(geo_event.resize, function () { + m_this.draw(); + }); + + }; + + inherit(legendWidget, svgWidget); + + registerWidget('dom', 'legend', legendWidget); + module.exports = legendWidget; + + +/***/ }, +/* 263 */ +/***/ function(module, exports, __webpack_require__) { + + var domWidget = __webpack_require__(260); + var inherit = __webpack_require__(4); + var registerWidget = __webpack_require__(131).registerWidget; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 geo.gui.svgWidget + * @extends geo.gui.domWidget + * @returns {geo.gui.svgWidget} + * + */ + ////////////////////////////////////////////////////////////////////////////// + var svgWidget = function (arg) { + 'use strict'; + if (!(this instanceof svgWidget)) { + return new svgWidget(arg); + } + + domWidget.call(this, arg); + + var d3Renderer = __webpack_require__(239); + + 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 = d3Renderer(rendererOpts); + + m_this.canvas(m_renderer.canvas()[0][0]); + }; + + return this; + }; + + inherit(svgWidget, domWidget); + + registerWidget('dom', 'svg', svgWidget); + module.exports = svgWidget; + + +/***/ }, +/* 264 */ +/***/ function(module, exports, __webpack_require__) { + + var svgWidget = __webpack_require__(263); + var inherit = __webpack_require__(4); + var registerWidget = __webpack_require__(131).registerWidget; + + ////////////////////////////////////////////////////////////////////////////// + /** + * Create a new instance of class sliderWidget + * + * @class geo.gui.sliderWidget + * @extends {geo.gui.svgWidget} + * @returns {geo.gui.sliderWidget} + */ + ////////////////////////////////////////////////////////////////////////////// + var sliderWidget = function (arg) { + 'use strict'; + if (!(this instanceof sliderWidget)) { + return new sliderWidget(arg); + } + svgWidget.call(this, arg); + + var d3 = __webpack_require__(132); + var geo_event = __webpack_require__(125); + + var m_this = this, + s_exit = this._exit, + s_createCanvas = this._createCanvas, + s_appendChild = this._appendChild, + m_xscale, + m_yscale, + m_plus, + m_minus, + m_nub, + m_width = 20, // Approximate size of the widget in pixels + m_height = 100, + m_nubSize = 10, + m_plusIcon, + m_minusIcon, + m_group, + m_lowContrast, + m_highlightDur = 100; + + /* http://icomoon.io */ + /* CC BY 3.0 http://creativecommons.org/licenses/by/3.0/ */ + 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'; + + // Define off-white gray colors for low contrast ui (unselected). + m_lowContrast = { + white: '#f4f4f4', + 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 + */ + ////////////////////////////////////////////////////////////////////////////// + function put_icon(icon, base, cx, cy, size) { + var g = base.append('g'); + + // the scale factor + var s = size / 1024; + + g.append('g') + .append('g') + .attr( + 'transform', + 'translate(' + cx + ',' + cy + ') scale(' + s + ') translate(-512,-512)' + ) + .append('path') + .attr('d', icon) + .attr('class', 'geo-glyphicon'); + + return g; + } + + ////////////////////////////////////////////////////////////////////////////// + /** + * Initialize the slider widget in the map. + * + * @function + * @returns {geo.gui.sliderWidget} + * @private + */ + ////////////////////////////////////////////////////////////////////////////// + this._init = function () { + 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(); + + // create d3 scales for positioning + // TODO: make customizable and responsive + 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]); + + // Create the main group element + svg = svg.append('g').classed('geo-ui-slider', true); + m_group = svg; + + // Create + zoom button + m_plus = svg.append('g'); + m_plus.append('circle') + .datum({ + fill: 'white', + stroke: null + }) + .classed('geo-zoom-in', true) + .attr('cx', m_xscale(0)) + .attr('cy', m_yscale(0.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 + }); + + // Create the - zoom button + m_minus = svg.append('g'); + m_minus.append('circle') + .datum({ + fill: 'white', + stroke: null + }) + .classed('geo-zoom-out', true) + .attr('cx', m_xscale(0)) + .attr('cy', m_yscale(1.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_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 + }); + + // Respond to a mouse event on the widget + 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; + if (trans) { + map.transition({ + zoom: z, + ease: d3.ease('cubic-in-out'), + duration: 500, + done: m_this._update() + }); + } else { + map.zoom(z); + m_this._update(); + } + evt.stopPropagation(); + } + + // Create the track + svg.append('rect') + .datum({ + fill: 'white', + stroke: 'black' + }) + .classed('geo-zoom-track', true) + .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, true); + }); + + // Create the nub + m_nub = svg.append('rect') + .datum({ + fill: 'black', + stroke: null + }) + .classed('geo-zoom-nub', true) + .attr('x', m_xscale(-4)) + .attr('y', m_yscale(0.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; + }); + + }; + + var 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); + + // Update the nub position on zoom + m_this.layer().geoOn(geo_event.zoom, function () { + m_this._update(); + }); + + mouseOut(); + m_this._update(); + }; + + ////////////////////////////////////////////////////////////////////////////// + /** + * 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 + */ + ////////////////////////////////////////////////////////////////////////////// + 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(true); + + m_nub.attr('y', m_yscale(zoomScale(zoom)) - m_nubSize / 2); + }; + }; + + inherit(sliderWidget, svgWidget); + + registerWidget('dom', 'slider', sliderWidget); + module.exports = sliderWidget; + + +/***/ } +/******/ ]) +}); +; \ No newline at end of file diff --git a/package.json b/package.json index 9803053cef..cc46faa1aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "geojs", - "version": "0.8.0", + "version": "0.9.0", "description": "JavaScript Geo visualization and Analysis Library", "homepage": "https://github.com/OpenGeoscience/geojs", "license": "Apache-2.0",