From 881b779103b4ab1d36ccfb696d960c5dea6b15c6 Mon Sep 17 00:00:00 2001 From: Mike Wu <41275384+mwu2018@users.noreply.github.com> Date: Thu, 14 Sep 2023 14:48:44 +1000 Subject: [PATCH] Support time window query (#6872) * Add time interval traits. * refactor * refactor * Add unit tests. * Refactor * Refactor. Not to specify current time in model dimensions. * Update doc. * Update doc. * Update based on review. * Refactor based on review. * Simplified based on review. * Remove unwanted import. * Update doc. * Refactor * Remove unwanted import. * Rename trait. * Minor refactoring. * Refactor based on review. * Refactor to make use of @compute. --- CHANGES.md | 1 + .../Esri/ArcGisMapServerCatalogItem.ts | 101 +++++++++++-- .../ArcGisMapServerCatalogItemTraits.ts | 24 +++ .../esri/ArcGisMapServerCatalogItemSpec.ts | 143 +++++++++++++++++- 4 files changed, 256 insertions(+), 13 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b4688010dc5..b4791b21cc7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ #### next release (8.3.4) +- Add `timeWindowDuration`, `timeWindowUnit` and `isForwardTimeWindow` traits to esri-mapServer type to support time window query. - Move map credits to map column so it don't get hidden by chart panel - TSify `MapColumn` module and reorganize components directory structure. - Add null check to `WebMapServiceCatalogItem` `rectangle` calculation - and now we ascend tree of WMS `Layers` until we find a rectangle. diff --git a/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts b/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts index 3f1ae936c05..05bab35c9e7 100644 --- a/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts +++ b/lib/Models/Catalog/Esri/ArcGisMapServerCatalogItem.ts @@ -36,6 +36,7 @@ import getToken from "../../getToken"; import proxyCatalogItemUrl from "../proxyCatalogItemUrl"; import MinMaxLevelMixin from "./../../../ModelMixins/MinMaxLevelMixin"; import { Extent, Layer, MapServer } from "./ArcGisInterfaces"; +import moment from "moment"; const proj4 = require("proj4").default; @@ -328,6 +329,13 @@ class MapServerStratum extends LoadableStratum( StratumOrder.addLoadStratum(MapServerStratum.stratumName); +interface TimeParams { + currentTime: number | undefined; + timeWindowDuration: number | undefined; + timeWindowUnit: string | undefined; + isForwardTimeWindow: boolean; +} + export default class ArcGisMapServerCatalogItem extends UrlMixin( DiscretelyTimeVaryingMixin( MinMaxLevelMixin( @@ -393,14 +401,33 @@ export default class ArcGisMapServerCatalogItem extends UrlMixin( return result; } - @computed - private get _currentImageryParts(): ImageryParts | undefined { - const dateAsUnix: string | undefined = + private getCurrentTime() { + const dateAsUnix: number | undefined = this.currentDiscreteTimeTag === undefined ? undefined - : new Date(this.currentDiscreteTimeTag).getTime().toString(); + : new Date(this.currentDiscreteTimeTag).getTime(); + return dateAsUnix; + } + + @computed + private get timeParams(): TimeParams { + const currentTime = this.getCurrentTime(); + const timeWindowDuration = this.timeWindowDuration; + const timeWindowUnit = this.timeWindowUnit; + const isForwardTimeWindow = this.isForwardTimeWindow; + const timeParams = { + currentTime, + timeWindowDuration, + timeWindowUnit, + isForwardTimeWindow + } as TimeParams; + return timeParams; + } - const imageryProvider = this._createImageryProvider(dateAsUnix); + @computed + private get _currentImageryParts(): ImageryParts | undefined { + const timeParams = this.timeParams; + const imageryProvider = this._createImageryProvider(timeParams); if (imageryProvider === undefined) { return undefined; } @@ -419,10 +446,8 @@ export default class ArcGisMapServerCatalogItem extends UrlMixin( !this.isPaused && this.nextDiscreteTimeTag ) { - const dateAsUnix: number = new Date(this.nextDiscreteTimeTag).getTime(); - const imageryProvider = this._createImageryProvider( - dateAsUnix.toString() - ); + const timeParams = this.timeParams; + const imageryProvider = this._createImageryProvider(timeParams); if (imageryProvider === undefined) { return undefined; } @@ -442,8 +467,46 @@ export default class ArcGisMapServerCatalogItem extends UrlMixin( } } + private windowDurationInMs( + rawTimeWindowDuration: number | undefined, + timeWindowUnit: string | undefined + ): number | undefined { + if ( + rawTimeWindowDuration === undefined || + rawTimeWindowDuration === 0 || + timeWindowUnit === undefined + ) { + return undefined; + } + + const rawTimeWindowData: any = {}; + rawTimeWindowData[timeWindowUnit] = rawTimeWindowDuration; + const duration = moment.duration(rawTimeWindowData).asMilliseconds(); + if (duration === 0) { + return undefined; + } else { + return duration; + } + } + + private getTimeWindowQueryString( + currentTime: number, + duration: number, + isForward: boolean = true + ) { + if (isForward) { + const toTime = Number(currentTime) + duration; + return currentTime + "," + toTime; + } else { + const fromTime = Number(currentTime) - duration; + return "" + fromTime + "," + currentTime; + } + } + private _createImageryProvider = createTransformerAllowUndefined( - (time: string | undefined): ArcGisMapServerImageryProvider | undefined => { + ( + timeParams: TimeParams | undefined + ): ArcGisMapServerImageryProvider | undefined => { const stratum = ( this.strata.get(MapServerStratum.stratumName) ); @@ -453,8 +516,22 @@ export default class ArcGisMapServerCatalogItem extends UrlMixin( } const params: any = Object.assign({}, this.parameters); - if (time !== undefined) { - params.time = time; + const currentTime = timeParams?.currentTime; + if (currentTime !== undefined) { + const windowDuration = this.windowDurationInMs( + timeParams?.timeWindowDuration, + timeParams?.timeWindowUnit + ); + + if (windowDuration !== undefined) { + params.time = this.getTimeWindowQueryString( + currentTime, + windowDuration, + timeParams?.isForwardTimeWindow + ); + } else { + params.time = currentTime; + } } const maximumLevel = scaleDenominatorToLevel( diff --git a/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts b/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts index bb6ccc18224..e58c22d3b13 100644 --- a/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts +++ b/lib/Traits/TraitsClasses/ArcGisMapServerCatalogItemTraits.ts @@ -57,4 +57,28 @@ export default class ArcGisMapServerCatalogItemTraits extends mixTraits( "date range when layer in time-enabled." }) maxRefreshIntervals: number = 1000; + + @primitiveTrait({ + name: "Time Window Duration", + description: + "Specify a time window duration when querying a time-enabled layer. Will not query with time window for non-positive value", + type: "number" + }) + timeWindowDuration?: number; + + @primitiveTrait({ + name: "Time Window Unit", + description: + "The time window unit for the `Time Window Duration`. Any units supported by `moment` module are valid, such as, `year`, `month`, `week`, `day`, `hour`, etc. Will not query time with window if the unit is invalid or undefined.", + type: "string" + }) + timeWindowUnit?: string; + + @primitiveTrait({ + name: "Is Forward Time Window", + description: + "If true, the time window is forward from the current time. Otherwise backward. Default to forward window.", + type: "boolean" + }) + isForwardTimeWindow: boolean = true; } diff --git a/test/Models/Catalog/esri/ArcGisMapServerCatalogItemSpec.ts b/test/Models/Catalog/esri/ArcGisMapServerCatalogItemSpec.ts index 47e92cfa73e..be483fc3108 100644 --- a/test/Models/Catalog/esri/ArcGisMapServerCatalogItemSpec.ts +++ b/test/Models/Catalog/esri/ArcGisMapServerCatalogItemSpec.ts @@ -451,7 +451,7 @@ describe("ArcGisMapServerCatalogItem", function () { }); describe("time-enabled layer", function () { - it("can load a time-enabled layer", async function () { + it("can load a layer, querying time without window", async function () { runInAction(() => { item = new ArcGisMapServerCatalogItem("test", new Terria()); item.setTrait( @@ -466,6 +466,147 @@ describe("ArcGisMapServerCatalogItem", function () { } expect(item.startTime).toBe("2004-11-26T09:43:22.000000000Z"); expect(item.stopTime).toBe("2019-11-03T14:00:00.000000000Z"); + const expectedTimeQueryString = 1572789600000; // from json file + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(expectedTimeQueryString); + }); + + it("can load a layer, querying time without window if timeWindowDuration is not defined", async function () { + runInAction(() => { + item = new ArcGisMapServerCatalogItem("test", new Terria()); + item.setTrait( + CommonStrata.definition, + "url", + "http://example.com/cadastre_history/MapServer" + ); + item.setTrait(CommonStrata.user, "timeWindowUnit", "year"); + }); + const defaultCurrentTime = 1572789600000; // from json file + await item.loadMapItems(); + const expectedTimeQueryString = defaultCurrentTime; + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(expectedTimeQueryString); + }); + + it("can load a layer, querying time without window if timeWindowUnit is not defined", async function () { + runInAction(() => { + item = new ArcGisMapServerCatalogItem("test", new Terria()); + item.setTrait( + CommonStrata.definition, + "url", + "http://example.com/cadastre_history/MapServer" + ); + item.setTrait(CommonStrata.user, "timeWindowDuration", 2); + }); + const defaultCurrentTime = 1572789600000; // from json file + await item.loadMapItems(); + const expectedTimeQueryString = defaultCurrentTime; + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(expectedTimeQueryString); + }); + + it("can load a layer, querying time with default forward window", async function () { + runInAction(() => { + item = new ArcGisMapServerCatalogItem("test", new Terria()); + item.setTrait( + CommonStrata.definition, + "url", + "http://example.com/cadastre_history/MapServer" + ); + item.setTrait(CommonStrata.user, "timeWindowDuration", 2); + item.setTrait(CommonStrata.user, "timeWindowUnit", "week"); + }); + const defaultCurrentTime = 1572789600000; // from json file + const twoWeekTime = 14 * 24 * 3600 * 1000; + const toTime = defaultCurrentTime + twoWeekTime; + await item.loadMapItems(); + const expectedTimeQueryString = `${defaultCurrentTime},${toTime}`; + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(expectedTimeQueryString); + }); + + it("can load a layer, querying time with explicit forward window", async function () { + runInAction(() => { + item = new ArcGisMapServerCatalogItem("test", new Terria()); + item.setTrait( + CommonStrata.definition, + "url", + "http://example.com/cadastre_history/MapServer" + ); + item.setTrait(CommonStrata.user, "timeWindowDuration", 2); + item.setTrait(CommonStrata.user, "timeWindowUnit", "week"); + item.setTrait(CommonStrata.user, "isForwardTimeWindow", true); + }); + const defaultCurrentTime = 1572789600000; // from json file + const twoWeekTime = 14 * 24 * 3600 * 1000; + const toTime = defaultCurrentTime + twoWeekTime; + await item.loadMapItems(); + const expectedTimeQueryString = `${defaultCurrentTime},${toTime}`; + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(expectedTimeQueryString); + }); + + it("can load a layer, querying time with backward time window", async function () { + runInAction(() => { + item = new ArcGisMapServerCatalogItem("test", new Terria()); + item.setTrait( + CommonStrata.definition, + "url", + "http://example.com/cadastre_history/MapServer" + ); + item.setTrait(CommonStrata.user, "timeWindowDuration", 2); + item.setTrait(CommonStrata.user, "timeWindowUnit", "week"); + item.setTrait(CommonStrata.user, "isForwardTimeWindow", false); + }); + const defaultCurrentTime = 1572789600000; // from json file + const twoWeekTime = 14 * 24 * 3600 * 1000; + const fromTime = defaultCurrentTime - twoWeekTime; + await item.loadMapItems(); + const expectedTimeQueryString = `${fromTime},${defaultCurrentTime}`; + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(expectedTimeQueryString); + }); + + it("can load a layer, querying time without window if timeWindowDuration is 0", async function () { + runInAction(() => { + item = new ArcGisMapServerCatalogItem("test", new Terria()); + item.setTrait( + CommonStrata.definition, + "url", + "http://example.com/cadastre_history/MapServer" + ); + item.setTrait(CommonStrata.user, "timeWindowDuration", 0); + item.setTrait(CommonStrata.user, "timeWindowUnit", "year"); + }); + const defaultCurrentTime = 1572789600000; // from json file + await item.loadMapItems(); + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(defaultCurrentTime); + }); + + it("can load a layer, querying time without window if timeWindowUnit is invalid", async function () { + runInAction(() => { + item = new ArcGisMapServerCatalogItem("test", new Terria()); + item.setTrait( + CommonStrata.definition, + "url", + "http://example.com/cadastre_history/MapServer" + ); + item.setTrait(CommonStrata.user, "timeWindowDuration", 2); + item.setTrait(CommonStrata.user, "timeWindowUnit", "fortnight"); + }); + const defaultCurrentTime = 1572789600000; // from json file + await item.loadMapItems(); + const imageryProvider = item.mapItems[0] + .imageryProvider as ArcGisMapServerImageryProvider; + expect(imageryProvider.parameters.time).toBe(defaultCurrentTime); }); });