UNPKG

terriajs

Version:

Geospatial data visualization platform.

967 lines (837 loc) 40.1 kB
"use strict"; /*global require*/ var URI = require('urijs'); var buildModuleUrl = require('terriajs-cesium/Source/Core/buildModuleUrl'); var CesiumEvent = require('terriajs-cesium/Source/Core/Event'); var combine = require('terriajs-cesium/Source/Core/combine'); var DataSourceCollection = require('terriajs-cesium/Source/DataSources/DataSourceCollection'); var defaultValue = require('terriajs-cesium/Source/Core/defaultValue'); var defined = require('terriajs-cesium/Source/Core/defined'); var deprecationWarning = require('terriajs-cesium/Source/Core/deprecationWarning'); var DeveloperError = require('terriajs-cesium/Source/Core/DeveloperError'); var knockout = require('terriajs-cesium/Source/ThirdParty/knockout'); var queryToObject = require('terriajs-cesium/Source/Core/queryToObject'); var Rectangle = require('terriajs-cesium/Source/Core/Rectangle'); var when = require('terriajs-cesium/Source/ThirdParty/when'); var {addMarker} = require ('./LocationMarkerUtils.js'); var CameraView = require('./CameraView'); var Catalog = require('./Catalog'); var Clock = require('./Clock'); var ConsoleAnalytics = require('../Core/ConsoleAnalytics'); var CorsProxy = require('../Core/CorsProxy'); var Feature = require('./Feature'); var GoogleAnalytics = require('../Core/GoogleAnalytics'); var hashEntity = require('../Core/hashEntity'); var isCommonMobilePlatform = require('../Core/isCommonMobilePlatform'); var loadJson5 = require('../Core/loadJson5'); var NoViewer = require('./NoViewer'); var NowViewing = require('./NowViewing'); var Promise = require('../Core/Promise'); var runLater = require('../Core/runLater'); var ServerConfig = require('../Core/ServerConfig'); var Services = require('./Services'); var TimeSeriesStack = require('./TimeSeriesStack'); var ViewerMode = require('./ViewerMode'); var defaultConfigParameters = { defaultMaximumShownFeatureInfos: 100, /* These services are not included within Terria, but this is where we expect them to be, by default. */ regionMappingDefinitionsUrl: 'data/regionMapping.json', conversionServiceBaseUrl: 'convert/', proj4ServiceBaseUrl: 'proj4/', corsProxyBaseUrl: 'proxy/', proxyableDomainsUrl: 'proxyabledomains/', shareUrl: 'share', feedbackUrl: undefined, initFragmentPaths: [ 'init/' ], interceptBrowserPrint: true, useCesiumIonTerrain: true, cesiumIonAccessToken: undefined, cesiumTerrainUrl: undefined }; // These properties can be directly passed in as properties of an init source. var directInitSourceProperties = ['baseMapName', 'fogSettings', 'splitPosition']; /** * The overall model for TerriaJS. * @alias Terria * @constructor * * @param {Object} options Object with the following properties: * @param {String} options.baseUrl The base directory in which TerriaJS can find its static assets. * @param {String} [options.cesiumBaseUrl='(options.baseUrl)/build/Cesium/build/'] The base directory in which Cesium can find its static assets. * @param {String} [options.appName] The name of the app. * @param {String} [options.supportEmail] The support email for the app. * @param {AddressGeocoder} [options.batchGeocoder] Geocoder to use for geocoding addresses in CSV files. */ var Terria = function(options) { // IE9 doesn't have a console object until the debugging tools are opened. // Add a shim. if (typeof window.console === 'undefined') { window.console = { log: function() {}, warn: function() {}, error: function() {} }; } // Polyfill Promise for old browsers if (!defined(window.Promise)) { deprecationWarning('promise-polyfill', 'This browser does not have Promise support. It will be polyfilled automatically, but an external polyfill (e.g. polyfill.io) will be required starting in TerriaJS v7.0'); window.Promise = Promise; } if (!defined(options) || !defined(options.baseUrl)) { throw new DeveloperError('options.baseUrl is required.'); } this.baseUrl = defaultValue(options.baseUrl, 'build/TerriaJS/'); if (this.baseUrl.lastIndexOf('/') !== this.baseUrl.length - 1) { this.baseUrl += '/'; } var cesiumBaseUrl = defaultValue(options.cesiumBaseUrl, this.baseUrl + 'build/Cesium/build/'); if (cesiumBaseUrl.lastIndexOf('/') !== cesiumBaseUrl.length - 1) { cesiumBaseUrl += '/'; } this.cesiumBaseUrl = cesiumBaseUrl; buildModuleUrl.setBaseUrl(cesiumBaseUrl); /** * Gets or sets the instance to which to report Google Analytics-style log events. * If a global `ga` function is defined, this defaults to `GoogleAnalytics`. Otherwise, it defaults * to `ConsoleAnalytics`. * @type {ConsoleAnalytics|GoogleAnalytics} */ this.analytics = options.analytics; if (!defined(this.analytics)) { if (typeof window !== 'undefined' && defined(window.ga)) { this.analytics = new GoogleAnalytics(); } else { this.analytics = new ConsoleAnalytics(); } } /** * The name of the app to be built upon Terria. This will appear in error messages to the user. * @type {String} * @default "TerriaJS App" */ this.appName = defaultValue(options.appName, "TerriaJS App"); /** * The support email for the app to be built upon Terria. This will appear in error messages to the user. * @type {String} * @default "support@terria.io" */ this.supportEmail = defaultValue(options.supportEmail, "support@terria.io"); /** * Indicates whether time-dynamic layers should start animating immediately upon load. * If false, the user will need to press play manually before the layer starts animating. * @type {Boolean} * @default true */ this.autoPlay = false; /** * The geocoder to use for batch geocoding addresses in CSV files. * @type {AddressGeocoder} */ this.batchGeocoder = options.batchGeocoder; /** * An event that is raised when a user-facing error occurs. This is especially useful for errors that happen asynchronously and so * cannot be raised as an exception because no one would be able to catch it. Subscribers are passed the {@link TerriaError} * that occurred as the only function parameter. * @type {CesiumEvent} */ this.error = new CesiumEvent(); /** * Gets or sets the map mode. * @type {ViewerMode} */ this.viewerMode = defaultValue(options.viewerMode, ViewerMode.CesiumTerrain); /** * Gets or sets the current base map. * @type {ImageryLayerCatalogItem} */ this.baseMap = undefined; /** * Gets or sets the current fog settings, used in the Cesium Scene/Fog constructor. * @type {Object} */ this.fogSettings = undefined; /** * Gets or sets the name of the base map to use. * @type {String} */ this.baseMapName = undefined; /** * Gets or sets a color that contrasts well with the base map. * @type {String} */ this.baseMapContrastColor = '#ffffff'; /** * Gets or sets the event that is raised just before switching between Cesium and Leaflet. * @type {Event} */ this.beforeViewerChanged = new CesiumEvent(); /** * Gets or sets the event that is raised just after switching between Cesium and Leaflet. * @type {Event} */ this.afterViewerChanged = new CesiumEvent(); /** * Gets or sets the collection of Cesium-style data sources that are currently active on the map. * @type {DataSourceCollection} */ this.dataSources = new DataSourceCollection(); /** * Gets or sets the clock that controls how time-varying data items are displayed. * @type {Clock} */ this.clock = new Clock({ shouldAnimate: false }); this.timeSeriesStack = new TimeSeriesStack(this.clock); // See the intialView property below. this._initialView = undefined; /** * Gets or sets the camera's home view. The home view is the one that the application * returns to when the user clicks the "Reset View" button in the Navigation widget. It is also used * as the {@link Terria#initialView} if one is not specified. * @type {CameraView} */ this.homeView = new CameraView(Rectangle.MAX_VALUE); /** * Gets or sets a value indicating whether the application should automatically zoom to the new view when * the {@link Terria#initialView} (or {@link Terria#homeView} if no initial view is specified). * @type {Boolean} * @default true */ this.zoomWhenInitialViewChanges = true; /** * Gets or sets the {@link this.corsProxy} used to determine if a URL needs to be proxied and to proxy it if necessary. * @type {CorsProxy} */ this.corsProxy = new CorsProxy(); /** * Gets or sets properties related to the Cesium globe. If the application is in 2D mode, this property will be * undefined and {@link Terria#leaflet} will be set. * @type {Cesium} */ this.cesium = undefined; /** * Gets or sets properties related to the Leaflet map. If the application is in 3D mode, this property will be * undefined and {@link Terria#cesium} will be set. * @type {Leaflet} */ this.leaflet = undefined; this._noViewer = new NoViewer(this); /** * Gets or sets a reference to the current viewer, which is a subclass of {@link Terria#globeOrMap} - * typically {@link Terria#cesium} or {@link Terria#leaflet}. * This property is observable. * @type {Cesium|Leaflet|NoViewer} */ this.currentViewer = this._noViewer; /** * Gets or sets the collection of user properties. User properties * can be set by specifying them in the hash portion of the URL. For example, if the application URL is * `http://localhost:3001/#foo=bar&someproperty=true`, this object will contain a property named 'foo' with the * value 'bar' and a property named 'someproperty' with the value 'true'. Currently recognised URL parameters include * 'map=[2D,3D]' (choose the Leaflet or Cesium view) and `mode=preview` (suppress warnings, when used as an embedded * previewer). * @type {Object} */ this.userProperties = {}; /** * Gets or sets the list of sources from which the catalog was populated. A source may be a string, in which case it * is expected to be a URL of an init file (like init_nm.json), or it can be a JSON-style object literal which is * the init content itself. * @type {Array} */ this.initSources = []; /** * Gets or sets the data source that represents the location marker. * @type {CustomDataSource} */ this.locationMarker = undefined; /** * Gets or sets the features that are currently picked. * @type {PickedFeatures} */ this.pickedFeatures = undefined; /** * Gets or sets the stack of map interactions modes. The mode at the top of the stack * (highest index) handles click interactions with the map * @type {MapInteractionMode[]} */ this.mapInteractionModeStack = []; /** * Gets or sets the catalog of geospatial data. * @type {Catalog} */ this.catalog = new Catalog(this); /** * Gets or sets the add-on services known to the application. * @type {Services} */ this.services = new Services(this); /** * Gets or sets the collection of geospatial data that is currently enabled. * @type {NowViewing} */ this.nowViewing = new NowViewing(this); /** * Gets or sets the currently-selected feature, or undefined if there is no selected feature. The selected * feature is highlighted by drawing a targetting cursor around it. * @type {Entity} */ this.selectedFeature = undefined; /** * Gets or sets the configuration parameters set at startup. * Contains: * * regionMappingDefinitionsUrl: URL of JSON file containing region mapping definitions * * conversionServiceBaseUrl: URL of OGR2OGR conversion service * * proj4ServiceBaseUrl: URL of proj4def lookup service * * corsProxyBaseUrl: URL of CORS proxy * @type {Object} */ this.configParameters = defaultConfigParameters; /** * Gets or sets the urlShorter to be used with terria. This is currently set in the start method * to allow the urlShortener object to properly initialize. See the GoogleUrlShortener for an * example urlShortener. * @type {Object} */ this.urlShortener = undefined; /** * Gets or sets the shareDataService to be used with Terria, which can save JSON or (in future) other user-provided * data somewhere. It can be used to generate short URLs. * @type {Object} */ this.shareDataService = undefined; /** * Gets or sets the ServerConfig object representing server-side configuration. * @type {Object} */ this.serverConfig = undefined; /** * Event that tracks changes to the progress in loading new tiles from either Cesium or Leaflet - events will be * raised with the number of tiles that still need to load. * * @type {CesiumEvent} */ this.tileLoadProgressEvent = new CesiumEvent(); this.disclaimerListener = function(catalogMember, callback) { window.alert(catalogMember.initialMessage.content); /*eslint no-alert: 0*/ callback(); }; /** * Gets or sets the selectBox function - set true when user requires a rectangle parameter from analytics. * @type {Boolean} */ this.selectBox = false; /** * Gets or sets a callback function that can modify any "start data" (e.g. a share URL) before it is loaded. * The function is passed the start data and may modify it in place or return a new instance. * @type {Function} */ this.filterStartDataCallback = undefined; /** * Gets or sets whether to show a splitter, if possible. Default false. This property is observable. * @type {Boolean} */ this.showSplitter = false; /** * Gets or sets the current position of the splitter (if {@link Terria#showSplitter} is true) as a fraction of the map window. * 0.0 is on the left, 0.5 is in the center, and 1.0 is on the right. This property is observable. */ this.splitPosition = 0.5; /** * Gets or sets the current vertical position of the splitter (if {@link Terria#showSplitter} is true) as a fraction of the map window. * 0.0 is on the top, 0.5 is in the center, and 1.0 is on the bottom. This property is observable. */ this.splitPositionVertical = 0.5; knockout.track(this, [ 'viewerMode', 'baseMap', 'baseMapName', 'fogSettings', '_initialView', 'homeView', 'locationMarker', 'pickedFeatures', 'selectedFeature', 'mapInteractionModeStack', 'configParameters', 'catalog', 'selectBox', 'currentViewer', 'showSplitter', 'splitPosition', 'splitPositionVertical', 'baseMapContrastColor' ]); /** * Gets or sets the camera's initial view. This is the view that the application has at startup. If this property * is not explicitly specified, the {@link Terria#homeView} is used. * @type {CameraView} */ knockout.defineProperty(this, 'initialView', { get: function() { if (this._initialView) { return this._initialView; } else { return this.homeView; } }, set: function(value) { this._initialView = value; } }); knockout.getObservable(this, 'initialView').subscribe(function() { if (this.zoomWhenInitialViewChanges && defined(this.currentViewer)) { this.currentViewer.zoomTo(this.initialView, 2.0); } }, this); }; /** * Starts up Terria. * * @param {Object} options Object with the following properties: * @param {String} [options.applicationUrl] The URL of the application. Typically this is obtained from window.location. This URL, if * supplied, is parsed for startup parameters. * @param {String} [options.configUrl='config.json'] The URL of the file containing configuration information, such as the list of domains to proxy. * @param {UrlShortener} [options.urlShortener] The URL shortener to use to expand short URLs. If this property is undefined, short URLs will not be expanded. * @param {Boolean} [options.persistViewerMode] Whether to use the ViewerMode stored in localStorage if avaliable (this takes priority over other ViewerMode options). If not specified the stored ViewerMode will be used. */ Terria.prototype.start = function(options) { function slashify(url) { return (url && url[url.length - 1] !== '/') ? url + '/' : url; } this.catalog.isLoading = true; var applicationUrl = defaultValue(options.applicationUrl, ''); this.urlShortener = options.urlShortener; this.shareDataService = options.shareDataService; var that = this; return loadJson5(options.configUrl).then(function(config) { if (defined(config.parameters)) { // allow config file to provide TerriaJS-Server URLs to facilitate purely static deployments relying on external services that.configParameters = combine(config.parameters, that.configParameters); } var cp = that.configParameters; cp.conversionServiceBaseUrl = slashify(cp.conversionServiceBaseUrl); cp.proj4ServiceBaseUrl = slashify(cp.proj4ServiceBaseUrl); cp.corsProxyBaseUrl = slashify(cp.corsProxyBaseUrl); that.appName = defaultValue(cp.appName, defaultValue(options.appName, that.appName)); that.supportEmail = defaultValue(cp.supportEmail, defaultValue(options.supportEmail, that.supportEmail)); if(defined(cp.autoPlay)) that.autoPlay = cp.autoPlay; that.analytics.start(that.configParameters); that.analytics.logEvent('launch', 'url', defined(applicationUrl.href) ? applicationUrl.href : 'empty'); var initializationUrls = config.initializationUrls; if (defined(initializationUrls)) { for (var i = 0; i < initializationUrls.length; i++) { that.initSources.push(generateInitializationUrl(initializationUrls[i])); } } showDisclaimer(that, options.globalDisclaimerHtml, options.developmentDisclaimerPreambleHtml); that.serverConfig = new ServerConfig(); let serverConfig; return that.serverConfig.init(cp.serverConfigUrl).then(function() { // All the "proxyableDomains" bits here are due to a pre-serverConfig mechanism for whitelisting domains. // We should deprecate it. var pdu = that.configParameters.proxyableDomainsUrl; if (pdu) { return loadJson5(pdu); } }).then(function(proxyableDomains) { if (proxyableDomains) { // format of proxyableDomains JSON file slightly differs from serverConfig format. proxyableDomains.allowProxyFor = proxyableDomains.allowProxyFor || proxyableDomains.proxyableDomains; } if (typeof that.serverConfig === 'object') { serverConfig = that.serverConfig.config; // if server config is unavailable, this remains undefined. } if (that.shareDataService) { that.shareDataService.init(serverConfig); } that.corsProxy.init(proxyableDomains || serverConfig, cp.corsProxyBaseUrl, config.proxyDomains); }).otherwise(function(e) { console.error(e); // There's no particular reason an error should be thrown here. that.error.raiseEvent({ title: 'Failed to initialize services', message: 'A problem occurred with the Terria server. This may cause some layers or the conversion service to be unavailable.' }); }).then(function() { return that.updateApplicationUrl(applicationUrl, that.urlShortener); }).then(function () { var persistViewerMode = defaultValue(options.persistViewerMode, true); if (persistViewerMode && defined(that.getLocalProperty('viewermode'))) { that.viewerMode = parseInt(that.getLocalProperty('viewermode'), 10); } else { // If we are running on a mobile platform set the viewerMode to the config specified default mobile viewer mode. if (isCommonMobilePlatform() && !defined(that.userProperties.map)) { // This is the default viewerMode to use if the configuration parameter is not set or is not set correctly. that.viewerMode = ViewerMode.Leaflet; if (defined(that.configParameters.mobileDefaultViewerMode) && (typeof that.configParameters.mobileDefaultViewerMode === 'string')) { const mobileDefault = that.configParameters.mobileDefaultViewerMode.toLowerCase(); if (mobileDefault === '3dterrain') { that.viewerMode = ViewerMode.CesiumTerrain; } else if (mobileDefault === '3dsmooth') { that.viewerMode = ViewerMode.CesiumEllipsoid; } else if (mobileDefault === '2d') { that.viewerMode = ViewerMode.Leaflet; } } } if (options.defaultTo2D && !defined(that.userProperties.map)) { that.viewerMode = ViewerMode.Leaflet; } } that.catalog.isLoading = false; }).otherwise(function(e) { console.error('Error from updateApplicationUrl: ', e); that.error.raiseEvent({ title: 'Problem loading URL', message: 'A problem occurred while initialising Terria with URL parameters.' }); }); }); }; /** * Updates the state of the application based on the hash portion of a URL. * @param {String} newUrl The new URL of the application. * @return {Promise} A promise that resolves when any new init sources specified in the URL have been loaded. */ Terria.prototype.updateApplicationUrl = function(newUrl) { var uri = new URI(newUrl); var hash = uri.fragment(); var hashProperties = queryToObject(hash); var initSources = this.initSources.slice(); var promise = interpretHash(this, hashProperties, this.userProperties, this.initSources, initSources); var that = this; return when(promise).then(function() { if (that.userProperties.map === '2d') { that.viewerMode = ViewerMode.Leaflet; } else if (that.userProperties.map === '3d') { that.viewerMode = ViewerMode.CesiumTerrain; } return loadInitSources(that, initSources); }); }; Terria.prototype.updateFromStartData = function(startData) { var initSources = this.initSources.slice(); interpretStartData(this, startData, this.initSources, initSources); return loadInitSources(this, initSources); }; /** * Gets the value of a user property. If the property doesn't exist, it is created as an observable property with the * value undefined. This way, if it becomes defined in the future, anyone depending on the value will be notified. * @param {String} propertyName The name of the user property for which to get the value. * @return {Object} The value of the property, or undefined if the property does not exist. */ Terria.prototype.getUserProperty = function(propertyName) { if (!knockout.getObservable(this.userProperties, propertyName)) { this.userProperties[propertyName] = undefined; knockout.track(this.userProperties, [propertyName]); } return this.userProperties[propertyName]; }; Terria.prototype.addInitSource = function(initSource) { var promise = when(); var that = this; // Extract the list of CORS-ready domains. if (defined(initSource.corsDomains)) { this.corsProxy.corsDomains.push.apply(this.corsProxy.corsDomains, initSource.corsDomains); } // The last init source to specify an initial/home camera view wins. if (defined(initSource.homeCamera)) { this.homeView = CameraView.fromJson(initSource.homeCamera); } if (defined(initSource.initialCamera)) { this.initialView = CameraView.fromJson(initSource.initialCamera); } // Extract the init source properties that require no deserialization. directInitSourceProperties.forEach(function(propertyName) { if (defined(initSource[propertyName])) { that[propertyName] = initSource[propertyName]; } }); if (defined(initSource.showSplitter)) { // If you try to show the splitter straight away, the browser hangs. runLater(function() { that.showSplitter = initSource.showSplitter; }); } if (defined(initSource.viewerMode) && !defined(this.userProperties.map)) { if (initSource.viewerMode === '2d') { this.viewerMode = ViewerMode.Leaflet; } else if (initSource.viewerMode === '3d') { this.viewerMode = ViewerMode.CesiumTerrain; } } if (defined(initSource.currentTime)) { // If the time is supplied we want to freeze the display at the specified time and not auto playing. this.autoPlay = false; const time = initSource.currentTime; this.clock.currentTime.dayNumber = parseInt(time.dayNumber, 10); this.clock.currentTime.secondsOfDay = parseInt(time.secondsOfDay, 10); } // Populate the list of services. if (defined(initSource.services)) { this.services.services.push.apply(this.services, initSource.services); } // Populate the catalog if (defined(initSource.catalog)) { var isUserSupplied = !initSource.isFromExternalFile; promise = promise.then(this.catalog.updateFromJson.bind(this.catalog, initSource.catalog, { isUserSupplied: isUserSupplied })); } if (defined(initSource.sharedCatalogMembers)) { promise = promise.then(this.catalog.updateByShareKeys.bind(this.catalog, initSource.sharedCatalogMembers)); } if (defined(initSource.locationMarker)) { var marker = { name: initSource.locationMarker.name, location: { latitude: initSource.locationMarker.latitude, longitude: initSource.locationMarker.longitude } }; addMarker(this, marker); } if (defined(initSource.pickedFeatures)) { promise.then(function() { var removeViewLoadedListener; var loadPickedFeatures = function() { if (defined(removeViewLoadedListener)) { removeViewLoadedListener(); } var vectorFeatures; var featureIndex = {}; var initSourceEntities = initSource.pickedFeatures.entities; if (initSourceEntities) { // Build index of terria features by a hash of their properties. var relevantItems = that.nowViewing.items.filter(function(item) { return item.isEnabled && item.isShown && defined(item.dataSource) && defined(item.dataSource.entities); }); relevantItems.forEach(function(item) { (item.dataSource.entities.values || []).forEach(function(entity) { var hash = hashEntity(entity, that.clock); var feature = Feature.fromEntityCollectionOrEntity(entity); featureIndex[hash] = featureIndex[hash] ? featureIndex[hash].concat([feature]) : [feature]; }); }); // Go through the features we've got from terria match them up to the id/name info we got from the // share link, filtering out any without a match. vectorFeatures = initSourceEntities.map(function(initSourceEntity) { var matches = defaultValue(featureIndex[initSourceEntity.hash], []) .filter(function(match) { return match.name === initSourceEntity.name; }); return matches.length && matches[0]; }).filter(function(feature) { return defined(feature); }); } that.currentViewer.pickFromLocation(initSource.pickedFeatures.pickCoords, initSource.pickedFeatures.providerCoords, vectorFeatures); that.pickedFeatures.allFeaturesAvailablePromise.then(function() { that.pickedFeatures.features.forEach(function(entity) { var hash = hashEntity(entity, that.clock); var feature = entity; featureIndex[hash] = featureIndex[hash] ? featureIndex[hash].concat([feature]) : [feature]; }); if (defined(initSource.pickedFeatures.current)) { var selectedFeatureMatches = defaultValue(featureIndex[initSource.pickedFeatures.current.hash], []) .filter(function(feature) { return feature.name === initSource.pickedFeatures.current.name; }); that.selectedFeature = selectedFeatureMatches.length && selectedFeatureMatches[0]; } }); }; if (that.currentViewer !== that._noViewer) { loadPickedFeatures(); } else { removeViewLoadedListener = that.afterViewerChanged.addEventListener(loadPickedFeatures); } }); } return promise; }; Terria.prototype.getLocalProperty = function(key) { try { if (!defined(window.localStorage)) { return undefined; } } catch (e) { // SecurityError can arise if 3rd party cookies are blocked in Chrome and we're served in an iFrame return undefined; } var v = window.localStorage.getItem(this.appName + '.' + key); if (v === 'true') { return true; } else if (v === 'false') { return false; } return v; }; Terria.prototype.setLocalProperty = function(key, value) { try { if (!defined(window.localStorage)) { return undefined; } } catch (e) { return undefined; } window.localStorage.setItem(this.appName + '.' + key, value); return true; }; var latestStartVersion = '0.0.05'; function interpretHash(terria, hashProperties, userProperties, persistentInitSources, temporaryInitSources) { var promise; // #share=xyz . Resolve with either share data service or URL shortener. if (defined(hashProperties.share)) { if (defined(terria.shareDataService)) { // get JSON directly promise = terria.shareDataService.resolveData(hashProperties.share); } else if (defined(terria.urlShortener)) { promise = terria.urlShortener.expand(hashProperties.share) // get URL, and extract the JSON part .then(longUrl => longUrl && queryToObject(new URI(longUrl).fragment())); } } return when(promise, function(shareProps) { Object.keys(hashProperties).forEach(function(property) { var propertyValue = hashProperties[property]; if (property === 'clean') { persistentInitSources.length = 0; temporaryInitSources.length = 0; } else if (property === 'start') { // a share link that hasn't been shortened: JSON embedded in URL (only works for small quantities of JSON) var startData = JSON.parse(propertyValue); interpretStartData(terria, startData, persistentInitSources, temporaryInitSources); } else if (defined(propertyValue) && propertyValue.length > 0) { userProperties[property] = propertyValue; knockout.track(userProperties, [property]); } else { var initSourceFile = generateInitializationUrl(property); persistentInitSources.push(initSourceFile); temporaryInitSources.push(initSourceFile); } }); if (shareProps) { interpretStartData(terria, shareProps, persistentInitSources, temporaryInitSources); } }); } function interpretStartData(terria, startData, persistentInitSources, temporaryInitSources) { if (defined(startData.version) && startData.version !== latestStartVersion) { adjustForBackwardCompatibility(startData); } if (defined(terria.filterStartDataCallback)) { startData = terria.filterStartDataCallback(startData) || startData; } // Include any initSources specified in the URL. if (defined(startData.initSources)) { for (var i = 0; i < startData.initSources.length; ++i) { var initSource = startData.initSources[i]; if (temporaryInitSources.indexOf(initSource) < 0) { temporaryInitSources.push(initSource); // Only add external files to the application's list of init sources. if (typeof initSource === 'string' && persistentInitSources.indexOf(initSource) < 0) { persistentInitSources.push(initSource); } } } } } function generateInitializationUrl(url) { if (url.toLowerCase().substring(url.length - 5) !== '.json') { return { initFragment: url }; } return url; } function loadInitSources(terria, initSources) { return initSources.reduce(function(promiseSoFar, initSource) { return promiseSoFar .then(loadInitSource.bind(undefined, terria, initSource)) .then(function(initSource) { if (defined(initSource)) { return terria.addInitSource(initSource); } }); }, when()); } function loadInitSource(terria, source) { let loadSource; let loadPromise; if (typeof source === 'string') { loadSource = source; loadPromise = loadJson5(terria.corsProxy.getURLProxyIfNecessary(source)); } else if (typeof source === 'object' && typeof source.initFragment === 'string') { const fragment = source.initFragment; const fragmentPaths = terria.configParameters.initFragmentPaths; if (fragmentPaths.length === 0) { terria.error.raiseEvent({ title: 'Error loading initialization source', message: 'Could not load initialization information from ' + fragment + ' because no initFragmentPaths are defined.' }); return undefined; } loadSource = fragmentPaths.map(path => buildInitUrlFromFragment(path, fragment)).join(', '); loadPromise = loadJson5(terria.corsProxy.getURLProxyIfNecessary(buildInitUrlFromFragment(fragmentPaths[0], fragment))); for (let i = 1; i < fragmentPaths.length; ++i) { loadPromise = loadPromise.otherwise(function() { return loadJson5(terria.corsProxy.getURLProxyIfNecessary(buildInitUrlFromFragment(fragmentPaths[i], fragment))); }); } } if (loadPromise) { return loadPromise.then(function (initSource) { initSource.isFromExternalFile = true; return initSource; }) .otherwise(function () { terria.error.raiseEvent({ title: 'Error loading initialization source', message: 'An error occurred while loading initialization information from ' + loadSource + '. This may indicate that you followed an invalid link or that there is a problem with your Internet connection.' }); return undefined; }); } else { return source; } } function buildInitUrlFromFragment(path, fragment) { return path + fragment + '.json'; } function adjustForBackwardCompatibility(startData) { if (startData.version === '0.0.03') { // In this version, there was just a single 'camera' property instead of a 'homeCamera' and 'initialCamera'. // Treat the one property as the initialCamera. for (var i = 0; i < startData.initSources.length; ++i) { if (defined(startData.initSources[i].camera)) { startData.initSources[i].initialCamera = startData.initSources[i].camera; startData.initSources[i].camera = undefined; } } } } function showDisclaimer(terria, globalDisclaimerHtml, developmentDisclaimerPreambleHtml) { // Show a modal disclaimer before user can do anything else. if (defined(terria.configParameters.globalDisclaimer) && defined(globalDisclaimerHtml)) { var globalDisclaimer = terria.configParameters.globalDisclaimer; var hostname = window.location.hostname; var enabled = !defined(globalDisclaimer.enabled) || globalDisclaimer.enabled; if (enabled && (globalDisclaimer.enableOnLocalhost || hostname.indexOf('localhost') === -1)) { var message = ''; // Sometimes we want to show a preamble if the user is viewing a site other than the official production instance. // This can be expressed as a devHostRegex ("any site starting with staging.") or a negative prodHostRegex ("any site not ending in .gov.au") if (defined(developmentDisclaimerPreambleHtml)) { if (defined(globalDisclaimer.devHostRegex) && hostname.match(globalDisclaimer.devHostRegex) || defined(globalDisclaimer.prodHostRegex) && !hostname.match(globalDisclaimer.prodHostRegex)) { message += developmentDisclaimerPreambleHtml; } } message += globalDisclaimerHtml; var options = { title: (globalDisclaimer.title !== undefined) ? globalDisclaimer.title : 'Disclaimer', confirmText: (globalDisclaimer.buttonTitle || "I Agree"), width: globalDisclaimer.width || 600, height: globalDisclaimer.height || 550, message: message, hideUi: globalDisclaimer.hideUi }; terria.error.raiseEvent(options); } } } module.exports = Terria;