import { action, runInAction, toJS, when } from "mobx";
import { http, HttpResponse } from "msw";
import buildModuleUrl from "terriajs-cesium/Source/Core/buildModuleUrl";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import RequestScheduler from "terriajs-cesium/Source/Core/RequestScheduler";
import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource";
import Entity from "terriajs-cesium/Source/DataSources/Entity";
import SplitDirection from "terriajs-cesium/Source/Scene/SplitDirection";
import hashEntity from "../../lib/Core/hashEntity";
import Result from "../../lib/Core/Result";
import TerriaError from "../../lib/Core/TerriaError";
import PickedFeatures from "../../lib/Map/PickedFeatures/PickedFeatures";
import CameraView from "../../lib/Models/CameraView";
import CsvCatalogItem from "../../lib/Models/Catalog/CatalogItems/CsvCatalogItem";
import MagdaReference from "../../lib/Models/Catalog/CatalogReferences/MagdaReference";
import UrlReference, {
UrlToCatalogMemberMapping
} from "../../lib/Models/Catalog/CatalogReferences/UrlReference";
import ArcGisFeatureServerCatalogItem from "../../lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem";
import ArcGisMapServerCatalogItem from "../../lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem";
import WebMapServiceCatalogGroup from "../../lib/Models/Catalog/Ows/WebMapServiceCatalogGroup";
import WebMapServiceCatalogItem from "../../lib/Models/Catalog/Ows/WebMapServiceCatalogItem";
import CommonStrata from "../../lib/Models/Definition/CommonStrata";
import { BaseModel } from "../../lib/Models/Definition/Model";
import TerriaFeature from "../../lib/Models/Feature/Feature";
import {
isInitFromData,
isInitFromDataPromise,
isInitFromOptions,
isInitFromUrl
} from "../../lib/Models/InitSource";
import Terria from "../../lib/Models/Terria";
import ViewerMode from "../../lib/Models/ViewerMode";
import ViewState from "../../lib/ReactViewModels/ViewState";
import { buildShareLink } from "../../lib/ReactViews/Map/Panels/SharePanel/BuildShareLink";
import SimpleCatalogItem from "../Helpers/SimpleCatalogItem";
import { defaultBaseMaps } from "../../lib/Models/BaseMaps/defaultBaseMaps";
import { worker } from "../mocks/browser";
import mapConfigBasicJson from "../../wwwroot/test/Magda/map-config-basic.json";
import mapConfigV7Json from "../../wwwroot/test/Magda/map-config-v7.json";
import mapConfigInlineInitJson from "../../wwwroot/test/Magda/map-config-inline-init.json";
import mapConfigDereferencedJson from "../../wwwroot/test/Magda/map-config-dereferenced.json";
import mapConfigDereferencedNewJson from "../../wwwroot/test/Magda/map-config-dereferenced-new.json";
import magdaRecord1 from "../../wwwroot/test/Magda/shareKeys/6b24aa39-1aa7-48d1-b6a6-9e755aff4476.json";
import magdaRecord2 from "../../wwwroot/test/Magda/shareKeys/bfc69476-1c85-4208-9046-4f736bab9b8e.json";
import magdaRecord3 from "../../wwwroot/test/Magda/shareKeys/12f26f07-f39e-4753-979d-2de01af54bd1.json";
import mapConfigOld from "../../wwwroot/test/Magda/shareKeys/map-config-example-old.json";
import mapConfigNew from "../../wwwroot/test/Magda/shareKeys/map-config-example-new.json";
import configProxy from "../../wwwroot/test/init/configProxy.json";
import serverConfig from "../../wwwroot/test/init/serverconfig.json";
import mapServerSimpleGroupJson from "../../wwwroot/test/Terria/applyInitData/MapServer/mapServerSimpleGroup.json";
import mapServerWithErrorJson from "../../wwwroot/test/Terria/applyInitData/MapServer/mapServerWithError.json";
import magdaGroupRecordJson from "../../wwwroot/test/Terria/applyInitData/MagdaReference/group_record.json";
import magdaWmsRecordJson from "../../wwwroot/test/Terria/applyInitData/MagdaReference/wms_record.json";
import esriFeatureServerJson from "../../wwwroot/test/Terria/applyInitData/FeatureServer/esri_feature_server.json";
import wmsCapabilitiesXml from "../../wwwroot/test/Terria/applyInitData/WmsServer/capabilities.xml";
import storyJson from "../../wwwroot/test/stories/TerriaJS App/my-story.json";
// i18nOptions for CI
const i18nOptions = {
// Skip calling i18next.init in specs
skipInit: true
};
describe("TerriaSpec", function () {
let terria: Terria;
beforeEach(function () {
terria = new Terria({
appBaseHref: "/",
baseUrl: "./"
});
worker.use(
http.get("*/serverconfig/*", () => HttpResponse.json({})),
http.get("*/test-config.json", () => HttpResponse.json({}))
);
});
describe("cesiumBaseUrl", function () {
it("is set when passed as an option when constructing Terria", function () {
terria = new Terria({
appBaseHref: "/",
baseUrl: "./",
cesiumBaseUrl: "some/path/to/cesium"
});
const path = new URL(terria.cesiumBaseUrl).pathname;
expect(path).toBe("/some/path/to/cesium/");
});
it("should default to a path relative to `baseUrl`", function () {
terria = new Terria({
appBaseHref: "/",
baseUrl: "some/path/to/terria"
});
const path = new URL(terria.cesiumBaseUrl).pathname;
expect(path).toBe("/some/path/to/terria/build/Cesium/build/");
});
it("should update the baseUrl setting in the cesium module", function () {
expect(
buildModuleUrl("Assets/some/image.png").endsWith(
"/build/Cesium/build/Assets/some/image.png"
)
).toBe(true);
terria = new Terria({
appBaseHref: "/",
baseUrl: "some/path/to/terria"
});
expect(
buildModuleUrl("Assets/some/image.png").endsWith(
"/some/path/to/terria/build/Cesium/build/Assets/some/image.png"
)
).toBe(true);
});
});
describe("terria refresh catalog members from magda", function () {
it("refreshes group aspect with given URL", async function () {
worker.use(http.get("*/serverconfig/*", () => HttpResponse.json({})));
function verifyGroups(groupAspect: any, groupNum: number) {
const ids = groupAspect.members.map((member: any) => member.id);
expect(terria.catalog.group.uniqueId).toEqual("/");
// ensure user added data co-exists with dereferenced magda members
expect(terria.catalog.group.members.length).toEqual(groupNum);
expect(terria.catalog.userAddedDataGroup).toBeDefined();
ids.forEach((id: string) => {
const model = terria.getModelById(MagdaReference, id);
if (!model) {
throw new Error(`no record id. ID = ${id}`);
}
expect(terria.modelIds).toContain(id);
expect(model.recordId).toEqual(id);
});
}
await terria.start({
configUrl: "test/Magda/map-config-dereferenced.json",
i18nOptions
});
verifyGroups(mapConfigDereferencedJson.aspects["group"], 3);
await terria.refreshCatalogMembersFromMagda(
"test/Magda/map-config-dereferenced-new.json"
);
verifyGroups(mapConfigDereferencedNewJson.aspects["group"], 2);
});
});
describe("terria start", function () {
beforeEach(function () {
worker.use(
http.get("*/serverconfig/*", () => HttpResponse.json({ foo: "bar" })),
http.get("*/proxyabledomains/*", () =>
HttpResponse.json({ foo: "bar" })
),
// from `terria.start()`
http.get("*/test/Magda/map-config-basic.json", () =>
HttpResponse.json(mapConfigBasicJson)
),
http.get("*/test/Magda/map-config-v7.json", () =>
HttpResponse.json(mapConfigV7Json)
),
// terria's "Magda derived url"
http.get("*/api/v0/registry/records/map-config-basic*", () =>
HttpResponse.json(mapConfigBasicJson)
),
// inline init
http.get("*map-config-inline-init*", () =>
HttpResponse.json(mapConfigInlineInitJson)
),
http.get("*map-config-dereferenced-new*", () =>
HttpResponse.json(mapConfigDereferencedNewJson)
),
http.get("*map-config-dereferenced*", () =>
HttpResponse.json(mapConfigDereferencedJson)
)
);
});
it("applies initSources in correct order", async function () {
expect(terria.initSources.length).toEqual(0);
worker.use(
http.get("*/config.json", () =>
HttpResponse.json({
initializationUrls: ["something"]
})
),
http.get("*/init/something.json", () =>
HttpResponse.json({
workbench: ["test"],
catalog: [
{ id: "test", type: "czml", url: "test.czml" },
{ id: "test-2", type: "czml", url: "test-2.czml" }
],
showSplitter: false,
splitPosition: 0.5
})
),
http.get("https://application.url/init/hash-init.json", () =>
HttpResponse.json({
// Override workbench in "init/something.json"
workbench: ["test-2"],
showSplitter: true
})
),
// This model is added to the workbench in "init/something.json" - which is loaded before "https://application.url/init/hash-init.json"
// So we add a long delay to make sure that `workbench` is overridden by `hash-init.json`
http.get("*/test.czml", async () => {
return HttpResponse.json([{ id: "document", version: "1.0" }]);
}),
// Note: no delay for "test-2.czml" - which is added to `workbench` by `hash-init.json
http.get("*/test-2.czml", () =>
HttpResponse.json([{ id: "document", version: "1.0" }])
)
);
await terria.start({
configUrl: `config.json`,
i18nOptions
});
await terria.updateApplicationUrl("https://application.url/#hash-init");
expect(terria.initSources.length).toEqual(2);
expect(terria.showSplitter).toBe(true);
expect(terria.splitPosition).toBe(0.5);
expect(terria.workbench.items.length).toBe(1);
expect(terria.workbench.items[0].uniqueId).toBe("test-2");
});
it("works with initializationUrls and initFragmentPaths", async function () {
expect(terria.initSources.length).toEqual(0);
worker.use(
http.get("*/path/to/config/configUrl.json", () =>
HttpResponse.json({
initializationUrls: ["something"],
parameters: {
applicationUrl: "https://application.url/",
initFragmentPaths: [
"path/to/init/",
"https://hostname.com/some/other/path/"
]
}
})
),
http.get("*/init/something.json", () =>
HttpResponse.json({
catalog: []
})
),
http.get("https://hostname.com/*", () => HttpResponse.json({}))
);
await terria.start({
configUrl: `path/to/config/configUrl.json`,
i18nOptions
});
expect(terria.initSources.length).toEqual(1);
const initSource = terria.initSources[0];
expect(isInitFromOptions(initSource)).toBeTruthy();
if (!isInitFromOptions(initSource))
throw "Init source is not from options";
// Note: initFragmentPaths in `initializationUrls` are resolved to the base URL of configURL
// - which is path/to/config/
expect(
initSource.options.map((source) =>
isInitFromUrl(source) ? source.initUrl : ""
)
).toEqual([
"path/to/config/path/to/init/something.json",
"https://hostname.com/some/other/path/something.json"
]);
});
describe("via loadMagdaConfig", function () {
it("should dereference uniqueId to `/`", async function () {
expect(terria.catalog.group.uniqueId).toEqual("/");
worker.use(
http.get("*/api/v0/registry/*", () =>
HttpResponse.json(mapConfigBasicJson)
)
);
// no init sources before starting
expect(terria.initSources.length).toEqual(0);
await terria.start({
configUrl: "test/Magda/map-config-basic.json",
i18nOptions
});
expect(terria.catalog.group.uniqueId).toEqual("/");
});
it("works with basic initializationUrls", async function () {
worker.use(
http.get("*/api/v0/registry/*", () =>
HttpResponse.json(mapConfigBasicJson)
)
);
// no init sources before starting
expect(terria.initSources.length).toEqual(0);
await terria.start({
configUrl: "test/Magda/map-config-basic.json",
i18nOptions
});
expect(terria.initSources.length).toEqual(1);
expect(isInitFromUrl(terria.initSources[0])).toEqual(true);
if (isInitFromUrl(terria.initSources[0])) {
expect(terria.initSources[0].initUrl).toEqual(
mapConfigBasicJson.aspects["terria-config"].initializationUrls[0]
);
} else {
throw "not init source";
}
});
it("works with v7initializationUrls", async function () {
worker.use(
http.get("*/api/v0/registry/*", () =>
HttpResponse.json(mapConfigBasicJson)
)
);
const groupName = "Simple converter test";
worker.use(
http.get("https://example.foo.bar/initv7.json", () =>
HttpResponse.json({
catalog: [{ name: groupName, type: "group", items: [] }]
})
)
);
// no init sources before starting
expect(terria.initSources.length).toBe(0);
await terria.start({
configUrl: "test/Magda/map-config-v7.json",
i18nOptions
});
expect(terria.initSources.length).toBe(1);
expect(isInitFromDataPromise(terria.initSources[0])).toBeTruthy(
"Expected initSources[0] to be an InitDataPromise"
);
if (isInitFromDataPromise(terria.initSources[0])) {
const data = await terria.initSources[0].data;
// JSON parse & stringify to avoid a problem where I think catalog-converter
// can return {"id": undefined} instead of no "id"
expect(
JSON.parse(JSON.stringify(data.ignoreError()?.data.catalog))
).toEqual([
{
name: groupName,
type: "group",
members: [],
shareKeys: [`Root Group/${groupName}`]
}
]);
}
});
it("works with inline init", async function () {
// inline init
worker.use(
http.get("*/api/v0/registry/*", () =>
HttpResponse.json(mapConfigInlineInitJson)
)
);
// no init sources before starting
expect(terria.initSources.length).toEqual(0);
await terria.start({
configUrl: "test/Magda/map-config-inline-init.json",
i18nOptions
});
const inlineInit = mapConfigInlineInitJson.aspects["terria-init"];
/** Check cors domains */
expect(terria.corsProxy.corsDomains).toEqual(inlineInit.corsDomains);
/** Camera setting */
expect(terria.mainViewer.homeCamera).toEqual(
CameraView.fromJson(inlineInit.homeCamera)
);
/** Ensure inlined data catalog from init sources */
expect(terria.initSources.length).toEqual(1);
if (isInitFromData(terria.initSources[0])) {
expect(terria.initSources[0].data.catalog).toEqual(
inlineInit.catalog
);
} else {
throw "not init source";
}
});
it("parses dereferenced group aspect", async function () {
expect(terria.catalog.group.uniqueId).toEqual("/");
// dereferenced res
worker.use(
http.get("*/api/v0/registry/*", () =>
HttpResponse.json(mapConfigDereferencedJson)
)
);
await terria.start({
configUrl: "test/Magda/map-config-dereferenced.json",
i18nOptions
});
const groupAspect = mapConfigDereferencedJson.aspects["group"];
const ids = groupAspect.members.map((member: any) => member.id);
expect(terria.catalog.group.uniqueId).toEqual("/");
// ensure user added data co-exists with dereferenced magda members
expect(terria.catalog.group.members.length).toEqual(3);
expect(terria.catalog.userAddedDataGroup).toBeDefined();
ids.forEach((id: string) => {
const model = terria.getModelById(MagdaReference, id);
if (!model) {
throw "no record id.";
}
expect(terria.modelIds).toContain(id);
expect(model.recordId).toEqual(id);
});
});
});
it("calls `beforeRestoreAppState` before restoring app state from share data", async function () {
terria = new Terria({
appBaseHref: "/",
baseUrl: "./"
});
const restoreAppState = spyOn(
terria,
"restoreAppState" as any
).and.callThrough();
const beforeRestoreAppState = jasmine
.createSpy("beforeRestoreAppState")
// It should also handle errors when calling beforeRestoreAppState
.and.callFake(() => Promise.reject("some error"));
expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Cesium);
await terria.start({
configUrl: "test-config.json",
applicationUrl: {
href: "http://test.com/#map=2d"
} as Location,
beforeRestoreAppState
});
expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Leaflet);
expect(beforeRestoreAppState).toHaveBeenCalledBefore(restoreAppState);
});
});
describe("updateApplicationUrl", function () {
it("works with initializationUrls and initFragmentPaths", async function () {
expect(terria.initSources.length).toEqual(0);
worker.use(
http.get("*/path/to/config/configUrl.json", () =>
HttpResponse.json({
initializationUrls: ["something"],
parameters: {
applicationUrl: "https://application.url/",
initFragmentPaths: [
"path/to/init/",
"https://hostname.com/some/other/path/"
]
}
})
),
http.get("*/init/something.json", () =>
HttpResponse.json({
catalog: []
})
),
http.get("https://application.url/*", () => HttpResponse.json({})),
http.get("https://hostname.com/*", () => HttpResponse.json({}))
);
await terria.start({
configUrl: `path/to/config/configUrl.json`,
i18nOptions
});
await terria.updateApplicationUrl(
"https://application.url/#someInitHash"
);
expect(terria.initSources.length).toEqual(2);
const initSource = terria.initSources[1];
expect(isInitFromOptions(initSource)).toBeTruthy();
if (!isInitFromOptions(initSource))
throw "Init source is not from options";
// Note: initFragmentPaths in hash parameters are resolved to the base URL of application URL
// - which is https://application.url/
expect(
initSource.options.map((source) =>
isInitFromUrl(source) ? source.initUrl : ""
)
).toEqual([
"https://application.url/path/to/init/someInitHash.json",
"https://hostname.com/some/other/path/someInitHash.json"
]);
});
it("processes #start correctly", async function () {
expect(terria.initSources.length).toEqual(0);
worker.use(
http.get("*/configUrl.json", () => HttpResponse.json({})),
http.get("http://something/*", () => HttpResponse.json({}))
);
await terria.start({
configUrl: `configUrl.json`,
i18nOptions
});
// Test #start with two init sources
// - one initURL = "http://something/init.json"
// - one initData which sets `splitPosition`
await terria.updateApplicationUrl(
"https://application.url/#start=" +
JSON.stringify({
version: "8.0.0",
initSources: ["http://something/init.json", { splitPosition: 0.3 }]
})
);
expect(terria.initSources.length).toEqual(2);
const urlInitSource = terria.initSources[0];
expect(isInitFromUrl(urlInitSource)).toBeTruthy();
if (!isInitFromUrl(urlInitSource)) throw "Init source is not from url";
expect(urlInitSource.initUrl).toBe("http://something/init.json");
const jsonInitSource = terria.initSources[1];
expect(isInitFromData(jsonInitSource)).toBeTruthy();
if (!isInitFromData(jsonInitSource)) throw "Init source is not from data";
expect(jsonInitSource.data.splitPosition).toBe(0.3);
});
describe("test via serialise & load round-trip", function () {
let newTerria: Terria;
let viewState: ViewState;
beforeEach(function () {
newTerria = new Terria({ appBaseHref: "/", baseUrl: "./" });
viewState = new ViewState({
terria: terria,
catalogSearchProvider: undefined
});
UrlToCatalogMemberMapping.register(
(_s) => true,
WebMapServiceCatalogItem.type,
true
);
terria.catalog.userAddedDataGroup.addMembersFromJson(
CommonStrata.user,
[
{
id: "itemABC",
name: "abc",
type: "wms",
url: "test/WMS/single_metadata_url.xml"
},
{
id: "groupABC",
name: "xyz",
type: "wms-group",
url: "test/WMS/single_metadata_url.xml"
}
]
);
terria.catalog.group.addMembersFromJson(CommonStrata.user, [
{
id: "itemDEF",
name: "def",
type: "wms",
url: "test/WMS/single_metadata_url.xml"
}
]);
});
it("initializes user added data group with shared items", async function () {
expect(newTerria.catalog.userAddedDataGroup.members).not.toContain(
"itemABC"
);
expect(newTerria.catalog.userAddedDataGroup.members).not.toContain(
"groupABC"
);
const shareLink = buildShareLink(terria, viewState);
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
expect(newTerria.catalog.userAddedDataGroup.members).toContain(
"itemABC"
);
expect(newTerria.catalog.userAddedDataGroup.members).toContain(
"groupABC"
);
});
it("initializes user added data group with shared UrlReference items", async function () {
terria.catalog.userAddedDataGroup.addMembersFromJson(
CommonStrata.user,
[
{
id: "url_test",
name: "foo",
type: "url-reference",
url: "test/WMS/single_metadata_url.xml"
}
]
);
const shareLink = buildShareLink(terria, viewState);
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
expect(newTerria.catalog.userAddedDataGroup.members).toContain(
"url_test"
);
const urlRef = newTerria.getModelById(BaseModel, "url_test");
expect(urlRef).toBeDefined();
expect(urlRef instanceof UrlReference).toBe(true);
if (urlRef instanceof UrlReference) {
await urlRef.loadReference();
expect(urlRef.target).toBeDefined();
}
});
it("initializes workbench with shared workbench items", async function () {
const model1 = terria.getModelById(
BaseModel,
"itemABC"
) as WebMapServiceCatalogItem;
const model2 = terria.getModelById(
BaseModel,
"itemDEF"
) as WebMapServiceCatalogItem;
await terria.workbench.add(model1);
await terria.workbench.add(model2);
expect(terria.workbench.itemIds).toContain("itemABC");
expect(terria.workbench.itemIds).toContain("itemDEF");
expect(newTerria.workbench.itemIds).toEqual([]);
const shareLink = buildShareLink(terria, viewState);
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
expect(newTerria.workbench.itemIds).toEqual(terria.workbench.itemIds);
});
it("initializes splitter correctly", async function () {
const model1 = terria.getModelById(
BaseModel,
"itemABC"
) as WebMapServiceCatalogItem;
await terria.workbench.add(model1);
runInAction(() => {
terria.showSplitter = true;
terria.splitPosition = 0.7;
model1.setTrait(
CommonStrata.user,
"splitDirection",
SplitDirection.RIGHT
);
});
const shareLink = buildShareLink(terria, viewState);
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
expect(newTerria.showSplitter).toEqual(true);
expect(newTerria.splitPosition).toEqual(0.7);
expect(newTerria.workbench.itemIds).toEqual(["itemABC"]);
const newModel1 = newTerria.getModelById(
BaseModel,
"itemABC"
) as WebMapServiceCatalogItem;
expect(newModel1).toBeDefined();
expect(newModel1.splitDirection).toEqual(SplitDirection.RIGHT as any);
});
it("opens and loads members of shared open groups", async function () {
const group = terria.getModelById(
BaseModel,
"groupABC"
) as WebMapServiceCatalogGroup;
await viewState.viewCatalogMember(group);
expect(group.isOpen).toBe(true);
expect(group.members.length).toBeGreaterThan(0);
const shareLink = buildShareLink(terria, viewState);
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
const newGroup = newTerria.getModelById(
BaseModel,
"groupABC"
) as WebMapServiceCatalogGroup;
expect(newGroup.isOpen).toBe(true);
expect(newGroup.members).toEqual(group.members);
});
});
describe("using story route", function () {
beforeEach(function () {
// These specs must run with a Terria constructed with "appBaseHref": "/"
// to make the specs work with browser runner
terria.updateParameters({
storyRouteUrlPrefix: "test/stories/TerriaJS%20App/"
});
worker.use(
http.get("*/test/stories/TerriaJS%20App/my-story", () =>
HttpResponse.json(storyJson)
)
);
});
it("sets playStory to 1", async function () {
await terria.updateApplicationUrl(
new URL("story/my-story", document.baseURI).toString()
);
expect(terria.userProperties.get("playStory")).toBe("1");
});
it("correctly adds the story share as a datasource", async function () {
await terria.updateApplicationUrl(
new URL("story/my-story", document.baseURI).toString()
);
expect(terria.initSources.length).toBe(1);
expect(terria.initSources[0].name).toMatch(/my-story/);
if (!isInitFromData(terria.initSources[0]))
throw new Error("Expected initSource to be InitData from my-story");
expect(toJS(terria.initSources[0].data)).toEqual(
(storyJson as any).initSources[0]
);
});
it("correctly adds the story share as a datasource when there's a trailing slash on story url", async function () {
await terria.updateApplicationUrl(
new URL("story/my-story/", document.baseURI).toString()
);
expect(terria.initSources.length).toBe(1);
expect(terria.initSources[0].name).toMatch(/my-story/);
if (!isInitFromData(terria.initSources[0]))
throw new Error("Expected initSource to be InitData from my-story");
expect(toJS(terria.initSources[0].data)).toEqual(
(storyJson as any).initSources[0]
);
});
});
});
// Test share keys by serialising from one catalog and deserialising with a reorganised catalog
describe("shareKeys", function () {
describe("with a JSON catalog", function () {
let newTerria: Terria;
let viewState: ViewState;
beforeEach(async function () {
// Create a config.json in a URL to pass to Terria.start
const configUrl = `data:application/json;base64,${btoa(
JSON.stringify({
initializationUrls: [],
parameters: {
regionMappingDefinitionsUrls: ["data/regionMapping.json"]
}
})
)}`;
newTerria = new Terria({ baseUrl: "./" });
viewState = new ViewState({
terria: terria,
catalogSearchProvider: undefined
});
await Promise.all(
[terria, newTerria].map((t) => t.start({ configUrl, i18nOptions }))
);
terria.catalog.group.addMembersFromJson(CommonStrata.definition, [
{
name: "Old group",
type: "group",
members: [
{
name: "Random CSV",
type: "csv",
url: "data:text/csv,lon%2Clat%2Cval%2Cdate%0A151%2C-31%2C15%2C2010%0A151%2C-31%2C15%2C2011"
}
]
}
]);
newTerria.catalog.group.addMembersFromJson(CommonStrata.definition, [
{
name: "New group",
type: "group",
members: [
{
name: "Extra group",
type: "group",
members: [
{
name: "My random CSV",
type: "csv",
url: "data:text/csv,lon%2Clat%2Cval%2Cdate%0A151%2C-31%2C15%2C2010%0A151%2C-31%2C15%2C2011",
shareKeys: ["//Old group/Random CSV"]
}
]
}
]
}
]);
});
it("correctly applies user stratum changes to moved item", async function () {
const csv = terria.getModelById(
CsvCatalogItem,
"//Old group/Random CSV"
);
expect(csv).toBeDefined("Can't find csv item in source terria");
csv?.setTrait(CommonStrata.user, "opacity", 0.5);
const shareLink = buildShareLink(terria, viewState);
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
const newCsv = newTerria.getModelById(
CsvCatalogItem,
"//New group/Extra group/My random CSV"
);
expect(newCsv).toBeDefined(
"Can't find newCsv item in destination newTerria"
);
expect(newCsv?.opacity).toBe(0.5);
});
it("correctly adds moved item to workbench and timeline", async function () {
const csv = terria.getModelById(
CsvCatalogItem,
"//Old group/Random CSV"
);
expect(csv).toBeDefined("csv not found in source terria");
if (csv === undefined) return;
await terria.workbench.add(csv);
terria.timelineStack.addToTop(csv);
const shareLink = buildShareLink(terria, viewState);
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
const newCsv = newTerria.getModelById(
CsvCatalogItem,
"//New group/Extra group/My random CSV"
);
expect(newCsv).toBeDefined("newCsv not found in destination newTerria");
if (newCsv === undefined) return;
expect(newTerria.workbench.contains(newCsv)).toBeTruthy(
"newCsv not found in destination newTerria workbench"
);
expect(newTerria.timelineStack.contains(newCsv)).toBeTruthy(
"newCsv not found in destination newTerria timeline"
);
});
});
describe("with a Magda catalog", function () {
// Simulate same as above but with Magda catalogs
// This is really messy before a proper MagdaCatalogProvider is made
// that can call a (currently not yet written) Magda API to find the location of
// any id within a catalog
// Could at least simulate moving an item deeper (similar to JSON catalog) and try having
// one of the knownContainerIds be shareKey linked to the new location?
// (hopefully that would trigger loading of the new group)
let newTerria: Terria;
let viewState: ViewState;
beforeEach(async function () {
// Create a config.json in a URL to pass to Terria.start
const configUrl =
"https://magda.example.com/api/v0/registry/records/map-config-example?optionalAspect=terria-config&optionalAspect=terria-init&optionalAspect=group&dereference=true";
viewState = new ViewState({
terria: terria,
catalogSearchProvider: undefined
});
newTerria = new Terria({ baseUrl: "./" });
// Simulate an update to catalog/config between terria and newTerria
// Track how many times configUrl has been requested to serve different responses
let configRequestCount = 0;
worker.use(
http.get("*/serverconfig/*", () => HttpResponse.json({})),
http.get(
"https://magda.example.com/api/v0/registry/records/6b24aa39-1aa7-48d1-b6a6-9e755aff4476",
() => HttpResponse.json(magdaRecord1)
),
http.get(
"https://magda.example.com/api/v0/registry/records/bfc69476-1c85-4208-9046-4f736bab9b8e",
() => HttpResponse.json(magdaRecord2)
),
http.get(
"https://magda.example.com/api/v0/registry/records/12f26f07-f39e-4753-979d-2de01af54bd1",
() => HttpResponse.json(magdaRecord3)
),
http.get(
"https://magda.example.com/api/v0/registry/records/map-config-example",
() => {
configRequestCount++;
if (configRequestCount === 1) {
return HttpResponse.json(mapConfigOld);
} else if (configRequestCount === 2) {
return HttpResponse.json(mapConfigNew);
}
// Don't allow more requests to configUrl once Terrias are set up
return HttpResponse.error();
}
)
);
await terria.start({
configUrl,
i18nOptions
});
await newTerria.start({
configUrl,
i18nOptions
});
});
it("correctly applies user stratum changes to moved item", async function () {
const oldGroupRef = terria.getModelById(
MagdaReference,
"6b24aa39-1aa7-48d1-b6a6-9e755aff4476"
);
expect(oldGroupRef).toBeDefined(
"Can't find Old group reference in source terria"
);
if (oldGroupRef === undefined) return;
await oldGroupRef.loadReference();
expect(oldGroupRef.target).toBeDefined(
"Can't dereference Old group in source terria"
);
const csv = terria.getModelById(
CsvCatalogItem,
"3432284e-a111-4844-97c8-26a1767f9986"
);
expect(csv).toBeDefined("Can't dereference csv in source terria");
if (csv === undefined) return;
csv.setTrait(CommonStrata.user, "opacity", 0.5);
const shareLink = buildShareLink(terria, viewState);
// Hack to make below test succeed. This needs to be there until we add a magda API that can locate any
// item by ID or share key within a Terria catalog
// Loads "New group" (bfc69476-1c85-4208-9046-4f736bab9b8e) which registers shareKeys for
// "Extra group" (12f26f07-f39e-4753-979d-2de01af54bd1). And "Extra group" has a share key
// that matches the ancestor of the serialised Random CSV, so loading is triggered on "Extra group"
// followed by 3432284e-a111-4844-97c8-26a1767f9986 which points to "My random CSV"
// (decfc787-0425-4175-a98c-a40db064feb3)
const newGroupRef = newTerria.getModelById(
MagdaReference,
"bfc69476-1c85-4208-9046-4f736bab9b8e"
);
if (newGroupRef === undefined) return;
await newGroupRef.loadReference();
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
// Why does this return a CSV item (when above hack isn't added)? It returns a brand new csv item without data or URL
// Does serialisation save enough attributes that upsertModelFromJson thinks it can create a new model?
// upsertModelFromJson should really be replaced with update + insert functions
// But is it always easy to work out when share data should use update and when it should insert?
// E.g. user added models should be inserted when deserialised, not updated
const newCsv = newTerria.getModelByIdOrShareKey(
CsvCatalogItem,
"3432284e-a111-4844-97c8-26a1767f9986"
);
expect(newCsv).toBeDefined(
"Can't find newCsv item in destination newTerria"
);
expect(newCsv?.uniqueId).toBe(
"decfc787-0425-4175-a98c-a40db064feb3",
"Failed to map share key to correct model"
);
expect(newCsv?.opacity).toBe(0.5);
});
it("correctly adds moved item to workbench and timeline", async function () {
const oldGroupRef = terria.getModelById(
MagdaReference,
"6b24aa39-1aa7-48d1-b6a6-9e755aff4476"
);
expect(oldGroupRef).toBeDefined(
"Can't find Old group reference in source terria"
);
if (oldGroupRef === undefined) return;
await oldGroupRef.loadReference();
expect(oldGroupRef.target).toBeDefined(
"Can't dereference Old group in source terria"
);
const csv = terria.getModelById(
CsvCatalogItem,
"3432284e-a111-4844-97c8-26a1767f9986"
);
expect(csv).toBeDefined("Can't dereference csv in source terria");
if (csv === undefined) return;
await terria.workbench.add(csv);
terria.timelineStack.addToTop(csv);
const shareLink = buildShareLink(terria, viewState);
// Hack to make below test succeed. Needs to be there until we add a magda API that can locate any
// item by ID or share key within a Terria catalog
// Loads "New group" (bfc69476-1c85-4208-9046-4f736bab9b8e) which registers shareKeys for
// "Extra group" (12f26f07-f39e-4753-979d-2de01af54bd1). And "Extra group" has a share key
// that matches the ancestor of the serialised Random CSV, so loading is triggered on "Extra group"
// followed by 3432284e-a111-4844-97c8-26a1767f9986 which points to "My random CSV"
// (decfc787-0425-4175-a98c-a40db064feb3)
const newGroupRef = newTerria.getModelById(
MagdaReference,
"bfc69476-1c85-4208-9046-4f736bab9b8e"
);
if (newGroupRef === undefined) return;
await newGroupRef.loadReference();
await newTerria.updateApplicationUrl(shareLink);
await newTerria.loadInitSources();
// Why does this return a CSV item (when above hack isn't added)? It returns a brand new csv item without data or URL
// Does serialisation save enough attributes that upsertModelFromJson thinks it can create a new model?
// upsertModelFromJson should really be replaced with update + insert functions
// But is it always easy to work out when share data should use update and when it should insert?
// E.g. user added models should be inserted when deserialised, not updated
const newCsv = newTerria.getModelByIdOrShareKey(
CsvCatalogItem,
"3432284e-a111-4844-97c8-26a1767f9986"
);
expect(newCsv).toBeDefined(
"Can't find newCsv item in destination newTerria"
);
if (newCsv === undefined) return;
expect(newCsv.uniqueId).toBe(
"decfc787-0425-4175-a98c-a40db064feb3",
"Failed to map share key to correct model"
);
expect(newTerria.workbench.contains(newCsv)).toBeTruthy(
"newCsv not found in destination newTerria workbench"
);
expect(newTerria.timelineStack.contains(newCsv)).toBeTruthy(
"newCsv not found in destination newTerria timeline"
);
});
});
});
describe("proxyConfiguration", function () {
beforeEach(function () {
worker.use(
http.get("*test/init/configProxy*", () =>
HttpResponse.json(configProxy)
),
http.get("*/serverconfig/*", () => HttpResponse.json(serverConfig))
);
});
it("initializes proxy with parameters from config file", async function () {
await terria.start({
configUrl: "test/init/configProxy.json",
i18nOptions
});
expect(terria.corsProxy.baseProxyUrl).toBe("/myproxy/");
expect(terria.corsProxy.proxyDomains).toEqual([
"example.com",
"csiro.au"
]);
});
});
describe("removeModelReferences", function () {
let model: SimpleCatalogItem;
beforeEach(function () {
model = new SimpleCatalogItem("testId", terria);
terria.addModel(model);
});
it("removes the model from workbench", async function () {
await terria.workbench.add(model);
terria.removeModelReferences(model);
expect(terria.workbench).not.toContain(model);
});
it(
"it removes picked features & selected feature for the model",
action(function () {
terria.pickedFeatures = new PickedFeatures();
const feature = new TerriaFeature({});
terria.selectedFeature = feature;
feature._catalogItem = model;
terria.pickedFeatures.features.push(feature);
terria.removeModelReferences(model);
expect(terria.pickedFeatures.features.length).toBe(0);
expect(terria.selectedFeature).toBeUndefined();
})
);
it("unregisters the model from Terria", function () {
terria.removeModelReferences(model);
expect(terria.getModelById(BaseModel, "testId")).toBeUndefined();
});
});
// it("tells us there's a time enabled WMS with `checkNowViewingForTimeWms()`", function(done) {
// terria
// .start({
// configUrl: "test/init/configProxy.json",
// i18nOptions
// })
// .then(function() {
// expect(terria.checkNowViewingForTimeWms()).toEqual(false);
// })
// .then(function() {
// const wmsItem = new WebMapServiceCatalogItem(terria);
// wmsItem.updateFromJson({
// url: "http://example.com",
// metadataUrl: "test/WMS/comma_sep_datetimes_inherited.xml",
// layers: "13_intervals",
// dataUrl: "" // to prevent a DescribeLayer request
// });
// wmsItem
// .load()
// .then(function() {
// terria.nowViewing.add(wmsItem);
// expect(terria.checkNowViewingForTimeWms()).toEqual(true);
// })
// .then(done)
// .catch(done.fail);
// })
// .catch(done.fail);
// });
describe("applyInitData", function () {
describe("when pickedFeatures is not present in initData", function () {
it("unsets the feature picking state if `canUnsetFeaturePickingState` is `true`", async function () {
terria.pickedFeatures = new PickedFeatures();
terria.selectedFeature = new Entity({
name: "selected"
}) as TerriaFeature;
await terria.applyInitData({
initData: {},
canUnsetFeaturePickingState: true
});
expect(terria.pickedFeatures).toBeUndefined();
expect(terria.selectedFeature).toBeUndefined();
});
it("otherwise, should not unset feature picking state", async function () {
terria.pickedFeatures = new PickedFeatures();
terria.selectedFeature = new Entity({
name: "selected"
}) as TerriaFeature;
await terria.applyInitData({
initData: {}
});
expect(terria.pickedFeatures).toBeDefined();
expect(terria.selectedFeature).toBeDefined();
});
});
describe("Sets workbench contents correctly", function () {
const mapServerSimpleGroupUrl =
"http://some.service.gov.au/arcgis/rest/services/mapServerSimpleGroup/MapServer";
const mapServerWithErrorUrl =
"http://some.service.gov.au/arcgis/rest/services/mapServerWithError/MapServer";
const magdaRecordFeatureServerGroupUrl =
"http://magda.reference.group.service.gov.au";
const magdaRecordDerefencedToWmsUrl =
"http://magda.references.wms.gov.au";
const mapServerGroupModel = {
type: "esri-mapServer-group",
name: "A simple map server group",
url: mapServerSimpleGroupUrl,
id: "a-test-server-group"
};
const magdaRecordDerefencedToFeatureServerGroup = {
type: "magda",
name: "A magda record derefenced to a simple feature server group",
url: magdaRecordFeatureServerGroupUrl,
recordId: "magda-record-id-dereferenced-to-feature-server-group",
id: "a-test-magda-record"
};
const magdaRecordDerefencedToWms = {
type: "magda",
name: "A magda record derefenced to wms",
url: magdaRecordDerefencedToWmsUrl,
recordId: "magda-record-id-dereferenced-to-wms",
id: "another-test-magda-record"
};
const mapServerModelWithError = {
type: "esri-mapServer-group",
name: "A map server with error",
url: mapServerWithErrorUrl,
id: "a-test-server-with-error"
};
const theOrderedItemsIds = [
"a-test-server-group/0",
"a-test-magda-record/0",
"another-test-magda-record"
];
let loadMapItemsWms: any = undefined;
let loadMapItemsArcGisMap: any = undefined;
let loadMapItemsArcGisFeature: any = undefined;
beforeEach(function () {
worker.use(
// MapServer group metadata
http.get(
"http://some.service.gov.au/arcgis/rest/services/mapServerSimpleGroup/MapServer",
() => HttpResponse.json(mapServerSimpleGroupJson)
),
http.get(
"http://some.service.gov.au/arcgis/rest/services/mapServerWithError/MapServer",
() => HttpResponse.json(mapServerWithErrorJson)
),
// Magda registry records
http.get(
"http://magda.reference.group.service.gov.au/api/v0/registry/records/:recordId",
() => HttpResponse.json(magdaGroupRecordJson)
),
http.get(
"http://magda.references.wms.gov.au/api/v0/registry/records/:recordId",
() => HttpResponse.json(magdaWmsRecordJson)
),
// Dereferenced service endpoints
http.get(
"https://services2.arcgis.com/iCBB4zKDwkw2iwDD/arcgis/rest/services/Forest_Management_Zones/FeatureServer",
() => HttpResponse.json(esriFeatureServerJson)
),
http.get(
"https://mapprod1.environment.nsw.gov.au/arcgis/services/VIS/Vegetation_SouthCoast_SCIVI_V14_E_2230/MapServer/WMSServer",
() => HttpResponse.xml(wmsCapabilitiesXml)
)
);
// Do not call through.
loadMapItemsArcGisMap = spyOn(
ArcGisMapServerCatalogItem.prototype,
"loadMapItems"
).and.callFake(() => Promise.resolve(Result.none()));
loadMapItemsArcGisFeature = spyOn(
ArcGisFeatureServerCatalogItem.prototype,
"loadMapItems"
).and.callFake(() => Promise.resolve(Result.none()));
loadMapItemsWms = spyOn(
WebMapServiceCatalogItem.prototype,
"loadMapItems"
).and.callFake(() => Promise.resolve(Result.none()));
});
it("when a workbench item is a simple map server group", async function () {
await terria.applyInitData({
initData: {
catalog: [mapServerGroupModel],
workbench: ["a-test-server-group"]
}
});
expect(terria.workbench.itemIds).toEqual(["a-test-server-group/0"]);
expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
});
it("when a workbench item is a referenced map server group", async function () {
await terria.applyInitData({
initData: {
catalog: [magdaRecordDerefencedToFeatureServerGroup],
workbench: ["a-test-magda-record"]
}
});
expect(terria.workbench.itemIds).toEqual(["a-test-magda-record/0"]);
expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
});
it("when a workbench item is a referenced wms", async function () {
await terria.applyInitData({
initData: {
catalog: [magdaRecordDerefencedToWms],
workbench: ["another-test-magda-record"]
}
});
expect(terria.workbench.itemIds).toEqual(["another-test-magda-record"]);
expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
});
it("when the workbench has more than one items", async function () {
await terria.applyInitData({
initData: {
catalog: [
mapServerGroupModel,
magdaRecordDerefencedToFeatureServerGroup,
magdaRecordDerefencedToWms
],
workbench: [
"a-test-server-group",
"a-test-magda-record",
"another-test-magda-record"
]
}
});
expect(terria.workbench.itemIds).toEqual(theOrderedItemsIds);
expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
});
it("when the workbench has an unknown item", async function () {
await terria.applyInitData({
initData: {
catalog: [
mapServerGroupModel,
magdaRecordDerefencedToFeatureServerGroup,
magdaRecordDerefencedToWms
],
workbench: [
"id_of_unknown_model",
"a-test-server-group",
"a-test-magda-record",
"another-test-magda-record"
]
}
});
expect(terria.workbench.itemIds).toEqual(theOrderedItemsIds);
expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
});
it("when a workbench item has errors", async function () {
let error: TerriaError | undefined = undefined;
try {
await terria.applyInitData({
initData: {
catalog: [
mapServerModelWithError,
mapServerGroupModel,
magdaRecordDerefencedToFeatureServerGroup,
magdaRecordDerefencedToWms
],
workbench: [
"a-test-server-with-error",
"a-test-server-group",
"a-test-magda-record",
"another-test-magda-record"
]
}
});
} catch (e) {
error = e as TerriaError;
expect(error.message === "models.terria.loadingInitSourceErrorTitle");
} finally {
expect(error).not.toEqual(undefined);
expect(terria.workbench.itemIds).toEqual(theOrderedItemsIds);
expect(loadMapItemsWms).toHaveBeenCalledTimes(1);
expect(loadMapItemsArcGisMap).toHaveBeenCalledTimes(1);
expect(loadMapItemsArcGisFeature).toHaveBeenCalledTimes(1);
}
});
});
describe("Enable/disable shorten share URL via init data", function () {
beforeEach(function () {
window.localStorage.clear();
});
it("should not change local property shortenShareUrls", async function () {
await terria.applyInitData({
initData: {}
});
expect(terria.getLocalProperty("shortenShareUrls")).toBe(null);
terria.setLocalProperty("shortenShareUrls", true);
await terria.applyInitData({
initData: {}
});
expect(terria.getLocalProperty("shortenShareUrls")).toBeTruthy();
terria.setLocalProperty("shortenShareUrls", false);
await terria.applyInitData({
initData: {}
});
expect(terria.getLocalProperty("shortenShareUrls")).toBeFalsy();
});
it("should set local property shortenShareUrls to true", async function () {
await terria.applyInitData({
initData: {
settings: {
shortenShareUrls: true
}
}
});
expect(terria.getLocalProperty("shortenShareUrls")).toBeTruthy();
});
it("should set local property shortenShareUrls to false", async function () {
await terria.applyInitData({
initData: {
settings: {
shortenShareUrls: false
}
}
});
expect(terria.getLocalProperty("shortenShareUrls")).toBeFalsy();
terria.setLocalProperty("shortenShareUrls", true);
await terria.applyInitData({
initData: {
settings: {
shortenShareUrls: false
}
}
});
expect(terria.getLocalProperty("shortenShareUrls")).toBeFalsy();
});
});
});
describe("mapSettings", function () {
it("properly interprets map hash parameter", async () => {
const getLocalPropertySpy = spyOn(terria, "getLocalProperty");
const location = {
href: "http://test.com/#map=2d"
} as Location;
await terria.start({
configUrl: "test-config.json",
applicationUrl: location
});
terria.loadPersistedMapSettings();
expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Leaflet);
expect(getLocalPropertySpy).not.toHaveBeenCalledWith("viewermode");
});
it("properly resolves persisted map viewer", async () => {
const getLocalPropertySpy = spyOn(
terria,
"getLocalProperty"
).and.returnValue("2d");
await terria.start({ configUrl: "test-config.json" });
terria.loadPersistedMapSettings();
expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Leaflet);
expect(getLocalPropertySpy).toHaveBeenCalledWith("viewermode");
});
it("properly interprets wrong map hash parameter and resolves persisted value", async () => {
const getLocalPropertySpy = spyOn(
terria,
"getLocalProperty"
).and.returnValue("3dsmooth");
const location = {
href: "http://test.com/#map=4d"
} as Location;
await terria.start({
configUrl: "test-config.json",
applicationUrl: location
});
terria.loadPersistedMapSettings();
expect(terria.mainViewer.viewerMode).toBe(ViewerMode.Cesium);
expect(terria.mainViewer.viewerOptions.useTerrain).toBe(false);
expect(getLocalPropertySpy).toHaveBeenCalledWith("viewermode");
});
it("uses `settings` in initsource", async () => {
const setBaseMapSpy = spyOn(terria.mainViewer, "setBaseMap");
await terria.start({ configUrl: "test-config.json" });
await terria.applyInitData({
initData: {
settings: {
baseMaximumScreenSpaceError: 1,
useNativeResolution: true,
alwaysShowTimeline: true,
baseMapId: "basemap-natural-earth-II",
terrainSplitDirection: -1,
depthTestAgainstTerrainEnabled: true
}
}
});
await terria.loadInitSources();
expect(terria.baseMaximumScreenSpaceError).toBe(1);
expect(terria.useNativeResolution).toBeTruthy();
expect(terria.timelineStack.alwaysShowingTimeline).toBeTruthy();
expect(setBaseMapSpy).toHaveBeenCalledWith(
terria.baseMapsModel.baseMapItems.find(
(item) => item.item.uniqueId === "basemap-natural-earth-II"
)?.item
);
expect(terria.terrainSplitDirection).toBe(SplitDirection.LEFT);
expect(terria.depthTestAgainstTerrainEnabled).toBeTruthy();
});
});
describe("basemaps", function () {
it("when no base maps are specified load defaultBaseMaps", async function () {
await terria.start({ configUrl: "test-config.json" });
await terria.applyInitData({
initData: {}
});
await terria.loadInitSources();
const _defaultBaseMaps = defaultBaseMaps(terria);
expect(terria.baseMapsModel).toBeDefined();
expect(terria.baseMapsModel.baseMapItems.length).toBe(
_defaultBaseMaps.length
);
});
it("correctly loads the base maps", async function () {
await terria.start({ configUrl: "test-config.json" });
await (
await terria._applyInitData({
initData: {
settings: { baseMapId: "basemap-2" },
baseMaps: {
items: [
{
item: {
id: "basemap-natural-earth-II",
name: "Natural Earth II",
type: "url-template-imagery",
url: "https://storage.googleapis.com/terria-datasets-public/basemaps/natural-earth-tiles/{z}/{x}/{reverseY}.png",
attribution:
"Natural Earth II - From Natural Earth. Public Domain.",
maximumLevel: 7,
opacity: 1.0
},
image: "build/TerriaJS/images/natural-earth.png",
contrastColor: "#000000"
},
{
item: {
id: "basemap-2",
name: "Base map 2",
type: "url-template-imagery",
url: "https://example.com"
}
}
]
}
}
})
).baseMapPromise;
const _defaultBaseMaps = defaultBaseMaps(terria);
expect(terria.baseMapsModel).toBeDefined();
expect(terria.baseMapsModel.baseMapItems.length).toEqual(
_defaultBaseMaps.length + 1
);
expect(terria.mainViewer.baseMap?.uniqueId).toBe("basemap-2");
});
});
describe("loadPickedFeatures", function () {
let container: HTMLElement;
beforeEach(async function () {
// Attach cesium viewer and wait for it to be loaded
container = document.createElement("div");
document.body.appendChild(container);
terria.mainViewer.viewerOptions.useTerrain = false;
terria.mainViewer.attach(container);
return terria.mainViewer.viewerLoadPromise;
});
afterEach(() => {
terria.mainViewer.destroy();
document.body.removeChild(container);
});
it("sets the pickCoords", async function () {
const Cesium = (await import("../../lib/Models/Cesium")).default;
expect(terria.currentViewer instanceof Cesium).toBeTruthy();
await terria.loadPickedFeatures({
pickCoords: {
lat: 84.93,
lng: 77.91,
height: -5400810.41
},
providerCoords: {
"https://foo": { x: 123, y: 456, level: 7 },
"https://bar": { x: 42, y: 42, level: 4 }
}
});
const pickPosition = terria.pickedFeatures?.pickPosition;
expect(pickPosition).toBeDefined();
if (pickPosition) {
const { x, y, z } = pickPosition;
expect(x.toFixed(2)).toBe("18483.85");
expect(y.toFixed(2)).toBe("86292.94");
expect(z.toFixed(2)).toBe("952035.13");
}
});
it("sets the selectedFeature", async function () {
const testItem = new SimpleCatalogItem("test", terria);
const ds = new CustomDataSource("ds");
const entity = new Entity({ name: "foo" });
ds.entities.add(entity);
testItem.mapItems = [ds];
await terria.workbench.add(testItem);
const entityHash = hashEntity(entity, terria);
await terria.loadPickedFeatures({
pickCoords: {
lat: 84.93,
lng: 77.91,
height: -5400810.41
},
providerCoords: {
"https://foo": { x: 123, y: 456, level: 7 },
"https://bar": { x: 42, y: 42, level: 4 }
},
entities: [
{
hash: entityHash,
name: "foo"
}
],
current: {
hash: entityHash,
name: "foo"
}
});
expect(terria.selectedFeature).toBeDefined();
expect(terria.selectedFeature?.name).toBe("foo");
});
});
it("customRequestSchedulerLimits sets RequestScheduler limits for domains", async function () {
const configUrl = `data:application/json;base64,${btoa(
JSON.stringify({
initializationUrls: [],
parameters: {
customRequestSchedulerLimits: {
"test.domain:333": 12
}
}
})
)}`;
await terria.start({ configUrl, i18nOptions });
expect(RequestScheduler.requestsByServer["test.domain:333"]).toBe(12);
});
describe("initial zoom", function () {
describe("behaviour of `initialCamera.focusWorkbenchItems`", function () {
let container: HTMLElement;
beforeEach(function () {
// Attach cesium viewer and wait for it to be loaded
container = document.createElement("div");
document.body.appendChild(container);
terria.mainViewer.viewerOptions.useTerrain = false;
terria.mainViewer.attach(container);
const configJson = {
initializationUrls: ["focus-workbench-items.json"]
};
// An init source with a pre-loaded workbench item
const initJson = {
initialCamera: { focusWorkbenchItems: true },
catalog: [
{
id: "points",
type: "geojson",
name: "Points",
geoJsonData: {
type: "Feature",
bbox: [-10.0, -10.0, 10.0, 10.0],
properties: {
foo: "hi",
bar: "bye"
},
geometry: {
type: "Polygon",
coordinates: [
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 1.0],
[100.0, 0.0]
],
[
[100.2, 0.2],
[100.8, 0.2],
[100.8, 0.8],
[100.2, 0.8],
[100.2, 0.2]
]
]
}
}
}
],
workbench: ["points"],
baseMaps: {
enabledBaseMaps: []
}
};
worker.use(
http.get("*/serverconfig/*", () => HttpResponse.json({})),
http.get("*/test-config.json", () => HttpResponse.json(configJson)),
http.get("*/focus-workbench-items.json", () =>
HttpResponse.json(initJson)
)
);
});
afterEach(() => {
terria.mainViewer.destroy();
document.body.removeChild(container);
});
it("zooms the map to focus on the workbench items", async function () {
await terria.start({ configUrl: "test-config.json" });
await terria.loadInitSources();
await when(() => terria.currentViewer.type === "Cesium");
const cameraPos = terria.cesium?.scene.camera.positionCartographic;
expect(cameraPos).toBeDefined();
const { longitude, latitude, height } = cameraPos!;
expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(100.5);
expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(0.5);
expect(height).toBeCloseTo(191276.7939);
});
it("works correctly even when there is a delay in a Cesium/Leaflet viewer becoming available", async function () {
// Start with NoViewer
runInAction(() => {
terria.mainViewer.viewerMode = undefined;
});
await terria.start({ configUrl: "test-config.json" });
await terria.loadInitSources();
expect(terria.currentViewer.type).toEqual("none");
// Switch to Cesium viewer
runInAction(() => {
terria.mainViewer.viewerMode = ViewerMode.Cesium;
});
// Wait for the switch to happen
await when(() => terria.mainViewer.currentViewer.type === "Cesium");
// Ensure that the camera position is correctly updated after the switch
const cameraPos = terria.cesium?.scene.camera.positionCartographic;
const { longitude, latitude, height } = cameraPos!;
expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(100.5);
expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(0.5);
expect(height).toBeCloseTo(191276.7939);
});
it("is not applied if subsequent init sources override the initialCamera settings", async function () {
await terria.start({ configUrl: "test-config.json" });
terria.initSources.push({
data: {
initialCamera: {
west: 42,
east: 44,
north: 44,
south: 42,
zoomDuration: 0
}
}
});
// Terria uses a 2 second flight duration when zooming to CameraView.
// Here we re-define zoomTo() to ignore duration and zoom to the target
// immediately so that we can observe the effects without delay.
const originalZoomTo = terria.currentViewer.zoomTo.bind(
terria.currentViewer
);
terria.currentViewer.zoomTo = (target, _duration) =>
originalZoomTo(target, 0.0);
await terria.loadInitSources();
await when(() => terria.currentViewer.type === "Cesium");
const cameraPos = terria.cesium?.scene.camera.positionCartographic;
expect(cameraPos).toBeDefined();
const { longitude, latitude, height } = cameraPos!;
expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(43);
expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(43);
expect(height).toBeCloseTo(384989.3092);
});
it("is not applied when share URL specifies a different initialCamera setting", async function () {
// Terria uses a 2 second flight duration when zooming to CameraView.
// Here we re-define zoomTo() to ignore duration and zoom to the target
// immediately so that we can observe the effects without delay.
await when(() => terria.currentViewer.type === "Cesium");
const originalZoomTo = terria.currentViewer.zoomTo.bind(
terria.currentViewer
);
terria.currentViewer.zoomTo = (target, _duration) =>
originalZoomTo(target, 0.0);
await terria.start({
configUrl: "test-config.json",
applicationUrl: {
// A share URL with a different `initialCamera` setting
href: "http://localhost:3001/#start=%7B%22version%22%3A%228.0.0%22%2C%22initSources%22%3A%5B%7B%22stratum%22%3A%22user%22%2C%22initialCamera%22%3A%7B%22east%22%3A80.48324442836365%2C%22west%22%3A74.16912021554141%2C%22north%22%3A10.82936711956377%2C%22south%22%3A7.882086009700934%7D%2C%22workbench%22%3A%5B%22points%22%5D%7D%5D%7D"
} as Location
});
await terria.loadInitSources();
await when(() => terria.currentViewer.type === "Cesium");
const cameraPos = terria.cesium?.scene.camera.positionCartographic;
expect(cameraPos).toBeDefined();
const { longitude, latitude, height } = cameraPos!;
expect(CesiumMath.toDegrees(longitude)).toBeCloseTo(77.3261);
expect(CesiumMath.toDegrees(latitude)).toBeCloseTo(9.3557);
expect(height).toBeCloseTo(591140.7251);
});
});
});
});