UNPKG

terriajs

Version:

Geospatial data visualization platform.

1,366 lines (1,206 loc) 64.3 kB
import { reaction, runInAction } from "mobx"; import { http, HttpResponse } from "msw"; import { GeomType, LineSymbolizer, PolygonSymbolizer } from "protomaps-leaflet"; import Cartesian2 from "terriajs-cesium/Source/Core/Cartesian2"; import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import Color from "terriajs-cesium/Source/Core/Color"; import Iso8601 from "terriajs-cesium/Source/Core/Iso8601"; import JulianDate from "terriajs-cesium/Source/Core/JulianDate"; import createGuid from "terriajs-cesium/Source/Core/createGuid"; import CustomDataSource from "terriajs-cesium/Source/DataSources/CustomDataSource"; import Entity from "terriajs-cesium/Source/DataSources/Entity"; import GeoJsonDataSource from "terriajs-cesium/Source/DataSources/GeoJsonDataSource"; import HeightReference from "terriajs-cesium/Source/Scene/HeightReference"; import StandardCssColors from "../../../../lib/Core/StandardCssColors"; import ContinuousColorMap from "../../../../lib/Map/ColorMap/ContinuousColorMap"; import ProtomapsImageryProvider from "../../../../lib/Map/ImageryProvider/ProtomapsImageryProvider"; import { GEOJSON_SOURCE_LAYER_NAME, ProtomapsGeojsonSource } from "../../../../lib/Map/Vector/Protomaps/ProtomapsGeojsonSource"; import { FEATURE_ID_PROP, getColor } from "../../../../lib/ModelMixins/GeojsonMixin"; import { ImageryParts, isDataSource } from "../../../../lib/ModelMixins/MappableMixin"; import GeoJsonCatalogItem from "../../../../lib/Models/Catalog/CatalogItems/GeoJsonCatalogItem"; import SplitItemReference from "../../../../lib/Models/Catalog/CatalogReferences/SplitItemReference"; import CommonStrata from "../../../../lib/Models/Definition/CommonStrata"; import updateModelFromJson from "../../../../lib/Models/Definition/updateModelFromJson"; import TerriaFeature from "../../../../lib/Models/Feature/Feature"; import { isTerriaFeatureData, TerriaFeatureData } from "../../../../lib/Models/Feature/FeatureData"; import Terria from "../../../../lib/Models/Terria"; import { worker } from "../../../mocks/browser"; import verysimpleCzml from "../../../../wwwroot/test/CZML/verysimple.czml" with { type: "json" }; import apiListGeojson from "../../../../wwwroot/test/GeoJSON/api-list.geojson" with { type: "json" }; import apiGeojson from "../../../../wwwroot/test/GeoJSON/api.geojson" with { type: "json" }; import bikeRacksGeojson from "../../../../wwwroot/test/GeoJSON/bike_racks.geojson" with { type: "json" }; import cemeteriesGeojson from "../../../../wwwroot/test/GeoJSON/cemeteries.geojson" with { type: "json" }; import emptyGeomsGeojson from "../../../../wwwroot/test/GeoJSON/empty-geoms.geojson" with { type: "json" }; import gmeGeojson from "../../../../wwwroot/test/GeoJSON/gme.geojson" with { type: "json" }; import heightGeojson from "../../../../wwwroot/test/GeoJSON/height.geojson" with { type: "json" }; import multipointGeojson from "../../../../wwwroot/test/GeoJSON/multipoint.geojson" with { type: "json" }; import multipolygonGeojson from "../../../../wwwroot/test/GeoJSON/multipolygon.geojson" with { type: "json" }; import pointsGeojson from "../../../../wwwroot/test/GeoJSON/points.geojson" with { type: "json" }; import polygonGeojson from "../../../../wwwroot/test/GeoJSON/polygon.geojson" with { type: "json" }; import polylineGeojson from "../../../../wwwroot/test/GeoJSON/polyline.geojson" with { type: "json" }; import timeBasedAutomaticStylesGeojson from "../../../../wwwroot/test/GeoJSON/time-based-automatic-styles.geojson" with { type: "json" }; import timeBasedGeojson from "../../../../wwwroot/test/GeoJSON/time-based.geojson" with { type: "json" }; describe("GeoJsonCatalogItemSpec", () => { beforeEach(() => { worker.use( http.get("test/GeoJSON/bike_racks.geojson", () => HttpResponse.json(bikeRacksGeojson) ), http.get("test/GeoJSON/cemeteries.geojson", () => HttpResponse.json(cemeteriesGeojson) ), http.get("test/GeoJSON/gme.geojson", () => HttpResponse.json(gmeGeojson)), http.get("test/CZML/verysimple.czml", () => HttpResponse.json(verysimpleCzml) ), http.get("test/GeoJSON/time-based.geojson", () => HttpResponse.json(timeBasedGeojson) ), http.get("test/GeoJSON/time-based-automatic-styles.geojson", () => HttpResponse.json(timeBasedAutomaticStylesGeojson) ), http.get("test/GeoJSON/height.geojson", () => HttpResponse.json(heightGeojson) ), http.get("test/GeoJSON/polyline.geojson", () => HttpResponse.json(polylineGeojson) ), http.get("test/GeoJSON/polygon.geojson", () => HttpResponse.json(polygonGeojson) ), http.get("test/GeoJSON/api.geojson", () => HttpResponse.json(apiGeojson)), http.get("test/GeoJSON/api-list.geojson", () => HttpResponse.json(apiListGeojson) ), http.get("test/GeoJSON/points.geojson", () => HttpResponse.json(pointsGeojson) ), http.get("test/GeoJSON/multipoint.geojson", () => HttpResponse.json(multipointGeojson) ), http.get("test/GeoJSON/multipolygon.geojson", () => HttpResponse.json(multipolygonGeojson) ), http.get("test/GeoJSON/empty-geoms.geojson", () => HttpResponse.json(emptyGeomsGeojson) ), http.get("test/KML/vic_police.kml", () => HttpResponse.text("this is not json") ) ); }); describe("- with cesium primitives", function () { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); geojson.setTrait(CommonStrata.user, "forceCesiumPrimitives", true); }); describe("GeoJsonCatalogItem", function () { it("handles features with null geom", async () => { geojson.setTrait(CommonStrata.user, "geoJsonData", { type: "FeatureCollection", features: [ { type: "Feature", properties: { LGA_CODE19: "19499", LGA_NAME19: "No usual address (NSW)", STE_CODE16: "1", STE_NAME16: "New South Wales", AREASQKM19: 0.0 }, geometry: null }, { type: "Feature", properties: {}, geometry: { type: "Polygon", coordinates: [ [ [144.806671142578125, -32.96258644191746], [145.008544921875, -33.19273094190691], [145.557861328125, -32.659031913817685], [145.042877197265625, -32.375322284319346], [144.7998046875, -32.96719522935591], [144.806671142578125, -32.96258644191746] ] ] } } ] }); await geojson.loadMapItems(); expect(geojson.readyData?.features.length).toBe(1); }); it("reloads when the URL is changed", async function () { geojson.setTrait( CommonStrata.user, "url", "data:application/json;base64,eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJwcm9wZXJ0aWVzIjp7fSwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlsxNDgsLTMxLjNdfX1dfQ==" ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ?.getValue(JulianDate.now()) ?.equalsEpsilon(Cartesian3.fromDegrees(148.0, -31.3), 0.0001) ).toBeTruthy("Doesn't match first location"); geojson.setTrait( CommonStrata.user, "url", "data:application/json;base64,eyJ0eXBlIjoiRmVhdHVyZUNvbGxlY3Rpb24iLCJmZWF0dXJlcyI6W3sidHlwZSI6IkZlYXR1cmUiLCJwcm9wZXJ0aWVzIjp7fSwiZ2VvbWV0cnkiOnsidHlwZSI6IlBvaW50IiwiY29vcmRpbmF0ZXMiOlsxNTEsLTMzLjhdfX1dfQ==" ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ?.getValue(JulianDate.now()) ?.equalsEpsilon(Cartesian3.fromDegrees(151.0, -33.8), 0.0001) ).toBeTruthy("Doesn't match updated location"); }); }); describe("loading in EPSG:28356", function () { it("works by URL", async function () { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/bike_racks.geojson" ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works by string", async function () { geojson.setTrait( CommonStrata.user, "geoJsonString", JSON.stringify(bikeRacksGeojson) ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works by data object", async function () { geojson.setTrait(CommonStrata.user, "geoJsonData", bikeRacksGeojson); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works with zip", async function () { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/bike_racks.zip" ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); /* it("have default dataUrl and dataUrlType", function() { geojson.updateFromJson({ url: "test/GeoJSON/bike_racks.geojson" }); expect(geojson.dataUrl).toBe("test/GeoJSON/bike_racks.geojson"); expect(geojson.dataUrlType).toBe("direct"); }) it("use provided dataUrl", async function() { geojson.url = "test/GeoJSON/bike_racks.geojson"; geojson.dataUrl = "test/test.html"; geojson.dataUrlType = "fake type"; await geojson.load(); expect(geojson.dataSource.entities.values.length).toBeGreaterThan(0); expect(geojson.dataUrl).toBe("test/test.html"); expect(geojson.dataUrlType).toBe("fake type"); }) it("works by blob", async function() { const blob = await loadBlob("test/GeoJSON/bike_racks.geojson"); geojson.data = blob; geojson.dataSourceUrl = "anything.geojson"; await geojson.load(); expect(geojson.dataSource.entities.values.length).toBeGreaterThan(0); }); */ }); describe("loading in CRS:84", function () { it("works by URL", async function () { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/cemeteries.geojson" ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works by string", async function () { geojson.setTrait( CommonStrata.user, "geoJsonString", JSON.stringify(cemeteriesGeojson) ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works by blob", async function () { geojson.setTrait(CommonStrata.user, "geoJsonData", cemeteriesGeojson); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works with zip", async function () { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/cemeteries.zip" ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); }); describe("loading without specified CRS (assumes EPSG:4326)", function () { it("works by URL", async function () { geojson.setTrait(CommonStrata.user, "url", "test/GeoJSON/gme.geojson"); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works by string", async function () { geojson.setTrait( CommonStrata.user, "geoJsonString", JSON.stringify(gmeGeojson) ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works by blob", async function () { geojson.setTrait(CommonStrata.user, "geoJsonData", gmeGeojson); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); it("works with zip", async function () { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/cemeteries.zip" ); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values.length ).toBeGreaterThan(0); expect( (geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position ).toBeDefined(); }); }); // describe('loading Esri-style GeoJSON with an "envelope"', function() { // it("works by URL", async function() { // geojson.setTrait( // CommonStrata.user, // "url", // "test/GeoJSON/EsriEnvelope.geojson" // ); // await geojson.loadMapItems(); // expect(geojson.mapItems.length).toEqual(1); // expect((geojson.mapItems[0] as GeoJsonDataSource).entities.values.length).toBeGreaterThan(0); // expect((geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position).toBeDefined(); // }); // // it("works by string", async function() { // const geojsonString = await loadText("test/GeoJSON/EsriEnvelope.geojson"); // geojson.setTrait(CommonStrata.user, "geoJsonString", geojsonString); // await geojson.loadMapItems(); // expect(geojson.mapItems.length).toEqual(1); // expect((geojson.mapItems[0] as GeoJsonDataSource).entities.values.length).toBeGreaterThan(0); // expect((geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position).toBeDefined(); // }); // // it("works by blob", async function() { // const blob = await loadJson("test/GeoJSON/EsriEnvelope.geojson"); // geojson.setTrait(CommonStrata.user, "geoJsonData", <JsonObject>blob); // await geojson.loadMapItems(); // expect(geojson.mapItems.length).toEqual(1); // expect((geojson.mapItems[0] as GeoJsonDataSource).entities.values.length).toBeGreaterThan(0); // expect((geojson.mapItems[0] as GeoJsonDataSource).entities.values[0].position).toBeDefined(); // }); // }); describe("error handling", function () { it("fails gracefully when the data at a URL is not JSON", async () => { geojson.setTrait(CommonStrata.user, "url", "test/KML/vic_police.kml"); const error = (await geojson.loadMapItems()).error; expect(error).toBeDefined("Load should not succeed"); }); it("fails gracefully when the provided string is not JSON", async () => { geojson.setTrait( CommonStrata.user, "geoJsonString", "this is not json" ); const error = (await geojson.loadMapItems()).error; expect(error).toBeDefined("Load should not succeed"); }); it("fails gracefully when the data at a URL is JSON but not GeoJSON", async () => { geojson.setTrait(CommonStrata.user, "url", "test/CZML/verysimple.czml"); const error = (await geojson.loadMapItems()).error; expect(error).toBeDefined("Load should not succeed"); }); it("fails gracefully when the provided string is JSON but not GeoJSON", async () => { geojson.setTrait( CommonStrata.user, "geoJsonString", JSON.stringify(verysimpleCzml) ); const error = (await geojson.loadMapItems()).error; expect(error).toBeDefined("Load should not succeed"); }); /* it("fails gracefully when the provided blob is JSON but not GeoJSON", function(done) { loadBlob("test/CZML/verysimple.czml").then(function(blob) { geojson.data = blob; geojson.dataSourceUrl = "anything.czml"; geojson .load() .then(function() { done.fail("Load should not succeed."); }) .catch(function(e) { expect(e instanceof TerriaError).toBe(true); done(); }); }); }); */ }); // describe("Adding and removing attribution", function() { // var currentViewer; // beforeEach(function() { // currentViewer = geojson.terria.currentViewer; // geojson.url = "test/GeoJSON/polygon.topojson"; // terria.disclaimerListener = function(member, callback) { // callback(); // }; // geojson.isEnabled = true; // }); // it("can add attribution", async function() { // spyOn(currentViewer, "addAttribution"); // geojson.load(); // await geojson._loadForEnablePromise; // expect(currentViewer.addAttribution).toHaveBeenCalled(); // }); // it("can remove attribution", async function() { // spyOn(currentViewer, "removeAttribution"); // geojson.load(); // await geojson._loadForEnablePromise; // geojson.isEnabled = false; // expect(currentViewer.removeAttribution).toHaveBeenCalled(); // }); // }); describe("Support for time-varying geojson", () => { it("Associates features with discrete times", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/time-based.geojson" ); geojson.setTrait(CommonStrata.user, "timeProperty", "year"); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); const entities = (geojson.mapItems[0] as GeoJsonDataSource).entities .values; expect(entities.length).toEqual(2); const entity1 = entities[0]; expect( entity1.availability?.start.equals( JulianDate.fromDate(new Date("2021")) ) ).toBeTruthy(); expect( entity1.availability?.stop.equals(Iso8601.MAXIMUM_VALUE) ).toBeTruthy(); const entity2 = entities[1]; expect( entity2.availability?.start.equals( JulianDate.fromDate(new Date("2019")) ) ).toBeTruthy(); expect( entity2.availability?.stop.equals( JulianDate.fromDate(new Date("2021")) ) ).toBeTruthy(); }); }); describe("Support for filterByProperties", () => { it("Filters correct features", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/time-based.geojson" ); geojson.setTrait(CommonStrata.user, "filterByProperties", { year: 2019 }); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); const entities = (geojson.mapItems[0] as GeoJsonDataSource).entities .values; expect(entities.length).toEqual(1); const entity1 = entities[0]; expect( entity1.properties?.getValue(terria.timelineClock.currentTime).year ).toBe(2019); }); }); describe("Support for geojson with extruded heights", () => { it("Sets polygon height properties correctly", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/height.geojson" ); geojson.setTrait(CommonStrata.user, "heightProperty", "someProperty"); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); const entities = (geojson.mapItems[0] as GeoJsonDataSource).entities .values; expect(entities.length).toEqual(2); const entity1 = entities[0]; expect( entity1.polygon?.extrudedHeight?.getValue( terria.timelineClock.currentTime ) ).toBe(10); expect( entity1.polygon?.heightReference?.getValue( terria.timelineClock.currentTime ) ).toBe(HeightReference.CLAMP_TO_GROUND); const entity2 = entities[1]; expect( entity2.polygon?.extrudedHeight?.getValue( terria.timelineClock.currentTime ) ).toBe(20); expect( entity2.polygon?.heightReference?.getValue( terria.timelineClock.currentTime ) ).toBe(HeightReference.CLAMP_TO_GROUND); expect( entity2.polygon?.extrudedHeightReference?.getValue( terria.timelineClock.currentTime ) ).toBe(HeightReference.RELATIVE_TO_GROUND); }); }); describe("Per features styling", function () { it("Applies styles to features according to their properties, respecting string case", async function () { const name = "PETER FAGANS GRAVE"; const fill = "#0000ff"; runInAction(() => { updateModelFromJson(geojson, CommonStrata.override, { name: "test", type: "geojson", url: "test/GeoJSON/cemeteries.geojson", perPropertyStyles: [ { properties: { NAME: name }, style: { fill: fill }, caseSensitive: true } ] }); }); await geojson.loadMapItems(); const entity = ( geojson.mapItems[0] as GeoJsonDataSource ).entities.values.find( (e) => e.properties?.getValue(JulianDate.now()).NAME === name ); expect(entity).toBeDefined(); expect(entity?.properties?.getValue(JulianDate.now())?.fill).toEqual( fill ); }); }); }); describe("- with CZML template", function () { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); }); describe("Support for czml templates", () => { it("supports points", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/points.geojson" ); geojson.setTrait(CommonStrata.user, "czmlTemplate", { cylinder: { length: { reference: "#properties.height" }, topRadius: { reference: "#properties.radius" }, bottomRadius: { reference: "#properties.radius" }, material: { solidColor: { color: { rgba: [0, 200, 0, 20] } } } } }); await geojson.loadMapItems(); const entities = (geojson.mapItems[0] as GeoJsonDataSource).entities .values; expect(entities.length).toEqual(5); const entity1 = entities[0]; expect( entity1.cylinder?.length?.getValue(terria.timelineClock.currentTime) ).toBe("10"); expect( entity1.cylinder?.bottomRadius?.getValue( terria.timelineClock.currentTime ) ).toBe("10"); expect(entity1.properties?.someOtherProp?.getValue()).toBe("what"); const entity2 = entities[1]; expect( entity2.cylinder?.length?.getValue(terria.timelineClock.currentTime) ).toBe("20"); expect( entity2.cylinder?.bottomRadius?.getValue( terria.timelineClock.currentTime ) ).toBe("5"); expect(entity2.properties?.someOtherProp?.getValue()).toBe("ok"); }); it("supports polygons", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/polygon.geojson" ); geojson.setTrait(CommonStrata.user, "czmlTemplate", { polygon: { height: 10, material: { solidColor: { color: { rgba: [0, 200, 0, 20] } } } } }); await geojson.loadMapItems(); const entities = (geojson.mapItems[0] as GeoJsonDataSource).entities .values; expect(entities.length).toEqual(1); const entity1 = entities[0]; expect( entity1.polygon?.height?.getValue(terria.timelineClock.currentTime) ).toBe(10); expect(entity1.properties?.foo?.getValue()).toBe("hi"); expect(entity1.properties?.bar?.getValue()).toBe("bye"); }); }); }); describe("- tablestyling - with geojson-vt and protomaps", function () { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); }); it("Creates ProtomapsImageryProvider - with table styles", async () => { geojson.setTrait(CommonStrata.user, "url", "test/GeoJSON/height.geojson"); await geojson.loadMapItems(); const mapItem = geojson.mapItems[0]; expect( "imageryProvider" in mapItem && mapItem.imageryProvider instanceof ProtomapsImageryProvider ).toBeTruthy(); const protomaps = "imageryProvider" in mapItem ? (mapItem.imageryProvider as ProtomapsImageryProvider) : undefined; if (!protomaps) throw "protomaps should be defined"; expect(geojson.activeStyle).toBe("someProperty"); expect(geojson.activeTableStyle.colorColumn?.name).toBe("someProperty"); expect(geojson.activeTableStyle.tableColorMap.minimumValue).toBe(10); expect(geojson.activeTableStyle.tableColorMap.maximumValue).toBe(20); expect( geojson.activeTableStyle.tableColorMap.colorMap instanceof ContinuousColorMap ).toBeTruthy(); const polygonSymbol = protomaps.paintRules[0] .symbolizer as PolygonSymbolizer; const polylineSymbol = protomaps.paintRules[1] .symbolizer as LineSymbolizer; const testFeature = { props: {}, geomType: GeomType.Polygon, numVertices: 0, geom: [], bbox: { minX: 0, minY: 0, maxX: 0, maxY: 0 } }; const rowIdToColor: [number, string][] = [ [0, "rgb(255,245,240)"], [1, "rgb(103,0,13)"] ]; rowIdToColor.forEach(([rowId, col]) => { expect( geojson.activeTableStyle.colorMap .mapValueToColor( geojson.activeTableStyle.colorColumn?.valuesForType?.[rowId] ) ?.toCssColorString() ).toBe(col); expect( polygonSymbol.fill.get(1, { ...testFeature, geomType: GeomType.Polygon, props: { _id_: rowId } }) ).toBe(col); expect( polygonSymbol.stroke.get(1, { ...testFeature, geomType: GeomType.Polygon, props: { _id_: rowId } }) ).toBe(getColor(terria.baseMapContrastColor).toCssHexString()); expect( polygonSymbol.width.get(1, { ...testFeature, geomType: GeomType.Polygon, props: { _id_: rowId } }) ).toBe( geojson.activeTableStyle.outlineStyleMap.traitValues.null.width ?? Infinity ); expect( polylineSymbol.color.get(1, { ...testFeature, geomType: GeomType.Line, props: { _id_: rowId } }) ).toBe(col); }); }); it("Creates table features for points", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/bike_racks.geojson" ); await geojson.loadMapItems(); const mapItem = geojson.mapItems[0] as CustomDataSource; expect(isDataSource(mapItem)).toBeTruthy(); expect(geojson.activeStyle).toBe("number_of_"); expect(geojson.activeTableStyle.colorColumn?.name).toBe("number_of_"); expect(geojson.activeTableStyle.tableColorMap.minimumValue).toBe(0); expect(geojson.activeTableStyle.tableColorMap.maximumValue).toBe(20); expect( geojson.activeTableStyle.tableColorMap.colorMap instanceof ContinuousColorMap ).toBeTruthy(); // Test some colors expect( mapItem.entities.values[10].point?.color ?.getValue(terria.timelineClock.currentTime) ?.toCssColorString() ).toBe("rgb(254,227,214)"); expect( mapItem.entities.values[20].point?.color ?.getValue(terria.timelineClock.currentTime) ?.toCssColorString() ).toBe("rgb(103,0,13)"); expect(geojson.disableSplitter).toBeTruthy(); }); it("Supports LegendOwnerTraits to override TableMixin.legends", async () => { geojson.setTrait(CommonStrata.user, "url", "test/GeoJSON/height.geojson"); await geojson.loadMapItems(); expect( "imageryProvider" in geojson.mapItems[0] && geojson.mapItems[0].imageryProvider instanceof ProtomapsImageryProvider ).toBeTruthy(); expect(geojson.legends.length).toBe(1); expect(geojson.legends[0].items.map((i) => i.color)).toEqual([ "rgb(103,0,13)", "rgb(176,18,24)", "rgb(226,48,40)", "rgb(249,105,76)", "rgb(252,160,130)", "rgb(253,211,193)", "rgb(255,245,240)" ]); runInAction(() => updateModelFromJson(geojson, CommonStrata.definition, { legends: [ { url: "some-url" } ] }) ); expect(geojson.legends.length).toBe(1); expect(geojson.legends[0].url).toBe("some-url"); }); it("Supports LegendOwnerTraits to override TableMixin.legends - with style disabled", async () => { geojson.setTrait(CommonStrata.user, "url", "test/GeoJSON/height.geojson"); await geojson.loadMapItems(); runInAction(() => { expect( "imageryProvider" in geojson.mapItems[0] && geojson.mapItems[0].imageryProvider instanceof ProtomapsImageryProvider ).toBeTruthy(); geojson.setTrait("user", "activeStyle", ""); expect(geojson.legends.length).toBe(1); expect(geojson.legends[0].items.length).toBe(1); expect( geojson.legends[0].items.map( (i) => // Look through default colors for disabled styles // This can change, as `createColorForIdTransformer` will use the least used color (to avoid clashes) !!StandardCssColors.modifiedBrewer8ClassSet2.find( (c) => Color.fromCssColorString(c).toCssColorString() === i.color ) ) ).toEqual([true]); updateModelFromJson(geojson, CommonStrata.definition, { legends: [ { url: "some-url" } ] }); expect(geojson.legends.length).toBe(1); expect(geojson.legends[0].url).toBe("some-url"); }); }); it("correctly builds `Feature` from picked Entity", function () { const picked = new Entity(); const feature = geojson.buildFeatureFromPickResult( Cartesian2.ZERO, picked ); expect(feature).toBeDefined(); if (feature) { expect(feature.cesiumEntity).toBe(picked); } }); }); describe("Disables protomaps (mvt) if geoJson simple styling is detected", () => { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); }); it("Unchanged - less than 50% features detected", async () => { geojson.setTrait(CommonStrata.user, "url", "test/GeoJSON/api.geojson"); geojson.setTrait( CommonStrata.user, "geoJsonString", `{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"stroke":"#555555","stroke-width":2,"stroke-opacity":1,"fill":"#ff0051","fill-opacity":0.5},"geometry":{"type":"Polygon","coordinates":[[[35.859375,53.54030739150022],[11.25,40.17887331434696],[15.1171875,14.604847155053898],[53.4375,44.84029065139799],[35.859375,53.54030739150022]]]}},{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[85.4296875,66.93006025862448],[53.4375,43.83452678223682],[89.296875,34.88593094075317],[91.40625,50.958426723359935],[85.4296875,66.93006025862448]]]}},{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[119.17968749999999,66.79190947341796],[100.1953125,53.74871079689897],[109.3359375,47.517200697839414],[119.17968749999999,66.79190947341796]]]}},{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[71.015625,-2.811371193331128],[99.49218749999999,-2.811371193331128],[99.49218749999999,18.646245142670608],[71.015625,18.646245142670608],[71.015625,-2.811371193331128]]]}},{"type":"Feature","properties":{},"geometry":{"type":"LineString","coordinates":[[140.9765625,19.642587534013032],[134.296875,-17.978733095556155],[88.9453125,-36.597889133070204],[119.53125,15.961329081596647],[130.078125,27.371767300523047]]}}]}` ); await geojson.loadMapItems(); expect(geojson.mapItems[0] instanceof GeoJsonDataSource).toBeFalsy(); expect(geojson.useTableStylingAndProtomaps).toBeTruthy(); expect(geojson.legends.length).toBe(1); expect(geojson.disableSplitter).toBeFalsy(); }); it("Disabled protomaps - More than 50% features detected", async () => { geojson.setTrait(CommonStrata.user, "url", "test/GeoJSON/api.geojson"); geojson.setTrait( CommonStrata.user, "geoJsonString", `{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"stroke":"#555555","stroke-width":2,"stroke-opacity":1,"fill":"#ff0051","fill-opacity":0.5},"geometry":{"type":"Polygon","coordinates":[[[35.859375,53.54030739150022],[11.25,40.17887331434696],[15.1171875,14.604847155053898],[53.4375,44.84029065139799],[35.859375,53.54030739150022]]]}},{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[85.4296875,66.93006025862448],[53.4375,43.83452678223682],[89.296875,34.88593094075317],[91.40625,50.958426723359935],[85.4296875,66.93006025862448]]]}},{"type":"Feature","properties":{},"geometry":{"type":"Polygon","coordinates":[[[119.17968749999999,66.79190947341796],[100.1953125,53.74871079689897],[109.3359375,47.517200697839414],[119.17968749999999,66.79190947341796]]]}},{"type":"Feature","properties":{"marker-color":"#e000ff","marker-size":"large"},"geometry":{"type":"Point","coordinates":[30.585937499999996,-2.108898659243126]}},{"type":"Feature","properties":{"stroke":"#feff00","stroke-width":4,"stroke-opacity":1,"fill":"#fcffff","fill-opacity":0.5},"geometry":{"type":"Polygon","coordinates":[[[71.015625,-2.811371193331128],[99.49218749999999,-2.811371193331128],[99.49218749999999,18.646245142670608],[71.015625,18.646245142670608],[71.015625,-2.811371193331128]]]}},{"type":"Feature","properties":{"stroke":"#00ff02","stroke-width":2,"stroke-opacity":1},"geometry":{"type":"LineString","coordinates":[[140.9765625,19.642587534013032],[134.296875,-17.978733095556155],[88.9453125,-36.597889133070204],[119.53125,15.961329081596647],[130.078125,27.371767300523047]]}}]}` ); await geojson.loadMapItems(); expect(geojson.mapItems[0] instanceof GeoJsonDataSource).toBeTruthy(); expect(geojson.useTableStylingAndProtomaps).toBeFalsy(); expect(geojson.legends.length).toBe(0); expect(geojson.disableSplitter).toBeTruthy(); }); it("correctly matches feature _id_ with table rowId - with features with empty geoms", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/empty-geoms.geojson" ); await geojson.loadMapItems(); expect(geojson.readyData?.features.length).toBe(4); // Check _id_ vs rowIds expect( geojson.readyData?.features.map((f) => f.properties?.[FEATURE_ID_PROP]) ).toEqual(geojson.rowIds); // Check "someOtherProp" column expect( geojson.readyData?.features.map((f) => f.properties?.someOtherProp) ).toEqual( geojson.tableColumns.find((c) => c.name === "someOtherProp") ?.values as string[] ); }); it("correctly matches feature _id_ with table rowId - with filterByProperties", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/time-based.geojson" ); geojson.setTrait(CommonStrata.user, "filterByProperties", { year: 2019 }); await geojson.loadMapItems(); expect(geojson.readyData?.features.length).toBe(1); expect( geojson.readyData?.features.map((f) => f.properties?.[FEATURE_ID_PROP]) ).toEqual(geojson.rowIds); }); it("supports time", async function () { geojson.setTrait( CommonStrata.definition, "url", "test/GeoJSON/time-based-automatic-styles.geojson" ); updateModelFromJson(geojson, CommonStrata.definition, { currentTime: "2018-01-01", defaultStyle: { time: { timeColumn: "date", idColumns: ["idProperty"] } } }); const observeMapItems = reaction( () => [geojson.mapItems], () => {} ); (await geojson.loadMapItems()).throwIfError(); expect(geojson.activeTableStyle.timeColumn?.name).toBe("date"); const firstProtomapsImageryProvider = "imageryProvider" in geojson.mapItems[0] ? (geojson.mapItems[0].imageryProvider as ProtomapsImageryProvider) : undefined; if (!firstProtomapsImageryProvider) throw "protomaps should be defined"; const testFeature = { props: {}, geomType: GeomType.Polygon, numVertices: 0, geom: [], bbox: { minX: 0, minY: 0, maxX: 0, maxY: 0 } }; const firstFilter = firstProtomapsImageryProvider.paintRules[0].filter; if (!firstFilter) { throw "filter should be defined"; } // Current time is 2018-01-01 // First feature maps to 2018-01-01 testFeature.props = { [FEATURE_ID_PROP]: 0 }; expect(firstFilter(0, testFeature)).toBeTruthy(); // Second feature maps to 2019-01-01 testFeature.props = { [FEATURE_ID_PROP]: 1 }; expect(firstFilter(0, testFeature)).toBeFalsy(); // Change time to 2019-01-01 geojson.setTrait(CommonStrata.definition, "currentTime", "2019-01-01"); // Check new imagery provider const nextProtomapsImageryProvider = "imageryProvider" in geojson.mapItems[0] ? (geojson.mapItems[0].imageryProvider as ProtomapsImageryProvider) : undefined; if (!nextProtomapsImageryProvider) throw "protomaps should be defined"; const nextFilter = nextProtomapsImageryProvider.paintRules[0].filter; if (!nextFilter) { throw "filter should be defined"; } testFeature.props = { [FEATURE_ID_PROP]: 0 }; expect(nextFilter(0, testFeature)).toBeFalsy(); testFeature.props = { [FEATURE_ID_PROP]: 1 }; expect(nextFilter(0, testFeature)).toBeTruthy(); expect( firstProtomapsImageryProvider === nextProtomapsImageryProvider ).toBeFalsy(); // Now change the currentTime to 2019- g01-02 - this should not trigger a new imagery provider - as it within the current time interval geojson.setTrait(CommonStrata.definition, "currentTime", "2019-01-02"); // Check new imagery provider const lastProtomapsImageryProvider = "imageryProvider" in geojson.mapItems[0] ? (geojson.mapItems[0].imageryProvider as ProtomapsImageryProvider) : undefined; if (!lastProtomapsImageryProvider) throw "protomaps should be defined"; expect( nextProtomapsImageryProvider === lastProtomapsImageryProvider ).toBeTruthy(); observeMapItems(); }); }); describe("Support geojson through apis", () => { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); }); it("Extracts geojson nested in a json object", async () => { geojson.setTrait(CommonStrata.user, "url", "test/GeoJSON/api.geojson"); geojson.setTrait(CommonStrata.user, "responseDataPath", "nested.data"); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); }); it("Extracts geojson from an array of json objects", async () => { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/api-list.geojson" ); geojson.setTrait(CommonStrata.user, "responseGeoJsonPath", "nested.data"); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); }); }); describe("When given multiple URLs", function () { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); geojson.setTrait(CommonStrata.user, "forceCesiumPrimitives", true); }); it("fetches and merges the responses as a single geojson feature collection", async function () { updateModelFromJson(geojson, CommonStrata.user, { urls: [ { url: "test/GeoJSON/api.geojson", responseDataPath: "nested.data" }, { url: "test/GeoJSON/points.geojson" } ] }); await geojson.loadMapItems(); expect(geojson.mapItems.length).toEqual(1); const mapItem = geojson.mapItems[0] as CustomDataSource; expect(isDataSource(mapItem)).toBeTruthy(); expect(mapItem.entities.values.length).toBe(7); }); }); describe("handling MultiPoint features", function () { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/multipoint.geojson" ); }); it("explodes multipoints as points", async function () { await geojson.loadMapItems(); const points = geojson.mapItems[0] as CustomDataSource; expect(points).toBeDefined(); expect(isDataSource(points)).toBeTruthy(); expect(points.entities.values.length).toEqual(5); }); }); describe("handling MultiPolygon features", function () { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/multipolygon.geojson" ); }); it("creates multi-polygon cesium primitives features", async function () { runInAction(() => geojson.setTrait(CommonStrata.user, "forceCesiumPrimitives", true) ); await geojson.loadMapItems(); const features = geojson.mapItems[0] as CustomDataSource; expect(features).toBeDefined(); expect(isDataSource(features)).toBeTruthy(); expect(features.entities.values.length).toEqual(5); expect(features.entities.values[0].polygon).toBeDefined(); expect(features.entities.values[1].polygon).toBeDefined(); expect(features.entities.values[2].polygon).toBeDefined(); expect(features.entities.values[3].polyline).toBeDefined(); expect(features.entities.values[4].polyline).toBeDefined(); }); }); describe("ProtomapsGeojsonSource", function () { let terria: Terria; let geojson: GeoJsonCatalogItem; beforeEach(function () { terria = new Terria({ baseUrl: "./" }); geojson = new GeoJsonCatalogItem("test-geojson", terria); }); it("creates multi-polygon protomaps features", async function () { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/multipolygon.geojson" ); geojson.setTrait(CommonStrata.user, "forceCesiumPrimitives", false); await geojson.loadMapItems(); const imageryParts = geojson.mapItems[0] as ImageryParts; expect(imageryParts).toBeDefined(); expect(ImageryParts.is(imageryParts)).toBeTruthy(); const imageryProvider = imageryParts.imageryProvider as ProtomapsImageryProvider; expect(imageryProvider instanceof ProtomapsImageryProvider).toBeTruthy(); const source = imageryProvider.source as ProtomapsGeojsonSource; expect(source instanceof ProtomapsGeojsonSource).toBeTruthy(); const features = await source.get( { x: 23, y: 35, z: 6 }, 256 ); expect(features.get(GEOJSON_SOURCE_LAYER_NAME)?.length).toEqual(1); const feature = features.get(GEOJSON_SOURCE_LAYER_NAME)?.[0]; expect(feature?.geom.length).toEqual(4); expect(feature?.bbox).toEqual({ maxX: 288, maxY: 204.3125, minX: -32, minY: -32 }); }); it("creates polygon protomaps features", async function () { geojson.setTrait( CommonStrata.user, "url", "test/GeoJSON/polygon.geojson" ); await geojson.loadMapItems(); const imageryParts = geojson.mapItems[0] as ImageryParts; expect(imageryParts).toBeDefined(); expect(ImageryParts.is(imageryParts)).toBeTruthy(); const imageryProvider = imageryParts.imageryProvider as ProtomapsImageryProvider; expect(imageryProvider instanceof ProtomapsImageryProvider).toBeTruthy(); const source = imageryProvider.source as ProtomapsGeojsonSource; expect(source instanceof ProtomapsGeojsonSource).toBeTruthy(); const features = await sour