import i18next from "i18next"; import { configure, runInAction } from "mobx"; import { http, HttpResponse, passthrough } from "msw"; import { GeomType } from "protomaps-leaflet"; import Color from "terriajs-cesium/Source/Core/Color"; import Request from "terriajs-cesium/Source/Core/Request"; import ProtomapsImageryProvider from "../../../../lib/Map/ImageryProvider/ProtomapsImageryProvider"; import { ProtomapsArcGisPbfSource } from "../../../../lib/Map/Vector/Protomaps/ProtomapsArcGisPbfSource"; import { GEOJSON_SOURCE_LAYER_NAME } from "../../../../lib/Map/Vector/Protomaps/ProtomapsGeojsonSource"; import { ImageryParts, isDataSource } from "../../../../lib/ModelMixins/MappableMixin"; import ArcGisFeatureServerCatalogItem from "../../../../lib/Models/Catalog/Esri/ArcGisFeatureServerCatalogItem"; import { convertEsriPointSizeToPixels } from "../../../../lib/Models/Catalog/Esri/esriStyleToTableStyle"; import CommonStrata from "../../../../lib/Models/Definition/CommonStrata"; import createStratumInstance from "../../../../lib/Models/Definition/createStratumInstance"; import Terria from "../../../../lib/Models/Terria"; import TableColumnType from "../../../../lib/Table/TableColumnType"; import TablePointStyleTraits, { PointSymbolTraits } from "../../../../lib/Traits/TraitsClasses/Table/PointStyleTraits"; import TableStyleTraits from "../../../../lib/Traits/TraitsClasses/Table/StyleTraits"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import { worker } from "../../../mocks/browser"; import waterNetwork2Json from "../../../../wwwroot/test/ArcGisFeatureServer/Water_Network/2.json"; import waterNetworkLayerJson from "../../../../wwwroot/test/ArcGisFeatureServer/Water_Network/layer.json"; import parks3Json from "../../../../wwwroot/test/ArcGisFeatureServer/Parks/3.json"; import parksLayerJson from "../../../../wwwroot/test/ArcGisFeatureServer/Parks/layer.json"; import stylesLinesJson from "../../../../wwwroot/test/ArcGisFeatureServer/styles/lines.json"; import stylesLayerJson from "../../../../wwwroot/test/ArcGisFeatureServer/styles/layer.json"; import tiledActVegMapJson from "../../../../wwwroot/test/ArcGisFeatureServer/Tiled/act-veg-map.json"; import waterNetworkMulti2Json from "../../../../wwwroot/test/ArcGisFeatureServer/Water_Network_Multi/2.json"; import waterNetworkMultiLayerJson from "../../../../wwwroot/test/ArcGisFeatureServer/Water_Network_Multi/layer.json"; import waterNetworkMultiLayerBJson from "../../../../wwwroot/test/ArcGisFeatureServer/Water_Network_Multi/layerB.json"; import tilePbfPath from "../../../../wwwroot/test/ArcGisFeatureServer/Tiled/test-tile.pbf"; import featurePickGeojson from "../../../../wwwroot/test/ArcGisFeatureServer/Tiled/feature-pick.geojson" with { type: "json" }; configure({ enforceActions: "observed", computedRequiresReaction: true }); describe("ArcGisFeatureServerCatalogItem", function () { const featureServerUrl = "http://example.com/arcgis/rest/services/Water_Network/FeatureServer/2"; const featureServerUrl2 = "http://example.com/arcgis/rest/services/Parks/FeatureServer/3"; const featureServerUrlStyleLines = "http://example.com/arcgis/rest/services/styles/FeatureServer/0"; const featureServerUrlTiled = "http://example.com/arcgis/rest/services/tiled/FeatureServer/0"; const featureServerUrlMulti = "http://example.com/arcgis/rest/services/Water_Network_Multi/FeatureServer/2"; let terria: Terria; let item: ArcGisFeatureServerCatalogItem; let multiCallCount: number; let tilePbfBuffer: ArrayBuffer; beforeAll(async function () { worker.use( http.get(/\.pbf$/, () => { return passthrough(); }) ); const [pbfRes] = await Promise.all([fetch(tilePbfPath)]); tilePbfBuffer = await pbfRes.arrayBuffer(); }); beforeEach(function () { terria = new Terria({ baseUrl: "./" }); item = new ArcGisFeatureServerCatalogItem("test", terria); multiCallCount = 0; worker.use( // Water_Network FeatureServer - layer query (geojson) http.get( "http://example.com/arcgis/rest/services/Water_Network/FeatureServer/2/query", () => HttpResponse.json(waterNetworkLayerJson) ), // Water_Network FeatureServer - metadata http.get( "http://example.com/arcgis/rest/services/Water_Network/FeatureServer/2", () => HttpResponse.json(waterNetwork2Json) ), // Parks FeatureServer - layer query (geojson) http.get( "http://example.com/arcgis/rest/services/Parks/FeatureServer/3/query", () => HttpResponse.json(parksLayerJson) ), // Parks FeatureServer - metadata http.get( "http://example.com/arcgis/rest/services/Parks/FeatureServer/3", () => HttpResponse.json(parks3Json) ), // styles FeatureServer - layer query (geojson) http.get( "http://example.com/arcgis/rest/services/styles/FeatureServer/0/query", () => HttpResponse.json(stylesLayerJson) ), // styles FeatureServer - metadata http.get( "http://example.com/arcgis/rest/services/styles/FeatureServer/0", () => HttpResponse.json(stylesLinesJson) ), // Tiled FeatureServer - pick feature request (query/query path) http.get( "http://example.com/arcgis/rest/services/tiled/FeatureServer/0/query/query", () => HttpResponse.json(featurePickGeojson) ), // Tiled FeatureServer - tile request (query path, returns PBF) http.get( "http://example.com/arcgis/rest/services/tiled/FeatureServer/0/query/", () => new HttpResponse(tilePbfBuffer, { headers: { "Content-Type": "application/x-protobuf" } }) ), // Tiled FeatureServer - metadata http.get( "http://example.com/arcgis/rest/services/tiled/FeatureServer/0", () => HttpResponse.json(tiledActVegMapJson) ), // Water_Network_Multi FeatureServer - layer query (geojson) - stateful http.get( "http://example.com/arcgis/rest/services/Water_Network_Multi/FeatureServer/2/query", () => { multiCallCount++; if (multiCallCount > 1) { return HttpResponse.json(waterNetworkMultiLayerBJson); } return HttpResponse.json(waterNetworkMultiLayerJson); } ), // Water_Network_Multi FeatureServer - metadata http.get( "http://example.com/arcgis/rest/services/Water_Network_Multi/FeatureServer/2", () => HttpResponse.json(waterNetworkMulti2Json) ) ); }); it("has a type and typeName", function () { expect(item.type).toBe("esri-featureServer"); expect(item.typeName).toBe( i18next.t("models.arcGisFeatureServerCatalogItem.name") ); }); it("supports show info", function () { expect(item.disableAboutData).toBeFalsy(); }); describe("when token is set", function () { beforeEach(() => { runInAction(() => { item.setTrait("definition", "url", featureServerUrl); item.setTrait(CommonStrata.definition, "token", "some-token-in-config"); }); }); it("adds the token to metadata request", async function () { worker.use( http.get( "http://example.com/arcgis/rest/services/Water_Network/FeatureServer/2", ({ request }) => { if ( new URL(request.url).searchParams.get("token") !== "some-token-in-config" ) return HttpResponse.error(); return HttpResponse.json(waterNetwork2Json); } ) ); await item.loadMapItems(); expect(item.rectangle).toBeDefined(); }); }); describe("after loading metadata", function () { beforeEach(async function () { runInAction(() => { item.setTrait("definition", "url", featureServerUrl); }); await item.loadMetadata(); }); it("defines a rectangle", function () { expect(item.rectangle).toBeDefined(); if (item.rectangle) { expect(item.rectangle.west).toEqual(-179.999987937519); expect(item.rectangle.south).toEqual(-55.90222504885724); expect(item.rectangle.east).toEqual(179.999987937519); expect(item.rectangle.north).toEqual(81.29054454173075); } }); it("defines min/max scale denominator", function () { expect(item.minScaleDenominator).toEqual(0); expect(item.maxScaleDenominator).toEqual(0); }); it("supports zooming to extent", function () { expect(item.disableZoomTo).toBeFalsy(); }); it("defines info", function () { const dataDescription = i18next.t( "models.arcGisMapServerCatalogItem.dataDescription" ); const copyrightText = i18next.t( "models.arcGisMapServerCatalogItem.copyrightText" ); expect(item.info.map(({ name }) => name)).toEqual([ dataDescription, copyrightText ]); expect(item.info.map(({ content }) => content)).toEqual([ "Water Data", "Water Copyright" ]); }); }); describe("after loading metadata - layer with min/max extent", function () { beforeEach(async function () { runInAction(() => { item.setTrait("definition", "url", featureServerUrl2); }); await item.loadMetadata(); }); it("defines a rectangle", function () { expect(item.rectangle).toBeDefined(); if (item.rectangle) { expect(item.rectangle.west).toEqual(150.01467801477867); expect(item.rectangle.south).toEqual(-34.33741590494015); expect(item.rectangle.east).toEqual(151.34389995591835); expect(item.rectangle.north).toEqual(-32.99606922215397); } }); it("defines min/max scale denominator", function () { expect(item.minScaleDenominator).toEqual(36111); expect(item.maxScaleDenominator).toEqual(18489298); }); it("for tiled services - limits imagery provider using MinMaxLevelMixin", async function () { runInAction(() => { item.setTrait("definition", "tileRequests", true); }); expect("imageryProvider" in item.mapItems[0]).toBeTruthy(); if ("imageryProvider" in item.mapItems[0]) { const imageryProvider = item.mapItems[0] .imageryProvider as ProtomapsImageryProvider; expect( imageryProvider.source instanceof ProtomapsArcGisPbfSource ).toBeTruthy(); expect(imageryProvider.minimumLevel).toEqual(0); expect(imageryProvider.softMinimumLevel).toEqual(5); expect(imageryProvider.maximumLevel).toEqual(14); // Expect image to be empty const test = await imageryProvider.requestImage(0, 0, 0, new Request()); expect(test.width).toEqual(1); expect(test.height).toEqual(1); } }); }); describe("loadMapItems - geojson query", function () { it("uses the token in url when loading feature server", async function () { runInAction(() => { item.setTrait("definition", "url", featureServerUrl); item.setTrait(CommonStrata.definition, "maxFeatures", 20); item.setTrait(CommonStrata.definition, "tileRequests", false); item.setTrait("definition", "token", "some-token-in-config"); }); worker.use( http.get( "http://example.com/arcgis/rest/services/Water_Network/FeatureServer/2/query", ({ request }) => { if ( !new URL(request.url).searchParams .get("token") ?.includes("some-token-in-config") ) return HttpResponse.error(); return HttpResponse.json(waterNetworkLayerJson); } ) ); await item.loadMapItems(); expect(item.mapItems.length).toEqual(1); }); it("properly loads a single layer", async function () { runInAction(() => { item.setTrait(CommonStrata.definition, "url", featureServerUrl); item.setTrait(CommonStrata.definition, "maxFeatures", 20); item.setTrait(CommonStrata.definition, "tileRequests", false); }); await item.loadMapItems(); expect(item.mapItems.length).toEqual(1); const mapItem = item.mapItems[0]; expect(isDataSource(mapItem)).toBeTruthy(); expect( isDataSource(mapItem) ? mapItem.entities.values.length : 0 ).toEqual(13); }); it("properly loads a single layer with multiple requests", async function () { runInAction(() => { item.setTrait(CommonStrata.definition, "url", featureServerUrlMulti); item.setTrait(CommonStrata.definition, "featuresPerRequest", 10); item.setTrait(CommonStrata.definition, "tileRequests", false); }); await item.loadMapItems(); expect(item.mapItems.length).toEqual(1); const mapItem = item.mapItems[0]; expect(isDataSource(mapItem)).toBeTruthy(); expect( isDataSource(mapItem) ? mapItem.entities.values.length : 0 ).toEqual(13); }); }); describe("loadMapItems - tiled query", function () { it("properly loads a layer", async function () { runInAction(() => { item.setTrait(CommonStrata.definition, "url", featureServerUrlTiled); }); await item.loadMapItems(); expect(item.tileRequests).toBeTruthy(); expect(item.maxTiledFeatures).toEqual(100000); expect(item.featuresPerTileRequest).toEqual(4000); expect(item.maxRecordCountFactor).toEqual(1); expect(item.supportsQuantization).toEqual(true); expect(item.objectIdField).toEqual("OBJECTID"); expect(item.outFields).toEqual(["OBJECTID", "class"]); expect(item.mapItems.length).toEqual(1); const mapItem = item.mapItems[0]; expect("imageryProvider" in mapItem).toBeTruthy(); if ("imageryProvider" in mapItem) { const imageryProvider = mapItem.imageryProvider as ProtomapsImageryProvider; expect( imageryProvider.source instanceof ProtomapsArcGisPbfSource ).toBeTruthy(); } expect(item.dataColumnMajor).toEqual([ ["OBJECTID"], ["GlobalID"], ["actGUID"], ["vegCommunity"], ["umcID"], ["tecACT"], ["tecEPBC"], ["tecID"], ["actConservationStatus"], ["nationalConservationStatus"], ["treesp1"], ["treesp2"], ["treesp3"], ["shrubsp1"], ["shrubsp2"], ["shrubsp3"], ["groundsp1"], ["groundsp2"], ["groundsp3"], ["hectares"], ["height_mean"], ["height_stdev"], ["canopyCover"], ["underCover"], ["structure"], ["formation"], ["class"], ["grassyStructure"], ["lastVegCommunity"], ["landscape"], ["lossReason"], ["lossYear"], ["Shape__Area"], ["Shape__Length"] ]); expect(item.activeTableStyle.colorColumn?.name).toEqual("class"); expect(item.activeTableStyle.colorColumn?.type).toEqual( TableColumnType.text ); expect(item.activeTableStyle.tableColorMap.type).toEqual("enum"); }); describe("ProtomapsImageryProvider - ArcGisPbfSource", function () { it("fetch tile - with single request", async function () { runInAction(() => { item.setTrait(CommonStrata.definition, "url", featureServerUrlTiled); }); await item.loadMapItems(); const mapItem = item.mapItems[0]; expect("imageryProvider" in mapItem).toBeTruthy(); if ("imageryProvider" in mapItem) { const imageryProvider = mapItem.imageryProvider as ProtomapsImageryProvider; expect( imageryProvider.source instanceof ProtomapsArcGisPbfSource ).toBeTruthy(); const source = imageryProvider.source as ProtomapsArcGisPbfSource; // Fetch a tile - this tile will return a single polygon const tileFeatures = await source.get( { x: 241014, y: 157088, z: 18 }, 256 ); const features = tileFeatures.get(GEOJSON_SOURCE_LAYER_NAME); expect(features?.length).toEqual(1); expect(features?.[0].props.OBJECTID).toEqual(1003937); expect(features?.[0].geomType).toEqual(GeomType.Polygon); } }); it("fetch tile - with token", async function () { runInAction(() => { item.setTrait(CommonStrata.definition, "url", featureServerUrlTiled); item.setTrait( CommonStrata.definition, "token", "some-token-in-config" ); }); worker.use( http.get( "http://example.com/arcgis/rest/services/tiled/FeatureServer/0/query/", ({ request }) => { if ( !new URL(request.url).searchParams .get("token") ?.includes("some-token-in-config") ) return HttpResponse.error(); return new HttpResponse(tilePbfBuffer, { headers: { "Content-Type": "application/x-protobuf" } }); } ) ); await item.loadMapItems(); const mapItem = item.mapItems[0]; expect("imageryProvider" in mapItem).toBeTruthy(); if ("imageryProvider" in mapItem) { const imageryProvider = mapItem.imageryProvider as ProtomapsImageryProvider; const source = imageryProvider.source as ProtomapsArcGisPbfSource; // Fetch a tile - this tile will return a single polygon const tileFeatures = await source.get( { x: 241014, y: 157088, z: 18 }, 256 ); const features = tileFeatures.get(GEOJSON_SOURCE_LAYER_NAME); expect(features?.length).toEqual(1); } }); it("fetch tile - with multiple requests", async function () { runInAction(() => { item.setTrait(CommonStrata.definition, "url", featureServerUrlTiled); // set feature per tile request to 1 - this will trigger a second request // Due to how requests are mocked, still will load the same tile twice - so we will get 2 identical features item.setTrait(CommonStrata.definition, "featuresPerTileRequest", 1); item.setTrait(CommonStrata.definition, "maxTiledFeatures", 2); }); let tileRequestCount = 0; worker.use( http.get( "http://example.com/arcgis/rest/services/tiled/FeatureServer/0/query/", ({ request }) => { const params = new URL(request.url).searchParams; const expectedOffset = String(tileRequestCount); tileRequestCount++; if (params.get("resultOffset") !== expectedOffset) return HttpResponse.error(); if (params.get("resultRecordCount") !== "1") return HttpResponse.error(); return new HttpResponse(tilePbfBuffer, { headers: { "Content-Type": "application/x-protobuf" } }); } ) ); await item.loadMapItems(); const mapItem = item.mapItems[0]; expect("imageryProvider" in mapItem).toBeTruthy(); if ("imageryProvider" in mapItem) { const imageryProvider = mapItem.imageryProvider as ProtomapsImageryProvider; expect( imageryProvider.source instanceof ProtomapsArcGisPbfSource ).toBeTruthy(); const source = imageryProvider.source as ProtomapsArcGisPbfSource; // Fetch a tile - this tile will return a single polygon const tileFeatures = await source.get( { x: 241014, y: 157088, z: 18 }, 256 ); const features = tileFeatures.get(GEOJSON_SOURCE_LAYER_NAME); expect(features?.length).toEqual(2); expect(features?.[0].props.OBJECTID).toEqual(1003937); expect(features?.[0].geomType).toEqual(GeomType.Polygon); expect(features?.[1].props.OBJECTID).toEqual(1003937); expect(features?.[1].geomType).toEqual(GeomType.Polygon); } }); it("pick feature", async function () { runInAction(() => { item.setTrait(CommonStrata.definition, "url", featureServerUrlTiled); }); await item.loadMapItems(); expect(item.allowFeaturePicking).toBeTruthy(); const mapItem = item.mapItems[0]; expect("imageryProvider" in mapItem).toBeTruthy(); if ("imageryProvider" in mapItem) { const imageryProvider = mapItem.imageryProvider as ProtomapsImageryProvider; expect( imageryProvider.source instanceof ProtomapsArcGisPbfSource ).toBeTruthy(); const source = imageryProvider.source as ProtomapsArcGisPbfSource; // Fetch a tile - this tile will return a single polygon const pickedFeatures = await source.pickFeatures( 0, 0, 18, 2.635084429672604, -0.5866868013895533 ); expect(pickedFeatures?.length).toEqual(1); const feature = pickedFeatures?.[0]; expect(feature.description?.startsWith("