From 70afc697374adfc4a2d359dcfdaa7e49c844c4bc Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 17 Aug 2018 15:19:00 -0400 Subject: [PATCH 1/2] Added style typedefs. Moved all style specifications into jsdoc typedefs called `.styleSpec`. --- jsdoc/plugins/typedef_augments.js | 6 +- src/action.js | 1 + src/annotation.js | 46 +++++------ src/contourFeature.js | 29 ++++--- src/feature.js | 18 +++-- src/isolineFeature.js | 59 +++++++------- src/lineFeature.js | 48 +++++++----- src/meshFeature.js | 12 +++ src/object.js | 15 ++-- src/pointFeature.js | 36 +++++---- src/polygonFeature.js | 38 +++++---- src/textFeature.js | 125 ++++++++++++++++-------------- src/tile.js | 1 + src/util/index.js | 5 +- 14 files changed, 245 insertions(+), 194 deletions(-) diff --git a/jsdoc/plugins/typedef_augments.js b/jsdoc/plugins/typedef_augments.js index 932c07c96d..9b47655e66 100644 --- a/jsdoc/plugins/typedef_augments.js +++ b/jsdoc/plugins/typedef_augments.js @@ -44,10 +44,10 @@ exports.handlers = { * reversing the augments list). */ doclet.augments.slice().reverse().forEach(function (augmentName) { if (augmentName !== name && typedefs[augmentName] && typedefs[augmentName].properties) { - typedefs[augmentName].properties.forEach(function (prop) { - if (!properties[prop.name]) { + typedefs[augmentName].properties.forEach(function (origprop) { + if (!properties[origprop.name]) { /* Make a copy so we don't mutate the original property. */ - prop = Object.assign(prop); + var prop = Object.assign({}, origprop); /* Add a value that a rendering template could use to show that * the property was inherted from a parent. Since that in turn * could have been inherited, preserve a known value. */ diff --git a/src/action.js b/src/action.js index afa1cc76dc..1d12db355f 100644 --- a/src/action.js +++ b/src/action.js @@ -4,6 +4,7 @@ * * @namespace * @alias geo.action + * @enum {string} */ var geo_action = { momentum: 'geo_action_momentum', diff --git a/src/annotation.js b/src/annotation.js index aa15db0c4d..5a3831bbce 100644 --- a/src/annotation.js +++ b/src/annotation.js @@ -1082,13 +1082,10 @@ var annotation = function (type, args) { * coordinates. These must be in order around the perimeter of the * rectangle (in either direction). * @property {geo.geoPosition[]} [coordinates] An alternate name for `corners`. - * @property {object} [style] The style to apply to a finished rectangle. This - * uses styles for polygons, including `fill`, `fillColor`, `fillOpacity`, - * `stroke`, `strokeWidth`, `strokeColor`, and `strokeOpacity`. - * @property {object} [editStyle] The style to apply to a rectangle in edit - * mode. This uses styles for polygons and lines, including `fill`, - * `fillColor`, `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and - * `strokeOpacity`. + * @property {geo.polygonFeature.styleSpec} [style] The style to apply to a + * finished rectangle. This uses styles for {@link geo.polygonFeature}. + * @property {geo.polygonFeature.styleSpec} [editStyle] The style to apply to a + * rectangle in edit mode. */ /** @@ -1470,13 +1467,10 @@ registerAnnotation('rectangle', rectangleAnnotation, rectangleRequiredFeatures); * (in either direction). * @property {geo.geoPosition[]} [coordinates] An alternate name for * `vertices`. - * @property {object} [style] The style to apply to a finished polygon. This - * uses styles for polygons, including `fill`, `fillColor`, `fillOpacity`, - * `stroke`, `strokeWidth`, `strokeColor`, and `strokeOpacity`. - * @property {object} [editStyle] The style to apply to a polygon in edit mode. - * This uses styles for polygons and lines, including `fill`, `fillColor`, - * `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, and - * `strokeOpacity`. + * @property {geo.polygonFeature.styleSpec} [style] The style to apply to a + * finished polygon. This uses styles for {@link geo.polygonFeature}. + * @property {geo.polygonFeature.styleSpec} [editStyle] The style to apply to ai + * polygon in edit mode. */ /** @@ -1735,12 +1729,10 @@ registerAnnotation('polygon', polygonAnnotation, polygonRequiredFeatures); * coordinates. * @property {geo.geoPosition[]} [coordinates] An alternate name for * `vertices`. - * @property {object} [style] The style to apply to a finished line. This uses - * styles for lines, including `strokeWidth`, `strokeColor`, - * `strokeOpacity`, `strokeOffset`, `closed`, `lineCap`, and `lineJoin`. - * @property {object} [editStyle] The style to apply to a line in edit mode. - * This uses styles for lines, including `strokeWidth`, `strokeColor`, - * `strokeOpacity`, `strokeOffset`, `closed`, `lineCap`, and `lineJoin`. + * @property {geo.lineFeature.styleSpec} [style] The style to apply to a + * finished line. This uses styles for {@link geo.lineFeature}. + * @property {geo.lineFeature.styleSpec} [editStyle] The style to apply to a + * line in edit mode. */ /** @@ -2120,14 +2112,14 @@ registerAnnotation('line', lineAnnotation, lineRequiredFeatures); * @property {geo.geoPosition} [position] A coordinate in map gcs coordinates. * @property {geo.geoPosition[]} [coordinates] An array with one coordinate to * use in place of `position`. - * @property {object} [style] The style to apply to a finished point. This - * uses styles for points, including `radius`, `fill`, `fillColor`, - * `fillOpacity`, `stroke`, `strokeWidth`, `strokeColor`, `strokeOpacity`, - * and `scaled`. If `scaled` is `false`, the point is not scaled with zoom - * level. If it is `true`, the radius is based on the zoom level at first - * instantiation. Otherwise, if it is a number, the radius is used at that + * @property {geo.pointFeature.styleSpec} [style] The style to apply to a + * finished point. This uses styles for {@link geo.pointFeature}. + * @property {boolean|number} [style.scaled=false] If `false`, the point is not + * scaled with zoom level. If `true`, the radius is based on the zoom level + * at first instantiation. If a number, the radius is used at the `scaled` * zoom level. - * @property {object} [editStyle] The style to apply to a point in edit mode. + * @property {geo.pointFeature.styleSpec} [editStyle] The style to apply to a + * point in edit mode. */ /** diff --git a/src/contourFeature.js b/src/contourFeature.js index 916feb86e5..95bd9f2077 100644 --- a/src/contourFeature.js +++ b/src/contourFeature.js @@ -7,22 +7,27 @@ var meshFeature = require('./meshFeature'); * @typedef {geo.feature.spec} geo.contourFeature.spec * @property {object[]} [data=[]] An array of arbitrary objects used to * construct the feature. - * @property {object} [style] An object that contains style values for the - * feature. - * @property {number|function} [style.opacity=1] The opacity on a scale of 0 to - * 1. - * @property {geo.geoPosition|function} [style.position=data] The position of - * each data element. This defaults to just using `x`, `y`, and `z` - * properties of the data element itself. The position is in the feature's - * gcs coordinates. - * @property {number|function} [style.value=data.z] The value of each data - * element. This defaults `z` properties of the data element. If the value - * of a grid point is `null` or `undefined`, that point and elements that - * use that point won't be included in the results. + * @property {geo.contourFeature.styleSpec} [style] An object that contains + * style values for the feature. * @property {geo.contourFeature.contourSpec} [contour] The contour * specification for the feature. */ +/** + * Style specification for a contour feature. + * + * @typedef {geo.feature.styleSpec} geo.contourFeature.styleSpec + * @extends geo.feature.styleSpec + * @property {geo.geoPosition|function} [position=data] The position of each + * data element. This defaults to just using `x`, `y`, and `z` properties + * of the data element itself. The position is in the feature's gcs + * coordinates. + * @property {number|function} [value=data.z] The value of each data element. + * This defaults to the `z` property of the data elements. If the value of + * a grid point is `null` or `undefined`, the point and elements that use + * that point won't be included in the results. + */ + /** * Contour specification. All of these properties can be functions, which get * passed the {@link geo.meshFeature.meshInfo} object. diff --git a/src/feature.js b/src/feature.js index 80e5624fe1..3cc178307e 100644 --- a/src/feature.js +++ b/src/feature.js @@ -33,10 +33,16 @@ var geo_event = require('./event'); * feature within its parent's list of children as the bin number. * @property {geo.renderer?} [renderer] A reference to the renderer used for * the feature. - * @property {object} [style] An object that contains style values for the - * feature. - * @property {number|function} [style.opacity=1] The opacity on a scale of 0 to - * 1. + * @property {geo.feature.styleSpec} [style] An object that contains style + * values for the feature. + */ + +/** + * Style specification for a feature. + * + * @typedef {object} geo.feature.styleSpec + * @property {number|function} [opacity=1] The opacity for the whole feature on + * a scale of 0 to 1. */ /** @@ -420,8 +426,8 @@ var feature = function (arg) { * has a subfeature style, with `(subfeatureElement, subfeatureIndex, * dataElement, dataIndex)`. * - * See the feature's specification ({@link geo.feature.spec}) for available - * styles. + * See the style specification + * styleSpec for available styles. * * @param {string|object} [arg1] If `undefined`, return the current style * object. If a string and `arg2` is undefined, return the style diff --git a/src/isolineFeature.js b/src/isolineFeature.js index 8427183849..84d246648f 100644 --- a/src/isolineFeature.js +++ b/src/isolineFeature.js @@ -9,37 +9,44 @@ var util = require('./util'); * @typedef {geo.feature.spec} geo.isolineFeature.spec * @property {object[]} [data=[]] An array of arbitrary objects used to * construct the feature. - * @property {object} [style] An object that contains style values for the - * feature. This includes {@link geo.lineFeature} and - * {@link geo.textFeature} style values. - * @property {number|function} [style.opacity=1] Overall opacity on a scale of - * 0 to 1. - * @property {geo.geoPosition|function} [style.position=data] The position of - * each data element. This defaults to just using `x`, `y`, and `z` - * properties of the data element itself. The position is in thea feature's - * gcs coordinates. - * @property {number|function} [style.value=data.z] The value of each data - * element. This defaults `z` properties of the data element. If the value - * of a grid point is `null` or `undefined`, that point and elements that - * use that point won't be included in the results. - * @property {geo.geoColor|function} [style.strokeColor='black'] Color to - * stroke each line. - * @property {number|function} [style.strokeWidth] The weight of the line - * stroke in pixels. This defaults to the line value's level + 0.5. - * @property {boolean|function} [style.rotateWithMap=true] Rotate label text - * when the map rotates. - * @property {number|function} [style.rotation] Text rotation in radians. This + * @property {geo.isolineFeature.styleSpec} [style] An object that contains + * style values for the feature. + * @property {geo.isolineFeature.isolineSpec} [isoline] The isoline + * specification for the feature. + */ + +/** + * Style specification for an isoline feature. Extends + * {@link geo.lineFeasture.styleSpec} and {@link geo.textFeasture.styleSpec}. + * + * @typedef {geo.feature.styleSpec} geo.isolineFeature.styleSpec + * @extends geo.feature.styleSpec + * @extends geo.textFeature.styleSpec + * @extends geo.lineFeature.styleSpec + * @property {geo.geoPosition|function} [position=data] The position of each + * data element. This defaults to just using `x`, `y`, and `z` properties + * of the data element itself. The position is in the feature's gcs + * coordinates. + * @property {number|function} [value=data.z] The value of each data element. + * This defaults to the `z` property of the data elements. If the value of + * a grid point is `null` or `undefined`, the point and elements that use + * that point won't be included in the results. + * @property {geo.geoColor|function} [strokeColor='black'] Color to stroke each + * line. + * @property {number|function} [strokeWidth] The weight of the line stroke in + * pixels. This defaults to the line value's level + 0.5. + * @property {boolean|function} [rotateWithMap=true] Rotate label text when the + * map rotates. + * @property {number|function} [rotation] Text rotation in radians. This * defaults to the label oriented so that top of the text is toward the * higher value. There is a utility function that can be used for common * rotation preferences. See {@link geo.isolineFeature#rotationFunction}. * For instance, `rotation=geo.isolineFeature.rotationFunction('map')`. - * @property {string|function} [style.fontSize='12px'] The font size. - * @property {geo.geoColor|function} [style.textStrokeColor='white'] Text + * @property {string|function} [fontSize='12px'] The font size. + * @property {geo.geoColor|function} [textStrokeColor='white'] Text * stroke color. This adds contrast between the label and the isoline. - * @property {geo.geoColor|function} [style.textStrokeWidth=2] Text stroke - * width in pixels. - * @property {geo.isolineFeature.isolineSpec} [isoline] The isoline - * specification for the feature. + * @property {geo.geoColor|function} [textStrokeWidth=2] Text stroke width in + * pixels. */ /** diff --git a/src/lineFeature.js b/src/lineFeature.js index 4d1b065010..eeb3d119c6 100644 --- a/src/lineFeature.js +++ b/src/lineFeature.js @@ -15,33 +15,41 @@ var util = require('./util'); * points. Only lines that have at least two points are rendered. The * position function is called for each point as `position(linePoint, * pointIndex, lineEntry, lineEntryIndex)`. - * @property {object} [style] Style object with default style options. - * @property {geo.geoColor|function} [style.strokeColor] Color to stroke each - * line. The color can vary by point. - * @property {number|function} [style.strokeOpacity] Opacity for each line - * stroke. The opacity can vary by point. Opacity is on a [0-1] scale. - * @property {number|function} [style.strokeWidth] The weight of the line - * stroke in pixels. The width can vary by point. - * @property {number|function} [style.strokeOffset] This is a value from -1 - * (left) to 1 (right), with 0 being centered. This can vary by point. - * @property {string|function} [style.lineCap='butt'] One of 'butt', 'square', - * or 'round'. This can vary by point. - * @property {string|function} [style.lineJoin='miter'] One of 'miter', - * 'bevel', 'round', or 'miter-clip'. This can vary by point. - * @property {boolean|function} [style.closed=false] If true and the renderer + * @property {geo.lineFeature.styleSpec} [style] Style object with default + * style options. + */ + +/** + * Style specification for a line feature. + * + * @typedef {geo.feature.styleSpec} geo.lineFeature.styleSpec + * @extends geo.feature.styleSpec + * @property {geo.geoColor|function} [strokeColor] Color to stroke each line. + * The color can vary by point. + * @property {number|function} [strokeOpacity] Opacity for each line stroke. + * The opacity can vary by point. Opacity is on a [0-1] scale. + * @property {number|function} [strokeWidth] The weight of the line stroke in + * pixels. The width can vary by point. + * @property {number|function} [strokeOffset] This is a value from -1 (left) to + * 1 (right), with 0 being centered. This can vary by point. + * @property {string|function} [lineCap='butt'] One of 'butt', 'square', or + * 'round'. This can vary by point. + * @property {string|function} [lineJoin='miter'] One of 'miter', 'bevel', + * 'round', or 'miter-clip'. This can vary by point. + * @property {boolean|function} [closed=false] If true and the renderer * supports it, connect the first and last points of a line if the line has * more than two points. This applies per line (if a function, it is called * with `(lineEntry, lineEntryIndex)`. - * @property {number|function} [style.miterLimit=10] For lines of more than two + * @property {number|function} [miterLimit=10] For lines of more than two * segments that are mitered, if the miter length exceeds the `strokeWidth` * divided by the sine of half the angle between segments, then a bevel join * is used instead. This is a single value that applies to all lines. If a * function, it is called with `(data)`. - * @property {number|function} [style.antialiasing] Antialiasing distance in - * pixels. Values must be non-negative. A value greater than 1 will produce - * a visible gradient. This is a single value that applies to all lines. - * @property {string|function} [style.debug] If 'debug', render lines in debug - * mode. This is a single value that applies to all lines. + * @property {number|function} [antialiasing] Antialiasing distance in pixels. + * Values must be non-negative. A value greater than 1 will produce a + * visible gradient. This is a single value that applies to all lines. + * @property {string|function} [debug] If 'debug', render lines in debug mode. + * This is a single value that applies to all lines. */ /** diff --git a/src/meshFeature.js b/src/meshFeature.js index ea0c5c77d9..ceded8671b 100644 --- a/src/meshFeature.js +++ b/src/meshFeature.js @@ -1,6 +1,18 @@ var inherit = require('./inherit'); var feature = require('./feature'); +/** + * Mesh feature specification. + * + * @typedef {geo.feature.spec} geo.meshFeature.spec + * @property {object[]} [data=[]] An array of arbitrary objects used to + * construct the feature. + * @property {geo.feature.styleSpec} [style] An object that contains style + * values for the feature. + * @property {geo.meshFeature.meshSpec} [mesh] The mesh specification for the + * feature. + */ + /** * A mesh formed by a set of triangular or square elements or a * squarely-connected grid that is of rectangular extent. The gird can be diff --git a/src/object.js b/src/object.js index 794593f0b9..0ce017e9ee 100644 --- a/src/object.js +++ b/src/object.js @@ -132,9 +132,9 @@ var object = function () { * Trigger an event (or events) on this object and call all handlers. * * @param {string|string[]} event An event or list of events from - * {@link geo.event} or defined by the user. + * {@link geo.event} or defined by the user. * @param {object} [args] Additional information to add to the - * {@link geo.event} object passed to the handlers. + * {@link geo.event} object passed to the handlers. * @returns {this} */ this.geoTrigger = function (event, args) { @@ -169,11 +169,11 @@ var object = function () { * provided all handlers will be removed. * * @param {string|string[]} [event] An event or a list of events from - * {@link geo.event} or defined by the user, or `undefined` to remove - * all events (in which case `arg` is ignored). + * {@link geo.event} or defined by the user, or `undefined` to remove all + * events (in which case `arg` is ignored). * @param {(function|function[])?} [arg] A function or array of functions to - * remove from the events or a falsey value to remove all handlers - * from the events. + * remove from the events or a falsy value to remove all handlers from the + * events. * @returns {this} */ this.geoOff = function (event, arg) { @@ -199,8 +199,7 @@ var object = function () { if (m_eventHandlers.hasOwnProperty(event)) { m_eventHandlers[event] = m_eventHandlers[event].filter(function (f) { return f !== arg; - } - ); + }); } return m_this; }; diff --git a/src/pointFeature.js b/src/pointFeature.js index ad2a589808..15ad1e40ee 100644 --- a/src/pointFeature.js +++ b/src/pointFeature.js @@ -7,21 +7,8 @@ var feature = require('./feature'); * @typedef {geo.feature.spec} geo.pointFeature.spec * @property {geo.geoPosition|function} [position] Position of the data. * Default is (data). - * @property {object} [style] Style object with default style options. - * @property {number|function} [style.radius=5] Radius of each point in pixels. - * This is the fill radius inside of the stroke. - * @property {boolean|function} [style.stroke=true] True to stroke point. - * @property {geo.geoColor|function} [style.strokeColor] Color to stroke each - * point. - * @property {number|function} [style.strokeOpacity=1] Opacity for each point's - * stroke. Opacity is on a [0-1] scale. - * @property {number|function} [style.strokeWidth=1.25] The weight of the - * point's stroke in pixels. - * @property {boolean|function} [style.fill=true] True to fill point. - * @property {geo.geoColor|function} [style.fillColor] Color to fill each - * point/ - * @property {number|function} [style.fillOpacity=1] Opacity for each point. - * Opacity is on a [0-1] scale. + * @property {geo.pointFeature.styleSpec} [style] Style object with default + * style options. * @property {boolean|geo.pointFeature.clusteringSpec} [clustering=false] * Enable point clustering. * @property {string} [primitiveShape='sprite'] For the gl renderer, select the @@ -33,6 +20,25 @@ var feature = require('./feature'); * truthy, webgl source buffers can be modifies and updated directly. */ +/** + * Style specification for a point feature. + * + * @typedef {geo.feature.styleSpec} geo.pointFeature.styleSpec + * @extends geo.feature.styleSpec + * @property {number|function} [radius=5] Radius of each point in pixels. This + * is the fill radius inside of the stroke. + * @property {boolean|function} [stroke=true] True to stroke point. + * @property {geo.geoColor|function} [strokeColor] Color to stroke each point. + * @property {number|function} [strokeOpacity=1] Opacity for each point's + * stroke. Opacity is on a [0-1] scale. + * @property {number|function} [strokeWidth=1.25] The weight of the point's + * stroke in pixels. + * @property {boolean|function} [fill=true] True to fill point. + * @property {geo.geoColor|function} [fillColor] Color to fill each point. + * @property {number|function} [fillOpacity=1] Opacity for each point. Opacity + * is on a [0-1] scale. + */ + /** * Point clustering specification. * diff --git a/src/polygonFeature.js b/src/polygonFeature.js index 737cfea481..959c3d34fb 100644 --- a/src/polygonFeature.js +++ b/src/polygonFeature.js @@ -11,22 +11,25 @@ var transform = require('./transform'); * Default is (data). * @property {geo.polygon|function} [polygon] Polygons from the data. Default * (data). - * @property {object} [style] Style object with default style options. - * @property {boolean|function} [style.fill=true] True to fill polygon. - * @property {geo.geoColor|function} [style.fillColor] Color to fill each - * polygon. The color can vary by vertex. - * @property {number|function} [style.fillOpacity] Opacity for each polygon. - * The opacity can vary by vertex. Opacity is on a [0-1] scale. - * @property {boolean|function} [style.stroke=false] True to stroke polygon. - * @property {geo.geoColor|function} [style.strokeColor] Color to stroke each - * polygon. The color can vary by vertex. - * @property {number|function} [style.strokeOpacity] Opacity for each polygon - * stroke. The opacity can vary by vertex. Opacity is on a [0-1] scale. - * @property {number|function} [style.strokeWidth] The weight of the polygon - * stroke in pixels. The width can vary by vertex. - * @property {boolean|function} [style.uniformPolygon=false] Boolean indicating - * if each polygon has a uniform style (uniform fill color, fill opacity, - * stroke color, and stroke opacity). Can vary by polygon. + * @property {geo.polygonFeature.styleSpec} [style] Style object with default + * style options. + */ + +/** + * Style specification for a polygon feature. + * + * @typedef {geo.lineFeature.styleSpec} geo.polygonFeature.styleSpec + * @extends geo.lineFeature.styleSpec + * @property {boolean|function} [fill=true] True to fill polygon. + * @property {geo.geoColor|function} [fillColor] Color to fill each polygon. + * The color can vary by vertex. + * @property {number|function} [fillOpacity] Opacity for each polygon. The + * opacity can vary by vertex. Opacity is on a [0-1] scale. + * @property {boolean|function} [stroke=false] True to stroke polygon. + * @property {boolean|function} [uniformPolygon=false] Boolean indicating if + * each polygon has a uniform style (uniform fill color, fill opacity, stroke + * color, and stroke opacity). Can vary by polygon. + * @property {boolean|function} [closed=true] Ignored. Always `true`. */ /** @@ -269,6 +272,9 @@ var polygonFeature = function (arg) { * Get/Set style used by the feature. This calls the super function, then * checks if strokes are required. * + * See the style specification + * styleSpec for available styles. + * * @param {string|object} [arg1] If `undefined`, return the current style * object. If a string and `arg2` is undefined, return the style * associated with the specified key. If a string and `arg2` is defined, diff --git a/src/textFeature.js b/src/textFeature.js index cddd10cda2..2460ecc764 100644 --- a/src/textFeature.js +++ b/src/textFeature.js @@ -6,66 +6,73 @@ var feature = require('./feature'); * * @typedef {geo.feature.spec} geo.textFeature.spec * @property {geo.geoPosition[]|function} [position] The position of each data - * element. Defaults to the `x`, `y`, and `z` properties of the data - * element. + * element. Defaults to the `x`, `y`, and `z` properties of the data + * element. * @property {string[]|function} [text] The text of each data element. - * Defaults to the `text` property of the data element. - * @property {object} [style] The style to apply to each data element. - * @property {boolean|function} [style.visible=true] If falsy, don't show this - * data element. - * @property {string|function} [style.font] A css font specification. This - * is of the form `[style] [variant] [weight] [stretch] size[/line-height] - * family`. Individual font styles override this value if a style is - * specified in each. See the individual font styles for details. - * @property {string|function} [style.fontStyle='normal'] The font style. One - * of `normal`, `italic`, or `oblique`. - * @property {string|function} [style.fontVariant='normal'] The font variant. - * This can have values such as `small-caps` or `slashed-zero`. - * @property {string|function} [style.fontWeight='normal'] The font weight. - * This may be a numeric value where 400 is normal and 700 is bold, or a - * string such as `bold` or `lighter`. - * @property {string|function} [style.fontStretch='normal'] The font stretch, - * such as `condensed`. - * @property {string|function} [style.fontSize='medium'] The font size. - * @property {string|function} [style.lineHeight='normal'] The font line - * height. - * @property {string|function} [style.fontFamily] The font family. - * @property {string|function} [style.textAlign='center'] The horizontal text - * alignment. One of `start`, `end`, `left`, `right`, or `center`. - * @property {string|function} [style.textBaseline='middle'] The vertical text - * alignment. One of `top`, `hanging`, `middle`, `alphabetic`, - * `ideographic`, or `bottom`. - * @property {geo.geoColor|function} [style.color='black'] Text color. May - * include opacity. - * @property {number|function} [style.textOpacity=1] The opacity of the text. - * If the color includes opacity, this is combined with that value. - * @property {number|function} [style.rotation=0] Text rotation in radians. - * @property {boolean|function} [style.rotateWithMap=false] If truthy, rotate - * the text when the map rotates. Otherwise, the text is always in the - * same orientation. - * @property {number|function} [style.textScaled] If defined, the text is - * scaled when the map zoomes and this is the basis zoom for the fontSize. - * @property {geo.screenPosition|function} [style.offset] Offset from the - * default position for the text. This is applied before rotation. - * @property {geo.geoColor|function} [style.shadowColor='black'] Text shadow - * color. May include opacity. - * @property {geo.screenPosition|function} [style.shadowOffset] Offset for a - * text shadow. This is applied before rotation. - * @property {number|null|function} [style.shadowBlur] If not null, add a text - * shadow with this much blur. - * @property {boolean|function} [style.shadowRotate=false] If truthy, rotate - * the shadow offset based on the text rotation (the `shadowOffset` is - * the offset if the text has a 0 rotation). - * @property {geo.geoColor|function} [style.textStrokeColor='transparent'] Text - * stroke color. May include opacity. - * @property {geo.geoColor|function} [style.textStrokeWidth=0] Text stroke - * width in pixels. - * @property {number|function} [style.renderThreshold] If this is a positive - * number, text elements may not be rendered if their base position - * (before offset and font effects are applied) is more than this distance - * in pixels outside of the current viewport. If it is known that such - * text elements cannot affect the current viewport, setting this can - * speed up rendering. This is computed once for the whole feature. + * Defaults to the `text` property of the data element. + * @property {geo.textFeature.styleSpec} [style] The style to apply to each + * data element. + */ + +/** + * Style specification for a text feature. + * + * @typedef {geo.feature.styleSpec} geo.textFeature.styleSpec + * @extends geo.feature.styleSpec + * @property {boolean|function} [visible=true] If falsy, don't show this data + * element. + * @property {string|function} [font] A css font specification. This is of the + * form `[style] [variant] [weight] [stretch] size[/line-height] family`. + * Individual font styles override this value if a style is specified in + * each. See the individual font styles for details. + * @property {string|function} [fontStyle='normal'] The font style. One of + * `normal`, `italic`, or `oblique`. + * @property {string|function} [fontVariant='normal'] The font variant. This + * can have values such as `small-caps` or `slashed-zero`. + * @property {string|function} [fontWeight='normal'] The font weight. This may + * be a numeric value where 400 is normal and 700 is bold, or a string such + * as `bold` or `lighter`. + * @property {string|function} [fontStretch='normal'] The font stretch, such as + * `condensed`. + * @property {string|function} [fontSize='medium'] The font size. + * @property {string|function} [lineHeight='normal'] The font line height. + * @property {string|function} [fontFamily] The font family. + * @property {string|function} [textAlign='center'] The horizontal text + * alignment. One of `start`, `end`, `left`, `right`, or `center`. + * @property {string|function} [textBaseline='middle'] The vertical text + * alignment. One of `top`, `hanging`, `middle`, `alphabetic`, + * `ideographic`, or `bottom`. + * @property {geo.geoColor|function} [color='black'] Text color. May include + * opacity. + * @property {number|function} [textOpacity=1] The opacity of the text. If the + * color includes opacity, this is combined with that value. + * @property {number|function} [rotation=0] Text rotation in radians. + * @property {boolean|function} [rotateWithMap=false] If truthy, rotate the + * text when the map rotates. Otherwise, the text is always in the same + * orientation. + * @property {number|function} [textScaled] If defined, the text is scaled when + * the map zoomes and this is the basis zoom for the fontSize. + * @property {geo.screenPosition|function} [offset] Offset from the default + * position for the text. This is applied before rotation. + * @property {geo.geoColor|function} [shadowColor='black'] Text shadow color. + * May include opacity. + * @property {geo.screenPosition|function} [shadowOffset] Offset for a text + * shadow. This is applied before rotation. + * @property {number|null|function} [shadowBlur] If not null, add a text shadow + * with this much blur. + * @property {boolean|function} [shadowRotate=false] If truthy, rotate the + * shadow offset based on the text rotation (the `shadowOffset` is the + * offset if the text has a 0 rotation). + * @property {geo.geoColor|function} [textStrokeColor='transparent'] Text + * stroke color. May include opacity. + * @property {geo.geoColor|function} [textStrokeWidth=0] Text stroke width in + * pixels. + * @property {number|function} [renderThreshold] If this is a positive number, + * text elements may not be rendered if their base position (before offset + * and font effects are applied) is more than this distance in pixels + * outside of the current viewport. If it is known that such text elements + * cannot affect the current viewport, setting this can speed up rendering. + * This is computed once for the whole feature. */ /** diff --git a/src/tile.js b/src/tile.js index 5bdc233524..4798fc706d 100644 --- a/src/tile.js +++ b/src/tile.js @@ -26,6 +26,7 @@ var $ = require('jquery'); * @param {object?} spec.overlap The size of overlap with neighboring tiles. * @param {number} [spec.overlap.x=0] * @param {number} [spec.overlap.y=0] + * @returns {geo.tile} */ var tile = function (spec) { if (!(this instanceof tile)) { diff --git a/src/util/index.js b/src/util/index.js index 4d7751051f..4fe9b70801 100644 --- a/src/util/index.js +++ b/src/util/index.js @@ -1710,12 +1710,13 @@ var util = module.exports = { * A list of regex and processing functions for color conversions to rgb * objects. Each entry is a {@link geo.util.cssColorConversionRecord}. In * general, these conversions are somewhat more forgiving than the css - * specification (see https://drafts.csswg.org/css-color/) in that + * specification (see {@link https://drafts.csswg.org/css-color/}) in that * percentages may be mixed with numbers, and that floating point values are * accepted for all numbers. Commas are optional. As per the latest draft * standard, `rgb` and `rgba` are aliases of each other, as are `hsl` and * `hsla`. - * @alias cssColorConversions + * @name cssColorConversions + * @property cssColorConversions * @memberof geo.util */ util.cssColorConversions = [{ From cdd014c1fe0f094e484c286ab686619968800b41 Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 17 Aug 2018 15:19:00 -0400 Subject: [PATCH 2/2] Update the fileReader and jsonReader docs. Remove a function that was untested and incorrect from the fileReader. Add tests for the fileReader. Rename the `jsonReader` to `geojsonReader`. An alias called `jsonReader` is maintained to not break existing code. The geojsonReader can take style defaults for points, lines, and polygons. This uses newer language features, such as the spread operator. This resolves #822. This resolves #297, though it does not add a load event. If that is still desired, a new issue should be made. --- CHANGELOG.md | 2 + docs/users.rst | 2 +- examples/geoJSON/main.css | 2 +- examples/geoJSON/main.js | 12 +- package.json | 1 + src/annotationLayer.js | 7 +- src/fileReader.js | 86 ++-- src/geojsonReader.js | 423 +++++++++++++++++++ src/index.js | 2 +- src/jsonReader.js | 308 -------------- tests/cases/fileReader.js | 73 ++++ tests/cases/geojsonReader.js | 696 +++++++++++++++++-------------- tests/cases/map.js | 10 +- tests/gl-cases/d3GeoJson.js | 2 +- tests/gl-cases/glPolygons.js | 2 +- tutorials/choropleth/index.pug | 4 +- tutorials/simple_point/index.pug | 6 +- webpack.base.config.js | 2 + 18 files changed, 985 insertions(+), 655 deletions(-) create mode 100644 src/geojsonReader.js delete mode 100644 src/jsonReader.js create mode 100644 tests/cases/fileReader.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ed76fa4f4a..d8653db57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Added an idle property to objects (#894) - Better handling and changing of camera clipbounds (#899) +- File readers (the geojsonReader) now returns a Promise. The layer will report that it is not idle until this promise is finalized (#905) ### Bug Fixes - Fixed an issue with overlapping, cropped tiles on old browsers (#901) @@ -13,6 +14,7 @@ ### Changes - Changed build process: optional dependencies are now included in the bundle by default (#890) - Transpile with Babel to support old browsers and new language features (#900) +- The geojsonReader has been renamed from `jsonReader` to `geojsonReader`. The old name still works as an alias (#905) ## Version 0.17.0 diff --git a/docs/users.rst b/docs/users.rst index b2ca12242b..83ac1f18cf 100644 --- a/docs/users.rst +++ b/docs/users.rst @@ -181,7 +181,7 @@ documentation for each of the classes. `geo.fileReader `_ This is an abstract class defining the interface for file readers. Currently, the only implemented reader is - `geo.jsonReader `_, + `geo.geojsonReader `_, which is an extendable geojson reader. Coordinate systems diff --git a/examples/geoJSON/main.css b/examples/geoJSON/main.css index 43824ba8a4..6bc3b6098c 100644 --- a/examples/geoJSON/main.css +++ b/examples/geoJSON/main.css @@ -3,7 +3,7 @@ left: 10px; top: 80px; width: calc(50% - 10px); - height: calc(70% - 100px) !important; + height: calc(87% - 80px) !important; z-index: 50; border-radius: 5px; border: 1px solid grey; diff --git a/examples/geoJSON/main.js b/examples/geoJSON/main.js index 43cac8ccfe..af355571e0 100644 --- a/examples/geoJSON/main.js +++ b/examples/geoJSON/main.js @@ -1,7 +1,11 @@ +/* globals utils */ + // Run after the DOM loads $(function () { 'use strict'; + var query = utils.getQuery(); + // Create a map object var map = geo.map({ node: '#map', @@ -27,11 +31,15 @@ $(function () { // Create a layer to put the features in. We could need point, line, and // polygon features, so ask for a layer that supports all of them. - var layer = map.createLayer('feature', {features: ['point', 'line', 'polygon']}); + // Optionally handle a query parameter to try out specific renderers. + var layer = map.createLayer('feature', { + renderer: query.renderer ? (query.renderer === 'html' ? null : query.renderer) : undefined, + features: query.renderer ? undefined : ['point', 'line', 'polygon'] + }); map.draw(); // Initialize the json reader. - var reader = geo.createFileReader('jsonReader', {'layer': layer}); + var reader = geo.createFileReader('geojsonReader', {'layer': layer}); // At this point we could just attach the reader to the map like // this: diff --git a/package.json b/package.json index 335373d1a8..c59aaebc85 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "babel-core": "^6.26.0", "babel-loader": "^7.1.5", "babel-plugin-istanbul": "^4.1.6", + "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.7.0", "body-parser": "^1.15.0", diff --git a/src/annotationLayer.js b/src/annotationLayer.js index 3d795f132a..11fb841cc2 100644 --- a/src/annotationLayer.js +++ b/src/annotationLayer.js @@ -12,8 +12,9 @@ var textFeature = require('./textFeature'); * @typedef {object} geo.annotationLayer.labelRecord * @property {string} text The text of the label * @property {geo.geoPosition} position The position of the label in map gcs - * coordinates. - * @property {object} [style] A {@link geo.textFeature} style object. + * coordinates. + * @property {geo.textFeature.styleSpec} [style] A {@link geo.textFeature} + * style object. */ /** @@ -601,7 +602,7 @@ var annotationLayer = function (args) { */ this.geojson = function (geojson, clear, gcs, includeCrs) { if (geojson !== undefined) { - var reader = registry.createFileReader('jsonReader', {layer: m_this}); + var reader = registry.createFileReader('geojsonReader', {layer: m_this}); if (!reader.canRead(geojson)) { return; } diff --git a/src/fileReader.js b/src/fileReader.js index 03dcc4055e..d9cb140df4 100644 --- a/src/fileReader.js +++ b/src/fileReader.js @@ -3,11 +3,19 @@ var featureLayer = require('./featureLayer'); var object = require('./object'); /** - * Create a new instance of class fileReader + * Object specification for a fileReader. + * + * @typedef {object} geo.fileReader.spec + * @property {geo.featureLayer} layer The target feature layer. + */ + +/** + * Create a new instance of class fileReader. * * @class * @alias geo.fileReader * @extends geo.object + * @param {geo.fileReader.spec} arg * @returns {geo.fileReader} */ var fileReader = function (arg) { @@ -29,7 +37,9 @@ var fileReader = function (arg) { var m_layer = arg.layer; /** - * Get the feature layer attached to the reader + * Get the feature layer attached to the reader. + * + * @returns {geo.featureLayer} The layer associated with the reader. */ this.layer = function () { return m_layer; @@ -37,23 +47,55 @@ var fileReader = function (arg) { /** * Tells the caller if it can handle the given file by returning a boolean. + * + * @param {File|Blob|string|object} file This is either a `File` object, a + * `Blob` object, a string representation of a file, or an object + * representing data from a file. + * @returns {boolean} `true` if this reader can read a file. */ - this.canRead = function () { + this.canRead = function (file) { 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 + * Reads the file and optionally calls a function when finished. The `done` + * function is called with a value that is truthy if the read was a success. + * Depending on the specific reader, this value may be an object with details * of the read operation. + * + * @param {File|Blob|string|object} file This is either a `File` object, a + * `Blob` object, a string representation of a file, or an object + * representing data from a file. + * @param {function} [done] An optional callback function when the read is + * complete. This is called with `false` on error or the object that was + * read and parsed by the reader. + * @param {function} [progress] A function which is passed `ProgressEvent` + * information from a `FileReader`. This includes `loaded` and `total` + * each with a number of bytes. + * @returns {Promise} A `Promise` that resolves with object parsed by the + * reader or is rejected if the reader fails. */ - this.read = function (file, done) { - done(false); + this.read = function (file, done, progress) { + var promise = new Promise(function (resolve, reject) { + if (done) { + done(false); + } + throw new Error('The default file reader always fails'); + }); + this.addPromise(promise); + return promise; }; /** - * Return a FileReader with handlers attached. + * Return a `FileReader` with handlers attached. + * + * @param {function} done A callback that receives either the string read + * from the file or a `DOMException` with an error. + * @param {function} [progress] A function which is passed `ProgressEvent` + * information from a `FileReader`. This includes `loaded` and `total` + * each with a number of bytes. + * @returns {FileReader} The `FileReader` with done and progress handles + * attached to it. */ function newFileReader(done, progress) { var reader = new FileReader(); @@ -61,33 +103,27 @@ var fileReader = function (arg) { reader.onprogress = progress; } reader.onloadend = function () { - if (!reader.result) { - done(reader.error); - } - done(reader.result); + done(reader.error || 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. + * Read a file object as a string. Calls `done` with the string content + * when finished or an error object if unsuccessful. + * + * @param {File|Blob} file A `File` or `Blob` object to read. + * @param {function} done A callback that receives either the string read + * from the file or a `DOMException` with an error. + * @param {function} [progress] A function which is passed `ProgressEvent` + * information from a `FileReader`. This includes `loaded` and `total` + * each with a number of bytes. */ 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; }; diff --git a/src/geojsonReader.js b/src/geojsonReader.js new file mode 100644 index 0000000000..b731765945 --- /dev/null +++ b/src/geojsonReader.js @@ -0,0 +1,423 @@ +var inherit = require('./inherit'); +var registerFileReader = require('./registry').registerFileReader; +var fileReader = require('./fileReader'); + +/** + * Object specification for a geojsonReader. + * + * @typedef {geo.fileReader.spec} geo.geojsonReader.spec + * @extends geo.fileReader.spec + * @property {geo.pointFeature.styleSpec} [pointStyle] Default style for + * points. + * @property {geo.pointFeature.styleSpec} [lineStyle] Default style for lines. + * @property {geo.pointFeature.styleSpec} [polygonStyle] Default style for + * polygons. + */ + +/** + * Create a new instance of class geo.geojsonReader. + * + * @class + * @alias geo.geojsonReader + * @extends geo.fileReader + * @param {geo.fileReader.spec} arg + * @returns {geo.geojsonReader} + */ +var geojsonReader = function (arg) { + 'use strict'; + if (!(this instanceof geojsonReader)) { + return new geojsonReader(arg); + } + + var $ = require('jquery'); + var convertColor = require('./util').convertColor; + + var m_this = this, + m_options = { + ...arg, + pointStyle: { + fill: true, + fillColor: '#ff7800', + fillOpacity: 0.8, + stroke: true, + strokeColor: '#000', + strokeWidth: 1, + strokeOpacity: 1, + radius: 8, + ...arg.pointStyle + }, + lineStyle: { + strokeColor: '#ff7800', + strokeWidth: 4, + strokeOpacity: 0.5, + strokeOffset: 0, + lineCap: 'butt', + lineJoin: 'miter', + closed: false, + ...arg.lineStyle + }, + polygonStyle: { + fill: true, + fillColor: '#b0de5c', + fillOpacity: 0.8, + stroke: true, + strokeColor: '#999999', + strokeWidth: 2, + strokeOpacity: 1, + ...arg.polygonStyle + } + }; + + fileReader.call(this, m_options); + + /** + * Tells the caller if it can handle the given file by returning a boolean. + * + * @param {File|Blob|string|object} file This is either a `File` object, a + * `Blob` object, a string representation of a file, or an object + * representing data from a file. + * @returns {boolean} `true` if this reader can read a file. + */ + this.canRead = function (file) { + if (file instanceof File || file instanceof Blob) { + return !!(file.type === 'application/json' || (file.name && 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; + }; + + /** + * Read or parse a file or object, then call a done function. + * + * @param {File|Blob|string|object} file This is either a `File` object, a + * `Blob` object, a string representation of a file, or an object + * representing data from a file. + * @param {function} done A callback function when the read is complete. + * This is called with `false` on error or the object that was read but + * not yet parsed. + * @param {function} [progress] A function which is passed `ProgressEvent` + * information from a `FileReader`. This includes `loaded` and `total` + * each with a number of bytes. + */ + this._readObject = function (file, done, progress) { + var object; + function onDone(fileString) { + // if fileString is not a JSON string, expect it to be a URL. + try { + object = JSON.parse(fileString); + done(object); + } catch (err) { + if (!object) { + $.ajax({ + type: 'GET', + url: fileString, + dataType: 'text' + }).done(function (data) { + try { + object = JSON.parse(data); + done(object); + } catch (err) { + if (!object) { + done(false); + } + } + }).fail(function () { + done(false); + }); + } + } + } + + if (file instanceof File || file instanceof Blob) { + m_this._getString(file, onDone, progress); + } else if (typeof file === 'string') { + onDone(file); + } else { + done(file); + } + }; + + /** + * Return an array of normalized geojson features. This turns bare + * geometries into features and multi-geometry features into single geometry + * features. + * + * Returns an array of Point, LineString, or Polygon features. + * @param {geojson.object} spec A parsed geojson object. + * @returns {geojson.FeatureObject[]} An array of feature objects, none of + * which include multi-geometries, and none have empty geometries. + */ + this._featureArray = function (spec) { + var features, normalized = []; + switch (spec.type) { + case 'FeatureCollection': + features = spec.features; + break; + + case 'Feature': + features = [spec]; + break; + + case 'GeometryCollection': + features = spec.geometries.map(function (g) { + return { + type: 'Feature', + geometry: g, + properties: {} + }; + }); + break; + + case 'Point': + case 'LineString': + case 'Polygon': + case 'MultiPoint': + case 'MultiLineString': + case 'MultiPolygon': + features = [{ + type: 'Feature', + geometry: spec, + properties: {} + }]; + break; + + default: + throw new Error('Invalid json type'); + } + + // flatten multi features + features.forEach(function (feature) { + Array.prototype.push.apply(normalized, m_this._feature(feature)); + }); + + // remove features with empty geometries + normalized = normalized.filter(function (feature) { + return feature.geometry && + feature.geometry.coordinates && + feature.geometry.coordinates.length; + }); + return normalized; + }; + + /** + * Normalize a feature object turning multi geometry features into an array + * of features, and single geometry features into an array containing one + * feature. + * + * @param {geojson.object} spec A parsed geojson object. + * @returns {geojson.FeatureObject[]} An array of feature objects, none of + * which include multi-geometries. + */ + this._feature = function (spec) { + if (spec.type !== 'Feature') { + throw new Error('Invalid feature object'); + } + switch (spec.geometry.type) { + case 'Point': + case 'LineString': + case 'Polygon': + return [spec]; + + case 'MultiPoint': + case 'MultiLineString': + case 'MultiPolygon': + return spec.geometry.coordinates.map(function (c) { + return { + type: 'Feature', + geometry: { + type: spec.geometry.type.replace('Multi', ''), + coordinates: c + }, + properties: spec.properties + }; + }); + + default: + throw new Error('Invalid geometry type'); + } + }; + + /** + * Convert from a geojson position array into a geojs position object. + * + * @param {number[]} p A coordinate in the form of an array with two or three + * components. + * @returns {geo.geoPosition} + */ + this._position = function (p) { + return { + x: p[0], + y: p[1], + z: p[2] || 0 + }; + }; + + /** + * Defines a style accessor the returns the given value of the property + * object or a default value. + * + * @param {string} prop The property name. + * @param {object} _default The default value. + * @returns {function} A style function for the property. + */ + this._style = function (prop, _default) { + var isColor = prop.toLowerCase().match(/color$/); + if (isColor) { + _default = convertColor(_default); + } + return function (v, j, d, i) { + var p; + if (d !== undefined && d.properties) { + p = d.properties; + } else { + p = v.properties; + } + if (p !== undefined && p.hasOwnProperty(prop)) { + return isColor ? convertColor(p[prop]) : p[prop]; + } + return _default; + }; + }; + + /** + * Reads the file and optionally calls a function when finished. The `done` + * function is called with a list of {@link geo.feature} on success or + * `false` on failure. + * + * @param {File|Blob|string|object} file This is either a `File` object, a + * `Blob` object, a string representation of a file, or an object + * representing data from a file. + * @param {function} [done] An optional callback function when the read is + * complete. This is called with `false` on error or the object that was + * read and parsed by the reader. + * @param {function} [progress] A function which is passed `ProgressEvent` + * information from a `FileReader`. This includes `loaded` and `total` + * each with a number of bytes. + * @returns {Promise} A `Promise` that resolves with a list of + * {@link geo.feature} or is rejected if the reader fails. + */ + this.read = function (file, done, progress) { + var promise = new Promise(function (resolve, reject) { + /** + * Given a parsed GeoJSON object, convert it into features on the + * reader's layer. + * + * @param {geojson.object|false} object Either a parse GeoJSON object or + * `false` for an error. + */ + function _done(object) { + if (object === false) { + if (done) { + done(object); + } + reject(new Error('Failed to parse GeoJSON')); + return; + } + let features, allFeatures = [], points, lines, polygons, feature; + + try { + features = m_this._featureArray(object); + } catch (err) { + reject(err); + return; + } + + // process points + points = features.filter(f => f.geometry.type === 'Point'); + if (points.length) { + feature = m_this.layer().createFeature('point'); + if (feature) { + feature + .data(points) + .position(d => m_this._position(d.geometry.coordinates)) + // create an object with each property in m_options.pointStyle, + // mapping the values through the _style function. + .style( + [{}].concat(Object.keys(m_options.pointStyle)).reduce( + (styleObj, key) => ({ + [key]: m_this._style(key, m_options.pointStyle[key]), + ...styleObj + } + )) + ); + allFeatures.push(feature); + } + } + + // process lines + lines = features.filter(f => f.geometry.type === 'LineString'); + if (lines.length) { + feature = m_this.layer().createFeature('line'); + if (feature) { + feature + .data(lines) + .line(d => d.geometry.coordinates) + .position(m_this._position) + // create an object with each property in m_options.lineStyle, + // mapping the values through the _style function. + .style( + [{}].concat(Object.keys(m_options.lineStyle)).reduce( + (styleObj, key) => ({ + [key]: m_this._style(key, m_options.lineStyle[key]), + ...styleObj + } + )) + ); + allFeatures.push(feature); + } + } + + // process polygons + polygons = features.filter(f => f.geometry.type === 'Polygon'); + if (polygons.length) { + feature = m_this.layer().createFeature('polygon'); + if (feature) { + feature + .data(polygons) + .polygon((d, i) => ({ + outer: d.geometry.coordinates[0], + inner: d.geometry.coordinates.slice(1) + })) + .position(m_this._position) + // create an object with each property in m_options.polygonStyle, + // mapping the values through the _style function. + .style( + [{}].concat(Object.keys(m_options.polygonStyle)).reduce( + (styleObj, key) => ({ + [key]: m_this._style(key, m_options.polygonStyle[key]), + ...styleObj + } + )) + ); + allFeatures.push(feature); + } + } + if (done) { + done(allFeatures); + } + resolve(allFeatures); + } + + m_this._readObject(file, _done, progress); + }); + this.addPromise(promise); + return promise; + }; +}; + +inherit(geojsonReader, fileReader); +registerFileReader('geojsonReader', geojsonReader); +// Also register under an alternate name +registerFileReader('jsonReader', geojsonReader); +module.exports = geojsonReader; diff --git a/src/index.js b/src/index.js index a0eed596a2..601b92fd86 100644 --- a/src/index.js +++ b/src/index.js @@ -54,7 +54,7 @@ module.exports = $.extend({ heatmapFeature: require('./heatmapFeature'), imageTile: require('./imageTile'), isolineFeature: require('./isolineFeature'), - jsonReader: require('./jsonReader'), + geojsonReader: require('./geojsonReader'), layer: require('./layer'), lineFeature: require('./lineFeature'), map: require('./map'), diff --git a/src/jsonReader.js b/src/jsonReader.js deleted file mode 100644 index d53ac7bc56..0000000000 --- a/src/jsonReader.js +++ /dev/null @@ -1,308 +0,0 @@ -var inherit = require('./inherit'); -var registerFileReader = require('./registry').registerFileReader; -var fileReader = require('./fileReader'); - -/** -* 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 $ = require('jquery'); - var convertColor = require('./util').convertColor; - - var m_this = this; - - 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); - } - }; - - /** - * Return an array of normalized geojson features. This - * will do the following: - * - * 1. Turn bare geometries into features - * 2. Turn multi-geometry features into single geometry features - * - * Returns an array of Point, LineString, or Polygon features. - * @protected - */ - this._featureArray = function (spec) { - var features, normalized = []; - switch (spec.type) { - case 'FeatureCollection': - features = spec.features; - break; - - case 'Feature': - features = [spec]; - break; - - case 'GeometryCollection': - features = spec.geometries.map(function (g) { - return { - type: 'Feature', - geometry: g, - properties: {} - }; - }); - break; - - case 'Point': - case 'LineString': - case 'Polygon': - case 'MultiPoint': - case 'MultiLineString': - case 'MultiPolygon': - features = [{ - type: 'Feature', - geometry: spec, - properties: {} - }]; - break; - - default: - throw new Error('Invalid json type'); - } - - // flatten multi features - features.forEach(function (feature) { - Array.prototype.push.apply(normalized, m_this._feature(feature)); - }); - - // remove features with empty geometries - normalized = normalized.filter(function (feature) { - return feature.geometry && - feature.geometry.coordinates && - feature.geometry.coordinates.length; - }); - return normalized; - }; - - /** - * Normalize a feature object turning multi geometry features - * into an array of features, and single geometry features into - * an array containing one feature. - */ - this._feature = function (spec) { - if (spec.type !== 'Feature') { - throw new Error('Invalid feature object'); - } - switch (spec.geometry.type) { - case 'Point': - case 'LineString': - case 'Polygon': - return [spec]; - - case 'MultiPoint': - case 'MultiLineString': - case 'MultiPolygon': - return spec.geometry.coordinates.map(function (c) { - return { - type: 'Feature', - geometry: { - type: spec.geometry.type.replace('Multi', ''), - coordinates: c - }, - properties: spec.properties - }; - }); - - default: - throw new Error('Invalid geometry type'); - } - }; - - /** - * Convert from a geojson position array into a geojs position object. - */ - this._position = function (p) { - return { - x: p[0], - y: p[1], - z: p[2] || 0 - }; - }; - - /** - * Defines a style accessor the returns the given - * value of the property object, or a default value. - * - * @protected - * @param {string} prop The property name - * @param {object} default The default value - * @param {object} [spec] The argument containing the main property object - * @param {function} [convert] An optional conversion function - */ - this._style = function (prop, _default, spec, convert) { - convert = convert || function (d) { return d; }; - _default = convert(_default); - return function (d, i, e, j) { - var p; - if (spec && j !== undefined && spec[j] !== undefined) { - p = spec[j].properties; - } else { - p = d.properties; - } - if (p !== undefined && p.hasOwnProperty(prop)) { - return convert(p[prop]); - } - return _default; - }; - }; - - this.read = function (file, done, progress) { - - function _done(object) { - var features, allFeatures = [], points, lines, polygons, feature; - - features = m_this._featureArray(object); - - // process points - points = features.filter(function (f) { return f.geometry.type === 'Point'; }); - if (points.length) { - feature = m_this.layer().createFeature('point'); - if (feature) { - feature - .data(points) - .position(function (d) { - return m_this._position(d.geometry.coordinates); - }) - .style({ - fill: m_this._style('fill', true), - fillColor: m_this._style('fillColor', '#ff7800', null, convertColor), - fillOpacity: m_this._style('fillOpacity', 0.8), - stroke: m_this._style('stroke', true), - strokeColor: m_this._style('strokeColor', '#000000', null, convertColor), - strokeWidth: m_this._style('strokeWidth', 1), - strokeOpacity: m_this._style('strokeOpacity', 1), - radius: m_this._style('radius', 8) - }); - allFeatures.push(feature); - } - } - - // process lines - lines = features.filter(function (f) { return f.geometry.type === 'LineString'; }); - if (lines.length) { - feature = m_this.layer().createFeature('line'); - if (feature) { - feature - .data(lines) - .line(function (d) { - return d.geometry.coordinates; - }) - .position(m_this._position) - .style({ - strokeColor: m_this._style('strokeColor', '#ff7800', lines, convertColor), - strokeWidth: m_this._style('strokeWidth', 4, lines), - strokeOpacity: m_this._style('strokeOpacity', 0.5, lines), - strokeOffset: m_this._style('strokeOffset', 0, lines), - lineCap: m_this._style('lineCap', 'butt', lines), - lineJoin: m_this._style('lineCap', 'miter', lines), - closed: m_this._style('closed', false, lines) - }); - allFeatures.push(feature); - } - } - - // process polygons - polygons = features.filter(function (f) { return f.geometry.type === 'Polygon'; }); - if (polygons.length) { - feature = m_this.layer().createFeature('polygon'); - if (feature) { - feature - .data(polygons) - .polygon(function (d, i) { - return { - outer: d.geometry.coordinates[0], - inner: d.geometry.coordinates.slice(1) - }; - }) - .position(m_this._position) - .style({ - fill: m_this._style('fill', true), - fillColor: m_this._style('fillColor', '#b0de5c', polygons, convertColor), - fillOpacity: m_this._style('fillOpacity', 0.8, polygons), - stroke: m_this._style('stroke', true), - strokeColor: m_this._style('strokeColor', '#999999', polygons, convertColor), - strokeWidth: m_this._style('strokeWidth', 2, polygons), - strokeOpacity: m_this._style('strokeOpacity', 1, polygons) - }); - allFeatures.push(feature); - } - } - if (done) { - done(allFeatures); - } - } - - m_this._readObject(file, _done, progress); - }; -}; - -inherit(jsonReader, fileReader); -registerFileReader('jsonReader', jsonReader); -module.exports = jsonReader; diff --git a/tests/cases/fileReader.js b/tests/cases/fileReader.js new file mode 100644 index 0000000000..c5df192ca0 --- /dev/null +++ b/tests/cases/fileReader.js @@ -0,0 +1,73 @@ +/* globals Blob */ + +var geo = require('../test-utils').geo; +var createMap = require('../test-utils').createMap; + +describe('geo.fileReader', function () { + 'use strict'; + + describe('create', function () { + it('create function', function () { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.fileReader({'layer': layer}); + expect(reader instanceof geo.fileReader).toBe(true); + expect(function () { + geo.fileReader(); + }).toThrow(new Error('fileReader must be given a feature layer')); + }); + }); + + describe('Check class accessors', function () { + it('layer', function () { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.fileReader({'layer': layer}); + expect(reader.layer()).toBe(layer); + }); + }); + + describe('Public utility methods', function () { + it('canRead', function () { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.fileReader({'layer': layer}); + // the default canRead implementation returns false + expect(reader.canRead('')).toBe(false); + }); + it('read', function (done) { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.fileReader({'layer': layer}); + expect(reader.read('', function (result) { + expect(result).toBe(false); + done(); + }) instanceof Promise); + }); + }); + + describe('Private utility methods', function () { + it('_getString', function (done) { + var map, layer, reader, file, progress; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.fileReader({'layer': layer}); + // The PhantomJS browser does't support `new File`, so use `new Blob` + file = new Blob(['This is ', 'a test'], {type: 'text/plain'}); + file.lastModifiedDate = new Date(); + file.name = 'test.txt'; + reader._getString(file, function (result) { + expect(progress.loaded).toBe(14); + expect(progress.total).toBe(14); + expect(result).toBe('This is a test'); + done(); + }, function (evt) { + progress = evt; + }); + }); + }); +}); diff --git a/tests/cases/geojsonReader.js b/tests/cases/geojsonReader.js index 008f8f75b6..ec68b39ea2 100644 --- a/tests/cases/geojsonReader.js +++ b/tests/cases/geojsonReader.js @@ -1,365 +1,457 @@ -describe('geojsonReader', function () { - var geo = require('../test-utils').geo; - var createMap = require('../test-utils').createMap; +/* globals Blob */ - describe('geojsonReader', function () { - 'use strict'; +var geo = require('../test-utils').geo; +var createMap = require('../test-utils').createMap; - var obj, map, layer; +describe('geo.geojsonReader', function () { + var obj = { + features: [{ + geometry: { + coordinates: [102.0, 0.5], + type: 'Point' + }, + properties: { + // these are deliberately properties that are not used + color: [1, 0, 0], + size: [10] + }, + type: 'Feature' + }, { + geometry: { + coordinates: [ + [102.0, 0.0, 0], + [103.0, 1.0, 1], + [104.0, 0.0, 2], + [105.0, 1.0, 3] + ], + type: 'LineString' + }, + properties: { + color: [0, 1, 0], + width: [3] + }, + type: 'Feature' + }, { + geometry: { + coordinates: [ + [10.0, 0.5], + [10.0, -0.5] + ], + type: 'MultiPoint' + }, + properties: { + fillColor: '#0000ff', + radius: 7 + }, + type: 'Feature' + }], + type: 'FeatureCollection' + }; - describe('Feature normalization', function () { - var reader; + describe('create', () => { + it('create function', () => { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.geojsonReader({'layer': layer}); + expect(reader instanceof geo.geojsonReader).toBe(true); + }); + it('create by name', () => { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.createFileReader('geojsonReader', {'layer': layer}); + expect(reader instanceof geo.geojsonReader).toBe(true); + // create by old name + reader = geo.createFileReader('jsonReader', {'layer': layer}); + expect(reader instanceof geo.geojsonReader).toBe(true); + }); - beforeEach(function () { - map = createMap({center: [0, 0], zoom: 3}); + describe('with styles', () => { + it('default', done => { + var map, layer, reader; + map = createMap(); layer = map.createLayer('feature', {renderer: 'd3'}); - sinon.stub(layer, 'createFeature'); - reader = geo.createFileReader('jsonReader', {'layer': layer}); + reader = geo.geojsonReader({'layer': layer}); + reader.read(obj).then(result => { + expect(layer.features()[0].style.get('fillColor')(layer.features()[0].data()[0], 0)).toEqual({r: 1, g: 0x78 / 0xff, b: 0}); + done(); + }); }); - afterEach(function () { - layer.createFeature.restore(); + it('specified', done => { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'd3'}); + reader = geo.geojsonReader({'layer': layer, pointStyle: {fillColor: 'lightblue'}}); + reader.read(obj).then(result => { + expect(layer.features()[0].style.get('fillColor')(layer.features()[0].data()[0], 0)).toEqual({r: 0xad / 0xff, g: 0xd8 / 0xff, b: 0xe6 / 0xff}); + done(); + }); }); + it('from data', done => { + var map, layer, reader; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'd3'}); + reader = geo.geojsonReader({'layer': layer, pointStyle: {fillColor: 'lightblue'}}); + obj.features[0].properties.fillColor = 'yellow'; + reader.read(obj).then(result => { + expect(layer.features()[0].style.get('fillColor')(layer.features()[0].data()[0], 0)).toEqual({r: 1, g: 1, b: 0}); + done(); + }); + }); + }); + }); - describe('bare geometry', function () { - it('Point', function () { - expect(reader._featureArray({ - type: 'Point', - coordinates: [1, 2] - })).toEqual([{ - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [1, 2] - } - }]); + describe('Public utility methods', () => { + it('canRead', () => { + var map, layer, reader, file; + map = createMap(); + layer = map.createLayer('feature'); + reader = geo.geojsonReader({'layer': layer}); + expect(reader.canRead('')).toBe(false); + expect(reader.canRead(['not a geojson object'])).toBe(false); + expect(reader.canRead(obj)).toBe(true); + // The PhantomJS browser don't support `new File`, so use `new Blob` + file = new Blob([JSON.stringify(obj)], {type: 'text/plain'}); + file.lastModifiedDate = new Date(); + file.name = 'test.txt'; + expect(reader.canRead(file)).toBe(false); + file.name = 'test.json'; + expect(reader.canRead(file)).toBe(true); + }); + // tests all of _readObject as part of the read test + describe('read', () => { + var map, layer, reader; + it('bad object', done => { + map = createMap(); + layer = map.createLayer('feature', {renderer: 'd3'}); + reader = geo.geojsonReader({'layer': layer}); + + reader.read(['not geojson object']).catch(err => { + expect(!!err.message.match(/Invalid json type/)).toBe(true); + done(); + reader.read(['not geojson object'], result => { + expect(result).toBe(false); + done(); + }); }); - it('LineString', function () { - expect(reader._featureArray({ - type: 'LineString', - coordinates: [[1, 2], [3, 4]] - })).toEqual([{ - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [[1, 2], [3, 4]] - } - }]); + }); + it('non geojson', done => { + reader.read('not geojson').catch(err => { + expect(!!err.message.match(/Failed to parse/)).toBe(true); + reader.read('not geojson', result => { + expect(result).toBe(false); + done(); + }); }); - it('Polygon', function () { - expect(reader._featureArray({ - type: 'Polygon', - coordinates: [[[1, 2], [3, 4], [5, 6]]] - })).toEqual([{ - type: 'Feature', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: [[[1, 2], [3, 4], [5, 6]]] - } - }]); + }); + it('object', done => { + reader.read(obj).then(result => { + expect(result.length).toBe(2); + done(); }); - it('MultiPoint', function () { - expect(reader._featureArray({ - type: 'MultiPoint', - coordinates: [[1, 2], [3, 4]] - })).toEqual([{ - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [1, 2] - } - }, { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [3, 4] - } - }]); + }); + it('blob', done => { + var file = new Blob([JSON.stringify(obj)], {type: 'text/plain'}); + file.lastModifiedDate = new Date(); + file.name = 'test.json'; + reader.read(file).then(result => { + expect(result.length).toBe(2); + done(); }); - it('MultiLineString', function () { - expect(reader._featureArray({ - type: 'MultiLineString', - coordinates: [[[1, 2], [3, 4]]] - })).toEqual([{ - type: 'Feature', - properties: {}, - geometry: { - type: 'LineString', - coordinates: [[1, 2], [3, 4]] - } - }]); + }); + it('url', done => { + reader.read('/testdata/sample.json').then(result => { + expect(result.length).toBe(2); + done(); }); - it('MultiPolygon', function () { - expect(reader._featureArray({ - type: 'MultiPolygon', - coordinates: [[[[1, 2], [3, 4], [5, 6]]]] - })).toEqual([{ - type: 'Feature', - properties: {}, - geometry: { - type: 'Polygon', - coordinates: [[[1, 2], [3, 4], [5, 6]]] - } - }]); + }); + it('url with non-json data', done => { + reader.read('/testdata/white.jpg').catch(err => { + expect(!!err.message.match(/Failed to parse/)).toBe(true); + done(); }); }); + }); + }); - it('GeometryCollection', function () { - expect(reader._featureArray({ - type: 'GeometryCollection', - geometries: [ - { - type: 'Point', - coordinates: [0, 0] - }, { - type: 'MultiPoint', - coordinates: [[0, 1], [2, 3]] - } - ] - })).toEqual([ - { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [0, 0] - } - }, { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [0, 1] - } - }, { - type: 'Feature', - properties: {}, - geometry: { - type: 'Point', - coordinates: [2, 3] - } - } - ]); - }); + describe('Feature normalization', function () { + var map, layer, reader; - it('Feature', function () { + beforeEach(function () { + map = createMap({center: [0, 0], zoom: 3}); + layer = map.createLayer('feature'); + sinon.stub(layer, 'createFeature'); + reader = geo.createFileReader('geojsonReader', {'layer': layer}); + }); + afterEach(function () { + layer.createFeature.restore(); + }); + + describe('bare geometry', function () { + it('Point', function () { expect(reader._featureArray({ + type: 'Point', + coordinates: [1, 2] + })).toEqual([{ type: 'Feature', + properties: {}, geometry: { type: 'Point', coordinates: [1, 2] - }, - properties: {a: 1} + } + }]); + }); + it('LineString', function () { + expect(reader._featureArray({ + type: 'LineString', + coordinates: [[1, 2], [3, 4]] })).toEqual([{ type: 'Feature', - properties: {a: 1}, + properties: {}, geometry: { - type: 'Point', - coordinates: [1, 2] + type: 'LineString', + coordinates: [[1, 2], [3, 4]] } }]); }); - - it('FeatureCollection', function () { + it('Polygon', function () { expect(reader._featureArray({ - type: 'FeatureCollection', - features: [{ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [1, 2] - }, - properties: {a: 1} - }, { - type: 'Feature', - geometry: { - type: 'MultiPoint', - coordinates: [[0, 0], [1, 1]] - }, - properties: {b: 2} - }] + type: 'Polygon', + coordinates: [[[1, 2], [3, 4], [5, 6]]] + })).toEqual([{ + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [[[1, 2], [3, 4], [5, 6]]] + } + }]); + }); + it('MultiPoint', function () { + expect(reader._featureArray({ + type: 'MultiPoint', + coordinates: [[1, 2], [3, 4]] })).toEqual([{ type: 'Feature', - properties: {a: 1}, + properties: {}, geometry: { type: 'Point', coordinates: [1, 2] } }, { type: 'Feature', - properties: {b: 2}, + properties: {}, + geometry: { + type: 'Point', + coordinates: [3, 4] + } + }]); + }); + it('MultiLineString', function () { + expect(reader._featureArray({ + type: 'MultiLineString', + coordinates: [[[1, 2], [3, 4]]] + })).toEqual([{ + type: 'Feature', + properties: {}, + geometry: { + type: 'LineString', + coordinates: [[1, 2], [3, 4]] + } + }]); + }); + it('MultiPolygon', function () { + expect(reader._featureArray({ + type: 'MultiPolygon', + coordinates: [[[[1, 2], [3, 4], [5, 6]]]] + })).toEqual([{ + type: 'Feature', + properties: {}, + geometry: { + type: 'Polygon', + coordinates: [[[1, 2], [3, 4], [5, 6]]] + } + }]); + }); + }); + + it('GeometryCollection', function () { + expect(reader._featureArray({ + type: 'GeometryCollection', + geometries: [ + { + type: 'Point', + coordinates: [0, 0] + }, { + type: 'MultiPoint', + coordinates: [[0, 1], [2, 3]] + } + ] + })).toEqual([ + { + type: 'Feature', + properties: {}, geometry: { type: 'Point', coordinates: [0, 0] } }, { type: 'Feature', - properties: {b: 2}, + properties: {}, geometry: { type: 'Point', - coordinates: [1, 1] + coordinates: [0, 1] } - }]); + }, { + type: 'Feature', + properties: {}, + geometry: { + type: 'Point', + coordinates: [2, 3] + } + } + ]); + }); - }); + it('Feature', function () { + expect(reader._featureArray({ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [1, 2] + }, + properties: {a: 1} + })).toEqual([{ + type: 'Feature', + properties: {a: 1}, + geometry: { + type: 'Point', + coordinates: [1, 2] + } + }]); + }); - it('empty geometry', function () { - expect(reader._featureArray({ - type: 'FeatureCollection', - features: [{ - type: 'Feature', - geometry: { - type: 'Point', - coordinates: [] - } - }, { + it('FeatureCollection', function () { + expect(reader._featureArray({ + type: 'FeatureCollection', + features: [{ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [1, 2] + }, + properties: {a: 1} + }, { + type: 'Feature', + geometry: { + type: 'MultiPoint', + coordinates: [[0, 0], [1, 1]] + }, + properties: {b: 2} + }] + })).toEqual([{ + type: 'Feature', + properties: {a: 1}, + geometry: { + type: 'Point', + coordinates: [1, 2] + } + }, { + type: 'Feature', + properties: {b: 2}, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + }, { + type: 'Feature', + properties: {b: 2}, + geometry: { + type: 'Point', + coordinates: [1, 1] + } + }]); + + }); + + it('empty geometry', function () { + expect(reader._featureArray({ + type: 'FeatureCollection', + features: [{ + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [] + } + }, { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [] + } + }, { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [] + } + }] + })).toEqual([]); + }); + + describe('Errors', function () { + it('Invalid geometry', function () { + expect(function () { + reader._feature({ type: 'Feature', + properties: {}, geometry: { - type: 'LineString', - coordinates: [] + type: 'pt', + coordinates: [0, 0] } - }, { - type: 'Feature', + }); + }).toThrow(); + }); + + it('Invalid feature', function () { + expect(function () { + reader._feature({ + properties: {}, geometry: { - type: 'Polygon', - coordinates: [] + type: 'Point', + coordinates: [0, 0] } - }] - })).toEqual([]); + }); + }).toThrow(); }); - - describe('Errors', function () { - it('Invalid geometry', function () { - expect(function () { - reader._feature({ - type: 'Feature', - properties: {}, - geometry: { - type: 'pt', - coordinates: [0, 0] - } - }); - }).toThrow(); - }); - - it('Invalid feature', function () { - expect(function () { - reader._feature({ - properties: {}, - geometry: { - type: 'Point', - coordinates: [0, 0] - } - }); - }).toThrow(); - }); - it('Invalid JSON', function () { - expect(function () { - reader._featureArray({ - features: [] - }); - }).toThrow(); - }); + it('Invalid JSON', function () { + expect(function () { + reader._featureArray({ + features: [] + }); + }).toThrow(); }); }); + }); - it('Setup map', function () { - map = createMap({center: [0, 0], zoom: 3}); - layer = map.createLayer('feature', {renderer: 'd3'}); - - obj = { - 'features': [ - { - 'geometry': { - 'coordinates': [ - 102.0, - 0.5 - ], - 'type': 'Point' - }, - 'properties': { - 'color': [1, 0, 0], - 'size': [10] - }, - 'type': 'Feature' - }, - { - 'geometry': { - 'coordinates': [ - [ - 102.0, - 0.0, - 0 - ], - [ - 103.0, - 1.0, - 1 - ], - [ - 104.0, - 0.0, - 2 - ], - [ - 105.0, - 1.0, - 3 - ] - ], - 'type': 'LineString' - }, - 'properties': { - 'color': [0, 1, 0], - 'width': [3] - }, - 'type': 'Feature' - }, - { - 'geometry': { - 'coordinates': [ - [ - 10.0, - 0.5 - ], - [ - 10.0, - -0.5 - ] - ], - 'type': 'MultiPoint' - }, - 'properties': { - 'fillColor': '#0000ff', - 'radius': 7 - }, - 'type': 'Feature' - } - ], - 'type': 'FeatureCollection' - }; - }); - it('read from object', function (done) { - var reader = geo.createFileReader('jsonReader', {'layer': layer}), - data, i; + it('read from object', function (done) { + var map = createMap({center: [0, 0], zoom: 3}); + var layer = map.createLayer('feature', {renderer: 'd3'}); + var reader = geo.createFileReader('geojsonReader', {'layer': layer}), + data, i; - expect(reader.canRead(obj)).toBe(true); - reader.read(obj, function (features) { - expect(features.length).toEqual(2); + expect(reader.canRead(obj)).toBe(true); + reader.read(obj, function (features) { + expect(features.length).toEqual(2); - // Validate that we are getting the correct Z values - data = features[1].data()[0]; - for (i = 0; i < data.length; i += 1) { - expect(data[i].z()).toEqual(i); - } + // Validate that we are getting the correct Z values + data = features[1].data()[0]; + for (i = 0; i < data.length; i += 1) { + expect(data[i].z()).toEqual(i); + } - done(); - }); + done(); }); - }); }); diff --git a/tests/cases/map.js b/tests/cases/map.js index 5bbb09f026..688fe41cf5 100644 --- a/tests/cases/map.js +++ b/tests/cases/map.js @@ -398,13 +398,13 @@ describe('geo.core.map', function () { var m = createMap(); expect(m.fileReader()).toBe(null); var layerCount = m.layers().length; - expect(m.fileReader('jsonReader')).toBe(m); + expect(m.fileReader('geojsonReader')).toBe(m); expect(m.fileReader()).not.toBe(null); expect(m.layers().length).toBe(layerCount + 1); expect(m.layers()[m.layers().length - 1].renderer().api()).not.toBe('d3'); - expect(m.fileReader('jsonReader', {renderer: 'd3'})).toBe(m); + expect(m.fileReader('geojsonReader', {renderer: 'd3'})).toBe(m); expect(m.layers()[m.layers().length - 1].renderer().api()).toBe('d3'); - var r = geo.createFileReader('jsonReader', {layer: m.layers()[m.layers().length - 1]}); + var r = geo.createFileReader('geojsonReader', {layer: m.layers()[m.layers().length - 1]}); expect(m.fileReader(r)).toBe(m); expect(m.fileReader()).toBe(r); }); @@ -988,7 +988,7 @@ describe('geo.core.map', function () { evt.originalEvent.dataTransfer = {}; $(m.node()).trigger(evt); expect(evt.originalEvent.dataTransfer.dropEffect).not.toBe('copy'); - m.fileReader('jsonReader'); + m.fileReader('geojsonReader'); evt = $.Event('dragover'); evt.originalEvent = new window.Event('dragover'); evt.originalEvent.dataTransfer = {}; @@ -997,7 +997,7 @@ describe('geo.core.map', function () { }); it('drop', function () { var m = createMap(); - m.fileReader('jsonReader', {renderer: 'd3'}); + m.fileReader('geojsonReader', {renderer: 'd3'}); var evt = $.Event('drop'); evt.originalEvent = new window.Event('drop'); evt.originalEvent.dataTransfer = {files: [{ diff --git a/tests/gl-cases/d3GeoJson.js b/tests/gl-cases/d3GeoJson.js index 882d01e98d..a8914abb7c 100644 --- a/tests/gl-cases/d3GeoJson.js +++ b/tests/gl-cases/d3GeoJson.js @@ -73,7 +73,7 @@ describe('d3GeoJSON', function () { var mapOptions = {center: {x: -105.0, y: 40.0}, zoom: 3.5}; myMap = common.createOsmMap(mapOptions, {}, true); var layer = myMap.createLayer('feature', {'renderer': 'd3'}); - var reader = geo.createFileReader('jsonReader', {'layer': layer}); + var reader = geo.createFileReader('geojsonReader', {'layer': layer}); reader.read(obj, function () { myMap.draw(); imageTest.imageTest('d3GeoJson', null, 0.0015, done, myMap.onIdle, 0, 2); diff --git a/tests/gl-cases/glPolygons.js b/tests/gl-cases/glPolygons.js index a62e8868ec..8b5e6bee7b 100644 --- a/tests/gl-cases/glPolygons.js +++ b/tests/gl-cases/glPolygons.js @@ -2442,7 +2442,7 @@ describe('glPolygons', function () { var layer = myMap.createLayer('feature'); - geo.createFileReader('jsonReader', { + geo.createFileReader('geojsonReader', { layer: layer }).read(JSON.stringify(data), function () { myMap.draw(); diff --git a/tutorials/choropleth/index.pug b/tutorials/choropleth/index.pug index fd29656648..a3052b2fa6 100644 --- a/tutorials/choropleth/index.pug +++ b/tutorials/choropleth/index.pug @@ -4,7 +4,7 @@ block mainTutorial :markdown-it # Tutorial - Choropleth Map First, let's create our map, add a base map, and create a feature layer. Also, to keep it simple, we import our data inline. - + +codeblock('javascript', 1, undefined, true). var map = geo.map({ node: '#map', @@ -21,7 +21,7 @@ block mainTutorial We can use GeoJSON reader to create the polygon feature. +codeblock('javascript', 2, 1, true). - var reader = geo.createFileReader('jsonReader', { 'layer': layer }); + var reader = geo.createFileReader('geojsonReader', { 'layer': layer }); var polygon = null; reader.read(states, function (features) { polygon = features[0]; diff --git a/tutorials/simple_point/index.pug b/tutorials/simple_point/index.pug index 6240ba8632..b9676fd23e 100644 --- a/tutorials/simple_point/index.pug +++ b/tutorials/simple_point/index.pug @@ -24,9 +24,9 @@ block mainTutorial :markdown-it We create a layer first. - + +codeblock('javascript', 2). - var layer = map.createLayer('feature'); + var layer = map.createLayer('feature'); :markdown-it @@ -49,7 +49,7 @@ block mainTutorial +codeblock('javascript', 10, 2, false, 'Step 3-B'). var geojsonCities = {"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"Point","coordinates":[-74.0059413,40.7127837]},"properties":{"name":"New York","population":"8405837"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-118.2436849,34.0522342]},"properties":{"name":"Los Angeles","population":"3884307"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-87.6297982,41.8781136]},"properties":{"name":"Chicago","population":"2718782"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-95.3698028,29.7604267]},"properties":{"name":"Houston","population":"2195914"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-75.1652215,39.9525839]},"properties":{"name":"Philadelphia","population":"1553165"}},{"type":"Feature","geometry":{"type":"Point","coordinates":[-112.0740373,33.4483771]},"properties":{"name":"Phoenix","population":"1513367"}}]}; - var reader = geo.createFileReader('jsonReader', { 'layer': layer }); + var reader = geo.createFileReader('geojsonReader', { 'layer': layer }); reader.read(geojsonCities, function (features) { features[0].draw(); // Or we can draw the whole map diff --git a/webpack.base.config.js b/webpack.base.config.js index 094059bb46..d87d983820 100644 --- a/webpack.base.config.js +++ b/webpack.base.config.js @@ -83,6 +83,7 @@ module.exports = { use: [{ loader: 'babel-loader', options: { + plugins: [require('babel-plugin-transform-object-rest-spread')], presets: [[ 'env', { targets: { @@ -105,6 +106,7 @@ module.exports = { use: [{ loader: 'babel-loader', options: { + plugins: [require('babel-plugin-transform-object-rest-spread')], presets: [[ 'env', { targets: {