Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions lib/Core/loadJson.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Resource from "terriajs-cesium/Source/Core/Resource";

export default function loadJson<T = any>(
urlOrResource: any,
headers?: any,
urlOrResource: string | Resource,
headers?: Record<string, unknown>,
body?: any,
asForm: boolean = false
): Promise<T> {
Expand All @@ -16,7 +16,7 @@ export default function loadJson<T = any>(

if (body !== undefined) {
// We need to send a POST
params.headers = headers ?? {};
params.headers ??= {};
params.headers["Content-Type"] = "application/json";

if (asForm) {
Expand Down Expand Up @@ -47,3 +47,29 @@ export default function loadJson<T = any>(

return jsonPromise;
}

export const loadJsonAbortable = async <T = any>(
urlOrResource: string | Resource,
{
abortSignal,
headers,
body,
asForm = false
}: {
abortSignal: AbortSignal;
headers?: Record<string, unknown>;
body?: any;
asForm?: boolean;
}
) => {
const resource =
urlOrResource instanceof Resource
? urlOrResource
: new Resource({ url: urlOrResource });

abortSignal.addEventListener("abort", () => {
resource.request.cancelFunction();
});

return loadJson<T>(resource, headers, body, asForm);
};
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ function LocationSearchProviderMixin<

async search(searchText: string, manuallyTriggered?: boolean) {
if (!this.autocompleteEnabled && !manuallyTriggered) {
this.searchResult.isSearching = false;
this.searchResult.isWaitingToStartSearch = false;
this.cancelSearch();
this.searchResult.state = "idle";
this.searchResult.message = {
content: "translate#viewModels.enterToStartSearch"
};
Expand Down
49 changes: 34 additions & 15 deletions lib/ModelMixins/SearchProviders/SearchProviderMixin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ function SearchProviderMixin<
protected debounceTime = 1000;
private _debouncedSearch: ReturnType<typeof debounce>;

// Store current AbortController
private _currentAbortController?: AbortController;

constructor(...args: any[]) {
super(...args);
makeObservable(this);
Expand All @@ -33,39 +36,39 @@ function SearchProviderMixin<

protected abstract doSearch(
searchText: string,
results: SearchProviderResult
results: AbortSignal
): Promise<void>;

@action
cancelSearch() {
this._debouncedSearch.cancel();

this.searchResult.isCanceled = true;
this.searchResult = new SearchProviderResult(this);
this._currentAbortController?.abort();
this._currentAbortController = undefined;

this.searchResult.cancel();
}

@action
async search(
searchText: string,
manuallyTriggered?: boolean
): Promise<void> {
this.searchResult.isWaitingToStartSearch = true;
this.cancelSearch();
if (!this.shouldRunSearch(searchText)) {
this._debouncedSearch.cancel();

this.searchResult.isSearching = false;
this.searchResult.state = "idle";
this.searchResult.message = {
content: "translate#viewModels.searchMinCharacters",
params: {
count: this.minCharacters
}
};
this.searchResult.isWaitingToStartSearch = false;
return;
}

this.searchResult.state = "waiting";

if (manuallyTriggered) {
this._debouncedSearch.cancel();
await this.performSearch(searchText);
} else {
await this._debouncedSearch(searchText);
Expand All @@ -75,12 +78,28 @@ function SearchProviderMixin<
@action
private async performSearch(searchText: string): Promise<void> {
this.logEvent(searchText);
this.searchResult.isWaitingToStartSearch = false;
this.searchResult.isSearching = true;

await this.doSearch(searchText, this.searchResult);

this.searchResult.isSearching = false;
this.searchResult.state = "searching";

const abortController = new AbortController();
this._currentAbortController = abortController;

try {
await this.doSearch(searchText, abortController.signal);

if (abortController.signal.aborted) {
return;
}
} catch (error) {
if (abortController.signal.aborted) {
this.searchResult.cancel();
throw error;
}
} finally {
this.searchResult.state = "idle";
if (this._currentAbortController === abortController) {
this._currentAbortController = undefined;
}
}
}

private shouldRunSearch(searchText: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import URI from "urijs";
import AbstractConstructor from "../../Core/AbstractConstructor";
import zoomRectangleFromPoint from "../../Map/Vector/zoomRectangleFromPoint";
import Model from "../../Models/Definition/Model";
import SearchProviderResult from "../../Models/SearchProviders/SearchProviderResults";
import SearchResult from "../../Models/SearchProviders/SearchResult";
import xml2json from "../../ThirdParty/xml2json";
import WebFeatureServiceSearchProviderTraits from "../../Traits/SearchProviders/WebFeatureServiceSearchProviderTraits";
Expand Down Expand Up @@ -34,35 +33,20 @@ function WebFeatureServiceSearchProviderMixin<
| ((feature: any, searchText: string) => number)
| undefined;

cancelRequest?: () => void;

private _waitingForResults: boolean = false;

getXml(url: string): Promise<XMLDocument> {
getXml(url: string, abortSignal: AbortSignal): Promise<XMLDocument> {
const resource = new Resource({ url });
this._waitingForResults = true;
const xmlPromise = resource.fetchXML()!;
this.cancelRequest = resource.request.cancelFunction;
return xmlPromise.finally(() => {
this._waitingForResults = false;
abortSignal.addEventListener("abort", () => {
resource.request.cancelFunction();
});
return xmlPromise;
}

protected doSearch(
protected async doSearch(
searchText: string,
results: SearchProviderResult
abortSignal: AbortSignal
): Promise<void> {
results.results.length = 0;
results.message = undefined;

if (this._waitingForResults) {
// There's been a new search! Cancel the previous one.
if (this.cancelRequest !== undefined) {
this.cancelRequest();
this.cancelRequest = undefined;
}
this._waitingForResults = false;
}
this.searchResult.clear();

const originalSearchText = searchText;

Expand All @@ -89,14 +73,17 @@ function WebFeatureServiceSearchProviderMixin<
filter: filter
});

return this.getXml(_wfsServiceUrl.toString())
return this.getXml(_wfsServiceUrl.toString(), abortSignal)
.then((xml: any) => {
if (abortSignal.aborted) {
// This search is aborted, so ignore the result.
return;
}

const json: any = xml2json(xml);
let features: any[];
if (json === undefined) {
results.message = {
content: "translate#viewModels.searchErrorOccurred"
};
this.searchResult.errorOccurred();
return;
}

Expand All @@ -105,9 +92,10 @@ function WebFeatureServiceSearchProviderMixin<
} else if (json.featureMember !== undefined) {
features = json.featureMember;
} else {
results.message = {
content: "translate#viewModels.searchNoPlaceNames"
};
this.searchResult.noResults(
"translate#viewModels.searchNoPlaceNames"
);

return;
}

Expand All @@ -124,9 +112,9 @@ function WebFeatureServiceSearchProviderMixin<
}

if (features.length === 0) {
results.message = {
content: "translate#viewModels.searchNoPlaceNames"
};
this.searchResult.noResults(
"translate#viewModels.searchNoPlaceNames"
);
return;
}

Expand Down Expand Up @@ -175,17 +163,15 @@ function WebFeatureServiceSearchProviderMixin<
});

// append new results to all results
results.results.push(...searchResults);
this.searchResult.results.push(...searchResults);
});
})
.catch((_e) => {
if (results.isCanceled) {
// A new search has superseded this one, so ignore the result.
if (abortSignal.aborted) {
// This search is aborted, so ignore the result.
return;
}
results.message = {
content: "translate#viewModels.searchErrorOccurred"
};
this.searchResult.errorOccurred();
});
}

Expand Down
83 changes: 35 additions & 48 deletions lib/Models/SearchProviders/BingMapsSearchProvider.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import i18next from "i18next";
import { makeObservable, override, runInAction } from "mobx";
import { action, makeObservable, override, runInAction } from "mobx";
import Rectangle from "terriajs-cesium/Source/Core/Rectangle";
import Resource from "terriajs-cesium/Source/Core/Resource";
import defined from "terriajs-cesium/Source/Core/defined";
import {
Category,
SearchAction
} from "../../Core/AnalyticEvents/analyticEvents";
import loadJson from "../../Core/loadJson";
import { loadJsonAbortable } from "../../Core/loadJson";
import { applyTranslationIfExists } from "../../Language/languageHelpers";
import LocationSearchProviderMixin, {
getMapCenter
Expand All @@ -16,7 +16,6 @@ import BingMapsSearchProviderTraits from "../../Traits/SearchProviders/BingMapsS
import CreateModel from "../Definition/CreateModel";
import Terria from "../Terria";
import CommonStrata from "./../Definition/CommonStrata";
import SearchProviderResult from "./SearchProviderResults";
import SearchResult from "./SearchResult";

export default class BingMapsSearchProvider extends LocationSearchProviderMixin(
Expand Down Expand Up @@ -63,12 +62,12 @@ export default class BingMapsSearchProvider extends LocationSearchProviderMixin(
);
}

protected doSearch(
@action
protected async doSearch(
searchText: string,
searchResults: SearchProviderResult
abortSignal: AbortSignal
): Promise<void> {
searchResults.results.length = 0;
searchResults.message = undefined;
this.searchResult.clear();

const searchQuery = new Resource({
url: this.url + "REST/v1/Locations",
Expand All @@ -88,52 +87,40 @@ export default class BingMapsSearchProvider extends LocationSearchProviderMixin(
});
}

const promise: Promise<any> = loadJson(searchQuery);
return promise
.then((result) => {
if (searchResults.isCanceled) {
// A new search has superseded this one, so ignore the result.
return;
}

if (result.resourceSets.length === 0) {
searchResults.message = {
content: "translate#viewModels.searchNoLocations"
};
return;
}
const promise: Promise<any> = loadJsonAbortable(searchQuery, {
abortSignal
});

const resourceSet = result.resourceSets[0];
if (resourceSet.resources.length === 0) {
searchResults.message = {
content: "translate#viewModels.searchNoLocations"
};
return;
}
try {
const result = await promise;
if (abortSignal.aborted) {
return;
}
if (result.resourceSets.length === 0) {
this.searchResult.noResults("translate#viewModels.searchNoLocations");
return;
}

runInAction(() => {
const locations = this.sortByPriority(resourceSet.resources);
const resourceSet = result.resourceSets[0];
if (resourceSet.resources.length === 0) {
this.searchResult.noResults("translate#viewModels.searchNoLocations");
return;
}

searchResults.results.push(...locations.primaryCountry);
searchResults.results.push(...locations.other);
});
const locations = this.sortByPriority(resourceSet.resources);

if (searchResults.results.length === 0) {
searchResults.message = {
content: "translate#viewModels.searchNoLocations"
};
}
})
.catch(() => {
if (searchResults.isCanceled) {
// A new search has superseded this one, so ignore the result.
return;
}
this.searchResult.results.push(...locations.primaryCountry);
this.searchResult.results.push(...locations.other);

searchResults.message = {
content: "translate#viewModels.searchErrorOccurred"
};
});
if (this.searchResult.results.length === 0) {
this.searchResult.noResults("translate#viewModels.searchNoLocations");
}
} catch {
if (abortSignal.aborted) {
return;
}
this.searchResult.errorOccurred();
}
}

protected sortByPriority(resources: any[]) {
Expand Down
Loading
Loading