terriajs
Version:
Geospatial data visualization platform.
388 lines (337 loc) • 11.8 kB
text/typescript
;
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 {
InitSourceData,
InitSourcePickedFeatures,
ShareInitSourceData,
ViewModeJson
} from "../../../../Models/InitSource";
import Terria from "../../../../Models/Terria";
import ViewState from "../../../../ReactViewModels/ViewState";
import getDereferencedIfExists from "../../../../Core/getDereferencedIfExists";
/** 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: Terria,
hashParams: { [key: string]: string }
) {
const uri = new URI(document.baseURI).fragment("").search("");
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: Terria,
viewState?: 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: Terria,
viewState?: 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: Terria,
viewState?: ViewState,
options = { includeStories: true }
): ShareInitSourceData {
return runInAction(() => {
const { includeStories } = options;
const initSource: InitSourceData = {};
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: Terria,
stratumId: string,
initSource: InitSourceData
) {
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: Terria, initSource: InitSourceData) {
initSource.workbench = terria.workbench.itemIds.filter(isShareable(terria));
}
function addTimelineItems(terria: Terria, initSources: InitSourceData) {
initSources.timeline = terria.timelineStack.itemIds.filter(
isShareable(terria)
);
}
function addModelStratum(
terria: Terria,
model: BaseModel,
stratumId: string,
force: boolean,
initSource: InitSourceData
) {
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: Terria) {
return function (modelId: string) {
const model = terria.getModelById(BaseModel, modelId);
// 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)!);
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: Terria) {
return terria.shareDataService && terria.shareDataService.isUsable;
}
/**
* Adds the details of the current view to the init sources.
* @private
*/
function addViewSettings(
terria: Terria,
viewState?: ViewState,
initSource: InitSourceData = {}
) {
const viewer = terria.mainViewer;
// const time = {
// dayNumber: terria.timelineClock.currentTime.dayNumber,
// secondsOfDay: terria.timelineClock.currentTime.secondsOfDay
// };
let viewerMode: ViewModeJson;
if (terria.mainViewer.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: Terria, initSource: InitSourceData) {
if (
isDefined(terria.pickedFeatures) &&
terria.pickedFeatures.features.length > 0 &&
terria.pickedFeatures.pickPosition
) {
const positionInRadians = Ellipsoid.WGS84.cartesianToCartographic(
terria.pickedFeatures.pickPosition
);
const pickedFeatures: InitSourcePickedFeatures = {
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: Terria, initSource: InitSourceData) {
if (isDefined(terria.stories)) {
initSource.stories = terria.stories.slice();
}
}