diff --git a/examples/geoJSON/data.json b/examples/geoJSON/data.json index 8802a75d02..2908eaaffd 100644 --- a/examples/geoJSON/data.json +++ b/examples/geoJSON/data.json @@ -43,7 +43,7 @@ }, "geometry": { "type": "Point", - "coordinates": [ [ -104.9847, 39.7392 ] ] + "coordinates": [ -104.9847, 39.7392 ] } }, { @@ -57,7 +57,7 @@ }, "geometry": { "type": "Point", - "coordinates": [ [ -112.0667, 33.45 ] ] + "coordinates": [ -112.0667, 33.45 ] } }, { @@ -71,7 +71,7 @@ }, "geometry": { "type": "Point", - "coordinates": [ [ -92.3311, 34.7361 ] ] + "coordinates": [ -92.3311, 34.7361 ] } }, { diff --git a/src/jsonReader.js b/src/jsonReader.js index e43d98ae33..67d7cd0ef5 100644 --- a/src/jsonReader.js +++ b/src/jsonReader.js @@ -18,15 +18,9 @@ var jsonReader = function (arg) { } var $ = require('jquery'); + var convertColor = require('./util').convertColor; - var m_this = this, m_style = arg.style || {}; - m_style = $.extend({ - 'strokeWidth': 2, - 'strokeColor': {r: 0, g: 0, b: 0}, - 'strokeOpacity': 1, - 'fillColor': {r: 1, g: 0, b: 0}, - 'fillOpacity': 1 - }, m_style); + var m_this = this; fileReader.call(this, arg); @@ -87,137 +81,204 @@ var jsonReader = function (arg) { } }; + /** + * 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) { - if (spec.type === 'FeatureCollection') { - return spec.features || []; - } - if (spec.type === 'GeometryCollection') { - throw 'GeometryCollection not yet implemented.'; - } - if (Array.isArray(spec.coordinates)) { - return spec; - } - throw 'Unsupported collection type: ' + spec.type; - }; + var features, normalized = []; + switch (spec.type) { + case 'FeatureCollection': + features = spec.features; + break; - this._featureType = function (spec) { - var geometry = spec.geometry || {}; - if (geometry.type === 'Point' || geometry.type === 'MultiPoint') { - return 'point'; - } - if (geometry.type === 'LineString') { - return 'line'; - } - if (geometry.type === 'Polygon') { - return 'polygon'; - } - if (geometry.type === 'MultiPolygon') { - return 'multipolygon'; - } - return null; - }; + case 'Feature': + features = [spec]; + break; - this._getCoordinates = function (spec) { - var geometry = spec.geometry || {}, - coordinates = geometry.coordinates || [], elv; + case 'GeometryCollection': + features = spec.geometries.map(function (g) { + return { + type: 'Feature', + geometry: g, + properties: {} + }; + }); + break; - if ((coordinates.length === 2 || coordinates.length === 3) && - (isFinite(coordinates[0]) && isFinite(coordinates[1]))) { + case 'Point': + case 'LineString': + case 'Polygon': + case 'MultiPoint': + case 'MultiLineString': + case 'MultiPolygon': + features = [{ + type: 'Feature', + geometry: spec, + properties: {} + }]; + break; - // Do we have a elevation component - if (isFinite(coordinates[2])) { - elv = coordinates[2]; - } + default: + throw new Error('Invalid json type'); + } + + // flatten multi features + features.forEach(function (feature) { + Array.prototype.push.apply(normalized, m_this._feature(feature)); + }); + return normalized; + }; - // special handling for single point coordinates - return [{x: coordinates[0], y: coordinates[1], z: elv}]; + /** + * 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 + }; + }); - // need better handling here, but we can plot simple polygons - // by taking just the outer linearring - if (Array.isArray(coordinates[0][0])) { - coordinates = coordinates[0]; + default: + throw new Error('Invalid geometry type'); } + }; - // return an array of points for LineString, MultiPoint, etc... - return coordinates.map(function (c) { - return { - x: c[0], - y: c[1], - z: c[2] - }; - }); + /** + * 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 + }; }; - this._getStyle = function (spec) { - return spec.properties; + /** + * 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) { + p = spec[j].properties; + } else { + p = d.properties; + } + if (p.hasOwnProperty(prop)) { + return convert(p[prop]); + } + return _default; + }; }; this.read = function (file, done, progress) { function _done(object) { - var features, allFeatures = []; + var features, allFeatures = [], points, lines, polygons; features = m_this._featureArray(object); - features.forEach(function (feature) { - var type = m_this._featureType(feature), - coordinates = m_this._getCoordinates(feature), - style = m_this._getStyle(feature); - if (type) { - if (type === 'line') { - style.fill = style.fill || false; - allFeatures.push(m_this._addFeature( - type, - [coordinates], - style, - feature.properties - )); - } else if (type === 'point') { - style.stroke = style.stroke || false; - allFeatures.push(m_this._addFeature( - type, - coordinates, - style, - feature.properties - )); - } else if (type === 'polygon') { - style.fill = style.fill === undefined ? true : style.fill; - style.fillOpacity = ( - style.fillOpacity === undefined ? 0.25 : style.fillOpacity - ); - // polygons not yet supported - allFeatures.push(m_this._addFeature( - type, - [[coordinates]], //double wrap for the data method below - style, - feature.properties - )); - } else if (type === 'multipolygon') { - style.fill = style.fill === undefined ? true : style.fill; - style.fillOpacity = ( - style.fillOpacity === undefined ? 0.25 : style.fillOpacity - ); - coordinates = feature.geometry.coordinates.map(function (c) { - return [m_this._getCoordinates({ - geometry: { - type: 'Polygon', - coordinates: c - } - })]; - }); - allFeatures.push(m_this._addFeature( - 'polygon', //there is no multipolygon feature class - coordinates, - style, - feature.properties - )); - } - } else { - console.log('unsupported feature type: ' + feature.geometry.type); - } - }); + // process points + points = features.filter(function (f) { return f.geometry.type === 'Point'; }); + if (points.length) { + allFeatures.push( + m_this.layer().createFeature('point') + .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) + }) + ); + } + + // process lines + lines = features.filter(function (f) { return f.geometry.type === 'LineString'; }); + if (lines.length) { + allFeatures.push( + m_this.layer().createFeature('line') + .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) + }) + ); + } + // process polygons + polygons = features.filter(function (f) { return f.geometry.type === 'Polygon'; }); + if (polygons.length) { + allFeatures.push( + m_this.layer().createFeature('polygon') + .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, polygons), + fillColor: m_this._style('fillColor', '#b0de5c', polygons, convertColor), + fillOpacity: m_this._style('fillOpacity', 0.8, polygons), + stroke: m_this._style('stroke', true, polygons), + strokeColor: m_this._style('strokeColor', '#999999', polygons, convertColor), + strokeWidth: m_this._style('strokeWidth', 2, polygons), + strokeOpacity: m_this._style('strokeOpacity', 1, polygons) + }) + ); + } if (done) { done(allFeatures); } @@ -225,58 +286,6 @@ var jsonReader = function (arg) { m_this._readObject(file, _done, progress); }; - - //////////////////////////////////////////////////////////////////////////// - /** - * Build the data array for a feature given the coordinates and properties - * from the geojson. - * - * @private - * @param {Object[]} coordinates Coordinate data array - * @param {Object} properties Geojson properties object - * @param {Object} style Global style defaults - * @returns {Object[]} - */ - ////////////////////////////////////////////////////////////////////////////// - this._buildData = function (coordinates, properties, style) { - return coordinates.map(function (coord) { - return { - coordinates: coord, - properties: properties, - style: style - }; - }); - }; - - this._addFeature = function (type, coordinates, style, properties) { - var _style = $.extend({}, m_style, style); - var feature = m_this.layer().createFeature(type) - .data(m_this._buildData(coordinates, properties, style)) - .style(_style); - - if (type === 'line') { - feature.line(function (d) { return d.coordinates; }); - } else if (type === 'polygon') { - feature.position(function (d) { - return { - x: d.x, - y: d.y, - z: d.z - }; - }).polygon(function (d) { - return { - 'outer': d.coordinates[0], - 'inner': d.coordinates[1] - }; - }); - } else { - feature.position(function (d) { - return d.coordinates; - }); - } - return feature; - }; - }; inherit(jsonReader, fileReader); diff --git a/testing/test-cases/selenium-tests/glMultiPolygons/include.js b/testing/test-cases/selenium-tests/glMultiPolygons/include.js index 26bc14a522..c91f16e75a 100644 --- a/testing/test-cases/selenium-tests/glMultiPolygons/include.js +++ b/testing/test-cases/selenium-tests/glMultiPolygons/include.js @@ -6,7 +6,8 @@ window.startTest = function (done) { "features": [{ "type": "Feature", "properties": { - "strokeColor": {"r": 1, "g": 0, "b": 0} + "fillColor": {"r": 1, "g": 0, "b": 0}, + "fillOpacity": 0.25 }, "geometry": { "type": "MultiPolygon", @@ -26,9 +27,6 @@ window.startTest = function (done) { var layer = myMap.createLayer("feature"); - data.features[0].properties.strokeColor = "#d62728"; - data.features[0].properties.strokeWidth = 4; - geo.createFileReader("jsonReader", { layer: layer }).read(JSON.stringify(data), function() { diff --git a/tests/cases/geojsonReader.js b/tests/cases/geojsonReader.js index c13b7396d0..ca00171eef 100644 --- a/tests/cases/geojsonReader.js +++ b/tests/cases/geojsonReader.js @@ -15,6 +15,241 @@ describe('geojsonReader', function () { var obj, map, layer; + describe('Feature normalization', function () { + var reader; + + beforeEach(function () { + map = geo.map({node: '#map-geojson-reader', center: [0, 0], zoom: 3}); + layer = map.createLayer('feature', {renderer: 'd3'}); + sinon.stub(layer, 'createFeature'); + reader = geo.createFileReader('jsonReader', {'layer': layer}); + }); + afterEach(function () { + layer.createFeature.restore(); + map.exit(); + }); + + 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] + } + }]); + }); + 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('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('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('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: {}, + geometry: { + type: 'Point', + 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('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] + } + }]); + }); + + 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('Setup map', function () { map = geo.map({node: '#map-geojson-reader', center: [0, 0], zoom: 3}); layer = map.createLayer('feature', {renderer: 'd3'}); @@ -82,8 +317,8 @@ describe('geojsonReader', function () { 'type': 'MultiPoint' }, 'properties': { - 'color': [0, 0, 1], - 'size': [7] + 'fillColor': '#0000ff', + 'radius': 7 }, 'type': 'Feature' } @@ -97,7 +332,7 @@ describe('geojsonReader', function () { expect(reader.canRead(obj)).toBe(true); reader.read(obj, function (features) { - expect(features.length).toEqual(3); + expect(features.length).toEqual(2); // Validate that we are getting the correct Z values data = features[1].data()[0];