terriajs
Version:
Geospatial data visualization platform.
280 lines (249 loc) • 8.49 kB
text/typescript
import { Feature, Point } from "geojson";
import i18next from "i18next";
import { makeObservable, override, runInAction } from "mobx";
import Rectangle from "terriajs-cesium/Source/Core/Rectangle";
import Resource from "terriajs-cesium/Source/Core/Resource";
import {
Category,
SearchAction
} from "../../Core/AnalyticEvents/analyticEvents";
import isDefined from "../../Core/isDefined";
import loadJson from "../../Core/loadJson";
import { applyTranslationIfExists } from "../../Language/languageHelpers";
import prettifyCoordinates from "../../Map/Vector/prettifyCoordinates";
import LocationSearchProviderMixin, {
getMapCenter
} from "../../ModelMixins/SearchProviders/LocationSearchProviderMixin";
import MapboxSearchProviderTraits from "../../Traits/SearchProviders/MapboxSearchProviderTraits";
import CommonStrata from "../Definition/CommonStrata";
import CreateModel from "../Definition/CreateModel";
import Terria from "../Terria";
import SearchProviderResults from "./SearchProviderResults";
import SearchResult from "./SearchResult";
enum MapboxGeocodeDirection {
Forward = "forward",
Reverse = "reverse"
}
interface MapboxGeocodingResponse {
features: Feature<Point>[];
type: string;
attribution?: string;
}
export default class MapboxSearchProvider extends LocationSearchProviderMixin(
CreateModel(MapboxSearchProviderTraits)
) {
static readonly type = "mapbox-search-provider";
get type() {
return MapboxSearchProvider.type;
}
constructor(uniqueId: string | undefined, terria: Terria) {
super(uniqueId, terria);
makeObservable(this);
}
override showWarning() {
if (!this.accessToken || this.accessToken === "") {
console.warn(
`The ${applyTranslationIfExists(this.name, i18next)}(${
this.type
}) geocoder will always return no results because a Mapbox token has not been provided. Please get a token from mapbox.com and add it to parameters.mapboxSearchProviderAccessToken in config.json.`
);
}
}
protected logEvent(searchText: string) {
this.terria.analytics?.logEvent(
Category.search,
SearchAction.mapbox,
searchText
);
}
protected doSearch(
searchText: string,
searchResults: SearchProviderResults
): Promise<void> {
searchResults.results.length = 0;
searchResults.message = undefined;
const isCoordinate = RegExp(
/^-?([0-9]{1,2}|1[0-7][0-9]|180)(\.[0-9]{1,17})$/
);
const isCSCoordinatePair = RegExp(
/([+-]?\d+\.?\d+)\s*,\s*([+-]?\d+\.?\d+)/
);
let searchDirection = isCSCoordinatePair.test(searchText)
? MapboxGeocodeDirection.Reverse
: MapboxGeocodeDirection.Forward;
let queryParams = {
access_token: this.accessToken,
autocomplete: this.partialMatch,
language: this.language
};
//check if geocoder should be reverse and set up.
if (searchDirection === MapboxGeocodeDirection.Reverse) {
let lonLat = searchText.split(/\s+/).join("").split(",");
if (
lonLat.length === 2 &&
isCoordinate.test(lonLat[0]) &&
isCoordinate.test(lonLat[1])
) {
// need to reverse the coord order if true.
if (this.latLonSearchOrder) {
lonLat = lonLat.reverse();
}
const [lonf, latf] = lonLat.map(parseFloat);
if (this.showCoordinatesInReverseGeocodeResult) {
const prettyCoords = prettifyCoordinates(lonf, latf);
searchResults.results.push(
new SearchResult({
name: `${prettyCoords.latitude}, ${prettyCoords.longitude}`,
clickAction: createZoomToFunction(this, {
geometry: {
coordinates: [lonf, latf]
},
properties: {}
}),
location: {
longitude: lonf,
latitude: latf
}
})
);
}
queryParams = {
...queryParams,
...{
longitude: lonLat[0],
latitude: lonLat[1],
limit: 1 //limit for reverse geocoder is per type
}
};
} else {
//if lonLat fails to parse, then assume is forward geocode
searchDirection = MapboxGeocodeDirection.Forward;
}
}
const searchQuery = new Resource({
url: new URL(searchDirection, this.url).toString(),
queryParameters: queryParams
});
if (searchDirection === MapboxGeocodeDirection.Forward) {
searchQuery.appendQueryParameters({
q: searchText,
limit: this.limit
});
}
if (searchDirection === MapboxGeocodeDirection.Forward && this.mapCenter) {
const mapCenter = getMapCenter(this.terria);
searchQuery.appendQueryParameters({
proximity: `${mapCenter.longitude}, ${mapCenter.latitude}`
});
}
if (
searchDirection === MapboxGeocodeDirection.Forward &&
this.terria.searchBarModel.boundingBoxLimit
) {
const bbox = this.terria.searchBarModel.boundingBoxLimit;
if (
isDefined(bbox.west) &&
isDefined(bbox.north) &&
isDefined(bbox.east) &&
isDefined(bbox.south)
) {
searchQuery.appendQueryParameters({
bbox: [bbox.west, bbox.north, bbox.east, bbox.south].join(",")
});
}
}
if (this.country) {
searchQuery.appendQueryParameters({
country: this.country
});
}
if (this.types) {
searchQuery.appendQueryParameters({
types: this.types
});
}
if (this.worldview) {
searchQuery.appendQueryParameters({
worldview: this.worldview
});
}
const promise: Promise<any> = loadJson(searchQuery);
return promise
.then((result: MapboxGeocodingResponse) => {
if (searchResults.isCanceled) {
// A new search has superseded this one, so ignore the result.
return;
}
if (
(result.features.length === 0 &&
searchDirection === MapboxGeocodeDirection.Forward) ||
//in the case where coordinate result is true, list is
//not empty.
(result.features.length === 0 &&
searchDirection === MapboxGeocodeDirection.Reverse &&
this.showCoordinatesInReverseGeocodeResult === false)
) {
searchResults.message = {
content: "translate#viewModels.searchNoLocations"
};
return;
}
const locations: SearchResult[] = result.features
.filter(
(feat) =>
feat.properties && feat.geometry && feat.properties.full_address
)
.map((feat) => {
return new SearchResult({
name: feat.properties!.full_address,
clickAction: createZoomToFunction(this, feat),
location: {
latitude: feat.geometry.coordinates[1],
longitude: feat.geometry.coordinates[0]
}
});
});
runInAction(() => {
searchResults.results.push(...locations);
});
if (searchResults.results.length === 0) {
searchResults.message = {
content: "translate#viewModels.searchNoLocations"
};
}
const attribution = result.attribution;
if (attribution) {
runInAction(() => {
this.setTrait(CommonStrata.underride, "attributions", [
attribution
]);
});
}
})
.catch(() => {
if (searchResults.isCanceled) {
// A new search has superseded this one, so ignore the result.
return;
}
searchResults.message = {
content: "translate#viewModels.searchErrorOccurred"
};
});
}
}
function createZoomToFunction(model: MapboxSearchProvider, resource: any) {
// mapbox doesn't return a bbox for street names etc, so we
// need to create it ourselves.
const [west, north, east, south] = resource.properties.bbox ?? [
resource.geometry.coordinates[0] - 0.01,
resource.geometry.coordinates[1] - 0.01,
resource.geometry.coordinates[0] + 0.01,
resource.geometry.coordinates[1] + 0.01
];
const rectangle = Rectangle.fromDegrees(west, south, east, north);
return function () {
const terria = model.terria;
terria.currentViewer.zoomTo(rectangle, model.flightDurationSeconds);
};
}