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/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/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/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 new file mode 100644 index 00000000000..e0e958c6f68 --- /dev/null +++ b/lib/Models/SearchProviders/CesiumIonSearchProvider.ts @@ -0,0 +1,124 @@ +import SearchProvider from "./SearchProvider"; +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"; +import Terria from "../Terria"; +import SearchProviderResults from "./SearchProviderResults"; +import SearchResult from "./SearchResult"; +import loadJson from "../../Core/loadJson"; +import { + Category, + SearchAction +} from "../../Core/AnalyticEvents/analyticEvents"; + +interface CesiumIonSearchProviderOptions { + terria: Terria; + url?: string; + key: string; + flightDurationSeconds?: number; +} + +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.name = i18next.t("viewModels.searchLocations"); + this.url = defaultValue( + options.url, + "https://api.cesium.com/v1/geocode/search" + ); + this.key = options.key; + this.flightDurationSeconds = defaultValue( + 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, + searchResults: SearchProviderResults + ): Promise { + if (searchText === undefined || /^\s*$/.test(searchText)) { + return Promise.resolve(); + } + + this.terria.analytics?.logEvent( + Category.search, + SearchAction.cesium, + searchText + ); + + let response: CesiumIonGeocodeResult; + try { + response = await loadJson( + `${this.url}?text=${searchText}&access_token=${this.key}` + ); + } catch (e) { + runInAction(() => { + searchResults.message = i18next.t("viewModels.searchErrorOccurred"); + }); + return; + } + + runInAction(() => { + if (!response.features) { + searchResults.message = i18next.t("viewModels.searchNoLocations"); + return; + } + + 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); + + 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); + }; +} 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"); + }); +});