From 438725f8a577f8acdb10dbd4abfb2f2caec23c6a Mon Sep 17 00:00:00 2001 From: David Manthey Date: Thu, 1 Nov 2018 16:42:30 -0400 Subject: [PATCH] Test layer.js. Move the layer specification to a typedef for better documentation. Make sure all layer types use typedefs. Fix issues with the layer id function. Ensures that canvas and renderer can actually be specified when creating a layer. This allows layers to share a renderer or a canvas. For the webgl renderer, sharing a canvas results in flashing, as each layer completely redraws the canvas. When sharing a renderer, there is no proper support for opacity (probably not for visibility) or z-order, but this is a first step to implementing #942. Removed some unused code. --- CHANGELOG.md | 3 + src/annotationLayer.js | 60 +++++---- src/canvas/canvasRenderer.js | 4 +- src/d3/d3Renderer.js | 2 +- src/featureLayer.js | 2 +- src/gl/vglRenderer.js | 2 +- src/layer.js | 143 ++++++++++---------- src/osmLayer.js | 13 +- src/tileLayer.js | 158 +++++++++++----------- src/ui/uiLayer.js | 2 +- tests/cases/layer.js | 252 +++++++++++++++++++++++++++++++++++ 11 files changed, 461 insertions(+), 180 deletions(-) create mode 100644 tests/cases/layer.js diff --git a/CHANGELOG.md b/CHANGELOG.md index fc18c3ad35..f6d42afc94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ - Removed the dependency on the vgl module for the `object` and `timestamp` classes (#918) - CSS color names are obtained from an npm package rather than being defined in the util module (#936) +### Bug Fixes +- Fixed some minor issues with layers (#949) + ## Version 0.18.1 ### Bug Fixes diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 8747b53d1a..51e5f25686 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -8,6 +8,34 @@ var $ = require('jquery'); var Mousetrap = require('mousetrap'); var textFeature = require('./textFeature'); +/** + * Object specification for an annotation layer. + * + * @typedef {geo.layer.spec} geo.annotationLayer.spec + * @extends {geo.layer.spec} + * @property {number} [dblClickTime=300] The delay in milliseconds that is + * treated as a double-click when working with annotations. + * @property {number} [adjacentPointProximity=5] The minimum distance in + * display coordinates (pixels) between two adjacent points when creating a + * polygon or line. A value of 0 requires an exact match. + * @property {number} [continuousPointProximity=5] The minimum distance in + * display coordinates (pixels) between two adjacent points when dragging + * to create an annotation. `false` disables continuous drawing mode. + * @property {number} [continuousPointColinearity=1.0deg] The minimum angle + * between a series of three points when dragging to not interpret them as + * colinear. Only applies if `continuousPointProximity` is not `false`. + * @property {number} [finalPointProximity=10] The maximum distance in display + * coordinates (pixels) between the starting point and the mouse coordinates + * to signal closing a polygon. A value of 0 requires an exact match. A + * negative value disables closing a polygon by clicking on the start point. + * @property {boolean} [showLabels=true] Truthy to show feature labels that are + * allowed by the associated feature to be shown. + * @property {boolean} [clickToEdit=false] Truthy to allow clicking an + * annotation to place it in edit mode. + * @property {geo.textFeature.styleSpec} [defaultLabelStyle] Default styles for + * labels. + */ + /** * @typedef {object} geo.annotationLayer.labelRecord * @property {string} text The text of the label @@ -26,41 +54,19 @@ var textFeature = require('./textFeature'); * @class * @alias geo.annotationLayer * @extends geo.featureLayer - * @param {object} [args] Layer options. - * @param {number} [args.dblClickTime=300] The delay in milliseconds that is - * treated as a double-click when working with annotations. - * @param {number} [args.adjacentPointProximity=5] The minimum distance in - * display coordinates (pixels) between two adjacent points when creating a - * polygon or line. A value of 0 requires an exact match. - * @param {number} [args.continuousPointProximity=5] The minimum distance in - * display coordinates (pixels) between two adjacent points when dragging - * to create an annotation. `false` disables continuous drawing mode. - * @param {number} [args.continuousPointColinearity=1.0deg] The minimum - * angle between a series of three points when dragging to not interpret - * them as colinear. Only applies if `continuousPointProximity` is not - * `false`. - * @param {number} [args.finalPointProximity=10] The maximum distance in - * display coordinates (pixels) between the starting point and the mouse - * coordinates to signal closing a polygon. A value of 0 requires an exact - * match. A negative value disables closing a polygon by clicking on the - * start point. - * @param {boolean} [args.showLabels=true] Truthy to show feature labels that - * are allowed by the associated feature to be shown. - * @param {boolean} [args.clickToEdit=false] Truthy to allow clicking an - * annotation to place it in edit mode. - * @param {object} [args.defaultLabelStyle] Default styles for labels. + * @param {geo.annotationLayer.spec} [arg] Specification for the new layer. * @returns {geo.annotationLayer} * @fires geo.event.annotation.state * @fires geo.event.annotation.coordinates * @fires geo.event.annotation.edit_action * @fires geo.event.annotation.select_edit_handle */ -var annotationLayer = function (args) { +var annotationLayer = function (arg) { 'use strict'; if (!(this instanceof annotationLayer)) { - return new annotationLayer(args); + return new annotationLayer(arg); } - featureLayer.call(this, args); + featureLayer.call(this, arg); var mapInteractor = require('./mapInteractor'); var timestamp = require('./timestamp'); @@ -130,7 +136,7 @@ var annotationLayer = function (args) { finalPointProximity: 10, // in pixels, 0 is exact showLabels: true, clickToEdit: false - }, args); + }, arg); /** * Process an action event. If we are in rectangle-creation mode, this diff --git a/src/canvas/canvasRenderer.js b/src/canvas/canvasRenderer.js index 2259d1e4d9..e7160b320f 100644 --- a/src/canvas/canvasRenderer.js +++ b/src/canvas/canvasRenderer.js @@ -61,7 +61,7 @@ var canvasRenderer = function (arg) { s_init.call(m_this); - var canvas = $(document.createElement('canvas')); + var canvas = arg.canvas || $(document.createElement('canvas')); m_this.context2d = canvas[0].getContext('2d'); canvas.addClass('canvas-canvas'); @@ -113,7 +113,7 @@ var canvasRenderer = function (arg) { var layer = m_this.layer(), map = layer.map(), mapSize = map.size(), - features = layer.features(), + features = layer.features ? layer.features() : [], i; for (i = 0; i < features.length; i += 1) { diff --git a/src/d3/d3Renderer.js b/src/d3/d3Renderer.js index ab4efd12d1..6ab51b586b 100644 --- a/src/d3/d3Renderer.js +++ b/src/d3/d3Renderer.js @@ -380,7 +380,7 @@ var d3Renderer = function (arg) { .attr('mode', 'normal'); if (!arg.widget) { - canvas = m_svg.append('g'); + canvas = arg.canvas || m_svg.append('g'); } shadow = m_defs.append('filter') diff --git a/src/featureLayer.js b/src/featureLayer.js index 39927af516..4e46f677df 100644 --- a/src/featureLayer.js +++ b/src/featureLayer.js @@ -10,7 +10,7 @@ var registry = require('./registry'); * @class * @alias geo.featureLayer * @extends geo.layer - * @param {object} arg Options for the new layer. + * @param {geo.layer.spec} [arg] Specification for the new layer. * @returns {geo.featureLayer} */ var featureLayer = function (arg) { diff --git a/src/gl/vglRenderer.js b/src/gl/vglRenderer.js index c766957371..190c03ad86 100644 --- a/src/gl/vglRenderer.js +++ b/src/gl/vglRenderer.js @@ -89,7 +89,7 @@ var vglRenderer = function (arg) { s_init.call(m_this); - var canvas = $(document.createElement('canvas')); + var canvas = arg.canvas || $(document.createElement('canvas')); canvas.addClass('webgl-canvas'); $(m_this.layer().node().get(0)).append(canvas); diff --git a/src/layer.js b/src/layer.js index c4cddc1e6b..59bdf4563b 100644 --- a/src/layer.js +++ b/src/layer.js @@ -6,41 +6,52 @@ var rendererForFeatures = require('./registry').rendererForFeatures; var rendererForAnnotations = require('./registry').rendererForAnnotations; /** - * Create a new layer. + * Object specification for a layer. * - * @class - * @alias geo.layer - * @extends geo.sceneObject - * @param {object} [arg] Options for the new layer. - * @param {number} [arg.id] The id of the layer. Defaults to a increasing + * @typedef {object} geo.layer.spec + * @property {number} [id] The id of the layer. Defaults to a increasing * sequence. - * @param {geo.map} [arg.map=null] Parent map of the layer. - * @param {string|geo.renderer} [arg.renderer] Renderer to associate with the - * layer. If not specified, either `arg.annotations` or `arg.features` can - * be used to determine the renderer. - * @param {string[]|object} [arg.annotations] A list of annotations that will - * be used on this layer, used to select a renderer. Instead of a list, if + * @property {geo.map} [map=null] Parent map of the layer. + * @property {string|geo.renderer} [renderer] Renderer to associate with the + * layer. If not specified, either `annotations` or `features` can be used + * to determine the renderer. If a `geo.renderer` instance, the renderer is + * not recreated; not all renderers can be shared by multiple layers. + * @property {HTMLElement} [canvas] If specified, use this canvas rather than + * a canvas associaied with the renderer directly. Renderers may not support + * sharing a canvas. + * @property {string[]|object} [annotations] A list of annotations that will be + * used on this layer, used to select a renderer. Instead of a list, if * this is an object, the keys are the annotation names, and the values are * each a list of modes that will be used with that annotation. See - * `featuresForAnnotations` more details. This is ignored if `arg.renderer` - * is specified. - * @param {string[]} [arg.features] A list of features that will be used on - * this layer, used to select a renderer. Features are the basic feature - * names (e.g., `'quad'`), or the feature name followed by a required - * capability (e.g., `'quad.image'`). This is ignored if `arg.renderer` or - * `arg.annotations` is specified. - * @param {boolean} [arg.active=true] Truthy if the layer has the `active` css + * `featuresForAnnotations` more details. This is ignored if `renderer` is + * specified. + * @property {string[]} [features] A list of features that will be used on this + * layer, used to select a renderer. Features are the basic feature names + * (e.g., `'quad'`), or the feature name followed by a required capability + * (e.g., `'quad.image'`). This is ignored if `renderer` or `annotations` is + * specified. + * @property {boolean} [active=true] Truthy if the layer has the `active` css * class and may receive native mouse events. - * @param {string} [arg.attribution] An attribution string to display. - * @param {number} [arg.opacity=1] The layer opacity on a scale of [0-1]. - * @param {string} [arg.name=''] A name for the layer for user convenience. - * @param {boolean} [arg.selectionAPI=true] Truthy if the layer can generate + * @property {string} [attribution] An attribution string to display. + * @property {number} [opacity=1] The layer opacity on a scale of [0-1]. + * @property {string} [name=''] A name for the layer for user convenience. If + * specified, this is also the `id` property of the containing DOM element. + * @property {boolean} [selectionAPI=true] Truthy if the layer can generate * selection and other interaction events. - * @param {boolean} [arg.sticky=true] Truthy if the layer should navigate with + * @property {boolean} [sticky=true] Truthy if the layer should navigate with * the map. - * @param {boolean} [arg.visible=true] Truthy if the layer is visible. - * @param {number} [arg.zIndex] The z-index to assign to the layer (defaults - * to the index of the layer inside the map). + * @property {boolean} [visible=true] Truthy if the layer is visible. + * @property {number} [zIndex] The z-index to assign to the layer (defaults to + * the index of the layer inside the map). + */ + +/** + * Create a new layer. + * + * @class + * @alias geo.layer + * @extends geo.sceneObject + * @param {geo.layer.spec} [arg] Specification for the new layer. * @returns {geo.layer} */ var layer = function (arg) { @@ -54,10 +65,9 @@ var layer = function (arg) { var $ = require('jquery'); var timestamp = require('./timestamp'); + var renderer = require('./renderer'); var createRenderer = require('./registry').createRenderer; - var newLayerId = require('./util').newLayerId; var geo_event = require('./event'); - var camera = require('./camera'); /** * @private @@ -68,10 +78,11 @@ var layer = function (arg) { m_name = arg.name === undefined ? '' : arg.name, m_map = arg.map === undefined ? null : arg.map, m_node = null, - m_canvas = null, - m_renderer = null, + m_canvas = arg.canvas === undefined ? null : arg.canvas, + m_renderer = arg.renderer instanceof renderer ? arg.renderer : null, m_initialized = false, - m_rendererName = arg.renderer !== undefined ? arg.renderer : ( + m_rendererName = arg.renderer !== undefined ? ( + arg.renderer instanceof renderer ? arg.renderer.api() : arg.renderer) : ( arg.annotations ? rendererForAnnotations(arg.annotations) : rendererForFeatures(arg.features)), m_dataTime = timestamp(), @@ -261,14 +272,15 @@ var layer = function (arg) { /** * Get/Set id of the layer. * - * @param {string} [val] If specified, the new id of the layer. + * @param {string|null} [val] If `null`, generate a new layer id. Otherwise, + * if specified, the new id of the layer. * @returns {string|this} */ this.id = function (val) { if (val === undefined) { return m_id; } - m_id = newLayerId(); + m_id = val === null ? layer.newLayerId() : val; m_this.modified(); return m_this; }; @@ -284,6 +296,7 @@ var layer = function (arg) { return m_name; } m_name = val; + m_node.attr('id', m_name); m_this.modified(); return m_this; }; @@ -361,9 +374,6 @@ var layer = function (arg) { * @returns {geo.geoPosition} Renderer coordinates. */ this.toLocal = function (input) { - if (m_this._toLocalMatrix) { - camera.applyTransform(m_this._toLocalMatrix, input); - } return input; }; @@ -374,9 +384,6 @@ var layer = function (arg) { * @returns {geo.geoPosition} World coordinates. */ this.fromLocal = function (input) { - if (m_this._fromLocalMatrix) { - camera.applyTransform(m_this._fromLocalMatrix, input); - } return input; }; @@ -454,9 +461,10 @@ var layer = function (arg) { var options = $.extend({}, arg); delete options.map; - if (m_rendererName === null) { - // if given a "null" renderer, then pass the map element as the - // canvas + if (m_renderer) { + m_canvas = m_renderer.canvas(); + } else 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 we have valid one @@ -512,8 +520,10 @@ var layer = function (arg) { * Update layer. * * This is a stub that should be subclasses. + * @returns {this} */ this._update = function () { + return m_this; }; /** @@ -537,13 +547,13 @@ var layer = function (arg) { /** * Get or set the current layer opacity. The opacity is in the range [0-1]. * - * @param {number} [opac] If specified, set the opacity. Otherwise, return - * the opacity. + * @param {number} [opacity] If specified, set the opacity. Otherwise, + * return the opacity. * @returns {number|this} The current opacity or the current layer. */ - this.opacity = function (opac) { - if (opac !== undefined) { - m_opacity = opac; + this.opacity = function (opacity) { + if (opacity !== undefined) { + m_opacity = opacity; m_node.css('opacity', m_opacity); return m_this; } @@ -553,7 +563,9 @@ var layer = function (arg) { // Create top level div for the layer m_node = $(document.createElement('div')); m_node.addClass('geojs-layer'); - m_node.attr('id', m_name); + if (m_name) { + m_node.attr('id', m_name); + } m_this.opacity(m_opacity); // set the z-index (this prevents duplication) @@ -589,13 +601,13 @@ layer.newLayerId = (function () { /** * General object specification for feature types. - * @typedef geo.layer.spec - * @type {object} + * @typedef {geo.layer.spec} geo.layer.createSpec + * @extends {geo.layer.spec} * @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 + * feature if none exists. + * @property {string} [renderer='vgl'] The renderer to use. * @property {geo.feature.spec[]} [features=[]] Features to add to the layer. */ @@ -603,7 +615,7 @@ layer.newLayerId = (function () { * 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 + * @param {geo.layer.createSpec} spec The layer specification. * @returns {geo.layer|null} */ layer.create = function (map, spec) { @@ -611,14 +623,9 @@ layer.create = function (map, spec) { spec = spec || {}; - // add osmLayer later - spec.type = 'feature'; - if (spec.type !== 'feature') { - console.warn('Unsupported layer type'); - return null; - } + spec.type = spec.type || 'feature'; - spec.renderer = spec.renderer || 'vgl'; + spec.renderer = spec.renderer === undefined ? 'vgl' : spec.renderer; spec.renderer = checkRenderer(spec.renderer); if (!spec.renderer) { @@ -632,11 +639,13 @@ layer.create = function (map, spec) { 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); - }); + if (spec.features) { + // 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; }; diff --git a/src/osmLayer.js b/src/osmLayer.js index e2f6af995e..ffc0aef9fb 100644 --- a/src/osmLayer.js +++ b/src/osmLayer.js @@ -4,6 +4,15 @@ var tileLayer = require('./tileLayer'); var registry = require('./registry'); var quadFeature = require('./quadFeature'); +/** + * Object specification for an OSM layer. + * + * @typedef {geo.tileLayer.spec} geo.osmLayer.spec + * @extends {geo.tileLayer.spec} + * @property {number} [mapOpacity] If specified, and `opacity` is not + * specified, use this as the layer opacity. + */ + /** * Create a new instance of osmLayer. This is a {@link geo.tileLayer} with * an OSM url and attribution defaults and with the tiles centered on the @@ -13,9 +22,7 @@ var quadFeature = require('./quadFeature'); * @alias geo.osmLayer * @extends geo.tileLayer * - * @param {object} arg - * @param {number} [arg.mapOpacity] If specified, and `arg.opacity` is not - * specified, use this as the layer opacity. + * @param {geo.osmLayer.spec} [arg] Specification for the layer. */ var osmLayer = function (arg) { diff --git a/src/tileLayer.js b/src/tileLayer.js index 25bb31e89d..e5da1a8e56 100644 --- a/src/tileLayer.js +++ b/src/tileLayer.js @@ -1,6 +1,68 @@ var inherit = require('./inherit'); var featureLayer = require('./featureLayer'); +/** + * Object specification for a tile layer. + * + * @typedef {geo.layer.spec} geo.tileLayer.spec + * @extends {geo.layer.spec} + * @property {number} [minLevel=0] The minimum zoom level available. + * @property {number} [maxLevel=18] The maximum zoom level available. + * @property {object} [tileOverlap] Pixel overlap between tiles. + * @property {number} [tileOverlap.x] Horizontal overlap. + * @property {number} [tileOverlap.y] Vertical overlap. + * @property {number} [tileWidth=256] The tile width without overlap. + * @property {number} [tileHeight=256] The tile height without overlap. + * @property {function} [tilesAtZoom=null] A function that is given a zoom + * level and returns `{x: (num), y: (num)}` with the number of tiles at that + * zoom level. + * @property {number} [cacheSize=400] The maximum number of tiles to cache. + * The default is 200 if keepLower is false. + * @property {boolean} [keepLower=true] When truthy, keep lower zoom level + * tiles when showing high zoom level tiles. This uses more memory but + * results in smoother transitions. + * @property {boolean} [wrapX=true] Wrap in the x-direction. + * @property {boolean} [wrapY=false] Wrap in the y-direction. + * @property {string|function} [url=null] A function taking the current tile + * indices `(x, y, level, subdomains)` 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 and may + * contain a comma-separated list of subdomains. + * @property {string|list} [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. + * @property {string} [baseUrl=null] If defined, use the old-style base url + * instead of the url parameter. This is functionally the same as using a + * url of `baseUrl/{z}/{x}/{y}.(imageFormat || png)`. If the specified + * string does not end in a slash, one is added. + * @property {string} [imageFormat='png'] This is only used if a `baseUrl` is + * specified, in which case it determines the image name extension used in + * the url. + * @property {number} [animationDuration=0] The number of milliseconds for the + * tile loading animation to occur. Only some renderers support this. + * @property {string} [attribution] An attribution to display with the layer + * (accepts HTML). + * @property {function} [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. + * @property {function} [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. + * @property {function} [tilesMaxBounds=null] This function takes a zoom level + * argument and returns an object with `x` and `y` in pixels which is used to + * crop the last row and column of tiles. Note that if tiles wrap, only + * complete tiles in the wrapping direction(s) are supported, and this max + * bounds will probably not behave properly. + * @property {boolean} [topDown=false] True if the gcs is top-down, false if + * bottom-up (the ingcs does not matter, only the gcs coordinate system). + * When falsy, this inverts the gcs y-coordinate when calculating local + * coordinates. + */ + /** * Standard modulo operator where the output is in [0, b) for all inputs. * @private @@ -90,73 +152,15 @@ function m_tileUrlFromTemplate(base) { * @class * @alias 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 {object} [options.tileOverlap] Pixel overlap between tiles. - * @param {number} [options.tileOverlap.x] Horizontal overlap. - * @param {number} [options.tileOverlap.y] Vertical overlap. - * @param {number} [options.tileWidth=256] The tile width without overlap. - * @param {number} [options.tileHeight=256] The tile height without overlap. - * @param {function} [options.tilesAtZoom=null] A function that is given a - * zoom level and returns `{x: (num), y: (num)}` with the number of tiles - * at that zoom level. - * @param {number} [options.cacheSize=400] The maximum number of tiles to - * cache. The default is 200 if keepLower is false. - * @param {boolean} [options.keepLower=true] When truthy, keep lower zoom - * level tiles when showing high zoom level tiles. This uses more memory - * but results in smoother transitions. - * @param {boolean} [options.wrapX=true] Wrap in the x-direction. - * @param {boolean} [options.wrapY=false] Wrap in the y-direction. - * @param {string|function} [options.url=null] A function taking the current - * tile indices `(x, y, level, subdomains)` 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 and may - * contain a comma-separated list of subdomains. - * @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. Only some renderers support - * this. - * @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 {function} [options.tilesMaxBounds=null] This function takes a zoom - * level argument and returns an object with `x` and `y` in pixels which is - * used to crop the last row and column of tiles. Note that if tiles wrap, - * only complete tiles in the wrapping direction(s) are supported, and this - * max bounds will probably not behave properly. - * @param {boolean} [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 falsy, this inverts the gcs y-coordinate when calculating - * local coordinates. + * @param {geo.tileLayer.spec} [arg] Specification for the layer. * @returns {geo.tileLayer} */ -var tileLayer = function (options) { +var tileLayer = function (arg) { 'use strict'; if (!(this instanceof tileLayer)) { - return new tileLayer(options); + return new tileLayer(arg); } - featureLayer.call(this, options); + featureLayer.call(this, arg); var $ = require('jquery'); var geo_event = require('./event'); @@ -166,27 +170,27 @@ var tileLayer = function (options) { var adjustLayerForRenderer = require('./registry').adjustLayerForRenderer; var Tile = require('./tile'); - options = $.extend(true, {}, this.constructor.defaults, options || {}); - if (!options.cacheSize) { + arg = $.extend(true, {}, this.constructor.defaults, arg || {}); + if (!arg.cacheSize) { // this size should be sufficient for a 4k display - options.cacheSize = options.keepLower ? 600 : 200; + arg.cacheSize = arg.keepLower ? 600 : 200; } - if ($.type(options.subdomains) === 'string') { - options.subdomains = options.subdomains.split(''); + if ($.type(arg.subdomains) === 'string') { + arg.subdomains = arg.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 (arg.baseUrl) { + var url = arg.baseUrl; if (url && url.charAt(url.length - 1) !== '/') { url += '/'; } - options.url = url + '{z}/{x}/{y}.' + (options.imageFormat || 'png'); + arg.url = url + '{z}/{x}/{y}.' + (arg.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); + arg.originalUrl = arg.url; + if ($.type(arg.url) === 'string') { + arg.url = m_tileUrlFromTemplate(arg.url); } var s_init = this._init, @@ -202,10 +206,10 @@ var tileLayer = function (options) { this._levelZIncrement = 1e-5; // copy the options into a private variable - this._options = $.extend(true, {}, options); + this._options = $.extend(true, {}, arg); // set the layer attribution text - this.attribution(options.attribution); + this.attribution(arg.attribution); // initialize the object that keeps track of actively drawn tiles this._activeTiles = {}; @@ -216,7 +220,7 @@ var tileLayer = function (options) { this._tileTree = {}; // initialize the in memory tile cache - this._cache = tileCache({size: options.cacheSize}); + this._cache = tileCache({size: arg.cacheSize}); // initialize the tile fetch queue this._queue = fetchQueue({ @@ -225,7 +229,7 @@ var tileLayer = function (options) { // 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, + track: arg.cacheSize, needed: function (tile) { return tile === this.cache.get(tile.toString(), true); }.bind(this) diff --git a/src/ui/uiLayer.js b/src/ui/uiLayer.js index 2d55121089..abfdf4b23d 100644 --- a/src/ui/uiLayer.js +++ b/src/ui/uiLayer.js @@ -8,7 +8,7 @@ var layer = require('../layer'); * @class * @alias geo.gui.uiLayer * @extends {geo.layer} - * @param {object} [arg] Options for the layer. + * @param {geo.layer.spec} [arg] Specification for the new layer. * @returns {geo.gui.uiLayer} */ var uiLayer = function (arg) { diff --git a/tests/cases/layer.js b/tests/cases/layer.js new file mode 100644 index 0000000000..df56580be8 --- /dev/null +++ b/tests/cases/layer.js @@ -0,0 +1,252 @@ +// Test geo.layer + +var geo = require('../test-utils').geo; +var createMap = require('../test-utils').createMap; + +describe('geo.layer', function () { + 'use strict'; + + beforeEach(function () { + sinon.stub(console, 'log', function () {}); + }); + afterEach(function () { + console.log.restore(); + }); + + describe('create', function () { + it('create function', function () { + var map, layer; + + map = createMap(); + layer = geo.layer({map: map}); + expect(layer instanceof geo.layer).toBe(true); + expect(layer.initialized()).toBe(false); + + expect(function () { + geo.layer({}); + }).toThrow(new Error('Layers must be initialized on a map.')); + }); + it('direct create', function () { + var map, layer, warn; + + map = createMap(); + layer = geo.layer.create(map, {renderer: 'canvas'}); + expect(layer instanceof geo.layer).toBe(true); + expect(layer.initialized()).toBe(true); + expect(layer.children().length).toBe(0); + + layer = geo.layer.create(map, {renderer: 'd3', features: [{type: 'point'}]}); + expect(layer instanceof geo.layer).toBe(true); + expect(layer.initialized()).toBe(true); + expect(layer.children().length).toBe(1); + + warn = sinon.stub(console, 'warn', function () {}); + layer = geo.layer.create(map, {renderer: 'notarenderer'}); + expect(warn.calledOnce).toBe(true); + console.warn.restore(); + + warn = sinon.stub(console, 'warn', function () {}); + layer = geo.layer.create(map, {type: 'notalayertype', renderer: 'canvas'}); + expect(warn.calledOnce).toBe(true); + console.warn.restore(); + }); + }); + describe('Check private class methods', function () { + var map, layer; + it('_init', function () { + map = createMap(); + layer = geo.layer({map: map}); + expect(layer.initialized()).toBe(false); + layer._init(); + expect(layer.initialized()).toBe(true); + layer._init(); + expect(layer.initialized()).toBe(true); + }); + it('_init with events', function () { + var count = 0; + + layer = geo.layer({map: map}); + map.addChild(layer); + layer._update = function () { count += 1; }; + layer._init(); + map.size({width: 600}); + map.pan({x: 0, y: 1}); + map.rotation(1); + map.zoom(1); + expect(count).toBe(7); // sie, rotation, zoom also trigger pan + map.removeChild(layer); + }); + it('_init without events', function () { + var count = 0; + + layer = geo.layer({map: map}); + map.addChild(layer); + layer._update = function () { count += 1; }; + layer._init(true); + map.size({width: 640}); + map.pan({x: 0, y: 1}); + map.rotation(1); + map.zoom(1); + expect(count).toBe(0); + map.removeChild(layer); + }); + it('_exit', function () { + layer = geo.layer({map: map}); + layer._init(); + expect(layer.renderer()).not.toBe(null); + layer._exit(); + expect(layer.renderer()).toBe(null); + }); + it('_update', function () { + layer = geo.layer({map: map}); + expect(layer._update()).toBe(layer); + }); + }); + describe('Check class (non-instance) methods', function () { + it('newLayerId', function () { + var id = geo.layer.newLayerId(); + expect(geo.layer.newLayerId()).toBeGreaterThan(id); + }); + }); + describe('Check public class methods', function () { + var map, layer; + it('active', function () { + map = createMap(); + layer = geo.layer({map: map}); + expect(layer.active()).toBe(true); + expect(layer.active(false)).toBe(layer); + expect(layer.active()).toBe(false); + expect(layer.active(true)).toBe(layer); + expect(layer.active()).toBe(true); + layer = geo.layer({map: map, active: false}); + expect(layer.active()).toBe(false); + expect(layer.active(true)).toBe(layer); + expect(layer.active()).toBe(true); + }); + it('attribution', function () { + expect(layer.attribution()).toBe(null); + layer = geo.layer({map: map, attribution: 'attribution1'}); + expect(layer.attribution()).toBe('attribution1'); + expect(layer.attribution('attribution2')).toBe(layer); + expect(layer.attribution()).toBe('attribution2'); + }); + it('canvas', function () { + layer = geo.layer({map: map}); + expect(layer.canvas()).toBe(null); + layer._init(); + expect(layer.canvas()).not.toBe(null); + expect(layer.canvas()).toBe(layer.renderer().canvas()); + var canvas = layer.canvas(); + var layer2 = geo.layer({map: map, canvas: canvas}); + layer2._init(); + expect(layer2.canvas()).toEqual(layer.canvas()); + var layer3 = geo.layer({map: map}); + layer2._init(); + expect(layer3.canvas()).not.toEqual(layer.canvas()); + }); + it('dataTime', function () { + expect(layer.dataTime() instanceof geo.timestamp).toBe(true); + }); + it('fromLocal', function () { + expect(layer.fromLocal('abc')).toBe('abc'); + }); + it('height', function () { + expect(layer.height()).toBe(map.node().height()); + }); + it('id', function () { + var id; + + expect(layer.id()).toBeGreaterThan(0); + id = layer.id(); + layer = geo.layer({map: map, id: 1}); + expect(layer.id()).toBe(1); + expect(layer.id(5)).toBe(layer); + expect(layer.id()).toBe(5); + expect(layer.id(null)).toBe(layer); + expect(layer.id()).toBeGreaterThan(id); + }); + it('initialized', function () { + layer = geo.layer({map: map}); + expect(layer.initialized()).toBe(false); + layer._init(); + expect(layer.initialized()).toBe(true); + expect(layer.initialized(false)).toBe(layer); + expect(layer.initialized()).toBe(false); + }); + it('map', function () { + expect(layer.map()).toBe(map); + }); + it('name', function () { + expect(layer.name()).toBe(''); + layer = geo.layer({map: map, name: 'name1'}); + expect(layer.name()).toBe('name1'); + expect(layer.name('name2')).toBe(layer); + expect(layer.name()).toBe('name2'); + }); + it('node', function () { + expect(layer.node().attr('id')).toBe(layer.name()); + }); + it('opacity', function () { + expect(layer.opacity()).toBe(1); + layer = geo.layer({map: map, opacity: 0.5}); + expect(layer.opacity()).toBe(0.5); + expect(layer.opacity(0.75)).toBe(layer); + expect(layer.opacity()).toBe(0.75); + }); + it('renderer', function () { + layer = geo.layer({map: map}); + expect(layer.renderer()).toBe(null); + layer._init(); + expect(layer.renderer() instanceof geo.renderer).toBe(true); + var renderer = layer.renderer(); + var layer2 = geo.layer({map: map, renderer: renderer}); + layer2._init(); + expect(layer2.renderer()).toEqual(layer.renderer()); + var layer3 = geo.layer({map: map}); + layer2._init(); + expect(layer3.renderer()).not.toEqual(layer.renderer()); + }); + it('rendererName', function () { + layer = geo.layer({map: map, renderer: 'canvas'}); + expect(layer.rendererName()).toBe('canvas'); + }); + it('selectionAPI', function () { + expect(layer.selectionAPI()).toBe(true); + expect(layer.selectionAPI(false)).toBe(layer); + expect(layer.selectionAPI()).toBe(false); + expect(layer.selectionAPI(true)).toBe(layer); + expect(layer.selectionAPI()).toBe(true); + layer = geo.layer({map: map, selectionAPI: false}); + expect(layer.selectionAPI()).toBe(false); + expect(layer.selectionAPI(true)).toBe(layer); + expect(layer.selectionAPI()).toBe(true); + }); + it('sticky', function () { + expect(layer.sticky()).toBe(true); + layer = geo.layer({map: map, sticky: false}); + expect(layer.sticky()).toBe(false); + }); + it('toLocal', function () { + expect(layer.toLocal('abc')).toBe('abc'); + }); + it('updateTime', function () { + expect(layer.updateTime() instanceof geo.timestamp).toBe(true); + }); + it('visible', function () { + expect(layer.visible()).toBe(true); + expect(layer.visible(false)).toBe(layer); + expect(layer.visible()).toBe(false); + expect(layer.visible(true)).toBe(layer); + expect(layer.visible()).toBe(true); + layer = geo.layer({map: map, visible: false}); + expect(layer.visible()).toBe(false); + expect(layer.visible(true)).toBe(layer); + expect(layer.visible()).toBe(true); + }); + it('width', function () { + expect(layer.width()).toBe(map.node().width()); + }); + /* zIndex, moveUp, moveDown, moveToTop, and moveToBottom tested in + * layerReorder.js */ + }); +});