"use strict";
/*global require*/
var CatalogMember = require("./CatalogMember");
var CesiumMath = require("terriajs-cesium/Source/Core/Math").default;
var clone = require("terriajs-cesium/Source/Core/clone").default;
var createCatalogMemberFromType = require("./createCatalogMemberFromType");
var Credit = require("terriajs-cesium/Source/Core/Credit").default;
var defined = require("terriajs-cesium/Source/Core/defined").default;
var DeveloperError = require("terriajs-cesium/Source/Core/DeveloperError")
.default;
var inherit = require("../Core/inherit");
var JulianDate = require("terriajs-cesium/Source/Core/JulianDate").default;
var knockout = require("terriajs-cesium/Source/ThirdParty/knockout").default;
var LegendUrl = require("../Map/LegendUrl");
var Metadata = require("./Metadata");
var raiseErrorOnRejectedPromise = require("./raiseErrorOnRejectedPromise");
var Rectangle = require("terriajs-cesium/Source/Core/Rectangle").default;
var when = require("terriajs-cesium/Source/ThirdParty/when").default;
var i18next = require("i18next").default;
/**
* A data item in a {@link CatalogGroup}.
*
* @alias CatalogItem
* @constructor
* @extends CatalogMember
* @abstract
*
* @param {Terria} terria The Terria instance.
*/
var CatalogItem = function(terria) {
CatalogMember.call(this, terria);
this._enabledDate = undefined;
this._shownDate = undefined;
this._loadForEnablePromise = undefined;
this._lastLoadInfluencingValues = undefined;
// The catalog item to show in the Now Viewing when this item is enabled, instead of this item.
// If undefined, this item itself is shown.
this.nowViewingCatalogItem = undefined;
// The catalog item that created this one. Usually this is undefined, but may be defined if
// the {@see CatalogItem} in the catalog acts like a factory to produce a different catalog item for the
// {@see NowViewing}, rather than being added to the {@see NowViewing} itself. In that scenario, this
// property on the item in the now viewing would be a reference to the item in the catalog.
// @type {CatalogItem}
this.creatorCatalogItem = undefined;
/**
* The index of the item in the Now Viewing list. Setting this property does not automatically change the order.
* This property is used intenally to save/restore the Now Viewing order and is not intended for general use.
* @private
* @type {Number}
*/
this.nowViewingIndex = undefined;
/**
* Gets or sets the geographic rectangle (extent or bounding box) containing this data item. This property is observable.
* @type {Rectangle}
*/
this.rectangle = undefined;
/**
* Gets or sets the URL of this data. This property is observable.
* @type {String}
*/
this.url = undefined;
/**
* Gets or sets a description of the custodian of this data item.
* This property is an HTML string that must be sanitized before display to the user.
* This property is observable.
* @type {String}
*/
this.dataCustodian = undefined;
/**
* Gets or sets an attribution displayed on the map when this catalog item is enabled.
* This property is observable.
* @type {Credit}
*/
this.attribution = undefined;
/**
* Gets or sets the URL from which this data item's metadata description can be retrieved, or undefined if
* metadata is not available for this data item. The format of the metadata depends on the type of data item.
* For example, Web Map Service (WMS) data items provide their metadata via their GetCapabilities document.
* This property is observable.
* @type {String}
*/
this.metadataUrl = undefined;
/**
* Gets or sets a value indicating whether this data item is enabled. An enabled data item appears in the
* "Now Viewing" pane, but is not necessarily shown on the map. This property is observable.
* @type {Boolean}
*/
this.isEnabled = false;
/**
* Gets or sets a value indicating whether this data item is currently shown on the map. In order to be shown,
* the item must also be enabled. This property is observable.
* @type {Boolean}
*/
this.isShown = false;
/**
* Gets or sets a value indicating whether the legend for this data item is currently visible.
* This property is observable.
* @type {Boolean}
*/
this.isLegendVisible = true;
/**
* Gets or sets a flag which determines whether the legend comes before (false) or after (true) the display variable choice.
* Default false.
* @type {Boolean}
*/
this.displayChoicesBeforeLegend = false;
/**
* Gets or sets the clock parameters for this data item. If this property is undefined, this data item
* does not have any time-varying data. This property is observable.
* @type {DataSourceClock}
*/
this.clock = undefined;
/**
* Gets or sets a value indicating whether this data source is currently loading. This property is observable.
* @type {Boolean}
*/
this.isLoading = false;
/*
* Gets or sets a value indicating whether this data source can be enabled via a checkbox in the Data Catalog Tab.
* This property is observable.
* @type {Boolean}
*/
this.isEnableable = true;
/**
* Gets or sets a value indicating whether this data source can be shown on the map (as opposed to a time-series dataset,
* for instance, which can only be shown in a chart).
* This property is observable.
* @type {Boolean}
*/
this.isMappable = true;
/**
* Gets or sets a value indicating whether this data source should show an info icon. This property is observable.
* @type {Boolean}
*/
this.showsInfo = true;
/**
* Gets or sets a message to show when this item is enabled for the first time in order to call attention to the Now Viewing panel.
* @type {String}
*/
this.nowViewingMessage = undefined;
/**
* Gets or sets a template to display message in a info box.
* May be a string or an object with template, name and/or partials properties.
* @type {String|Object}
*/
this.featureInfoTemplate = undefined;
/**
* The maximum number of features whose information can be shown at one time in the Feature Info Panel, from this item.
* Defaults to terria.configParameters.defaultMaximumShownFeatureInfos
* @type {Number}
*/
this.maximumShownFeatureInfos =
terria.configParameters.defaultMaximumShownFeatureInfos;
/**
* Gets or sets a value indicating whether the map will automatically zoom to this catalog item when it is enabled.
*
* Note that within a single init source:
*
* * Catalog items with both `isEnabled` and `zoomOnEnable` set to true will override the top-level `initialCamera` property.
* * If multiple catalog items have both `isEnabled` and `zoomOnEnable` set to true, it is undefined which one will affect the camera.
*
* In the case of multiple init sources, however, the camera will reflect whatever happens in the _last_ init source, whether
* it is a result of a `zoomOnEnable` or an `initialCamera`,
* @type {Boolean}
* @default false
*/
this.zoomOnEnable = false;
/**
* Options for formatting current time and timeline tic labels. Options are:
* currentTime // Current time in time slider will be shown in this format. For example "mmmm yyyy" for Jan 2016.
* timelineTic // Timeline tics will have this label. For example "yyyy" will cause each tic to be labelled with the year.
* @type {Object}
*/
this.dateFormat = {};
/**
* Gets or sets a flag indicating whether imagery should be displayed using this item's own clock (currentTime, multiplier),
* or, if false, the terria clock (whose current time is shown in the timeline UI). Default false.
* This property is observable.
* @type {Boolean}
*/
this.useOwnClock = false;
/**
* Gets or sets a flag indicating whether the preview on the Add Data panel should be disabled. This is useful when
* the preview will be very slow to load.
*/
this.disablePreview = false;
// _currentTime is effectively a view of clock.currentTime. It is defined as a separate state (mirror state) so that
// it can be an independent knockout observable with all of the machinary that implies for free, without tracking a sub
// property of a cesium primitive (namely clock.currentTime) which seems kind of nasty.
this._currentTime = undefined;
this._legendUrl = undefined;
this._legendUrls = undefined;
this._dataUrl = undefined;
this._dataUrlType = undefined;
knockout.track(this, [
"rectangle",
"dataCustodian",
"attribution",
"metadataUrl",
"isEnabled",
"isShown",
"isLegendVisible",
"clock",
"_currentTime",
"isLoading",
"isMappable",
"nowViewingMessage",
"zoomOnEnable",
"isEnableable",
"showsInfo",
"nowViewingMessage",
"url",
"_legendUrl",
"_legendUrls",
"_dataUrl",
"_dataUrlType",
"nowViewingCatalogItem",
"useOwnClock",
"disablePreview"
]);
var evaluatingLegendUrl = false;
/**
* Gets or sets the URLs of the legends to show when this catalog item is enabled.
* @member {LegendUrl} legendUrls
* @memberOf CatalogItem.prototype
*/
knockout.defineProperty(this, "legendUrls", {
get: function() {
if (!defined(this._legendUrls) || this._legendUrls.length === 0) {
var legendUrl = evaluatingLegendUrl ? undefined : this.legendUrl;
if (
defined(legendUrl) &&
defined(legendUrl.url) &&
legendUrl.url.length > 0
) {
return [legendUrl];
}
}
return this._legendUrls;
},
set: function(value) {
this._legendUrls = value;
this._legendUrl = undefined;
}
});
/**
* Gets or sets the URL of the legend to show when this catalog item is enabled. If there is more than one
* legend URL, this property returns the first one.
* @member {LegendUrl} legendUrl
* @memberOf CatalogItem.prototype
*/
knockout.defineProperty(this, "legendUrl", {
get: function() {
evaluatingLegendUrl = true;
try {
if (defined(this._legendUrl)) {
return this._legendUrl;
} else {
var legendUrls = this.legendUrls;
if (defined(legendUrls)) {
return this.legendUrls[0];
}
return undefined;
}
} finally {
evaluatingLegendUrl = false;
}
},
set: function(value) {
this._legendUrl = value;
this._legendUrls = undefined;
}
});
/**
* Gets or sets the URL from which this data item's raw data can be retrieved, or undefined if raw data for
* this data item is not available. This property is observable.
* @member {String} dataUrl
* @memberOf CatalogItem.prototype
*/
knockout.defineProperty(this, "dataUrl", {
get: function() {
// dataUrl is derived from url if not explicitly specified.
if (defined(this._dataUrl)) {
return this._dataUrl;
}
return this.url;
},
set: function(value) {
this._dataUrl = value;
}
});
/**
* Gets or sets the type of the {@link CatalogItem#dataUrl}, or undefined if raw data for this data
* source is not available. This property is observable.
* Valid values are:
* * `direct` - A direct link to the data.
* * `wfs` - A Web Feature Service (WFS) base URL. If {@link CatalogItem#dataUrl} is not
* specified, the base URL will be this data item's URL.
* * `wfs-complete` - A complete, ready-to-use link to download features from a WFS server.
* * `none` - There is no data link.
* @member {String} dataUrlType
* @memberOf CatalogItem.prototype
*/
knockout.defineProperty(this, "dataUrlType", {
get: function() {
if (defined(this._dataUrlType)) {
return this._dataUrlType;
} else {
return "direct";
}
},
set: function(value) {
this._dataUrlType = value;
}
});
/**
* Gets / sets the CatalogItems current time.
*
* This property is an observable version of clock.currentTime, they will always have the same value.
*
* When setting the currentTime through this property the correct clock (terria.clock or this.clock) is updated
* depending on whether .useOwnClock is true or false so that the catalog items state will reflect the new time
* correctly.
*
* The get component of this property is effectively an interface adapter for clock.definitionChanged which changes
* the structure from an Event when the current time changes to a knockout property which can be observed.
*
* @member {JulianDate} currentTime
* @memberOf CatalogItem.prototype
*/
knockout.defineProperty(this, "currentTime", {
get: function() {
return this._currentTime;
},
set: function(value) {
// Note: We don't explicitly need to set this._currentTime since our other machinery regarding updating
// this._currentTime should take care of this.
if (this.useOwnClock) {
updateCurrentTime(this, value);
} else {
this.terria.clock.currentTime = JulianDate.clone(value);
this.terria.clock.tick();
}
if (this._currentChartData) {
const index = this.intervals.indexOf(value);
this.selectedIndex = index;
this._currentChartData.selectedIndex = index;
if (this._currentChartData.renderer)
this._currentChartData.renderer.highlightMoment(index);
}
}
});
/**
* Gets the CatalogItems current time as the discrete time that the CatalogItem has information for.
* Returns undefined if the clock is beyond the range of the intervals specified by the layer.
* Returns undefined if it is not possible to query the time (i.e. the item doesn't have a clock, availableDates or
* intervals).
*
* See also clampedDiscreteTime if you want the discrete time that is clamped to the first / last value if the current
* time is beyond the range of the intervals specified by the item.
*
* @member {Date} discreteTime
* @memberOf CatalogItem.prototype
*/
knockout.defineProperty(this, "discreteTime", {
get: function() {
return timeAtIndex(this, getCurrentIndex(this));
}
});
/**
* Gets the CatalogItems current time as the discrete time that the CatalogItem has information for.
* Returns the nearest time in-range if the clock is beyond the range of the intervals specified by the layer.
* Returns undefined if it is not possible to query the time (i.e. the item doesn't have a clock, availableDates or
* intervals).
*
* See also discreteTime if you want the discrete time that is undefined if the current time is beyond the range of
* the intervals specified by the item.
*
* @member {Date} clampedDiscreteTime
* @memberOf CatalogItem.prototype
*/
knockout.defineProperty(this, "clampedDiscreteTime", {
get: function() {
if (defined(this.discreteTime)) {
return this.discreteTime;
}
if (!hasValidCurrentTimeAndIntervals(this)) {
return undefined;
}
if (timeIsBeforeStart(this, this.currentTime)) {
return timeAtIndex(this, 0);
}
if (timeIsAfterStop(this, this.currentTime)) {
return timeAtIndex(this, this.intervals.length - 1);
}
return undefined;
}
});
// A property which defines when we are using the terria clock.
// We define this as a knockout property so that we can watch for changes to the propery triggered by observables
// that it depends on and update the subscription (watcher) on the terria.clock when the state changes.
knockout.defineProperty(this, "_mirroringTerriaClock", {
get: function() {
return this.isEnabled && !this.useOwnClock && defined(this.clock);
}
});
// A property which defines when the clock is defined.
// We define this as a knockout property so that we can watch for all changes to the propery including when
// .clock is overriden from derrived classes (and not just base CatalogItem.clock).
knockout.defineProperty(this, "_clockDefined", {
get: function() {
return defined(this.clock);
}
});
knockout.getObservable(this, "_clockDefined").subscribe(function(newValue) {
clockChanged(this);
}, this);
knockout.getObservable(this, "isEnabled").subscribe(function(newValue) {
isEnabledChanged(this);
}, this);
knockout.getObservable(this, "isShown").subscribe(function(newValue) {
isShownChanged(this);
}, this);
knockout
.getObservable(this, "_mirroringTerriaClock")
.subscribe(function(newValue) {
updateTerriaClockWatcher(this);
}, this);
knockout.getObservable(this, "useOwnClock").subscribe(function(newValue) {
useOwnClockChanged(this);
}, this);
};
inherit(CatalogMember, CatalogItem);
Object.defineProperties(CatalogItem.prototype, {
/**
* Gets a value indicating whether this data item, when enabled, can be reordered with respect to other data items.
* Data items that cannot be reordered are typically displayed above reorderable data items.
* @memberOf CatalogItem.prototype
* @type {Boolean}
*/
supportsReordering: {
get: function() {
return false;
}
},
/**
* Gets a value indicating whether the visibility of this data item can be toggled.
* @memberOf CatalogItem.prototype
* @type {Boolean}
*/
supportsToggleShown: {
get: function() {
return true;
}
},
/**
* Gets a value indicating whether the opacity of this data item can be changed.
* @memberOf CatalogItem.prototype
* @type {Boolean}
*/
supportsOpacity: {
get: function() {
return false;
}
},
/**
* Gets a value indicating whether this layer can be split so that it is
* only shown on the left or right side of the screen.
* @memberOf CatalogItem.prototype
*/
supportsSplitting: {
get: function() {
return false;
}
},
/**
* Gets a value indicating whether this data item has a legend.
* @memberOf CatalogItem.prototype
* @type {Boolean}
*/
hasLegend: {
get: function() {
return defined(this.legendUrl);
}
},
/**
* Returns true if this item currently has a rectangle to zoom to. Depends on observable properties, and so updates once loaded.
* @memberOf CatalogItem.prototype
* @type {Boolean}
*/
canZoomTo: {
get: function() {
if (defined(this.nowViewingCatalogItem)) {
return this.nowViewingCatalogItem.canZoomTo;
}
return defined(this.rectangle);
}
},
/**
* Gets the metadata associated with this data item and the server that provided it, if applicable.
* @memberOf CatalogItem.prototype
* @type {Metadata}
*/
metadata: {
get: function() {
return CatalogItem.defaultMetadata;
}
},
/**
* Gets the set of functions used to update individual properties in {@link CatalogMember#updateFromJson}.
* When a property name in the returned object literal matches the name of a property on this instance, the value
* will be called as a function and passed a reference to this instance, a reference to the source JSON object
* literal, and the name of the property.
* @memberOf CatalogItem.prototype
* @type {Object}
*/
updaters: {
get: function() {
return CatalogItem.defaultUpdaters;
}
},
/**
* Gets the set of functions used to serialize individual properties in {@link CatalogMember#serializeToJson}.
* When a property name on the model matches the name of a property in the serializers object literal,
* the value will be called as a function and passed a reference to the model, a reference to the destination
* JSON object literal, and the name of the property.
* @memberOf CatalogItem.prototype
* @type {Object}
*/
serializers: {
get: function() {
return CatalogItem.defaultSerializers;
}
},
/**
* Gets the set of names of the properties to be serialized for this object for a share link.
* @memberOf CatalogItem.prototype
* @type {String[]}
*/
propertiesForSharing: {
get: function() {
return CatalogItem.defaultPropertiesForSharing;
}
}
});
/**
* Gets or sets the default metadata to use for data items that don't provide anything better from their
* {@link CatalogItem#metadata} property. The default simply indicates that no metadata is available.
* @type {Metadata}
*/
CatalogItem.defaultMetadata = new Metadata();
CatalogItem.defaultMetadata.isLoading = false;
CatalogItem.defaultMetadata.dataSourceErrorMessage = i18next.t(
"models.catalog.dataSourceErrorMessage"
);
CatalogItem.defaultMetadata.serviceErrorMessage = i18next.t(
"models.catalog.serviceErrorMessage"
);
Object.freeze(CatalogItem.defaultMetadata);
/**
* Gets or sets the set of default updater functions to use in {@link CatalogMember#updateFromJson}. Types derived from this type
* should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#updaters} property.
* @type {Object}
*/
CatalogItem.defaultUpdaters = clone(CatalogMember.defaultUpdaters);
CatalogItem.defaultUpdaters.rectangle = function(
catalogItem,
json,
propertyName
) {
if (defined(json.rectangle)) {
catalogItem.rectangle = Rectangle.fromDegrees(
json.rectangle[0],
json.rectangle[1],
json.rectangle[2],
json.rectangle[3]
);
} else {
catalogItem.rectangle = Rectangle.MAX_VALUE;
}
};
CatalogItem.defaultUpdaters.attribution = function(
catalogItem,
json,
prototypeName
) {
if (defined(json.attribution)) {
if (
typeof json.attribution === "object" &&
json.attribution.text &&
json.attribution.link
) {
const a = document.createElement("a");
a.href = json.attribution.link;
a.target = "_blank";
a.innerText = json.attribution.text;
catalogItem.attribution = new Credit(a.outerHTML);
} else if (typeof json.attribution === "object" && json.attribution.text) {
catalogItem.attribution = new Credit(json.attribution.text);
} else if (typeof json.attribution === "string") {
catalogItem.attribution = new Credit(json.attribution);
}
}
};
CatalogItem.defaultUpdaters.legendUrl = function(
catalogItem,
json,
prototypeName
) {
if (defined(json.legendUrl)) {
var url, mimeType;
if (typeof json.legendUrl === "string") {
url = json.legendUrl;
} else {
url = json.legendUrl.url;
mimeType = json.legendUrl.mimeType;
}
catalogItem.legendUrl = new LegendUrl(url, mimeType);
}
};
CatalogItem.defaultUpdaters.legendUrls = function(
catalogItem,
json,
prototypeName
) {
if (defined(json.legendUrls)) {
catalogItem.legendUrls = json.legendUrls.map(function(legendUrl) {
var url, mimeType;
if (typeof legendUrl === "string") {
url = legendUrl;
} else {
url = legendUrl.url;
mimeType = legendUrl.mimeType;
}
return new LegendUrl(url, mimeType);
});
}
};
CatalogItem.defaultUpdaters.nowViewingCatalogItem = function(
catalogItem,
json,
prototypeName,
options
) {
if (defined(json.nowViewingCatalogItem)) {
return when(catalogItem.load()).then(function() {
if (!defined(catalogItem.nowViewingCatalogItem)) {
catalogItem.nowViewingCatalogItem = createCatalogMemberFromType(
json.nowViewingCatalogItem.type,
catalogItem.terria
);
}
return catalogItem.nowViewingCatalogItem.updateFromJson(
json.nowViewingCatalogItem,
options
);
});
}
};
CatalogItem.defaultUpdaters.currentTime = function(
catalogItem,
json,
propertyName
) {
// Do not update .currentTime as it is a view of .clock.currentTime.
};
CatalogItem.defaultUpdaters.discreteTime = function(
catalogItem,
json,
propertyName
) {
// Do not update .currentTime as it is a view of .clock.currentTime.
};
Object.freeze(CatalogItem.defaultUpdaters);
/**
* Gets or sets the set of default serializer functions to use in {@link CatalogMember#serializeToJson}. Types derived from this type
* should expose this instance - cloned and modified if necesary - through their {@link CatalogMember#serializers} property.
* @type {Object}
*/
CatalogItem.defaultSerializers = clone(CatalogMember.defaultSerializers);
CatalogItem.defaultSerializers.rectangle = function(
catalogItem,
json,
propertyName
) {
if (defined(catalogItem.rectangle)) {
json.rectangle = [
CesiumMath.toDegrees(catalogItem.rectangle.west),
CesiumMath.toDegrees(catalogItem.rectangle.south),
CesiumMath.toDegrees(catalogItem.rectangle.east),
CesiumMath.toDegrees(catalogItem.rectangle.north)
];
}
};
// Serialize the underlying properties instead of the public views of them.
CatalogItem.defaultSerializers.legendUrl = function(
catalogItem,
json,
propertyName
) {
if (defined(catalogItem._legendUrl)) {
json.legendUrl = catalogItem._legendUrl;
}
};
CatalogItem.defaultSerializers.legendUrls = function(
catalogItem,
json,
propertyName
) {
if (defined(catalogItem._legendUrls) && catalogItem._legendUrls.length > 0) {
json.legendUrls = catalogItem._legendUrls;
}
};
CatalogItem.defaultSerializers.attribution = function(
catalogItem,
json,
propertyName
) {
if (defined(catalogItem.attribution)) {
if (defined(catalogItem.attribution.link)) {
json.attribution = {
text: catalogItem.attribution.text,
link: catalogItem.attribution.link
};
} else {
json.attribution = catalogItem.attribution.text;
}
}
};
CatalogItem.defaultSerializers.dataUrl = function(
catalogItem,
json,
prototypeName
) {
if (defined(catalogItem._dataUrl)) {
json.dataUrl = catalogItem._dataUrl;
}
};
CatalogItem.defaultSerializers.dataUrlType = function(
catalogItem,
json,
prototypeName
) {
if (defined(catalogItem._dataUrlType)) {
json.dataUrlType = catalogItem._dataUrlType;
}
};
CatalogItem.defaultSerializers.nowViewingCatalogItem = function(
catalogItem,
json,
prototypeName,
options
) {
if (catalogItem.isEnabled && defined(catalogItem.nowViewingCatalogItem)) {
json.nowViewingCatalogItem = catalogItem.nowViewingCatalogItem.serializeToJson(
options
);
}
};
CatalogItem.defaultSerializers.currentTime = function(
catalogItem,
json,
propertyName
) {
// Do not serialise .currentTime as it is a view of .clock.currentTime.
};
CatalogItem.defaultSerializers.discreteTime = function(
catalogItem,
json,
propertyName
) {
// Do not serialise .discreteTime as it is a view of .clock.currentTime.
};
CatalogItem.defaultSerializers.clampedDiscreteTime = function(
catalogItem,
json,
propertyName
) {
// Do not serialise .clampedDiscreteTime as it is a view of .clock.currentTime.
};
Object.freeze(CatalogItem.defaultSerializers);
/**
* Gets or sets the default set of properties that are serialized when serializing a {@link CatalogItem}-derived object
* for a share link.
* @type {String[]}
*/
CatalogItem.defaultPropertiesForSharing = clone(
CatalogMember.defaultPropertiesForSharing
);
CatalogItem.defaultPropertiesForSharing.push("isEnabled");
CatalogItem.defaultPropertiesForSharing.push("isShown");
CatalogItem.defaultPropertiesForSharing.push("isLegendVisible");
CatalogItem.defaultPropertiesForSharing.push("nowViewingIndex");
CatalogItem.defaultPropertiesForSharing.push("nowViewingCatalogItem");
CatalogItem.defaultPropertiesForSharing.push("useOwnClock");
Object.freeze(CatalogItem.defaultPropertiesForSharing);
/**
* Loads this catalog item, if it's not already loaded. It is safe to
* call this method multiple times. The {@link CatalogItem#isLoading} flag will be set while the load is in progress.
* Derived classes should implement {@link CatalogItem#_load} to perform the actual loading for the item.
* Derived classes may optionally implement {@link CatalogItem#_getValuesThatInfluenceLoad} to provide an array containing
* the current value of all properties that influence this item's load process. Each time that {@link CatalogItem#load}
* is invoked, these values are checked against the list of values returned last time, and {@link CatalogItem#_load} is
* invoked again if they are different. If {@link CatalogItem#_getValuesThatInfluenceLoad} is undefined or returns an
* empty array, {@link CatalogItem#_load} will only be invoked once, no matter how many times
* {@link CatalogItem#load} is invoked.
*
* @returns {Promise} A promise that resolves when the load is complete, or undefined if the item is already loaded.
*
*/
CatalogItem.prototype.load = function() {
var parentPromise = CatalogMember.prototype.load.call(this);
if (parentPromise) {
return parentPromise
.then(
function(loadResult) {
if (loadResult instanceof CatalogItem) {
this.nowViewingCatalogItem = loadResult;
loadResult.creatorCatalogItem = this;
}
this.terria.currentViewer.notifyRepaintRequired();
}.bind(this)
)
.otherwise(
function(e) {
this.isEnabled = false;
throw e; // keep throwing this so we can chain more otherwises.
}.bind(this)
);
}
};
/**
* Enables this catalog item, and returns a promise that resolves when the load process, if any, completes.
* @return {Promise} The promise.
*/
CatalogItem.prototype.loadAndEnable = function() {
this.isEnabled = true;
return this._loadingPromise;
};
/**
* When implemented in a derived class, this method loads the item. The base class implementation does nothing.
* This method should not be called directly; call {@link CatalogItem#load} instead.
* @return {Promise} A promise that resolves when the load is complete.
* @protected
*/
CatalogItem.prototype._load = function() {
return when();
};
var emptyArray = Object.freeze([]);
/**
* When implemented in a derived class, gets an array containing the current value of all properties that
* influence this item's load process. See {@link CatalogItem#load} for more information on when and
* how this is used. The base class implementation returns an empty array.
* @return {Array} The array of values that influence the load process.
* @protected
*/
CatalogItem.prototype._getValuesThatInfluenceLoad = function() {
// In the future, we can implement auto-reloading when any of these properties change. Just create a knockout
// computed property that calls this method and subscribe to change notifications on that computed property.
// (Will need to use the rateLimit extender, presumably).
return emptyArray;
};
/**
* Toggles the {@link CatalogItem#isEnabled} property of this item. If it is enabled, calling this method
* will disable it. If it is disabled, calling this method will enable it.
*
* @returns {Boolean} true if the item is now enabled, false if it is now disabled.
*/
CatalogItem.prototype.toggleEnabled = function() {
this.isEnabled = !this.isEnabled;
return this.isEnabled;
};
/**
* Toggles the {@link CatalogItem#isShown} property of this item. If it is shown, calling this method
* will hide it. If it is hidden, calling this method will show it.
*
* @returns {Boolean} true if the item is now shown, false if it is now hidden.
*/
CatalogItem.prototype.toggleShown = function() {
this.isShown = !this.isShown;
return this.isShown;
};
/**
* Toggles the {@link CatalogItem#isLegendVisible} property of this item. If it is visible, calling this
* method will hide it. If it is hidden, calling this method will make it visible.
* @return {Boolean} true if the legend is now visible, false if it is now hidden.
*/
CatalogItem.prototype.toggleLegendVisible = function() {
this.isLegendVisible = !this.isLegendVisible;
return this.isLegendVisible;
};
var scratchRectangle = new Rectangle();
/**
* Moves the camera so that the item's bounding rectangle is visible. If {@link CatalogItem#rectangle} is
* undefined or covers more than about half the world in the longitude direction, or if the data item is not enabled
* or not shown, this method does nothing. Because the zoom may happen asynchronously (for example, if the item's
* rectangle is not yet known), this method returns a Promise that resolves when the zoom animation starts.
* @returns {Promise} A promise that resolves when the zoom animation starts.
*/
CatalogItem.prototype.zoomTo = function() {
var that = this;
return when(this.load(), function() {
if (defined(that.nowViewingCatalogItem)) {
return that.nowViewingCatalogItem.zoomTo();
}
if (!defined(that.rectangle)) {
return;
}
var rect = Rectangle.clone(that.rectangle, scratchRectangle);
if (rect.east - rect.west > 3.14) {
rect = Rectangle.clone(that.terria.homeView.rectangle, scratchRectangle);
console.log("Extent is wider than world so using homeView.");
}
var terria = that.terria;
terria.analytics.logEvent("dataSource", "zoomTo", that.path);
var epsilon = CesiumMath.EPSILON3;
if (rect.east === rect.west) {
rect.east += epsilon;
rect.west -= epsilon;
}
if (rect.north === rect.south) {
rect.north += epsilon;
rect.south -= epsilon;
}
return terria.currentViewer.zoomTo(rect);
});
};
/**
* Uses the {@link CatalogItem#clock} settings from this data item. If this data item
* has no clock settings, or has the useOwnClock property true, this method does nothing.
* Because the clock update may happen asynchronously (for example, if the item's clock parameters are not yet known),
* this method returns a Promise that resolves when the clock has been updated.
* @returns {Promise} A promise that resolves when the clock has been updated.
*/
CatalogItem.prototype.useClock = function() {
var that = this;
return when(this.load(), function() {
if (defined(that.nowViewingCatalogItem)) {
return that.nowViewingCatalogItem.useClock();
}
if (defined(that.clock)) {
if (that.isEnabled && that.isShown && !that.useOwnClock) {
that.terria.timeSeriesStack.addLayerToTop(that);
} else {
that.terria.timeSeriesStack.removeLayer(that);
}
}
});
};
/**
* Move the current time to the next time interval.
*/
CatalogItem.prototype.moveToNextTime = function() {
updateIndex(this, nextIndex(this));
};
/**
* Move the current time to the previous time interval.
*/
CatalogItem.prototype.moveToPreviousTime = function() {
updateIndex(this, previousIndex(this));
};
/**
* Whether it is possible to move to a time interval after the current time.
* @returns {Boolean} True if it is possible.
*/
CatalogItem.prototype.isNextTimeAvaliable = function() {
return defined(nextIndex(this));
};
/**
* Whether it is possible to move to a time interval before the current time.
* @returns {Boolean} True if it is possible.
*/
CatalogItem.prototype.isPreviousTimeAvaliable = function() {
return defined(previousIndex(this));
};
/**
* Moves the camera so that the data item's bounding rectangle is visible, and updates the TerriaJS clock according to this
* data item's clock settings. This method simply calls {@link CatalogItem#zoomTo} and
* {@link CatalogItem#useClock}. Because the zoom and clock update may happen asynchronously (for example, if the item's
* rectangle is not yet known), this method returns a Promise that resolves when the zoom animation starts and the clock
* has been updated.
* @returns {Promise} A promise that resolves when the clock has been updated and the zoom animation has started.
*/
CatalogItem.prototype.zoomToAndUseClock = function() {
return when.all([this.zoomTo(), this.useClock()]);
};
/**
* Enables this data item on the globe or map. This method:
* * Should not be called directly. Instead, set the {@link CatalogItem#isEnabled} property to true.
* * Will not necessarily be called immediately when {@link CatalogItem#isEnabled} is set to true; it will be deferred until
* {@link CatalogItem#isLoading} is false.
* * Should NOT also show the data item on the globe/map (see {@link CatalogItem#_show}), so in some cases it may not do
* anything at all.
* * Calls {@link CatalogItem#_enableInCesium} or {@link CatalogItem#_enableInLeaflet} in the base-class implementation,
* depending on which viewer is active. Derived classes that have identical enable logic for both viewers may override
* this method instead of the viewer-specific ones.
* @protected
*/
CatalogItem.prototype._enable = function() {
if (defined(this.nowViewingCatalogItem)) {
this.nowViewingCatalogItem.isEnabled = true;
return;
}
var terria = this.terria;
if (defined(terria.cesium)) {
terria.cesium.stoppedRendering = true;
this._enableInCesium();
}
if (defined(terria.leaflet)) {
this._enableInLeaflet();
}
};
/**
* Disables this data item on the globe or map. This method:
* * Should not be called directly. Instead, set the {@link CatalogItem#isEnabled} property to false.
* * Will not be called if {@link CatalogItem#_enable} was not called (for example, because the previous call was deferred
* while the data item loaded, and the user disabled the data item before the load completed).
* * Will only be called after {@link CatalogItem#_hide} when a shown data item is disabled.
* * Calls {@link CatalogItem#_disableInCesium} or {@link CatalogItem#_disableInLeaflet} in the base-class implementation,
* depending on which viewer is active. Derived classes that have identical disable logic for both viewers may override
* this method instead of the viewer-specific ones.
* @protected
*/
CatalogItem.prototype._disable = function() {
if (defined(this.nowViewingCatalogItem)) {
this.nowViewingCatalogItem.isEnabled = false;
return;
}
var terria = this.terria;
if (defined(terria.cesium)) {
this._disableInCesium();
}
if (defined(terria.leaflet)) {
this._disableInLeaflet();
}
};
/**
* Shows this data item on the globe or map. This method:
* * Should not be called directly. Instead, set the {@link CatalogItem#isShown} property to true.
* * Will only be called after {@link CatalogItem#_enable}; you can count on that method having been called first.
* * Will not necessarily be called immediately when {@link CatalogItem#isShown} is set to true; it will be deferred until
* {@link CatalogItem#isLoading} is false.
* * Calls {@link CatalogItem#_showInCesium} or {@link CatalogItem#_showInLeaflet} in the base-class implementation,
* depending on which viewer is active. Derived classes that have identical show logic for both viewers
* may override this method instead of the viewer-specific ones.
* @protected
*/
CatalogItem.prototype._show = function() {
if (defined(this.nowViewingCatalogItem)) {
this.nowViewingCatalogItem.isShown = true;
return;
}
var terria = this.terria;
if (defined(terria.cesium)) {
this._showInCesium();
}
if (defined(terria.leaflet)) {
this._showInLeaflet();
}
};
/**
* Hides this data item on the globe or map. This method:
* * Should not be called directly. Instead, set the {@link CatalogItem#isShown} property to false.
* * Will not be called if {@link CatalogItem#_show} was not called (for example, because the previous call was deferred
* while the data item loaded, and the user hid the data item before the load completed).
* * Calls {@link CatalogItem#_hideInCesium} or {@link CatalogItem#_hideInLeaflet} in the base-class implementation,
* depending on which viewer is active. Derived classes that have identical hide logic for both viewers may override
* this method instead of the viewer-specific ones.
* @protected
*/
CatalogItem.prototype._hide = function() {
if (defined(this.nowViewingCatalogItem)) {
this.nowViewingCatalogItem.isShown = false;
return;
}
var terria = this.terria;
if (defined(terria.cesium)) {
this._hideInCesium();
}
if (defined(terria.leaflet)) {
this._hideInLeaflet();
}
};
/**
* When implemented in a derived class, enables this data item on the Cesium globe. You should not call this
* directly, but instead set the {@link CatalogItem#isEnabled} property to true. See
* {@link CatalogItem#_enable} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._enableInCesium = function() {
throw new DeveloperError(
"_enableInCesium must be implemented in the derived class."
);
};
/**
* When implemented in a derived class, disables this data item on the Cesium globe. You should not call this
* directly, but instead set the {@link CatalogItem#isEnabled} property to false. See
* {@link CatalogItem#_disable} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._disableInCesium = function() {
throw new DeveloperError(
"_disableInCesium must be implemented in the derived class."
);
};
/**
* When implemented in a derived class, shows this data item on the Cesium globe. You should not call this
* directly, but instead set the {@link CatalogItem#isShown} property to true. See
* {@link CatalogItem#_show} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._showInCesium = function() {
throw new DeveloperError(
"_showInCesium must be implemented in the derived class."
);
};
/**
* When implemented in a derived class, hides this data item on the Cesium globe. You should not call this
* directly, but instead set the {@link CatalogItem#isShown} property to false. See
* {@link CatalogItem#_hide} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._hideInCesium = function() {
throw new DeveloperError(
"_hideInCesium must be implemented in the derived class."
);
};
/**
* When implemented in a derived class, enables this data item on the Leaflet map. You should not call this
* directly, but instead set the {@link CatalogItem#isEnabled} property to true. See
* {@link CatalogItem#_enable} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._enableInLeaflet = function() {
throw new DeveloperError(
"enableInLeaflet must be implemented in the derived class."
);
};
/**
* When implemented in a derived class, disables this data item on the Leaflet map. You should not call this
* directly, but instead set the {@link CatalogItem#isEnabled} property to false. See
* {@link CatalogItem#_disable} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._disableInLeaflet = function() {
throw new DeveloperError(
"disableInLeaflet must be implemented in the derived class."
);
};
/**
* When implemented in a derived class, shows this data item on the Leaflet map. You should not call this
* directly, but instead set the {@link CatalogItem#isShown} property to true. See
* {@link CatalogItem#_show} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._showInLeaflet = function() {
throw new DeveloperError(
"_showInLeaflet must be implemented in the derived class."
);
};
/**
* When implemented in a derived class, hides this data item on the Leaflet map. You should not call this
* directly, but instead set the {@link CatalogItem#isShown} property to false. See
* {@link CatalogItem#_hide} for more information.
* @abstract
* @protected
*/
CatalogItem.prototype._hideInLeaflet = function() {
throw new DeveloperError(
"_hideInLeaflet must be implemented in the derived class."
);
};
CatalogItem.prototype.enableWithParents = function() {
this.isEnabled = true;
if (this.parent) {
this.parent.enableWithParents();
}
};
/**
* Handles an error in loading a tile. If this function returns a promise that resolves successfully,
* the tile request will be retried. If the returned promise rejects, it must reject with an instance
* of `RequestErrorEvent` with the details of the failure, and the default handling of tile
* failures will be used. The default handling takes into account the `treat404AsError`, `treat403AsError`,
* and `ignoreUnknownTileErrors` properties. The default implementation simply returns `detailsRequestPromise`.
*
* @param {Promise} detailsRequestPromise A promise which is the result of a simple call to `loadWithXhr` for the URL
* that failed. If it resolves, it will resolve to the successfully-download content of the tile URL,
* as text. If it rejects, it will reject with a `RequestErrorEvent`.
* @param {ImageryProvider} imageryProvider The imagery provider that generated the failed request.
* @param {Number} x The x coordinate of the failed tile.
* @param {Number} y The y coordinate of the failed tile.
* @param {Number} level The level of the failed tile.
* @returns {Promise} A promise, as described above.
*/
CatalogItem.prototype.handleTileError = function(
detailsRequestPromise,
imageryProvider,
x,
y,
level
) {
return detailsRequestPromise;
};
function clockChanged(catalogItem) {
removeCurrentTimeSubscription(catalogItem);
if (defined(catalogItem.clock)) {
catalogItem._removeCurrentTimeChange = catalogItem.clock.definitionChanged.addEventListener(
function() {
catalogItem._currentTime = JulianDate.clone(
catalogItem.clock.currentTime
);
}
);
catalogItem._currentTime = JulianDate.clone(catalogItem.clock.currentTime);
}
}
// Removes catalogItem.clock.definitionChanged subscriptions.
function removeCurrentTimeSubscription(catalogItem) {
if (defined(catalogItem._removeCurrentTimeChange)) {
catalogItem._removeCurrentTimeChange();
catalogItem._removeCurrentTimeChange = undefined;
}
}
function isEnabledChanged(catalogItem) {
var terria = catalogItem.terria;
if (defined(catalogItem.creatorCatalogItem)) {
catalogItem.creatorCatalogItem.isEnabled = catalogItem.isEnabled;
}
if (catalogItem.isEnabled) {
terria.nowViewing.add(catalogItem);
// Load this catalog item's data (if we haven't already) when it is enabled.
// Don't actually enable until the load finishes.
// Be careful not to call _enable multiple times or to call _enable
// after the item has already been disabled.
if (!defined(catalogItem._loadForEnablePromise)) {
var resolvedOrRejected = false;
var loadPromise = when
.all([catalogItem.load(), catalogItem.waitForDisclaimerIfNeeded()])
.then(function() {
if (catalogItem.isEnabled) {
// If there's a separate now viewing item, remove this catalog item from the
// now viewing list, if it exists.
if (defined(catalogItem.nowViewingCatalogItem)) {
catalogItem.terria.nowViewing.items.remove(catalogItem);
}
catalogItem._enable();
catalogItem.terria.currentViewer.notifyRepaintRequired();
catalogItem.terria.currentViewer.addAttribution(
catalogItem.attribution
);
if (defined(catalogItem.imageryLayer)) {
catalogItem.imageryLayer.featureInfoTemplate =
catalogItem.featureInfoTemplate;
}
// Zoom to this catalog item if requested.
if (catalogItem.zoomOnEnable) {
return catalogItem.zoomTo();
}
}
});
raiseErrorOnRejectedPromise(catalogItem.terria, loadPromise);
loadPromise.always(function() {
resolvedOrRejected = true;
catalogItem._loadForEnablePromise = undefined;
});
// Make sure we know about it when the promise already resolved/rejected.
catalogItem._loadForEnablePromise = resolvedOrRejected
? undefined
: loadPromise;
}
catalogItem.isShown = true;
terria.analytics.logEvent("dataSource", "added", catalogItem.path);
catalogItem._enabledDate = Date.now();
} else {
catalogItem.isShown = false;
// Disable this data item on the map, but only if the previous request to enable it has
// actually gone through.
if (!defined(catalogItem._loadForEnablePromise)) {
catalogItem._disable();
catalogItem.terria.currentViewer.removeAttribution(
catalogItem.attribution
);
}
terria.nowViewing.remove(catalogItem);
var duration;
if (catalogItem._enabledDate) {
duration = ((Date.now() - catalogItem._enabledDate) / 1000.0) | 0;
}
terria.analytics.logEvent(
"dataSource",
"removed",
catalogItem.path,
duration
);
}
catalogItem.terria.currentViewer.notifyRepaintRequired();
}
function isShownChanged(catalogItem) {
if (defined(catalogItem.creatorCatalogItem)) {
catalogItem.creatorCatalogItem.isShown = catalogItem.isShown;
}
if (catalogItem.isShown) {
// If the item is not enabled, do that first. This way things will work even if isShown is
// deserialized before isEnabled.
catalogItem.isEnabled = true;
// If enabling is waiting on an async load, we need to wait on it, too.
raiseErrorOnRejectedPromise(
catalogItem.terria,
when(catalogItem._loadForEnablePromise, function() {
if (catalogItem.isEnabled && catalogItem.isShown) {
catalogItem._show();
catalogItem.useClock();
catalogItem.terria.currentViewer.notifyRepaintRequired();
}
})
);
catalogItem.terria.analytics.logEvent(
"dataSource",
"shown",
catalogItem.path
);
catalogItem._shownDate = Date.now();
} else {
// Hide this data item on the map, but only if the previous request to show it has
// actually gone through.
if (!defined(catalogItem._loadForEnablePromise)) {
catalogItem._hide();
catalogItem.useClock();
}
var duration;
if (defined(catalogItem._shownDate)) {
duration = ((Date.now() - catalogItem._shownDate) / 1000.0) | 0;
} else if (catalogItem._enabledDate) {
duration = ((Date.now() - catalogItem._enabledDate) / 1000.0) | 0;
}
catalogItem.terria.analytics.logEvent(
"dataSource",
"hidden",
catalogItem.path,
duration
);
}
catalogItem.terria.currentViewer.notifyRepaintRequired();
}
/**
* This function adds / removes a subscription to copy the time from the terria clock into the items clock when .useOwnClock is false.
*
* @param catalogItem The item to add / remove the terria clock watching subscription from.
* @private
*/
function updateTerriaClockWatcher(catalogItem) {
if (
catalogItem._mirroringTerriaClock &&
!defined(catalogItem._removeTerriaClockWatch)
) {
// We are using the terria clock, add a subscription to copy the current time into this items clock.
catalogItem._removeTerriaClockWatch = catalogItem.terria.clock.onTick.addEventListener(
terriaClock => {
if (
!JulianDate.equals(
terriaClock.currentTime,
catalogItem.clock.currentTime
)
) {
updateCurrentTime(catalogItem, terriaClock.currentTime);
}
}
);
} else if (
!catalogItem._mirroringTerriaClock &&
defined(catalogItem._removeTerriaClockWatch)
) {
// We are no longer using the terria clock, remove the subscription.
catalogItem._removeTerriaClockWatch();
catalogItem._removeTerriaClockWatch = undefined;
}
}
/**
* Updates the item.clock.currentTime using the value provided.
* This function does this so that all clients subscribed to the items clock with DataSourceClock.definitionChanged are notified.
*
* @param catalogItem The CatalogItem to set the current time for.
* @param updatedTime The new current time to use.
* @private
*/
function updateCurrentTime(catalogItem, updatedTime) {
if (defined(catalogItem.clock)) {
catalogItem.clock.currentTime = JulianDate.clone(updatedTime);
}
}
/**
* Syncs the time between the terria.clock and the catalogItem.clock.
*
* The sync is performed in the direction that is required depending on the state change true->false / false->true of useOwnClock.
*
* @param catalogItem The CatalogItem to sync the time for.
* @private
*/
function useOwnClockChanged(catalogItem) {
// If we are changing the state, copy the time from the clock that was in use to the clock that will be in use.
if (catalogItem._lastUseOwnClock !== catalogItem.useOwnClock) {
// Check that both clocks are defined before syncing the time (they may not both be defined during load due to construction order).
if (
defined(catalogItem.clock) &&
defined(catalogItem.terria) &&
defined(catalogItem.terria.clock) &&
defined(catalogItem.terria.clock.currentTime)
) {
if (catalogItem.useOwnClock) {
// This is probably not needed, since we should be doing it on update, but just do this here explicitly
// to be sure it is immediately current before we change.
updateCurrentTime(catalogItem, catalogItem.terria.clock.currentTime);
} else {
catalogItem.terria.clock.currentTime = JulianDate.clone(
catalogItem.clock.currentTime
);
}
catalogItem._lastUseOwnClock = catalogItem.useOwnClock;
catalogItem.useClock();
}
}
}
function timeAtIndex(catalogItem, index) {
if (
defined(index) &&
defined(catalogItem.intervals) &&
index >= 0 &&
index < catalogItem.intervals.length
) {
return JulianDate.toDate(catalogItem.intervals.get(index).start);
}
return undefined;
}
function hasValidCurrentTimeAndIntervals(catalogItem) {
return (
defined(catalogItem.currentTime) &&
defined(catalogItem.intervals) &&
catalogItem.intervals.length !== 0
);
}
function getCurrentIndex(catalogItem) {
if (!hasValidCurrentTimeAndIntervals(catalogItem)) {
return undefined;
}
if (
timeIsBeforeStart(catalogItem, catalogItem.currentTime) ||
timeIsAfterStop(catalogItem, catalogItem.currentTime)
) {
return undefined;
}
return catalogItem.intervals.indexOf(catalogItem.currentTime);
}
function updateIndex(catalogItem, index) {
if (!defined(index)) {
return;
}
const time = timeAtIndex(catalogItem, index);
if (defined(time)) {
catalogItem.currentTime = JulianDate.fromDate(new Date(time));
}
}
function timeIsBeforeStart(catalogItem, time) {
const firstInterval = catalogItem.intervals.get(0);
if (
JulianDate.lessThan(time, firstInterval.start) ||
(JulianDate.equals(time, firstInterval.start) &&
!firstInterval.isStartIncluded)
) {
return true;
}
return false;
}
function timeIsAfterStop(catalogItem, time) {
const lastIndex = catalogItem.intervals.length - 1;
const lastInterval = catalogItem.intervals.get(lastIndex);
if (
JulianDate.greaterThan(time, lastInterval.stop) ||
(JulianDate.equals(time, lastInterval.stop) && !lastInterval.isStopIncluded)
) {
return true;
}
return false;
}
function nextIndex(catalogItem) {
if (!hasValidCurrentTimeAndIntervals(catalogItem)) {
return undefined;
}
if (timeIsAfterStop(catalogItem, catalogItem.currentTime)) {
return undefined;
}
if (timeIsBeforeStart(catalogItem, catalogItem.currentTime)) {
return 0;
}
const index = getCurrentIndex(catalogItem);
if (defined(index) && index < catalogItem.intervals.length - 1) {
return index + 1;
}
return undefined;
}
function previousIndex(catalogItem) {
if (!hasValidCurrentTimeAndIntervals(catalogItem)) {
return undefined;
}
if (timeIsBeforeStart(catalogItem, catalogItem.currentTime)) {
return undefined;
}
if (timeIsAfterStop(catalogItem, catalogItem.currentTime)) {
return catalogItem.intervals.length - 1;
}
const index = getCurrentIndex(catalogItem);
if (defined(index) && index > 0) {
return index - 1;
}
return undefined;
}
module.exports = CatalogItem;