From feabaf2b42470e278d96883ba356873456baa9cc Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Tue, 17 Oct 2023 10:40:52 +1000 Subject: [PATCH 1/6] Create CesiumIonSearchProvider --- lib/Models/SearchProviders/CesiumIonSearchProvider.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 lib/Models/SearchProviders/CesiumIonSearchProvider.ts diff --git a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts new file mode 100644 index 00000000000..e69de29bb2d From b73034f610b5178422a60d2b0f89a1fe8307a858 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Thu, 19 Oct 2023 17:15:41 +1000 Subject: [PATCH 2/6] Create CesiumIonSearchProvider - add type generic to loadJson --- lib/Core/loadJson.ts | 6 +- .../CesiumIonSearchProvider.ts | 98 +++++++++++++++++++ 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/lib/Core/loadJson.ts b/lib/Core/loadJson.ts index 7067f9fc5b9..61be0e52a59 100644 --- a/lib/Core/loadJson.ts +++ b/lib/Core/loadJson.ts @@ -1,14 +1,14 @@ import Resource from "terriajs-cesium/Source/Core/Resource"; -export default function loadJson( +export default function loadJson( urlOrResource: any, headers?: any, body?: any, asForm: boolean = false -): Promise { +): Promise { let responseType: XMLHttpRequestResponseType = "json"; - let jsonPromise: Promise; + let jsonPromise: Promise; let params: any = { url: urlOrResource, headers: headers diff --git a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts index e69de29bb2d..2c77f78001f 100644 --- a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts +++ b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts @@ -0,0 +1,98 @@ +import Cesium from "../Cesium"; +import SearchProvider from "./SearchProvider"; +import { observable, makeObservable } from "mobx"; +import Terria from "../Terria"; +import { defaultValue } from "terriajs-cesium"; +import Scene from "terriajs-cesium/Source/Scene/Scene"; +import SearchProviderResults from "./SearchProviderResults"; +import SearchResult from "./SearchResult"; +import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; +import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; +import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; +import GeocoderService from "terriajs-cesium/Source/Core/GeocoderService"; +import loadJson from "../../Core/loadJson"; +import bbox from "@turf/bbox"; +interface CesiumIonSearchProviderOptions { + terria: Terria; + url?: string; + key?: string; + flightDurationSeconds?: number; + primaryCountry?: string; + culture?: string; +} + +interface CesiumIonGeocodeResultFeature { + bbox: [number, number, number, number]; + properties: { label: string }; +} + +interface CesiumIonGeocodeResult { + features: CesiumIonGeocodeResultFeature[]; +} + +export default class CesiumIonSearchProvider extends SearchProvider { + readonly terria: Terria; + @observable key: string | undefined; + @observable flightDurationSeconds: number; + @observable url: string; + + constructor(options: CesiumIonSearchProviderOptions) { + super(); + + makeObservable(this); + + this.terria = options.terria; + this.url = defaultValue( + options.url, + "https://api.cesium.com/v1/geocode/search" + ); + this.key = options.key; + this.flightDurationSeconds = defaultValue( + options.flightDurationSeconds, + 1.5 + ); + } + + protected async doSearch( + searchText: string, + results: SearchProviderResults + ): Promise { + if (searchText === undefined || /^\s*$/.test(searchText)) { + return Promise.resolve(); + } + + const response = await loadJson( + `${this.url}?text=${searchText}&access_token=${this.key}` + ); + + if (!response.features) { + return; + } + + results.results.push( + ...response.features.map((feature) => { + const [w, s, e, n] = feature.bbox; + const rectangle = Rectangle.fromDegrees(w, s, e, n); + + return new SearchResult({ + name: feature.properties.label, + clickAction: createZoomToFunction(this, rectangle), + location: { + latitude: (s + n) / 2, + longitude: (e + w) / 2 + } + }); + }) + ); + } +} + +function createZoomToFunction( + model: CesiumIonSearchProvider, + rectangle: Rectangle +) { + return function () { + const terria = model.terria; + terria.currentViewer.zoomTo(rectangle, model.flightDurationSeconds); + }; +} From 58792614d0065cc69e8b8777e1058425b02b8b42 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Wed, 1 Nov 2023 15:57:12 +1000 Subject: [PATCH 3/6] Build using cesium-ion-geocoder terriaMap branch --- buildprocess/ci-deploy.sh | 2 +- lib/Models/SearchProviders/CesiumIonSearchProvider.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/buildprocess/ci-deploy.sh b/buildprocess/ci-deploy.sh index 06ff60bc1b0..ab453002d23 100644 --- a/buildprocess/ci-deploy.sh +++ b/buildprocess/ci-deploy.sh @@ -23,7 +23,7 @@ npm install -g yarn@^1.19.0 # Clone and build TerriaMap, using this version of TerriaJS TERRIAJS_COMMIT_HASH=$(git rev-parse HEAD) -git clone -b main https://github.com/TerriaJS/TerriaMap.git +git clone -b use-cesium-ion-geocoder https://github.com/TerriaJS/TerriaMap.git cd TerriaMap TERRIAMAP_COMMIT_HASH=$(git rev-parse HEAD) sed -i -e 's@"terriajs": ".*"@"terriajs": "'$GITHUB_REPOSITORY'#'${GITHUB_BRANCH}'"@g' package.json diff --git a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts index 2c77f78001f..c0dbc4b5ce3 100644 --- a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts +++ b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts @@ -7,11 +7,7 @@ import Scene from "terriajs-cesium/Source/Scene/Scene"; import SearchProviderResults from "./SearchProviderResults"; import SearchResult from "./SearchResult"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; -import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; -import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; -import GeocoderService from "terriajs-cesium/Source/Core/GeocoderService"; import loadJson from "../../Core/loadJson"; -import bbox from "@turf/bbox"; interface CesiumIonSearchProviderOptions { terria: Terria; url?: string; From c3057446fb68699b0a46e7b58dd75197e8b357b3 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Thu, 2 Nov 2023 11:14:28 +1000 Subject: [PATCH 4/6] Fix terriajs-cesium import --- lib/Models/SearchProviders/CesiumIonSearchProvider.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts index c0dbc4b5ce3..032b6348a7f 100644 --- a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts +++ b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts @@ -1,9 +1,7 @@ -import Cesium from "../Cesium"; import SearchProvider from "./SearchProvider"; import { observable, makeObservable } from "mobx"; import Terria from "../Terria"; -import { defaultValue } from "terriajs-cesium"; -import Scene from "terriajs-cesium/Source/Scene/Scene"; +import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; import SearchProviderResults from "./SearchProviderResults"; import SearchResult from "./SearchResult"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; From 44c1592fa76581b724b14adc4a427eea8cf3affa Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Tue, 14 Nov 2023 12:39:18 +1000 Subject: [PATCH 5/6] - Handle errors - log to analytics - handle empty result - add unit test --- CHANGES.md | 2 +- lib/Core/AnalyticEvents/analyticEvents.ts | 3 +- .../CesiumIonSearchProvider.ts | 71 +++++++++++++------ .../CesiumIonSearchProviderSpec.ts | 59 +++++++++++++++ 4 files changed, 111 insertions(+), 24 deletions(-) create mode 100644 test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts diff --git a/CHANGES.md b/CHANGES.md index f350fab13ea..196b54828b5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,7 +15,7 @@ - See [ASGS 2021](https://www.abs.gov.au/statistics/standards/australian-statistical-geography-standard-asgs-edition-3/jul2021-jun2026/access-and-downloads/digital-boundary-files) - Added [Melbourne CLUE blocks](https://data.melbourne.vic.gov.au/pages/clue/) to region mapping. - Fix WMS `GetMap`/`GetFeatureInfo` requests not having `styles` parameter (will use empty string instead of `undefined`) -- [The next improvement] +- Add CesiumIon geocoder #### 8.3.7 - 2023-10-26 diff --git a/lib/Core/AnalyticEvents/analyticEvents.ts b/lib/Core/AnalyticEvents/analyticEvents.ts index daecf95c482..39b4cbc4abe 100644 --- a/lib/Core/AnalyticEvents/analyticEvents.ts +++ b/lib/Core/AnalyticEvents/analyticEvents.ts @@ -16,7 +16,8 @@ export enum SearchAction { bing = "Bing", catalog = "Catalog", gazetteer = "Gazetteer", - nominatim = "nominatim" + nominatim = "nominatim", + cesium = "Cesium" } export enum LaunchAction { diff --git a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts index 032b6348a7f..48bfd1f3986 100644 --- a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts +++ b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts @@ -1,18 +1,22 @@ import SearchProvider from "./SearchProvider"; import { observable, makeObservable } from "mobx"; -import Terria from "../Terria"; +import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; +import i18next from "i18next"; +import Terria from "../Terria"; import SearchProviderResults from "./SearchProviderResults"; import SearchResult from "./SearchResult"; -import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import loadJson from "../../Core/loadJson"; +import { + Category, + SearchAction +} from "../../Core/AnalyticEvents/analyticEvents"; + interface CesiumIonSearchProviderOptions { terria: Terria; url?: string; - key?: string; + key: string; flightDurationSeconds?: number; - primaryCountry?: string; - culture?: string; } interface CesiumIonGeocodeResultFeature { @@ -36,6 +40,7 @@ export default class CesiumIonSearchProvider extends SearchProvider { makeObservable(this); this.terria = options.terria; + this.name = i18next.t("viewModels.searchLocations"); this.url = defaultValue( options.url, "https://api.cesium.com/v1/geocode/search" @@ -45,39 +50,61 @@ export default class CesiumIonSearchProvider extends SearchProvider { options.flightDurationSeconds, 1.5 ); + + if (!this.key) { + console.warn( + "The " + + this.name + + " geocoder will always return no results because a CesiumIon key has not been provided. Please get a CesiumIon key from ion.cesium.com, ensure it has geocoding permission and add it to parameters.cesiumIonAccessToken in config.json." + ); + } } protected async doSearch( searchText: string, - results: SearchProviderResults + searchResults: SearchProviderResults ): Promise { if (searchText === undefined || /^\s*$/.test(searchText)) { return Promise.resolve(); } - const response = await loadJson( - `${this.url}?text=${searchText}&access_token=${this.key}` + this.terria.analytics?.logEvent( + Category.search, + SearchAction.cesium, + searchText ); + let response; + try { + response = await loadJson( + `${this.url}?text=${searchText}&access_token=${this.key}` + ); + } catch (e) { + searchResults.message = i18next.t("viewModels.searchErrorOccurred"); + return; + } if (!response.features) { + searchResults.message = i18next.t("viewModels.searchNoLocations"); return; } - results.results.push( - ...response.features.map((feature) => { - const [w, s, e, n] = feature.bbox; - const rectangle = Rectangle.fromDegrees(w, s, e, n); + if (response.features.length === 0) { + searchResults.message = i18next.t("viewModels.searchNoLocations"); + } - return new SearchResult({ - name: feature.properties.label, - clickAction: createZoomToFunction(this, rectangle), - location: { - latitude: (s + n) / 2, - longitude: (e + w) / 2 - } - }); - }) - ); + searchResults.results = response.features.map((feature) => { + const [w, s, e, n] = feature.bbox; + const rectangle = Rectangle.fromDegrees(w, s, e, n); + + return new SearchResult({ + name: feature.properties.label, + clickAction: createZoomToFunction(this, rectangle), + location: { + latitude: (s + n) / 2, + longitude: (e + w) / 2 + } + }); + }); } } diff --git a/test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts b/test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts new file mode 100644 index 00000000000..f000088d6f9 --- /dev/null +++ b/test/Models/SearchProviders/CesiumIonSearchProviderSpec.ts @@ -0,0 +1,59 @@ +import CesiumIonSearchProvider from "../../../lib/Models/SearchProviders/CesiumIonSearchProvider"; +import Terria from "../../../lib/Models//Terria"; +import * as loadJson from "../../../lib/Core/loadJson"; + +const fixture = { + features: [ + { + properties: { + label: "West End, Australia" + }, + bbox: [ + 152.99620056152344, -27.490509033203125, 153.0145721435547, + -27.474090576171875 + ] + } + ] +}; + +describe("CesiumIonSearchProvider", () => { + const searchProvider = new CesiumIonSearchProvider({ + key: "testkey", + url: "api.test.com", + terria: { + currentViewer: { + zoomTo: () => {} + } + } as unknown as Terria + }); + it("Handles valid results", async () => { + spyOn(loadJson, "default").and.returnValue( + new Promise((resolve) => resolve(fixture)) + ); + + const result = await searchProvider.search("test"); + expect(loadJson.default).toHaveBeenCalledWith( + "api.test.com?text=test&access_token=testkey" + ); + expect(result.results.length).toBe(1); + expect(result.results[0].name).toBe("West End, Australia"); + expect(result.results[0].location?.latitude).toBe(-27.4822998046875); + }); + + it("Handles empty result", async () => { + spyOn(loadJson, "default").and.returnValue( + new Promise((resolve) => resolve([])) + ); + const result = await searchProvider.search("test"); + console.log(result); + expect(result.results.length).toBe(0); + expect(result.message).toBe("viewModels.searchNoLocations"); + }); + + it("Handles error", async () => { + spyOn(loadJson, "default").and.throwError("error"); + const result = await searchProvider.search("test"); + expect(result.results.length).toBe(0); + expect(result.message).toBe("viewModels.searchErrorOccurred"); + }); +}); From a6748e20d1b7a17b23be6b58e29ad7e5cbb1d710 Mon Sep 17 00:00:00 2001 From: Lawrence Owen Date: Tue, 14 Nov 2023 16:14:44 +1000 Subject: [PATCH 6/6] Wrap state changes in runInAction --- .../CesiumIonSearchProvider.ts | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts index 48bfd1f3986..e0e958c6f68 100644 --- a/lib/Models/SearchProviders/CesiumIonSearchProvider.ts +++ b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts @@ -1,5 +1,5 @@ import SearchProvider from "./SearchProvider"; -import { observable, makeObservable } from "mobx"; +import { observable, makeObservable, runInAction } from "mobx"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import defaultValue from "terriajs-cesium/Source/Core/defaultValue"; import i18next from "i18next"; @@ -73,36 +73,41 @@ export default class CesiumIonSearchProvider extends SearchProvider { SearchAction.cesium, searchText ); - let response; + + let response: CesiumIonGeocodeResult; try { response = await loadJson( `${this.url}?text=${searchText}&access_token=${this.key}` ); } catch (e) { - searchResults.message = i18next.t("viewModels.searchErrorOccurred"); + runInAction(() => { + searchResults.message = i18next.t("viewModels.searchErrorOccurred"); + }); return; } - if (!response.features) { - searchResults.message = i18next.t("viewModels.searchNoLocations"); - return; - } + runInAction(() => { + if (!response.features) { + searchResults.message = i18next.t("viewModels.searchNoLocations"); + return; + } - if (response.features.length === 0) { - searchResults.message = i18next.t("viewModels.searchNoLocations"); - } + if (response.features.length === 0) { + searchResults.message = i18next.t("viewModels.searchNoLocations"); + } + + searchResults.results = response.features.map((feature) => { + const [w, s, e, n] = feature.bbox; + const rectangle = Rectangle.fromDegrees(w, s, e, n); - searchResults.results = response.features.map((feature) => { - const [w, s, e, n] = feature.bbox; - const rectangle = Rectangle.fromDegrees(w, s, e, n); - - return new SearchResult({ - name: feature.properties.label, - clickAction: createZoomToFunction(this, rectangle), - location: { - latitude: (s + n) / 2, - longitude: (e + w) / 2 - } + return new SearchResult({ + name: feature.properties.label, + clickAction: createZoomToFunction(this, rectangle), + location: { + latitude: (s + n) / 2, + longitude: (e + w) / 2 + } + }); }); }); }