diff --git a/Apps/CesiumViewer/CesiumViewer.js b/Apps/CesiumViewer/CesiumViewer.js index 9668076590c7..82aa19e2a2b5 100644 --- a/Apps/CesiumViewer/CesiumViewer.js +++ b/Apps/CesiumViewer/CesiumViewer.js @@ -1,6 +1,7 @@ /*global define*/ define([ 'DynamicScene/CzmlDataSource', + 'DynamicScene/GeoJsonDataSource', 'Scene/PerformanceDisplay', 'Widgets/checkForChromeFrame', 'Widgets/Viewer/Viewer', @@ -9,6 +10,7 @@ define([ 'domReady!' ], function( CzmlDataSource, + GeoJsonDataSource, PerformanceDisplay, checkForChromeFrame, Viewer, @@ -50,6 +52,12 @@ define([ window.alert(e); }); + function endsWith(str, suffix) { + var strLength = str.length; + var suffixLength = suffix.length; + return (suffixLength < strLength) && (str.indexOf(suffix, strLength - suffixLength) !== -1); + } + function startup() { var viewer = new Viewer('cesiumContainer'); viewer.extend(viewerDragDropMixin); @@ -75,7 +83,12 @@ define([ } if (typeof endUserOptions.source !== 'undefined') { - var source = new CzmlDataSource(); + var source; + if (endsWith(endUserOptions.source.toUpperCase(), ".GEOJSON")) { + source = new GeoJsonDataSource(); + } else { + source = new CzmlDataSource(); + } source.loadUrl(endUserOptions.source).then(function() { viewer.dataSources.add(source); diff --git a/CHANGES.md b/CHANGES.md index 3057e99b5743..9eb7e10f9de4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Beta Releases * `ImageryProvider.loadImage` now requires that the calling imagery provider instance be passed as its first parameter. * Removed `CesiumViewerWidget` and replaced it with a new `Viewer` widget with mixin architecture. This new widget does not depend on Dojo and is part of the combined Cesium.js file. It is intended to be a flexible base widget for easily building robust applications. See [#838](https://github.com/AnalyticalGraphicsInc/cesium/pull/838) for the full details. * Removed the Dojo-based `checkForChromeFrame` function, and replaced it with a new standalone version that returns a promise to signal when the asynchronous check has completed. +* Added initial support for [GeoJSON](http://www.geojson.org/) see [#890](https://github.com/AnalyticalGraphicsInc/cesium/pull/890) for details. * Added `Context.getAntialias`. * Added rotation, aligned axis, width, and height properties to `Billboard`s. * Improved the performance of "missing tile" checking, especially for Bing imagery. diff --git a/Source/DynamicScene/ConstantProperty.js b/Source/DynamicScene/ConstantProperty.js new file mode 100644 index 000000000000..5f068cf3511e --- /dev/null +++ b/Source/DynamicScene/ConstantProperty.js @@ -0,0 +1,35 @@ +/*global define*/ +define(function() { + "use strict"; + + /** + * Represents a single value which does not change with regard to simulation time. + * + * @alias ConstantProperty + * @constructor + * + * @see DynamicProperty + */ + var ConstantProperty = function(value) { + this._value = value; + this._clonable = typeof value !== 'undefined' && typeof value.clone === 'function'; + }; + + /** + * Gets the value of the property, optionally cloning it. + * @memberof ConstantProperty + * + * @param {JulianDate} time The time for which to retrieve the value. This parameter is unused. + * @param {Object} [result] The object to store the value into if the value is clonable. If the result is omitted or the value does not implement clone, the actual value is returned. + * @returns The modified result parameter or the actual value instance if the value is not clonable. + */ + ConstantProperty.prototype.getValue = function(time, result) { + var value = this._value; + if (this._clonable) { + return value.clone(result); + } + return value; + }; + + return ConstantProperty; +}); \ No newline at end of file diff --git a/Source/DynamicScene/CzmlDataSource.js b/Source/DynamicScene/CzmlDataSource.js index 3c7e08033c6c..a646afd773e8 100644 --- a/Source/DynamicScene/CzmlDataSource.js +++ b/Source/DynamicScene/CzmlDataSource.js @@ -5,9 +5,9 @@ define(['../Core/ClockRange', '../Core/Event', '../Core/Iso8601', '../Core/loadJson', - '../DynamicScene/DynamicClock', - '../DynamicScene/processCzml', - '../DynamicScene/DynamicObjectCollection' + './DynamicClock', + './processCzml', + './DynamicObjectCollection' ], function( ClockRange, ClockStep, @@ -65,7 +65,7 @@ define(['../Core/ClockRange', /** * Gets an event that will be raised when non-time-varying data changes * or if the return value of getIsTimeVarying changes. - * @memberof DataSource + * @memberof CzmlDataSource * * @returns {Event} The event. */ @@ -97,7 +97,7 @@ define(['../Core/ClockRange', /** * Gets the DynamicObjectCollection generated by this data source. - * @memberof DataSource + * @memberof CzmlDataSource * * @returns {DynamicObjectCollection} The collection of objects generated by this data source. */ @@ -108,7 +108,7 @@ define(['../Core/ClockRange', /** * Gets a value indicating if the data varies with simulation time. If the return value of * this function changes, the changed event will be raised. - * @memberof DataSource + * @memberof CzmlDataSource * * @returns {Boolean} True if the data is varies with simulation time, false otherwise. */ diff --git a/Source/DynamicScene/DynamicObject.js b/Source/DynamicScene/DynamicObject.js index 72dc23d3f24d..9b5cbc34efe3 100644 --- a/Source/DynamicScene/DynamicObject.js +++ b/Source/DynamicScene/DynamicObject.js @@ -192,6 +192,23 @@ define([ return availabilityValue; }; + /** + * Merge all of the properties of the supplied object onto this object. + * Properties which are already defined are not overwritten. + * @param other {DynamicObject} The object to merge. + * @private + */ + DynamicObject.prototype.merge = function(other) { + if (typeof other === 'undefined') { + throw new DeveloperError('other is required'); + } + for ( var property in other) { + if (other.hasOwnProperty(property)) { + this[property] = defaultValue(this[property], other[property]); + } + } + }; + /** * Processes a single CZML packet and merges its data into the provided DynamicObject's position * property. This method is not normally called directly, but is part of the array of CZML processing diff --git a/Source/DynamicScene/GeoJsonDataSource.js b/Source/DynamicScene/GeoJsonDataSource.js new file mode 100644 index 000000000000..9335b5dea669 --- /dev/null +++ b/Source/DynamicScene/GeoJsonDataSource.js @@ -0,0 +1,456 @@ +/*global define*/ +define(['../Core/createGuid', + '../Core/Cartographic', + '../Core/Color', + '../Core/defineProperties', + '../Core/DeveloperError', + '../Core/RuntimeError', + '../Core/Ellipsoid', + '../Core/Event', + '../Core/loadJson', + './ConstantProperty', + './DynamicObject', + './DynamicPoint', + './DynamicPolyline', + './DynamicPolygon', + './DynamicMaterialProperty', + './DynamicObjectCollection', + '../ThirdParty/when'], function( + createGuid, + Cartographic, + Color, + defineProperties, + DeveloperError, + RuntimeError, + Ellipsoid, + Event, + loadJson, + ConstantProperty, + DynamicObject, + DynamicPoint, + DynamicPolyline, + DynamicPolygon, + DynamicMaterialProperty, + DynamicObjectCollection, + when) { + "use strict"; + + //DynamicPositionProperty is pretty hard to use with non-CZML based data + //For now we create two of our own properties for exposing GeoJSON + //data. + var ConstantPositionProperty = function(value) { + this._value = value; + }; + + ConstantPositionProperty.prototype.getValueCartesian = function(time, result) { + var value = this._value; + if (typeof value.clone === 'function') { + return value.clone(result); + } + return value; + }; + + ConstantPositionProperty.prototype.setValue = function(value) { + this._value = value; + }; + + //GeoJSON specifies only the Feature object has a usable id property + //But since "multi" geometries create multiple dynamicObject, + //we can't use it for them either. + function createObject(geoJson, dynamicObjectCollection) { + var id = geoJson.id; + if (typeof id === 'undefined' || geoJson.type !== 'Feature') { + id = createGuid(); + } else { + var i = 2; + var finalId = id; + while (typeof dynamicObjectCollection.getObject(finalId) !== 'undefined') { + finalId = id + "_" + i; + i++; + } + id = finalId; + } + var dynamicObject = dynamicObjectCollection.getOrCreateObject(id); + dynamicObject.geoJson = geoJson; + return dynamicObject; + } + + function coordinatesArrayToCartesianArray(coordinates, crsFunction) { + var positions = new Array(coordinates.length); + for ( var i = 0; i < coordinates.length; i++) { + positions[i] = crsFunction(coordinates[i]); + } + return positions; + } + + // GeoJSON processing functions + function processFeature(dataSource, feature, notUsed, crsFunction, source) { + if (typeof feature.geometry === 'undefined') { + throw new RuntimeError('feature.geometry is required.'); + } + + if (feature.geometry === null) { + //Null geometry is allowed, so just create an empty dynamicObject instance for it. + createObject(feature, dataSource._dynamicObjectCollection); + } else { + var geometryType = feature.geometry.type; + var geometryHandler = geometryTypes[geometryType]; + if (typeof geometryHandler === 'undefined') { + throw new RuntimeError('Unknown geometry type: ' + geometryType); + } + geometryHandler(dataSource, feature, feature.geometry, crsFunction, source); + } + } + + function processFeatureCollection(dataSource, featureCollection, notUsed, crsFunction, source) { + var features = featureCollection.features; + for ( var i = 0, len = features.length; i < len; i++) { + processFeature(dataSource, features[i], undefined, crsFunction, source); + } + } + + function processGeometryCollection(dataSource, geoJson, geometryCollection, crsFunction, source) { + var geometries = geometryCollection.geometries; + for ( var i = 0, len = geometries.length; i < len; i++) { + var geometry = geometries[i]; + var geometryType = geometry.type; + var geometryHandler = geometryTypes[geometryType]; + if (typeof geometryHandler === 'undefined') { + throw new RuntimeError('Unknown geometry type: ' + geometryType); + } + geometryHandler(dataSource, geoJson, geometry, crsFunction, source); + } + } + + function processPoint(dataSource, geoJson, geometry, crsFunction, source) { + var dynamicObject = createObject(geoJson, dataSource._dynamicObjectCollection); + dynamicObject.merge(dataSource.defaultPoint); + dynamicObject.position = new ConstantPositionProperty(crsFunction(geometry.coordinates)); + } + + function processMultiPoint(dataSource, geoJson, geometry, crsFunction, source) { + var coordinates = geometry.coordinates; + for ( var i = 0; i < coordinates.length; i++) { + var dynamicObject = createObject(geoJson, dataSource._dynamicObjectCollection); + dynamicObject.merge(dataSource.defaultPoint); + dynamicObject.position = new ConstantPositionProperty(crsFunction(coordinates[i])); + } + } + + function processLineString(dataSource, geoJson, geometry, crsFunction, source) { + var dynamicObject = createObject(geoJson, dataSource._dynamicObjectCollection); + dynamicObject.merge(dataSource.defaultLine); + dynamicObject.vertexPositions = new ConstantPositionProperty(coordinatesArrayToCartesianArray(geometry.coordinates, crsFunction)); + } + + function processMultiLineString(dataSource, geoJson, geometry, crsFunction, source) { + var lineStrings = geometry.coordinates; + for ( var i = 0; i < lineStrings.length; i++) { + var dynamicObject = createObject(geoJson, dataSource._dynamicObjectCollection); + dynamicObject.merge(dataSource.defaultLine); + dynamicObject.vertexPositions = new ConstantPositionProperty(coordinatesArrayToCartesianArray(lineStrings[i], crsFunction)); + } + } + + function processPolygon(dataSource, geoJson, geometry, crsFunction, source) { + //TODO Holes + var dynamicObject = createObject(geoJson, dataSource._dynamicObjectCollection); + dynamicObject.merge(dataSource.defaultPolygon); + dynamicObject.vertexPositions = new ConstantPositionProperty(coordinatesArrayToCartesianArray(geometry.coordinates[0], crsFunction)); + } + + function processMultiPolygon(dataSource, geoJson, geometry, crsFunction, source) { + //TODO holes + var polygons = geometry.coordinates; + for ( var i = 0; i < polygons.length; i++) { + var polygon = polygons[i]; + var dynamicObject = createObject(geoJson, dataSource._dynamicObjectCollection); + dynamicObject.merge(dataSource.defaultPolygon); + dynamicObject.vertexPositions = new ConstantPositionProperty(coordinatesArrayToCartesianArray(polygon[0], crsFunction)); + } + } + + var geoJsonObjectTypes = { + Feature : processFeature, + FeatureCollection : processFeatureCollection, + GeometryCollection : processGeometryCollection, + LineString : processLineString, + MultiLineString : processMultiLineString, + MultiPoint : processMultiPoint, + MultiPolygon : processMultiPolygon, + Point : processPoint, + Polygon : processPolygon + }; + + var geometryTypes = { + GeometryCollection : processGeometryCollection, + LineString : processLineString, + MultiLineString : processMultiLineString, + MultiPoint : processMultiPoint, + MultiPolygon : processMultiPolygon, + Point : processPoint, + Polygon : processPolygon + }; + + /** + * A {@link DataSource} which processes GeoJSON. Since GeoJSON has no standard for styling content, + * we provide default graphics via the defaultPoint, defaultLine, and defaultPolygon properties. + * Any changes to these objects will affect the resulting {@link DynamicObject} collection. + * @alias GeoJsonDataSource + * @constructor + * + * @see DataSourceDisplay + * @see GeoJSON specification. + * + * @example + * //Use a billboard instead of a point. + * var dataSource = new GeoJsonDataSource(); + * var defaultPoint = dataSource.defaulPoint; + * defaultPoint.point = undefined; + * var billboard = new DynamicBillboard(); + * billboard.image = new ConstantProperty('image.png'); + * defaultPoint.billboard = billboard; + * dataSource.loadUrl('sample.geojson'); + */ + var GeoJsonDataSource = function() { + //default point + var defaultPoint = new DynamicObject('GeoJsonDataSource.defaultPoint'); + var point = new DynamicPoint(); + point.color = new ConstantProperty(Color.YELLOW); + point.pixelSize = new ConstantProperty(10); + point.outlineColor = new ConstantProperty(Color.BLACK); + point.outlineWidth = new ConstantProperty(1); + defaultPoint.point = point; + + //default line + var defaultLine = new DynamicObject('GeoJsonDataSource.defaultLine'); + var polyline = new DynamicPolyline(); + polyline.color = new ConstantProperty(Color.YELLOW); + polyline.width = new ConstantProperty(2); + polyline.outlineColor = new ConstantProperty(Color.BLACK); + polyline.outlineWidth = new ConstantProperty(1); + defaultLine.polyline = polyline; + + //default polygon + var defaultPolygon = new DynamicObject('GeoJsonDataSource.defaultPolygon'); + var polygonMaterial = new DynamicMaterialProperty(); + polyline = new DynamicPolyline(); + polyline.color = new ConstantProperty(Color.YELLOW); + polyline.width = new ConstantProperty(1); + polyline.outlineColor = new ConstantProperty(Color.BLACK); + polyline.outlineWidth = new ConstantProperty(0); + defaultPolygon.polyline = polyline; + var polygon = new DynamicPolygon(); + polygon.material = polygonMaterial; + polygonMaterial.processCzmlIntervals({ + solidColor : { + color : { + rgba : [255, 255, 0, 25] + } + } + }, undefined, undefined); + defaultPolygon.polygon = polygon; + + this._changed = new Event(); + this._error = new Event(); + this._dynamicObjectCollection = new DynamicObjectCollection(); + + /** + * Gets or sets the default graphics to be applied to GeoJSON Point and MultiPoint geometries. + * @type DynamicObject + */ + this.defaultPoint = defaultPoint; + + /** + * Gets or sets the default graphics to be applied to GeoJSON LineString and MultiLineString geometries. + * @type DynamicObject + */ + this.defaultLine = defaultLine; + + /** + * Gets or sets the default graphics to be applied to GeoJSON Polygon and MultiPolygon geometries. + * @type DynamicObject + */ + this.defaultPolygon = defaultPolygon; + }; + + /** + * Gets an event that will be raised when non-time-varying data changes + * or if the return value of getIsTimeVarying changes. + * @memberof GeoJsonDataSource + * + * @returns {Event} The event. + */ + GeoJsonDataSource.prototype.getChangedEvent = function() { + return this._changed; + }; + + /** + * Gets an event that will be raised if an error is encountered during processing. + * @memberof GeoJsonDataSource + * + * @returns {Event} The event. + */ + GeoJsonDataSource.prototype.getErrorEvent = function() { + return this._error; + }; + + /** + * Since GeoJSON is a static format, this function always returns undefined. + * @memberof GeoJsonDataSource + */ + GeoJsonDataSource.prototype.getClock = function() { + return undefined; + }; + + /** + * Gets the DynamicObjectCollection generated by this data source. + * @memberof GeoJsonDataSource + * + * @returns {DynamicObjectCollection} The collection of objects generated by this data source. + */ + GeoJsonDataSource.prototype.getDynamicObjectCollection = function() { + return this._dynamicObjectCollection; + }; + + /** + * Gets a value indicating if the data varies with simulation time. If the return value of + * this function changes, the changed event will be raised. + * @memberof GeoJsonDataSource + * + * @returns {Boolean} True if the data is varies with simulation time, false otherwise. + */ + GeoJsonDataSource.prototype.getIsTimeVarying = function() { + return false; + }; + + /** + * Asynchronously loads the GeoJSON at the provided url, replacing any existing data. + * + * @param {Object} url The url to be processed. + * + * @returns {Promise} a promise that will resolve when the GeoJSON is loaded. + * + * @exception {DeveloperError} url is required. + */ + GeoJsonDataSource.prototype.loadUrl = function(url) { + if (typeof url === 'undefined') { + throw new DeveloperError('url is required.'); + } + + var dataSource = this; + return loadJson(url).then(function(geoJson) { + return dataSource.load(geoJson, url); + }, function(error) { + dataSource._error.raiseEvent(dataSource, error); + }); + }; + + /** + * Asynchronously loads the provided GeoJSON object, replacing any existing data. + * + * @param {Object} geoJson The object to be processed. + * @param {String} [source] The base URI of any relative links in the geoJson object. + * + * @returns {Promise} a promise that will resolve when the GeoJSON is loaded. + * + * @exception {DeveloperError} geoJson is required. + * @exception {DeveloperError} Unsupported GeoJSON object type. + * @exception {RuntimeError} crs is null. + * @exception {RuntimeError} crs.properties is undefined. + * @exception {RuntimeError} Unknown crs name. + * @exception {RuntimeError} Unable to resolve crs link. + * @exception {RuntimeError} Unknown crs type. + */ + GeoJsonDataSource.prototype.load = function(geoJson, source) { + if (typeof geoJson === 'undefined') { + throw new DeveloperError('geoJson is required.'); + } + + var typeHandler = geoJsonObjectTypes[geoJson.type]; + if (typeof typeHandler === 'undefined') { + throw new DeveloperError('Unsupported GeoJSON object type: ' + geoJson.type); + } + + //Check for a Coordinate Reference System. + var crsFunction = defaultCrsFunction; + var crs = geoJson.crs; + if (typeof crs !== 'undefined') { + if (crs === null) { + throw new RuntimeError('crs is null.'); + } + if (typeof crs.properties === 'undefined') { + throw new RuntimeError('crs.properties is undefined.'); + } + + var properties = crs.properties; + if (crs.type === 'name') { + crsFunction = GeoJsonDataSource.crsNames[properties.name]; + if (typeof crsFunction === 'undefined') { + throw new RuntimeError('Unknown crs name: ' + properties.name); + } + } else if (crs.type === 'link') { + var handler = GeoJsonDataSource.crsLinkHrefs[properties.href]; + if (typeof handler === 'undefined') { + handler = GeoJsonDataSource.crsLinkTypes[properties.type]; + } + + if (typeof handler === 'undefined') { + throw new RuntimeError('Unable to resolve crs link: ' + JSON.stringify(properties)); + } + + crsFunction = handler(properties); + } else { + throw new RuntimeError('Unknown crs type: ' + crs.type); + } + } + + this._dynamicObjectCollection.clear(); + + var that = this; + return when(crsFunction, function(crsFunction) { + typeHandler(that, geoJson, geoJson, crsFunction, source); + that._changed.raiseEvent(that); + }); + }; + + function defaultCrsFunction(coordinates) { + var cartographic = Cartographic.fromDegrees(coordinates[0], coordinates[1], coordinates[2]); + return Ellipsoid.WGS84.cartographicToCartesian(cartographic); + } + + /** + * An object that maps the name of a crs to a callback function + * which takes a GeoJSON coordinate and transforms it into a + * WGS84 Earth-fixed Cartesian. + * @memberof GeoJsonDataSource + * @type Object + */ + GeoJsonDataSource.crsNames = { + 'urn:ogc:def:crs:OGC:1.3:CRS84' : defaultCrsFunction, + 'EPSG:4326' : defaultCrsFunction + }; + + /** + * An object that maps the href property of a crs link to a callback function + * which takes the crs properties object and returns a Promise that resolves + * to a function that takes a GeoJSON coordinate and transforms it into a WGS84 Earth-fixed Cartesian. + * Items in this object take precedence over those defined in crsLinkHrefs, assuming + * the link has a type specified. + * @memberof GeoJsonDataSource + * @type Object + */ + GeoJsonDataSource.crsLinkHrefs = {}; + + /** + * An object that maps the type property of a crs link to a callback function + * which takes the crs properties object and returns a Promise that resolves + * to a function that takes a GeoJSON coordinate and transforms it into a WGS84 Earth-fixed Cartesian. + * Items in crsLinkHrefs take precedence over this object. + * @memberof GeoJsonDataSource + * @type Object + */ + GeoJsonDataSource.crsLinkTypes = {}; + + return GeoJsonDataSource; +}); \ No newline at end of file diff --git a/Source/Scene/CameraController.js b/Source/Scene/CameraController.js index 7ce3d00eb374..0265149ff478 100644 --- a/Source/Scene/CameraController.js +++ b/Source/Scene/CameraController.js @@ -947,7 +947,7 @@ define([ return result; } /** - * Get the camera position neede to view an extent on an ellipsoid or map + * Get the camera position needed to view an extent on an ellipsoid or map * @memberof CameraController * * @param {Extent} extent The extent to view. diff --git a/Source/Widgets/Viewer/viewerDragDropMixin.js b/Source/Widgets/Viewer/viewerDragDropMixin.js index bc1630d4e6d5..1739284b3ec4 100644 --- a/Source/Widgets/Viewer/viewerDragDropMixin.js +++ b/Source/Widgets/Viewer/viewerDragDropMixin.js @@ -6,6 +6,8 @@ define([ '../../Core/Event', '../../Core/wrapFunction', '../../DynamicScene/CzmlDataSource', + '../../DynamicScene/GeoJsonDataSource', + '../../ThirdParty/when', '../getElement' ], function( defaultValue, @@ -14,8 +16,11 @@ define([ Event, wrapFunction, CzmlDataSource, + GeoJsonDataSource, + when, getElement) { "use strict"; + /*global console*/ /** * A mixin which adds default drag and drop support for CZML files to the Viewer widget. @@ -191,22 +196,40 @@ define([ dropTarget.addEventListener('dragexit', stop, false); } + function endsWith(str, suffix) { + var strLength = str.length; + var suffixLength = suffix.length; + return (suffixLength < strLength) && (str.indexOf(suffix, strLength - suffixLength) !== -1); + } + function createOnLoadCallback(viewer, source, firstTime) { + var DataSource; + if (endsWith(source.toUpperCase(), ".CZML")) { + DataSource = CzmlDataSource; + } else if (endsWith(source.toUpperCase(), '.GEOJSON')) { + DataSource = GeoJsonDataSource; + } else { + viewer.onDropError.raiseEvent(viewer, source, 'Unrecognized file extension: ' + source); + } + return function(evt) { - var czmlSource = new CzmlDataSource(); + var dataSource = new DataSource(); try { - czmlSource.load(JSON.parse(evt.target.result), source); - viewer.dataSources.add(czmlSource); - if (firstTime) { - var dataClock = czmlSource.getClock(); - if (typeof dataClock !== 'undefined') { - dataClock.clone(viewer.clock); - if (typeof viewer.timeline !== 'undefined') { - viewer.timeline.updateFromClock(); - viewer.timeline.zoomTo(dataClock.startTime, dataClock.stopTime); + when(dataSource.load(JSON.parse(evt.target.result), source), function() { + viewer.dataSources.add(dataSource); + if (firstTime) { + var dataClock = dataSource.getClock(); + if (typeof dataClock !== 'undefined') { + dataClock.clone(viewer.clock); + if (typeof viewer.timeline !== 'undefined') { + viewer.timeline.updateFromClock(); + viewer.timeline.zoomTo(dataClock.startTime, dataClock.stopTime); + } } } - } + }, function(error) { + viewer.onDropError.raiseEvent(viewer, source, error); + }); } catch (error) { viewer.onDropError.raiseEvent(viewer, source, error); } diff --git a/Specs/Data/test.geojson b/Specs/Data/test.geojson new file mode 100644 index 000000000000..71609fe2143a --- /dev/null +++ b/Specs/Data/test.geojson @@ -0,0 +1,40 @@ +{ + "type" : "FeatureCollection", + "features" : [ + { + "type" : "Feature", + "geometry" : { + "type" : "Point", + "coordinates" : [ 102.0, 0.5 ] + }, + "properties" : { + "prop0" : "value0" + } + }, + { + "type" : "Feature", + "geometry" : { + "type" : "LineString", + "coordinates" : [ [ 102.0, 0.0 ], [ 103.0, 1.0 ], + [ 104.0, 0.0 ], [ 105.0, 1.0 ] ] + }, + "properties" : { + "prop0" : "value0", + "prop1" : 0.0 + } + }, + { + "type" : "Feature", + "geometry" : { + "type" : "Polygon", + "coordinates" : [ [ [ 100.0, 0.0 ], [ 101.0, 0.0 ], + [ 101.0, 1.0 ], [ 100.0, 1.0 ], [ 100.0, 0.0 ] ] ] + }, + "properties" : { + "prop0" : "value0", + "prop1" : { + "this" : "that" + } + } + } ] +} \ No newline at end of file diff --git a/Specs/DynamicScene/GeoJsonDataSourceSpec.js b/Specs/DynamicScene/GeoJsonDataSourceSpec.js new file mode 100644 index 000000000000..e1682fc32542 --- /dev/null +++ b/Specs/DynamicScene/GeoJsonDataSourceSpec.js @@ -0,0 +1,563 @@ +/*global defineSuite*/ +defineSuite(['DynamicScene/GeoJsonDataSource', + 'DynamicScene/DynamicObjectCollection', + 'Core/Cartographic', + 'Core/Cartesian3', + 'Core/Ellipsoid', + 'Core/Event', + 'ThirdParty/when' + ], function( + GeoJsonDataSource, + DynamicObjectCollection, + Cartographic, + Cartesian3, + Ellipsoid, + Event, + when) { + "use strict"; + /*global jasmine,describe,xdescribe,it,xit,expect,beforeEach,afterEach,beforeAll,afterAll,spyOn,runs,waits,waitsFor*/ + + function coordinatesToCartesian(coordinates) { + return Ellipsoid.WGS84.cartographicToCartesian(Cartographic.fromDegrees(coordinates[0], coordinates[1])); + } + + function coordinatesArrayToCartesian(coordinates) { + var result = []; + for ( var i = 0; i < coordinates.length; i++) { + result.push(coordinatesToCartesian(coordinates[i])); + } + return result; + } + + function multiLineToCartesian(geometry) { + var coordinates = geometry.coordinates; + var result = []; + for (var i = 0; i < coordinates.length; i++) { + result.push(coordinatesArrayToCartesian(coordinates[i])); + } + return result; + } + + function polygonCoordinatesToCartesian(coordinates) { + return coordinatesArrayToCartesian(coordinates[0]); + } + + function multiPolygonCoordinatesToCartesian(coordinates) { + var result = []; + for (var i = 0; i < coordinates.length; i++) { + result.push(coordinatesArrayToCartesian(coordinates[i][0])); + } + return result; + } + + var point = { + type : 'Point', + coordinates : [102.0, 0.5] + }; + + var pointNamedCrs = { + type : 'Point', + coordinates : [102.0, 0.5], + crs : { + type : 'name', + properties : { + name : 'EPSG:4326' + } + } + }; + + var pointCrsLinkHref = { + type : 'Point', + coordinates : [102.0, 0.5], + crs : { + type : 'link', + properties : { + href : 'http://crs.invalid' + } + } + }; + + var lineString = { + type : 'LineString', + coordinates : [[100.0, 0.0], [101.0, 1.0]] + }; + + var polygon = { + type : 'Polygon', + coordinates : [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]] + }; + + var polygonWithHoles = { + type : 'Polygon', + coordinates : [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] + }; + + var multiPoint = { + type : 'MultiPoint', + coordinates : [[100.0, 0.0], [101.0, 1.0], [101.0, 3.0]] + }; + + var multiLineString = { + type : 'MultiLineString', + coordinates : [[[100.0, 0.0], [101.0, 1.0]], [[102.0, 2.0], [103.0, 3.0]]] + }; + + var multiPolygon = { + type : 'MultiPolygon', + coordinates : [[[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], + [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], + [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]]] + }; + + var geometryCollection = { + type : 'GeometryCollection', + 'geometries' : [{ + type : 'Point', + coordinates : [100.0, 0.0] + }, { + type : 'LineString', + coordinates : [[101.0, 0.0], [102.0, 1.0]] + }] + }; + + var feature = { + type : 'Feature', + geometry : point + }; + + var featureWithId = { + id : 'myId', + type : 'Feature', + geometry : geometryCollection + }; + + var featureUndefinedGeometry = { + type : 'Feature' + }; + + var featureNullGeometry = { + type : 'Feature', + geometry : null + }; + + var unknownGeometry = { + type : 'TimeyWimey', + coordinates : [0, 0] + }; + + var featureUnknownGeometry = { + type : 'Feature', + geometry : unknownGeometry + }; + + var geometryCollectionUnknownType = { + type : 'GeometryCollection', + 'geometries' : [unknownGeometry] + }; + + it('default constructor has expected values', function() { + var dataSource = new GeoJsonDataSource(); + expect(dataSource.getChangedEvent()).toBeInstanceOf(Event); + expect(dataSource.getErrorEvent()).toBeInstanceOf(Event); + expect(dataSource.getClock()).toBeUndefined(); + expect(dataSource.getDynamicObjectCollection()).toBeInstanceOf(DynamicObjectCollection); + expect(dataSource.getDynamicObjectCollection().getObjects().length).toEqual(0); + expect(dataSource.getIsTimeVarying()).toEqual(false); + }); + + it('Works with null geometry', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(featureNullGeometry); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var pointObject = dynamicObjectCollection.getObjects()[0]; + expect(pointObject.geoJson).toBe(featureNullGeometry); + expect(pointObject.position).toBeUndefined(); + }); + }); + + it('Works with feature', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(feature); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var pointObject = dynamicObjectCollection.getObjects()[0]; + expect(pointObject.geoJson).toBe(feature); + expect(pointObject.position.getValueCartesian()).toEqual(coordinatesToCartesian(feature.geometry.coordinates)); + expect(pointObject.point).toBeDefined(); + }); + }); + + it('Works with feature with id', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(featureWithId); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 2; + }); + runs(function() { + var pointObject = dynamicObjectCollection.getObjects()[0]; + expect(pointObject.id).toEqual(featureWithId.id); + var lineString = dynamicObjectCollection.getObjects()[1]; + expect(lineString.id).toEqual(featureWithId.id + '_2'); + }); + }); + + it('Works with point geometry', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(point); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var pointObject = dynamicObjectCollection.getObjects()[0]; + expect(pointObject.geoJson).toBe(point); + expect(pointObject.position.getValueCartesian()).toEqual(coordinatesToCartesian(point.coordinates)); + expect(pointObject.point).toBeDefined(); + }); + }); + + it('Works with multipoint geometry', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(multiPoint); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === multiPoint.coordinates.length; + }); + runs(function() { + var objects = dynamicObjectCollection.getObjects(); + var expectedPositions = coordinatesArrayToCartesian(multiPoint.coordinates); + for ( var i = 0; i < multiPoint.coordinates.length; i++) { + var object = objects[i]; + expect(object.geoJson).toBe(multiPoint); + expect(object.position.getValueCartesian()).toEqual(expectedPositions[i]); + expect(object.point).toBeDefined(); + } + }); + }); + + it('Works with lineString geometry', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(lineString); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var object = dynamicObjectCollection.getObjects()[0]; + expect(object.geoJson).toBe(lineString); + expect(object.vertexPositions.getValueCartesian()).toEqual(coordinatesArrayToCartesian(lineString.coordinates)); + expect(object.polyline).toBeDefined(); + }); + }); + + it('Works with multiLineString geometry', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(multiLineString); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 2; + }); + runs(function() { + var objects = dynamicObjectCollection.getObjects(); + var lines = multiLineToCartesian(multiLineString); + for ( var i = 0; i < multiLineString.coordinates.length; i++) { + var object = objects[i]; + expect(object.geoJson).toBe(multiLineString); + expect(object.vertexPositions.getValueCartesian()).toEqual(lines[i]); + expect(object.polyline).toBeDefined(); + } + }); + }); + + it('Works with polygon geometry', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(polygon); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var object = dynamicObjectCollection.getObjects()[0]; + expect(object.geoJson).toBe(polygon); + expect(object.vertexPositions.getValueCartesian()).toEqual(polygonCoordinatesToCartesian(polygon.coordinates)); + expect(object.polyline).toBeDefined(); + expect(object.polygon).toBeDefined(); + }); + }); + + it('Works with polygon geometry with holes', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(polygonWithHoles); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var object = dynamicObjectCollection.getObjects()[0]; + expect(object.geoJson).toBe(polygonWithHoles); + expect(object.vertexPositions.getValueCartesian()).toEqual(polygonCoordinatesToCartesian(polygonWithHoles.coordinates)); + expect(object.polyline).toBeDefined(); + expect(object.polygon).toBeDefined(); + }); + }); + + it('Works with multiPolygon geometry', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(multiPolygon); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 2; + }); + runs(function() { + var objects = dynamicObjectCollection.getObjects(); + var positions = multiPolygonCoordinatesToCartesian(multiPolygon.coordinates); + for ( var i = 0; i < multiPolygon.coordinates.length; i++) { + var object = objects[i]; + expect(object.geoJson).toBe(multiPolygon); + expect(object.vertexPositions.getValueCartesian()).toEqual(positions[i]); + expect(object.polyline).toBeDefined(); + expect(object.polygon).toBeDefined(); + } + }); + }); + + it('Works with geometrycollection', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(geometryCollection); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 2; + }); + runs(function() { + var object = dynamicObjectCollection.getObjects()[0]; + expect(object.geoJson).toBe(geometryCollection); + expect(object.position.getValueCartesian()).toEqual(coordinatesToCartesian(geometryCollection.geometries[0].coordinates)); + expect(object.point).toBeDefined(); + + object = dynamicObjectCollection.getObjects()[1]; + expect(object.geoJson).toBe(geometryCollection); + expect(object.vertexPositions.getValueCartesian()).toEqual(coordinatesArrayToCartesian(geometryCollection.geometries[1].coordinates)); + expect(object.polyline).toBeDefined(); + }); + }); + + it('Works with named crs', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.load(pointNamedCrs); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var pointObject = dynamicObjectCollection.getObjects()[0]; + expect(pointObject.position.getValueCartesian()).toEqual(coordinatesToCartesian(point.coordinates)); + }); + }); + + it('Works with link crs href', function() { + var projectedPosition = new Cartesian3(1, 2, 3); + + var dataSource = new GeoJsonDataSource(); + GeoJsonDataSource.crsLinkHrefs[pointCrsLinkHref.crs.properties.href] = function(properties) { + expect(properties).toBe(pointCrsLinkHref.crs.properties); + return when(properties.href, function(href) { + return function(coordinate) { + expect(coordinate).toBe(pointCrsLinkHref.coordinates); + return projectedPosition; + }; + }); + }; + dataSource.load(pointCrsLinkHref); + + var dynamicObjectCollection = dataSource.getDynamicObjectCollection(); + waitsFor(function() { + return dynamicObjectCollection.getObjects().length === 1; + }); + runs(function() { + var pointObject = dynamicObjectCollection.getObjects()[0]; + expect(pointObject.position.getValueCartesian()).toEqual(projectedPosition); + }); + }); + + it('loadUrl works', function() { + var dataSource = new GeoJsonDataSource(); + dataSource.loadUrl('Data/test.geojson'); + + waitsFor(function() { + return dataSource.getDynamicObjectCollection().getObjects().length === 3; + }); + }); + + it('Fails when encountering unknown geometry', function() { + var dataSource = new GeoJsonDataSource(); + + var failed = false; + dataSource.load(featureUnknownGeometry).then(undefined, function(e) { + failed = true; + }); + + waitsFor(function() { + return failed; + }); + }); + + it('Fails with undefined geomeetry', function() { + var dataSource = new GeoJsonDataSource(); + + var failed = false; + dataSource.load(featureUndefinedGeometry).then(undefined, function(e) { + failed = true; + }); + + waitsFor(function() { + return failed; + }); + }); + + it('Fails with unknown geomeetry in geometryCollection', function() { + var dataSource = new GeoJsonDataSource(); + + var failed = false; + dataSource.load(geometryCollectionUnknownType).then(undefined, function(e) { + failed = true; + }); + + waitsFor(function() { + return failed; + }); + }); + + it('load throws with undefined geoJson', function() { + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.load(undefined); + }).toThrow(); + }); + + it('load throws with unknown geometry', function() { + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.load(unknownGeometry); + }).toThrow(); + }); + + it('loadUrl throws with undefined Url', function() { + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.loadUrl(undefined); + }).toThrow(); + }); + + it('loadUrl raises error with invalud url', function() { + var dataSource = new GeoJsonDataSource(); + var thrown = false; + dataSource.getErrorEvent().addEventListener(function() { + thrown = true; + }); + dataSource.loadUrl('invalid.geojson'); + waitsFor(function() { + return thrown; + }); + }); + + it('load throws with null crs', function() { + var featureWithNullCrs = { + type : 'Feature', + geometry : point, + crs : null + }; + + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.load(featureWithNullCrs); + }).toThrow(); + }); + + it('load throws with unknown crs type', function() { + var featureWithUnknownCrsType = { + type : 'Feature', + geometry : point, + crs : { + type : 'potato', + properties : {} + } + }; + + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.load(featureWithUnknownCrsType); + }).toThrow(); + }); + + it('load throws with undefined crs properties', function() { + var featureWithUnknownCrsType = { + type : 'Feature', + geometry : point, + crs : { + type : 'name' + } + }; + + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.load(featureWithUnknownCrsType); + }).toThrow(); + }); + + it('load throws with unknown crs', function() { + var featureWithUnknownCrsType = { + type : 'Feature', + geometry : point, + crs : { + type : 'name', + properties : { + name : 'failMe' + } + } + }; + + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.load(featureWithUnknownCrsType); + }).toThrow(); + }); + + it('load throws with unknown crs link', function() { + var featureWithUnknownCrsType = { + type : 'Feature', + geometry : point, + crs : { + type : 'link', + properties : { + href : 'failMe', + type : 'failMeTwice' + } + } + }; + + var dataSource = new GeoJsonDataSource(); + expect(function() { + dataSource.load(featureWithUnknownCrsType); + }).toThrow(); + }); +}); diff --git a/Specs/Widgets/Viewer/viewerDragDropMixinSpec.js b/Specs/Widgets/Viewer/viewerDragDropMixinSpec.js index 73b409158753..1ef8a40ebf69 100644 --- a/Specs/Widgets/Viewer/viewerDragDropMixinSpec.js +++ b/Specs/Widgets/Viewer/viewerDragDropMixinSpec.js @@ -93,7 +93,7 @@ defineSuite([ var mockEvent = { dataTransfer : { files : [{ - name : 'czml1', + name : 'czml1.czml', czmlString : JSON.stringify(czml1) }] }, @@ -129,10 +129,10 @@ defineSuite([ var mockEvent = { dataTransfer : { files : [{ - name : 'czml1', + name : 'czml1.czml', czmlString : JSON.stringify(czml1) }, { - name : 'czml2', + name : 'czml2.czml', czmlString : JSON.stringify(czml2) }] }, @@ -171,10 +171,10 @@ defineSuite([ var mockEvent = { dataTransfer : { files : [{ - name : 'czml1', + name : 'czml1.czml', czmlString : JSON.stringify(czml1) }, { - name : 'czml2', + name : 'czml2.czml', czmlString : JSON.stringify(czml2) }] }, @@ -254,7 +254,7 @@ defineSuite([ var mockEvent = { dataTransfer : { files : [{ - name : 'czml1', + name : 'czml1.czml', czmlString : 'bad JSON' }] }, @@ -270,7 +270,7 @@ defineSuite([ var called = false; var callback = function(viewerArg, source, error) { expect(viewerArg).toBe(viewer); - expect(source).toEqual('czml1'); + expect(source).toEqual('czml1.czml'); expect(error).toBeInstanceOf(SyntaxError); called = true; }; @@ -291,7 +291,7 @@ defineSuite([ var mockEvent = { dataTransfer : { files : [{ - name : 'czml1', + name : 'czml1.czml', errorMessage : 'bad JSON' }] },