terriajs
Version:
Geospatial data visualization platform.
285 lines • 12 kB
JavaScript
import { uniq } from "lodash-es";
import { runInAction, toJS } from "mobx";
import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import URI from "urijs";
import hashEntity from "../../../../Core/hashEntity";
import isDefined from "../../../../Core/isDefined";
import TerriaError from "../../../../Core/TerriaError";
import ReferenceMixin from "../../../../ModelMixins/ReferenceMixin";
import CommonStrata from "../../../../Models/Definition/CommonStrata";
import { BaseModel } from "../../../../Models/Definition/Model";
import saveStratumToJson from "../../../../Models/Definition/saveStratumToJson";
import GlobeOrMap from "../../../../Models/GlobeOrMap";
import HasLocalData from "../../../../Models/HasLocalData";
import getDereferencedIfExists from "../../../../Core/getDereferencedIfExists";
import CatalogMemberMixin from "../../../../ModelMixins/CatalogMemberMixin";
import ViewerMode from "../../../../Models/ViewerMode";
/** User properties (generated from URL hash parameters) to add to share link URL in PRODUCTION environment.
* If in Dev, we add all user properties.
*/
const userPropsToShare = ["hideExplorerPanel", "activeTabId"];
export const SHARE_VERSION = "8.0.0";
/** Create base share link URL - with `hashParameters` applied on top.
* This will copy over some `userProperties` - see `userPropsToShare`
*/
function buildBaseShareUrl(terria, hashParams) {
const uri = new URI(document.baseURI).fragment("").search("");
const fragmentsToShare = new URL(document.URL).hash
.split(/[#&]/)
.filter((elem) => elem !== "" && !elem.includes("share=") && !elem.includes("start="));
fragmentsToShare.forEach((sub) => {
uri.addSearch(sub);
});
if (terria.developmentEnv) {
uri.addSearch(toJS(terria.userProperties));
}
else {
userPropsToShare.forEach((key) => uri.addSearch({ [key]: terria.userProperties.get(key) }));
}
uri.addSearch(hashParams);
return uri.fragment(uri.query()).query("").toString();
}
/**
* Builds a share link that reflects the state of the passed Terria instance.
*
* @param terria The terria instance to serialize.
* @param {ViewState} [viewState] The viewState to read whether we're viewing the catalog or not
* @param {Object} [options] Options for building the share link.
* @param {Boolean} [options.includeStories=true] True to include stories in the share link, false to exclude them.
* @returns {String} A URI that will rebuild the current state when viewed in a browser.
*/
export function buildShareLink(terria, viewState, options = { includeStories: true }) {
return buildBaseShareUrl(terria, {
start: JSON.stringify(getShareData(terria, viewState, options))
});
}
/**
* Like {@link buildShareLink}, but shortens the result using {@link Terria#urlShortener}.
*
* @returns {Promise<String>} A promise that will return the shortened url when complete.
*/
export async function buildShortShareLink(terria, viewState, options = { includeStories: true }) {
if (!isDefined(terria.shareDataService))
throw TerriaError.from("Could not generate share token - `shareDataService` is `undefined`");
const token = await terria.shareDataService?.getShareToken(getShareData(terria, viewState, options));
if (typeof token === "string") {
return buildBaseShareUrl(terria, {
share: token
});
}
throw TerriaError.from("Could not generate share token");
}
/**
* Returns just the JSON that defines the current view.
* @param {Terria} terria The Terria object.
* @param {ViewState} [viewState] Current viewState.
* @return {Object}
*/
export function getShareData(terria, viewState, options = { includeStories: true }) {
return runInAction(() => {
const { includeStories } = options;
const initSource = {};
const initSources = [initSource];
addStratum(terria, CommonStrata.user, initSource);
addWorkbench(terria, initSource);
addTimelineItems(terria, initSource);
addViewSettings(terria, viewState, initSource);
addFeaturePicking(terria, initSource);
if (includeStories) {
// info that are not needed in scene share data
addStories(terria, initSource);
}
return {
version: SHARE_VERSION,
initSources: initSources
};
});
}
/**
* Serialise all model data from a given stratum except feature highlight
* and serialise all ancestors of any models serialised
* @param {Terria} terria
* @param {CommonStrata} stratumId
* @param {Object} initSource
*/
function addStratum(terria, stratumId, initSource) {
initSource.stratum = stratumId;
initSource.models = {};
terria.modelValues.forEach((model) => {
if (model.uniqueId === GlobeOrMap.featureHighlightID)
return;
const force = terria.workbench.contains(model);
addModelStratum(terria, model, stratumId, force, initSource);
});
// Go through knownContainerUniqueIds and make sure they exist in models
Object.keys(initSource.models).forEach((modelId) => {
const model = terria.getModelById(BaseModel, modelId);
if (model)
model.completeKnownContainerUniqueIds.forEach((containerId) => {
if (!initSource.models?.[containerId]) {
const containerModel = terria.getModelById(BaseModel, containerId);
if (containerModel)
addModelStratum(terria, containerModel, stratumId, true, initSource);
}
});
});
}
function addWorkbench(terria, initSource) {
initSource.workbench = terria.workbench.itemIds.filter(isShareable(terria));
}
function addTimelineItems(terria, initSources) {
initSources.timeline = terria.timelineStack.itemIds.filter(isShareable(terria));
}
function addModelStratum(terria, model, stratumId, force, initSource) {
const models = initSource.models;
const id = model.uniqueId;
if (!id || !models || models?.[id] !== undefined) {
return;
}
const stratum = model.strata.get(stratumId);
const dereferenced = ReferenceMixin.isMixedInto(model)
? model.target
: undefined;
const dereferencedStratum = dereferenced
? dereferenced.strata.get(stratumId)
: undefined;
if (!force && stratum === undefined && dereferencedStratum === undefined) {
return;
}
if (!isShareable(terria)(id)) {
return;
}
models[id] = stratum ? saveStratumToJson(model.traits, stratum) : {};
if (dereferenced && dereferencedStratum) {
models[id].dereferenced = saveStratumToJson(dereferenced.traits, dereferencedStratum);
}
if (model.knownContainerUniqueIds &&
model.knownContainerUniqueIds.length > 0) {
models[id].knownContainerUniqueIds = model.knownContainerUniqueIds.slice();
}
const members = toJS(models[id].members);
if (Array.isArray(members)) {
models[id].members = uniq(models[id].members?.filter((member) => typeof member === "string" ? isShareable(terria)(member) : false));
}
models[id].type = model.type;
}
/**
* Returns a function which determines whether a modelId represents a model that can be shared
* @param {Object} terria The Terria object.
* @return {Function} The function which determines whether a modelId can be shared
*/
export function isShareable(terria) {
return function (modelId) {
const model = terria.getModelById(BaseModel, modelId);
if (CatalogMemberMixin.isMixedInto(model) && !model.shareable) {
return false;
}
// If this is a Reference, then use the model.target, otherwise use the model
const dereferenced = typeof model === "undefined"
? model
: getDereferencedIfExists(terria.getModelById(BaseModel, modelId));
if (CatalogMemberMixin.isMixedInto(dereferenced) &&
!dereferenced.shareable) {
return false;
}
return (model &&
((HasLocalData.is(dereferenced) && !dereferenced.hasLocalData) ||
!HasLocalData.is(dereferenced)));
};
}
/**
* Is it currently possible to generate short URLs?
* @param {Object} terria The Terria object.
* @return {Boolean}
*/
export function canShorten(terria) {
return terria.shareDataService && terria.shareDataService.isUsable;
}
/**
* Adds the details of the current view to the init sources.
* @private
*/
function addViewSettings(terria, viewState, initSource = {}) {
const viewer = terria.mainViewer;
let viewerMode;
if (terria.mainViewer.viewerMode === ViewerMode.Cesium) {
if (terria.mainViewer.viewerOptions.useTerrain) {
viewerMode = "3d";
}
else {
viewerMode = "3dSmooth";
}
}
else {
viewerMode = "2d";
}
initSource.initialCamera = terria.currentViewer
.getCurrentCameraView()
.toJson();
initSource.homeCamera = terria.mainViewer.homeCamera.toJson();
initSource.viewerMode = viewerMode;
initSource.showSplitter = terria.showSplitter;
initSource.splitPosition = terria.splitPosition;
initSource.settings = {
baseMaximumScreenSpaceError: terria.baseMaximumScreenSpaceError,
useNativeResolution: terria.useNativeResolution,
alwaysShowTimeline: terria.timelineStack.alwaysShowingTimeline,
baseMapId: viewer.baseMap?.uniqueId,
terrainSplitDirection: terria.terrainSplitDirection,
depthTestAgainstTerrainEnabled: terria.depthTestAgainstTerrainEnabled
};
if (isDefined(viewState)) {
const itemIdToUse = viewState.viewingUserData()
? isDefined(viewState.userDataPreviewedItem) &&
viewState.userDataPreviewedItem.uniqueId
: isDefined(viewState.previewedItem) && viewState.previewedItem.uniqueId;
// don't persist the not-visible-to-user previewed id in the case of sharing from outside the catalog
if (viewState.explorerPanelIsVisible && itemIdToUse) {
initSource.previewedItemId = itemIdToUse;
}
}
}
/**
* Add details of currently picked features.
* @private
*/
function addFeaturePicking(terria, initSource) {
if (isDefined(terria.pickedFeatures) &&
terria.pickedFeatures.features.length > 0 &&
terria.pickedFeatures.pickPosition) {
const positionInRadians = Ellipsoid.WGS84.cartesianToCartographic(terria.pickedFeatures.pickPosition);
const pickedFeatures = {
providerCoords: terria.pickedFeatures.providerCoords,
pickCoords: {
lat: CesiumMath.toDegrees(positionInRadians.latitude),
lng: CesiumMath.toDegrees(positionInRadians.longitude),
height: positionInRadians.height
}
};
if (isDefined(terria.selectedFeature)) {
// Sometimes features have stable ids and sometimes they're randomly generated every time, so include both
// id and name as a fallback.
pickedFeatures.current = {
name: terria.selectedFeature.name,
hash: hashEntity(terria.selectedFeature, terria)
};
}
// Remember the ids of vector features only, the raster ones we can reconstruct from providerCoords.
pickedFeatures.entities = terria.pickedFeatures.features
.filter((feature) => !isDefined(feature.imageryLayer?.imageryProvider))
.map((entity) => {
return {
name: entity.name,
hash: hashEntity(entity, terria)
};
});
initSource.pickedFeatures = pickedFeatures;
}
}
function addStories(terria, initSource) {
if (isDefined(terria.stories)) {
initSource.stories = terria.stories.slice();
}
}
//# sourceMappingURL=BuildShareLink.js.map