terriajs
Version:
Geospatial data visualization platform.
232 lines (202 loc) • 7.86 kB
text/typescript
import { makeObservable, runInAction } from "mobx";
import Resource from "terriajs-cesium/Source/Core/Resource";
import URI from "urijs";
import AbstractConstructor from "../../Core/AbstractConstructor";
import zoomRectangleFromPoint from "../../Map/Vector/zoomRectangleFromPoint";
import Model from "../../Models/Definition/Model";
import SearchProviderResults from "../../Models/SearchProviders/SearchProviderResults";
import SearchResult from "../../Models/SearchProviders/SearchResult";
import xml2json from "../../ThirdParty/xml2json";
import WebFeatureServiceSearchProviderTraits from "../../Traits/SearchProviders/WebFeatureServiceSearchProviderTraits";
import LocationSearchProviderMixin from "./LocationSearchProviderMixin";
function WebFeatureServiceSearchProviderMixin<
T extends AbstractConstructor<Model<WebFeatureServiceSearchProviderTraits>>
>(Base: T) {
abstract class WebFeatureServiceSearchProviderMixin extends LocationSearchProviderMixin(
Base
) {
constructor(...args: any[]) {
super(...args);
makeObservable(this);
}
protected abstract featureToSearchResultFunction: (
feature: any
) => SearchResult;
protected abstract transformSearchText:
| ((searchText: string) => string)
| undefined;
protected abstract searchResultFilterFunction:
| ((feature: any) => boolean)
| undefined;
protected abstract searchResultScoreFunction:
| ((feature: any, searchText: string) => number)
| undefined;
cancelRequest?: () => void;
private _waitingForResults: boolean = false;
getXml(url: string): 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;
});
}
protected doSearch(
searchText: string,
results: SearchProviderResults
): 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;
}
const originalSearchText = searchText;
searchText = searchText.trim();
if (this.transformSearchText !== undefined) {
searchText = this.transformSearchText(searchText);
}
if (searchText.length < 2) {
return Promise.resolve();
}
// Support for matchCase="false" is patchy, but we try anyway
const filter = `<ogc:Filter><ogc:PropertyIsLike wildCard="*" matchCase="false">
<ogc:ValueReference>${this.searchPropertyName}</ogc:ValueReference>
<ogc:Literal>*${searchText}*</ogc:Literal></ogc:PropertyIsLike></ogc:Filter>`;
const _wfsServiceUrl = new URI(this.url);
_wfsServiceUrl.setSearch({
service: "WFS",
request: "GetFeature",
typeName: this.searchPropertyTypeName,
version: "1.1.0",
srsName: "urn:ogc:def:crs:EPSG::4326", // srsName must be formatted like this for correct lat/long order >:(
filter: filter
});
return this.getXml(_wfsServiceUrl.toString())
.then((xml: any) => {
const json: any = xml2json(xml);
let features: any[];
if (json === undefined) {
results.message = {
content: "translate#viewModels.searchErrorOccurred"
};
return;
}
if (json.member !== undefined) {
features = json.member;
} else if (json.featureMember !== undefined) {
features = json.featureMember;
} else {
results.message = {
content: "translate#viewModels.searchNoPlaceNames"
};
return;
}
// if there's only one feature, make it an array
if (!Array.isArray(features)) {
features = [features];
}
const resultSet = new Set();
runInAction(() => {
if (this.searchResultFilterFunction !== undefined) {
features = features.filter(this.searchResultFilterFunction);
}
if (features.length === 0) {
results.message = {
content: "translate#viewModels.searchNoPlaceNames"
};
return;
}
if (this.searchResultScoreFunction !== undefined) {
features = features.sort(
(featureA, featureB) =>
this.searchResultScoreFunction!(
featureB,
originalSearchText
) -
this.searchResultScoreFunction!(featureA, originalSearchText)
);
}
let searchResults = features
.map(this.featureToSearchResultFunction)
.map((result) => {
result.clickAction = createZoomToFunction(
this,
result.location
);
return result;
});
// If we don't have a scoring function, sort the search results now
// We can't do this earlier because we don't know what the schema of the unprocessed feature looks like
if (this.searchResultScoreFunction === undefined) {
// Put shorter results first
// They have a larger percentage of letters that the user actually typed in them
searchResults = searchResults.sort(
(featureA, featureB) =>
featureA.name.length - featureB.name.length
);
}
// Remove results that have the same name and are close to each other
searchResults = searchResults.filter((result) => {
const hash = `${result.name},${result.location?.latitude.toFixed(
1
)},${result.location?.longitude.toFixed(1)}`;
if (resultSet.has(hash)) {
return false;
}
resultSet.add(hash);
return true;
});
// append new results to all results
results.results.push(...searchResults);
});
})
.catch((_e) => {
if (results.isCanceled) {
// A new search has superseded this one, so ignore the result.
return;
}
results.message = {
content: "translate#viewModels.searchErrorOccurred"
};
});
}
get isWebFeatureServiceSearchProviderMixin() {
return true;
}
}
return WebFeatureServiceSearchProviderMixin;
}
namespace WebFeatureServiceSearchProviderMixin {
export interface Instance extends InstanceType<
ReturnType<typeof WebFeatureServiceSearchProviderMixin>
> {}
export function isMixedInto(model: any): model is Instance {
return model && model.isWebFeatureServiceSearchProviderMixin;
}
}
export default WebFeatureServiceSearchProviderMixin;
function createZoomToFunction(
model: WebFeatureServiceSearchProviderMixin.Instance,
location: any
) {
// Server does not return information of a bounding box, just a location.
// bboxSize is used to expand a point
const bboxSize = 0.2;
const rectangle = zoomRectangleFromPoint(
location.latitude,
location.longitude,
bboxSize
);
const flightDurationSeconds: number =
model.flightDurationSeconds ||
model.terria.searchBarModel.flightDurationSeconds;
return function () {
model.terria.currentViewer.zoomTo(rectangle, flightDurationSeconds);
};
}