terriajs
Version:
Geospatial data visualization platform.
562 lines (467 loc) • 17.9 kB
JavaScript
;
import Cartographic from "terriajs-cesium/Source/Core/Cartographic";
import CesiumMath from "terriajs-cesium/Source/Core/Math";
import CesiumTileLayer from "../../lib/Map/CesiumTileLayer";
import Color from "terriajs-cesium/Source/Core/Color";
import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid";
import Entity from "terriajs-cesium/Source/DataSources/Entity";
import GeoJsonDataSource from "terriajs-cesium/Source/DataSources/GeoJsonDataSource";
import ImageryLayerFeatureInfo from "terriajs-cesium/Source/Scene/ImageryLayerFeatureInfo";
import L from "leaflet";
import Leaflet from "../../lib/Models/Leaflet";
import loadJson from "../../lib/Core/loadJson";
import Terria from "../../lib/Models/Terria";
import runLater from "../../lib/Core/runLater";
var DEFAULT_ZOOM_LEVEL = 5;
describe("Leaflet Model", function () {
var terria;
var leaflet;
var container, map, layers;
beforeEach(function () {
terria = new Terria({
baseUrl: "./"
});
container = document.createElement("div");
container.id = "container";
document.body.appendChild(container);
map = L.map("container").setView([-28.5, 135], DEFAULT_ZOOM_LEVEL);
spyOn(terria.tileLoadProgressEvent, "raiseEvent");
layers = [
new L.TileLayer("http://example.com"),
new L.TileLayer("http://example.com"),
// Make sure there's a non-tile layer in there to make sure we're able to handle those.
new L.ImageOverlay("http://example.com", L.latLngBounds([1, 1], [3, 3]))
];
});
afterEach(function () {
document.body.removeChild(container);
});
function initLeaflet() {
leaflet = new Leaflet(terria, map);
terria.leaflet = leaflet;
layers.forEach(function (layer) {
map.addLayer(layer);
});
}
describe("should trigger a tileLoadProgressEvent", function () {
["tileloadstart", "tileload", "load"].forEach(function (event) {
it("on " + event, function () {
initLeaflet();
layers[0].fire(event);
expect(terria.tileLoadProgressEvent.raiseEvent).toHaveBeenCalled();
});
});
});
it("should be able to reference its container", function () {
initLeaflet();
expect(leaflet.getContainer()).toBe(container);
});
it("should trigger a tileLoadProgressEvent with the total number of tiles to be loaded for all layers", function () {
initLeaflet();
layers[0]._tiles = {
1: { loaded: undefined },
2: { loaded: undefined },
a: { loaded: +new Date() }, // This is how Leaflet marks loaded tiles
b: { loaded: undefined }
};
layers[1]._tiles = {
3: { loaded: +new Date() },
4: { loaded: undefined },
c: { loaded: +new Date() },
d: { loaded: undefined }
};
layers[1].fire("tileload");
expect(terria.tileLoadProgressEvent.raiseEvent).toHaveBeenCalledWith(5, 5);
});
describe("should change the max", function () {
it("to whatever the highest count of loading tiles so far was", function () {
initLeaflet();
changeTileLoadingCount(3);
changeTileLoadingCount(6);
changeTileLoadingCount(8);
changeTileLoadingCount(2);
expect(
terria.tileLoadProgressEvent.raiseEvent.calls.mostRecent().args
).toEqual([2, 8]);
});
it("to 0 when loading tile count reaches 0", function () {
initLeaflet();
changeTileLoadingCount(3);
changeTileLoadingCount(6);
changeTileLoadingCount(8);
changeTileLoadingCount(0);
expect(
terria.tileLoadProgressEvent.raiseEvent.calls.mostRecent().args
).toEqual([0, 0]);
changeTileLoadingCount(3);
expect(
terria.tileLoadProgressEvent.raiseEvent.calls.mostRecent().args
).toEqual([3, 3]);
});
function changeTileLoadingCount(count) {
var tiles = {};
// Add loading tiles
for (var i = 0; i < count; i++) {
tiles["tile " + i] = { loaded: undefined };
}
layers[0]._tiles = tiles;
layers[1]._tiles = {};
layers[0].fire("tileload");
}
});
describe("feature picking", function () {
var latlng = { lat: 50, lng: 50 };
var deferred1 = {},
deferred2 = {};
beforeEach(function () {
deferred1.promise = new Promise((resolve, reject) => {
deferred1.resolve = resolve;
deferred1.reject = reject;
});
deferred2.promise = new Promise((resolve, reject) => {
deferred2.resolve = resolve;
deferred2.reject = reject;
});
terria.nowViewing.items = [
{
isEnabled: true,
isShown: true,
imageryLayer: new CesiumTileLayer({
pickFeatures: jasmine
.createSpy("pickFeatures")
.and.resolveTo(deferred1.promise),
url: "http://example.com/1",
ready: true,
tilingScheme: {
positionToTileXY: function () {
return { x: 1, y: 2 };
}
}
})
},
{
isEnabled: true,
isShown: true,
imageryLayer: new CesiumTileLayer({
pickFeatures: jasmine
.createSpy("pickFeatures")
.and.resolveTo(deferred2.promise),
url: "http://example.com/2",
ready: true,
tilingScheme: {
positionToTileXY: function () {
return { x: 4, y: 5 };
}
}
})
},
{
isEnabled: false,
isShown: true,
imageryLayer: new CesiumTileLayer({
pickFeatures: jasmine.createSpy("pickFeatures"),
url: "http://example.com/3",
ready: true,
tilingScheme: {
positionToTileXY: function () {
return { x: 1, y: 2 };
}
}
})
},
{
isEnabled: false,
isShown: true,
imageryLayer: new CesiumTileLayer({
pickFeatures: jasmine.createSpy("pickFeatures"),
url: "http://example.com/4",
ready: true,
tilingScheme: {
positionToTileXY: function () {
return { x: 1, y: 2 };
}
}
})
}
];
});
describe("from location", function () {
beforeEach(function () {
initLeaflet();
});
commonFeaturePickingTests(function () {
leaflet.pickFromLocation(latlng, {});
});
it("uses tileCoordinates when provided", function () {
leaflet.pickFromLocation(latlng, {
"http://example.com/1": { x: 100, y: 200, level: 300 },
"http://example.com/2": { x: 400, y: 500, level: 600 }
});
expect(
terria.nowViewing.items[0].imageryLayer.imageryProvider.pickFeatures.calls.argsFor(
0
)[0]
).toBe(100);
expect(
terria.nowViewing.items[0].imageryLayer.imageryProvider.pickFeatures.calls.argsFor(
0
)[1]
).toBe(200);
expect(
terria.nowViewing.items[0].imageryLayer.imageryProvider.pickFeatures.calls.argsFor(
0
)[2]
).toBe(300);
expect(
terria.nowViewing.items[1].imageryLayer.imageryProvider.pickFeatures.calls.argsFor(
0
)[0]
).toBe(400);
expect(
terria.nowViewing.items[1].imageryLayer.imageryProvider.pickFeatures.calls.argsFor(
0
)[1]
).toBe(500);
expect(
terria.nowViewing.items[1].imageryLayer.imageryProvider.pickFeatures.calls.argsFor(
0
)[2]
).toBe(600);
});
it("adds existingFeatures to end result", async function () {
var existing = new ImageryLayerFeatureInfo();
existing.name = "existing";
leaflet.pickFromLocation(latlng, {}, [existing]);
finishPickingPromise();
await terria.pickedFeatures.allFeaturesAvailablePromise;
expect(terria.pickedFeatures.features[0].name).toBe("existing");
});
});
describe("from click", function () {
var click;
beforeEach(function () {
spyOn(map, "on").and.callFake(function (type, callback) {
if (type === "click") {
click = callback;
}
});
initLeaflet();
});
commonFeaturePickingTests(function () {
click({
latlng: latlng
});
});
describe("when combining vector and raster features", function () {
var vectorFeature1, vectorFeature2;
beforeEach(function () {
vectorFeature1 = new Entity({
name: "vector1"
});
vectorFeature2 = new Entity({
name: "vector2"
});
});
it("includes vector features with click events both before and after the map click event", async function () {
// vector and map clicks can come in any order.
leaflet.scene.featureClicked.raiseEvent(vectorFeature1, {
latlng: latlng
});
click({
latlng: latlng
});
leaflet.scene.featureClicked.raiseEvent(vectorFeature2, {
latlng: latlng
});
finishPickingPromise();
await terria.pickedFeatures.allFeaturesAvailablePromise;
expect(terria.pickedFeatures.features.length).toBe(5);
expect(terria.pickedFeatures.features[0].name).toBe("vector1");
expect(terria.pickedFeatures.features[1].name).toBe("vector2");
expect(terria.pickedFeatures.features[2].name).toBe("1");
});
it("resets the picked vector features if a subsequent map click is made", async function () {
leaflet.scene.featureClicked.raiseEvent(vectorFeature1, {
latlng: latlng
});
click({
latlng: latlng
});
leaflet.scene.featureClicked.raiseEvent(vectorFeature2, {
latlng: latlng
});
// The reset happens in a runLater, which a second click will always come behind in a browser,
// but this isn't guaranteed in unit tests because they're just two setTimeouts racing each other,
// so give this a healthy 50ms delay to make sure it comes in behind the 0ms delay in Leaflet.js.
await runLater(() => {}, 50);
click({
latlng: latlng
});
finishPickingPromise();
await terria.pickedFeatures.allFeaturesAvailablePromise;
expect(terria.pickedFeatures.features.length).toBe(3);
expect(terria.pickedFeatures.features[0].name).toBe("1");
});
});
});
function commonFeaturePickingTests(trigger) {
it("correctly tracks loading state", async function () {
expect(terria.pickedFeatures).toBeUndefined();
trigger();
expect(terria.pickedFeatures.isLoading).toBe(true);
finishPickingPromise();
await terria.pickedFeatures.allFeaturesAvailablePromise;
expect(terria.pickedFeatures.isLoading).toBe(false);
});
it("should load imagery layer features when feature info requests are enabled", async function () {
terria.allowFeatureInfoRequests = true;
trigger();
expect(terria.pickedFeatures.isLoading).toBe(true);
var featureInfo1 = new ImageryLayerFeatureInfo();
var featureInfo2 = new ImageryLayerFeatureInfo();
featureInfo1.name = "name1";
featureInfo2.name = "name2";
deferred1.resolve([featureInfo1]);
deferred2.resolve([featureInfo2]);
await terria.pickedFeatures.allFeaturesAvailablePromise;
expect(terria.pickedFeatures.isLoading).toBe(false);
expect(terria.pickedFeatures.features.length).toBe(2);
expect(terria.pickedFeatures.features[0].name).toBe("name1");
expect(terria.pickedFeatures.features[1].name).toBe("name2");
});
it("should not load imagery layer features when feature info requests are disabled", async function () {
terria.allowFeatureInfoRequests = false;
trigger();
await terria.pickedFeatures.allFeaturesAvailablePromise;
expect(terria.pickedFeatures.isLoading).toBe(false);
expect(terria.pickedFeatures.features.length).toBe(0);
});
it("records pickPosition", function () {
trigger();
expect(terria.pickedFeatures.pickPosition).toEqual(
Ellipsoid.WGS84.cartographicToCartesian(
Cartographic.fromDegrees(50, 50)
)
);
});
describe("after feature picked", function () {
beforeEach(trigger);
it("populates terria.pickedFeatures", function () {
expect(terria.pickedFeatures).toBeDefined();
expect(
terria.pickedFeatures.allFeaturesAvailablePromise
).toBeDefined();
});
it("calls pickFeatures for all enabled and shown layers", function () {
expect(
terria.nowViewing.items[0].imageryLayer.imageryProvider.pickFeatures
).toHaveBeenCalledWith(
1,
2,
DEFAULT_ZOOM_LEVEL,
CesiumMath.toRadians(50),
CesiumMath.toRadians(50)
);
expect(
terria.nowViewing.items[1].imageryLayer.imageryProvider.pickFeatures
).toHaveBeenCalledWith(
4,
5,
DEFAULT_ZOOM_LEVEL,
CesiumMath.toRadians(50),
CesiumMath.toRadians(50)
);
expect(
terria.nowViewing.items[2].imageryLayer.imageryProvider.pickFeatures
).not.toHaveBeenCalled();
expect(
terria.nowViewing.items[3].imageryLayer.imageryProvider.pickFeatures
).not.toHaveBeenCalled();
});
describe("after pickFeatures returns for all layers", function () {
beforeEach(async function () {
finishPickingPromise();
await terria.pickedFeatures.allFeaturesAvailablePromise;
});
it("combines promise results", function () {
expect(terria.pickedFeatures.features[0].name).toBe("1");
expect(terria.pickedFeatures.features[1].name).toBe("2");
expect(terria.pickedFeatures.features[2].name).toBe("3");
});
it("records pick coords", function () {
expect(terria.pickedFeatures.providerCoords).toEqual({
"http://example.com/1": { x: 1, y: 2, level: DEFAULT_ZOOM_LEVEL },
"http://example.com/2": { x: 4, y: 5, level: DEFAULT_ZOOM_LEVEL }
});
});
it("sets imageryLayer on features", function () {
expect(terria.pickedFeatures.features[0].imageryLayer).toBe(
terria.nowViewing.items[0].imageryLayer
);
expect(terria.pickedFeatures.features[1].imageryLayer).toBe(
terria.nowViewing.items[1].imageryLayer
);
});
});
});
}
it("should create GeoJSON for polygon when a rasterized polygon feature is selected", async function () {
const polygonGeoJson = await loadJson("test/GeoJSON/polygon.geojson");
initLeaflet();
var entity = new Entity();
entity.data = polygonGeoJson;
terria.selectedFeature = entity;
expect(terria.leaflet._highlightPromise).toBeDefined();
expect(terria.leaflet._removeHighlightCallback).toBeDefined();
await terria.leaflet._highlightPromise;
expect(terria.dataSources.length).toBe(1);
expect(terria.dataSources.get(0) instanceof GeoJsonDataSource).toBe(true);
});
it("should create GeoJSON for polyline when a rasterized polyline feature is selected", async function () {
const polylineGeoJson = await loadJson("test/GeoJSON/polyline.geojson");
initLeaflet();
var entity = new Entity();
entity.data = polylineGeoJson;
terria.selectedFeature = entity;
expect(terria.leaflet._highlightPromise).toBeDefined();
expect(terria.leaflet._removeHighlightCallback).toBeDefined();
await terria.leaflet._highlightPromise;
expect(terria.dataSources.length).toBe(1);
expect(terria.dataSources.get(0) instanceof GeoJsonDataSource).toBe(true);
});
it("should update the style of a vector polygon when selected", async function () {
const dataSource = await GeoJsonDataSource.load(
"test/GeoJSON/polygon.geojson"
);
initLeaflet();
terria.dataSources.add(dataSource);
var entity = dataSource.entities.values[0];
terria.selectedFeature = entity;
expect(entity.polygon.outlineColor.getValue()).toEqual(Color.WHITE);
terria.selectedFeature = undefined;
expect(entity.polygon.outlineColor.getValue()).not.toEqual(Color.WHITE);
});
it("should update the style of a vector polyline when selected", async function () {
const dataSource = await GeoJsonDataSource.load(
"test/GeoJSON/polyline.geojson"
);
initLeaflet();
terria.dataSources.add(dataSource);
var entity = dataSource.entities.values[0];
terria.selectedFeature = entity;
expect(entity.polyline.width.getValue()).toEqual(2);
terria.selectedFeature = undefined;
expect(entity.polyline.width.getValue()).not.toEqual(2);
});
function finishPickingPromise() {
var featureInfo1 = new ImageryLayerFeatureInfo();
featureInfo1.name = "1";
var featureInfo2 = new ImageryLayerFeatureInfo();
featureInfo2.name = "2";
var featureInfo3 = new ImageryLayerFeatureInfo();
featureInfo3.name = "3";
deferred1.resolve([featureInfo1]);
deferred2.resolve([featureInfo2, featureInfo3]);
}
});
});