"use strict";
define([
"jquery",
"underscore",
"backbone",
"cesium",
"models/maps/assets/MapAsset",
], function ($, _, Backbone, Cesium, MapAsset) {
/**
* @classdesc A CesiumImagery Model contains the information required for Cesium to
* request and draw high-resolution image tiles using several standards (Cesium
* "imagery providers"), including Cesium Ion and Bing Maps. Imagery layers have
* brightness, contrast, gamma, hue, and saturation properties that can be dynamically
* changed.
* @classcategory Models/Maps/Assets
* @class CesiumImagery
* @name CesiumImagery
* @extends MapAsset
* @since 2.18.0
* @constructor
*/
var CesiumImagery = MapAsset.extend(
/** @lends CesiumImagery.prototype */ {
/**
* The name of this type of model
* @type {string}
*/
type: "CesiumImagery",
/**
* Options that are supported for creating imagery tiles. Any properties provided
* here are passed to the Cesium constructor function, so other properties that
* are documented in Cesium are also supported. See
* {@link https://cesium.com/learn/cesiumjs/ref-doc/BingMapsImageryProvider.html#.ConstructorOptions}
* and
* {@link https://cesium.com/learn/cesiumjs/ref-doc/IonImageryProvider.html#.ConstructorOptions}.
* @typedef {Object} CesiumImagery#cesiumOptions
* @property {string|number} ionAssetId - If this imagery is hosted by Cesium
* Ion, then Ion asset ID.
* @property {string|number} key - A key or token required to access the tiles.
* For example, if this is a BingMapsImageryProvider, then the Bing maps key. If
* one is required and not set, the model will look in the {@link AppModel} for a
* key, for example, {@link AppModel#bingMapsKey}
* @property {'GeographicTilingScheme'|'WebMercatorTilingScheme'} tilingScheme -
* The tiling scheme to use when constructing an imagery provider. If not set,
* Cesium uses WebMercatorTilingScheme by default.
* @property {Number[]} rectangle - The rectangle covered by the layer. The list
* of west, south, east, north bounding degree coordinates, respectively. This
* will be passed to Cesium.Rectangle.fromDegrees to define the bounding box of
* the imagery layer. If left undefined, the layer will cover the entire globe.
*/
/**
* Default attributes for CesiumImagery models
* @name CesiumImagery#defaults
* @extends MapAsset#defaults
* @type {Object}
* @property {'BingMapsImageryProvider'|'IonImageryProvider'|'TileMapServiceImageryProvider'|'WebMapTileServiceImageryProvider'} type
* A string indicating a Cesium Imagery Provider type. See
* {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#more-imagery-providers}
* @property {Cesium.ImageryLayer} cesiumModel A model created and used by Cesium
* that organizes the data to display in the Cesium Widget. See
* {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLayer}
* and
* {@link https://cesium.com/learn/cesiumjs/ref-doc/?classFilter=ImageryProvider}
* @property {CesiumImagery#cesiumOptions} cesiumOptions options that are passed
* to the function that creates the Cesium model. The properties of options are
* specific to each type of asset.
*/
defaults: function () {
return _.extend(this.constructor.__super__.defaults(), {
type: "",
cesiumModel: null,
cesiumOptions: {},
// brightness: 1, contrast: 1, gamma: 1, hue: 0, saturation: 1,
});
},
/**
* Executed when a new CesiumImagery model is created.
* @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
* attributes, which will be set on the model.
*/
initialize: function (assetConfig) {
try {
MapAsset.prototype.initialize.call(this, assetConfig);
if (assetConfig.type == "NaturalEarthII") {
this.initNaturalEarthII(assetConfig);
} else if (assetConfig.type == "USGSImageryTopo") {
this.initUSGSImageryTopo(assetConfig);
}
this.createCesiumModel();
this.getThumbnail();
} catch (e) {
console.log("Error initializing a CesiumImagery model: ", e);
}
},
/**
* Initializes a CesiumImagery model for the Natural Earth II asset.
* @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
* attributes, which will be set on the model.
*/
initNaturalEarthII: function (assetConfig) {
try {
if (
!assetConfig.cesiumOptions ||
typeof assetConfig.cesiumOptions !== "object"
) {
assetConfig.cesiumOptions = {};
}
assetConfig.cesiumOptions.url = Cesium.buildModuleUrl(
"Assets/Textures/NaturalEarthII",
);
this.set("type", "TileMapServiceImageryProvider");
this.set("cesiumOptions", assetConfig.cesiumOptions);
} catch (error) {
console.log(
"There was an error initializing NaturalEarthII in a CesiumImagery" +
". Error details: " +
error,
);
}
},
/**
* Initializes a CesiumImagery model for the USGS Imagery Topo asset.
* @param {MapConfig#MapAssetConfig} [assetConfig] The initial values of the
* attributes, which will be set on the model.
*/
initUSGSImageryTopo: function (assetConfig) {
try {
if (
!assetConfig.cesiumOptions ||
typeof assetConfig.cesiumOptions !== "object"
) {
assetConfig.cesiumOptions = {};
}
this.set("type", "WebMapServiceImageryProvider");
assetConfig.cesiumOptions.url =
"https://basemap.nationalmap.gov:443/arcgis/services/USGSImageryTopo/MapServer/WmsServer";
assetConfig.cesiumOptions.layers = "0";
assetConfig.cesiumOptions.parameters = {
transparent: true,
format: "image/png",
};
this.set("cesiumOptions", assetConfig.cesiumOptions);
if (!assetConfig.moreInfoLink) {
this.set(
"moreInfoLink",
"https://basemap.nationalmap.gov/arcgis/rest/services/USGSImageryTopo/MapServer",
);
}
if (!assetConfig.attribution) {
this.set(
"attribution",
"USGS The National Map: Orthoimagery and US Topo. Data refreshed January, 2022.",
);
}
if (!assetConfig.description) {
this.set(
"description",
"USGS Imagery Topo is a tile cache base map of orthoimagery in The National Map and US Topo vectors visible to the 1:9,028 scale.",
);
}
if (!assetConfig.label) {
this.set("label", "USGS Imagery Topo");
}
} catch (error) {
console.log(
"There was an error initializing USGSImageryTopo in a CesiumImagery" +
". Error details: " +
error,
);
}
},
/**
* Creates a Cesium ImageryLayer that contains information about how the imagery
* should render in Cesium. See
* {@link https://cesium.com/learn/cesiumjs/ref-doc/ImageryLayer.html?classFilter=ImageryLay}
* @param {Boolean} recreate - Set recreate to true to force the function create
* the Cesium Model again. Otherwise, if a cesium model already exists, that is
* returned instead.
*/
createCesiumModel: function (recreate = false) {
var model = this;
const cesiumOptions = this.getCesiumOptions();
var type = this.get("type");
var providerFunction = Cesium[type];
// If the cesium model already exists, don't create it again unless specified
if (!recreate && this.get("cesiumModel")) {
console.log("returning existing cesium model");
return this.get("cesiumModel");
}
model.resetStatus();
var initialAppearance = {
alpha: this.get("opacity"),
show: this.get("visible"),
saturation: this.get("saturation"),
// TODO: brightness, contrast, gamma, etc.
};
if (type === "BingMapsImageryProvider") {
cesiumOptions.key =
cesiumOptions.key || MetacatUI.AppConfig.bingMapsKey;
} else if (type === "IonImageryProvider") {
cesiumOptions.assetId = Number(cesiumOptions.ionAssetId);
delete cesiumOptions.ionAssetId;
cesiumOptions.accessToken =
cesiumOptions.cesiumToken || MetacatUI.appModel.get("cesiumToken");
} else if (type === "OpenStreetMapImageryProvider") {
cesiumOptions.url =
cesiumOptions.url || "https://a.tile.openstreetmap.org/";
}
if (cesiumOptions && cesiumOptions.tilingScheme) {
const ts = cesiumOptions.tilingScheme;
const availableTS = [
"GeographicTilingScheme",
"WebMercatorTilingScheme",
];
if (availableTS.indexOf(ts) > -1) {
cesiumOptions.tilingScheme = new Cesium[ts]();
} else {
console.log(
`${ts} is not a valid tiling scheme. Using WebMercatorTilingScheme`,
);
cesiumOptions.tilingScheme = new Cesium.WebMercatorTilingScheme();
}
}
if (cesiumOptions.rectangle) {
cesiumOptions.rectangle = Cesium.Rectangle.fromDegrees(
...cesiumOptions.rectangle,
);
}
if (providerFunction && typeof providerFunction === "function") {
let provider = new providerFunction(cesiumOptions);
provider.readyPromise
.then(function () {
// Imagery must be converted from a Cesium Imagery Provider to a Cesium
// Imagery Layer. See
// https://cesium.com/learn/cesiumjs-learn/cesiumjs-imagery/#imagery-providers-vs-layers
model.set(
"cesiumModel",
new Cesium.ImageryLayer(provider, initialAppearance),
);
model.set("status", "ready");
model.setListeners();
})
.otherwise(function (error) {
// See https://cesium.com/learn/cesiumjs/ref-doc/RequestErrorEvent.html
let details = error;
// Write a helpful error message
switch (error.statusCode) {
case 404:
details = "The resource was not found (error code 404).";
break;
case 500:
details = "There was a server error (error code 500).";
break;
}
model.set("status", "error");
model.set("statusDetails", details);
});
} else {
model.set("status", "error");
model.set(
"statusDetails",
type + " is not a supported imagery type.",
);
}
},
/**
* Set listeners that update the cesium model when the backbone model is updated.
*/
setListeners: function () {
try {
var cesiumModel = this.get("cesiumModel");
// Make sure the listeners are only set once!
this.stopListening(this);
this.listenTo(this, "change:opacity", function (model, opacity) {
cesiumModel.alpha = opacity;
// Let the map and/or other parent views know that a change has been made
// that requires the map to be re-rendered
model.trigger("appearanceChanged");
});
this.listenTo(this, "change:visible", function (model, visible) {
cesiumModel.show = visible;
// Let the map and/or other parent views know that a change has been made
// that requires the map to be re-rendered
model.trigger("appearanceChanged");
});
} catch (error) {
console.log(
"There was an error setting listeners in a cesium Imagery model" +
". Error details: " +
error,
);
}
},
/**
* Gets a Cesium Bounding Sphere that can be used to navigate to view the full
* extent of the imagery. See
* {@link https://cesium.com/learn/cesiumjs/ref-doc/BoundingSphere.html}
* @returns {Promise} Returns a promise that resolves to a Cesium Bounding Sphere
* when ready
*/
getBoundingSphere: function () {
return this.whenReady()
.then(function (model) {
return model.get("cesiumModel").getViewableRectangle();
})
.then(function (rectangle) {
return Cesium.BoundingSphere.fromRectangle3D(rectangle);
});
},
/**
* Requests a tile from the imagery provider that is at the center of the layer's
* bounding box and at the minimum level. Once the image is fetched, sets its URL
* on the thumbnail property of this model. This function is first called when the
* layer initialized, but waits for the cesiumModel to be ready.
*/
getThumbnail: function () {
try {
if (this.get("status") !== "ready") {
this.listenToOnce(this, "change:status", this.getThumbnail);
return;
}
const model = this;
const cesImageryLayer = this.get("cesiumModel");
const provider = cesImageryLayer.imageryProvider;
const rect = cesImageryLayer.rectangle;
var x = (rect.east + rect.west) / 2;
var y = (rect.north + rect.south) / 2;
var level = provider.minimumLevel;
provider
.requestImage(x, y, level)
.then(function (response) {
let data = response.blob;
let objectURL = null;
if (!data && response instanceof ImageBitmap) {
objectURL = model.getDataUriFromBitmap(response);
} else {
objectURL = URL.createObjectURL(data);
}
model.set("thumbnail", objectURL);
})
.otherwise(function (e) {
console.log(
"Error requesting an image tile to use as a thumbnail for an " +
"Imagery Layer. Error message: " +
e,
);
});
} catch (error) {
console.log(
"There was an error getting a thumbnail for a CesiumImagery layer" +
". Error details: " +
error,
);
}
},
/**
* Gets a data URI from a bitmap image.
* @param {ImageBitmap} bitmap The bitmap image to convert to a data URI
* @returns {String} Returns a string containing the requested data URI.
*/
getDataUriFromBitmap: function (imageBitmap) {
try {
const canvas = document.createElement("canvas");
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
const ctx = canvas.getContext("2d");
// y-flip the image - Natural Earth II bitmaps appear upside down otherwise
// TODO: Test with other imagery layers
ctx.translate(0, imageBitmap.height);
ctx.scale(1, -1);
ctx.drawImage(imageBitmap, 0, 0);
return canvas.toDataURL();
} catch (error) {
console.log(
"There was an error converting an ImageBitmap to a data URL" +
". Error details: " +
error,
);
}
},
// /**
// * Parses the given input into a JSON object to be set on the model.
// *
// * @param {TODO} input - The raw response object
// * @return {TODO} - The JSON object of all the Imagery attributes
// */
// parse: function (input) {
// try {
// var modelJSON = {};
// return modelJSON
// }
// catch (error) {console.log('There was an error parsing a Imagery model' + '.
// Error details: ' + error
// );
// }
// },
// /**
// * Overrides the default Backbone.Model.validate.function() to check if this if
// * the values set on this model are valid.
// *
// * @param {Object} [attrs] - A literal object of model attributes to validate.
// * @param {Object} [options] - A literal object of options for this validation
// * process
// *
// * @return {Object} - Returns a literal object with the invalid attributes and
// * their corresponding error message, if there are any. If there are no errors,
// * returns nothing.
// */
// validate: function (attrs, options) {try {
// }
// catch (error) {console.log('There was an error validating a CesiumImagery
// model' + '. Error details: ' + error
// );
// }
// },
// /**
// * Creates a string using the values set on this model's attributes.
// * @return {string} The Imagery string
// */
// serialize: function () {try {var serializedImagery = "";
// return serializedImagery;
// }
// catch (error) {console.log('There was an error serializing a CesiumImagery
// model' + '. Error details: ' + error
// );
// }
// },
},
);
return CesiumImagery;
});