"use strict";
define([
"underscore",
"backbone",
"cesium",
"models/geocoder/GeocoderSearch",
"models/maps/GeoPoint",
], (_, Backbone, Cesium, GeocoderSearch, GeoPoint) => {
const EMAIL = MetacatUI.appModel.get("emailContact");
const NO_RESULTS_MESSAGE =
"No search results found, try using another place name.";
const API_ERROR =
"We're having trouble identifying locations on the map right now. Please reach out to support for help with this issue" +
(EMAIL ? `: ${EMAIL}` : ".");
const PLACES_API_ERROR = API_ERROR;
const GEOCODING_API_ERROR = API_ERROR;
/**
* @class ViewfinderModel
* @classdesc ViewfinderModel maintains state for the ViewfinderView and
* interfaces with location searching services.
* @classcategory Models/Maps
* @since 2.28.0
* @extends Backbone.Model
*/
const ViewfinderModel = Backbone.Model.extend(
/** @lends ViewfinderModel.prototype */ {
/**
* @name ViewfinderModel#defaults
* @type {Object}
* @property {string} error is the current error string to be displayed
* in the UI.
* @property {number} focusIndex is the index of the element
* in the list of predictions that shoudl be highlighted as focus.
* @property {Prediction[]} predictions a list of Predictions models that
* correspond to the user's search query.
* @property {string} query the user's search query.
* @since 2.28.0
*/
defaults() {
return {
error: "",
focusIndex: -1,
predictions: [],
query: "",
zoomPresets: [],
};
},
/**
* @param {Map} mapModel is the Map model that the ViewfinderModel is
* managing for the corresponding ViewfinderView.
*/
initialize({ mapModel }) {
this.geocoderSearch = new GeocoderSearch();
this.mapModel = mapModel;
this.set(
"zoomPresets",
mapModel.get("zoomPresetsCollection")?.models || [],
);
},
/**
* Get autocompletion predictions from the GeocoderSearch model.
* @param {string} rawQuery is the user's search query with spaces.
*/
async autocompleteSearch(rawQuery) {
const query = rawQuery.trim();
if (this.get("query") === query) {
return;
} else if (!query) {
this.set({ error: "", predictions: [], query: "", focusIndex: -1 });
return;
} else if (GeoPoint.couldBeLatLong(query)) {
this.set({ predictions: [], query: "", focusIndex: -1 });
return;
}
// Unset error so the error will fire a change event even if it is the
// same error as already exists.
this.unset("error", { silent: true });
try {
// User is looking for autocompletions.
const predictions = await this.geocoderSearch.autocomplete(query);
const error = predictions.length === 0 ? NO_RESULTS_MESSAGE : "";
this.set({ error, focusIndex: -1, predictions, query });
} catch (e) {
if (
e.code === "REQUEST_DENIED" &&
e.endpoint === "PLACES_AUTOCOMPLETE"
) {
this.set({
error: PLACES_API_ERROR,
focusIndex: -1,
predictions: [],
query,
});
} else {
this.set({
error: NO_RESULTS_MESSAGE,
focusIndex: -1,
predictions: [],
query,
});
}
}
},
/**
* Decrement the focused index with a minimum value of 0. This corresponds
* to an ArrowUp key down event.
* Note: An ArrowUp key press while the current index is -1 will
* result in highlighting the first element in the list.
*/
decrementFocusIndex() {
const currentIndex = this.get("focusIndex");
this.set("focusIndex", Math.max(0, currentIndex - 1));
},
/**
* Increment the focused index with a maximum value of the last value in
* the list. This corresponds to an ArrowDown key down event.
*/
incrementFocusIndex() {
const currentIndex = this.get("focusIndex");
this.set(
"focusIndex",
Math.min(currentIndex + 1, this.get("predictions").length - 1),
);
},
/**
* Reset the focused index back to the initial value so that no element
* in the UI is highlighted.
*/
resetFocusIndex() {
this.set("focusIndex", -1);
},
/**
* Navigate to the GeocodedLocation.
* @param {GeocodedLocation} geocoding is the location that corresponds
* to the the selected prediction.
*/
goToLocation(geocoding) {
if (!geocoding) return;
const coords = geocoding.get("box").getCoords();
this.mapModel.zoomTo({
destination: Cesium.Rectangle.fromDegrees(
coords.west,
coords.south,
coords.east,
coords.north,
),
});
},
/**
* Select a ZoomPresetModel from the list of presets and navigate there.
* This function hides all layers that are not to be visible according to
* the ZoomPresetModel configuration.
* @param {ZoomPresetModel} preset A user selected preset for which to
* enable layers and navigate.
*/
selectZoomPreset(preset) {
const enabledLayerIds = preset.get("enabledLayerIds");
this.mapModel.get("allLayers").forEach((layer) => {
const isVisible = enabledLayerIds.includes(layer.get("layerId"));
// Show or hide the layer according to the preset.
layer.set("visible", isVisible);
});
this.mapModel.zoomTo(preset.get("geoPoint"));
},
/**
* Select a prediction from the list of predictions and navigate there.
* @param {Prediction} prediction is the user-selected Prediction that
* needs to be geocoded and navigated to.
*/
async selectPrediction(prediction) {
if (!prediction) return;
try {
const geocodings = await this.geocoderSearch.geocode(prediction);
if (geocodings.length === 0) {
this.set("error", NO_RESULTS_MESSAGE);
return;
}
if (geocodings.length === 0) {
this.set("error", NO_RESULTS_MESSAGE);
return;
}
this.trigger("selection-made", prediction.get("description"));
this.goToLocation(geocodings[0]);
} catch (e) {
if (
e.code === "REQUEST_DENIED" &&
e.endpoint === "GEOCODER_GEOCODE"
) {
this.set({
error: GEOCODING_API_ERROR,
focusIndex: -1,
predictions: [],
});
} else {
this.set("error", NO_RESULTS_MESSAGE);
}
}
},
/**
* Event handler for Backbone.View configuration that is called whenever
* the user clicks the search button or hits the Enter key.
* @param {string} value is the query string.
*/
async search(value) {
if (!value) return;
// This is not a lat,long value, so geocode the prediction instead.
if (!GeoPoint.couldBeLatLong(value)) {
const focusedIndex = Math.max(0, this.get("focusIndex"));
this.selectPrediction(this.get("predictions")[focusedIndex]);
return;
}
// Unset error so the error will fire a change event even if it is the
// same error as already exists.
this.unset("error", { silent: true });
try {
const geoPoint = new GeoPoint(value, { parse: true });
geoPoint.set("height", 10000 /* meters */);
if (geoPoint.isValid()) {
this.set("error", "");
this.mapModel.zoomTo(geoPoint);
return;
}
const errors = geoPoint.validationError;
if (errors.latitude) {
this.set("error", errors.latitude);
} else if (errors.longitude) {
this.set("error", errors.longitude);
}
} catch (e) {
this.set("error", e.message);
}
},
},
);
return ViewfinderModel;
});