123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555 |
- import CartographicGeocoderService from "../../Core/CartographicGeocoderService.js";
- import defaultValue from "../../Core/defaultValue.js";
- import defined from "../../Core/defined.js";
- import DeveloperError from "../../Core/DeveloperError.js";
- import Event from "../../Core/Event.js";
- import GeocodeType from "../../Core/GeocodeType.js";
- import IonGeocoderService from "../../Core/IonGeocoderService.js";
- import CesiumMath from "../../Core/Math.js";
- import Matrix4 from "../../Core/Matrix4.js";
- import Rectangle from "../../Core/Rectangle.js";
- import sampleTerrainMostDetailed from "../../Core/sampleTerrainMostDetailed.js";
- import computeFlyToLocationForRectangle from "../../Scene/computeFlyToLocationForRectangle.js";
- import knockout from "../../ThirdParty/knockout.js";
- import createCommand from "../createCommand.js";
- import getElement from "../getElement.js";
- // The height we use if geocoding to a specific point instead of an rectangle.
- const DEFAULT_HEIGHT = 1000;
- /**
- * The view model for the {@link Geocoder} widget.
- * @alias GeocoderViewModel
- * @constructor
- *
- * @param {Object} options Object with the following properties:
- * @param {Scene} options.scene The Scene instance to use.
- * @param {GeocoderService[]} [options.geocoderServices] Geocoder services to use for geocoding queries.
- * If more than one are supplied, suggestions will be gathered for the geocoders that support it,
- * and if no suggestion is selected the result from the first geocoder service wil be used.
- * @param {Number} [options.flightDuration] The duration of the camera flight to an entered location, in seconds.
- * @param {Geocoder.DestinationFoundFunction} [options.destinationFound=GeocoderViewModel.flyToDestination] A callback function that is called after a successful geocode. If not supplied, the default behavior is to fly the camera to the result destination.
- */
- function GeocoderViewModel(options) {
- //>>includeStart('debug', pragmas.debug);
- if (!defined(options) || !defined(options.scene)) {
- throw new DeveloperError("options.scene is required.");
- }
- //>>includeEnd('debug');
- if (defined(options.geocoderServices)) {
- this._geocoderServices = options.geocoderServices;
- } else {
- this._geocoderServices = [
- new CartographicGeocoderService(),
- new IonGeocoderService({ scene: options.scene }),
- ];
- }
- this._viewContainer = options.container;
- this._scene = options.scene;
- this._flightDuration = options.flightDuration;
- this._searchText = "";
- this._isSearchInProgress = false;
- this._geocodePromise = undefined;
- this._complete = new Event();
- this._suggestions = [];
- this._selectedSuggestion = undefined;
- this._showSuggestions = true;
- this._handleArrowDown = handleArrowDown;
- this._handleArrowUp = handleArrowUp;
- const that = this;
- this._suggestionsVisible = knockout.pureComputed(function () {
- const suggestions = knockout.getObservable(that, "_suggestions");
- const suggestionsNotEmpty = suggestions().length > 0;
- const showSuggestions = knockout.getObservable(that, "_showSuggestions")();
- return suggestionsNotEmpty && showSuggestions;
- });
- this._searchCommand = createCommand(function (geocodeType) {
- geocodeType = defaultValue(geocodeType, GeocodeType.SEARCH);
- that._focusTextbox = false;
- if (defined(that._selectedSuggestion)) {
- that.activateSuggestion(that._selectedSuggestion);
- return false;
- }
- that.hideSuggestions();
- if (that.isSearchInProgress) {
- cancelGeocode(that);
- } else {
- geocode(that, that._geocoderServices, geocodeType);
- }
- });
- this.deselectSuggestion = function () {
- that._selectedSuggestion = undefined;
- };
- this.handleKeyDown = function (data, event) {
- const downKey =
- event.key === "ArrowDown" || event.key === "Down" || event.keyCode === 40;
- const upKey =
- event.key === "ArrowUp" || event.key === "Up" || event.keyCode === 38;
- if (downKey || upKey) {
- event.preventDefault();
- }
- return true;
- };
- this.handleKeyUp = function (data, event) {
- const downKey =
- event.key === "ArrowDown" || event.key === "Down" || event.keyCode === 40;
- const upKey =
- event.key === "ArrowUp" || event.key === "Up" || event.keyCode === 38;
- const enterKey = event.key === "Enter" || event.keyCode === 13;
- if (upKey) {
- handleArrowUp(that);
- } else if (downKey) {
- handleArrowDown(that);
- } else if (enterKey) {
- that._searchCommand();
- }
- return true;
- };
- this.activateSuggestion = function (data) {
- that.hideSuggestions();
- that._searchText = data.displayName;
- const destination = data.destination;
- clearSuggestions(that);
- that.destinationFound(that, destination);
- };
- this.hideSuggestions = function () {
- that._showSuggestions = false;
- that._selectedSuggestion = undefined;
- };
- this.showSuggestions = function () {
- that._showSuggestions = true;
- };
- this.handleMouseover = function (data, event) {
- if (data !== that._selectedSuggestion) {
- that._selectedSuggestion = data;
- }
- };
- /**
- * Gets or sets a value indicating if this instance should always show its text input field.
- *
- * @type {Boolean}
- * @default false
- */
- this.keepExpanded = false;
- /**
- * True if the geocoder should query as the user types to autocomplete
- * @type {Boolean}
- * @default true
- */
- this.autoComplete = defaultValue(options.autocomplete, true);
- /**
- * Gets and sets the command called when a geocode destination is found
- * @type {Geocoder.DestinationFoundFunction}
- */
- this.destinationFound = defaultValue(
- options.destinationFound,
- GeocoderViewModel.flyToDestination
- );
- this._focusTextbox = false;
- knockout.track(this, [
- "_searchText",
- "_isSearchInProgress",
- "keepExpanded",
- "_suggestions",
- "_selectedSuggestion",
- "_showSuggestions",
- "_focusTextbox",
- ]);
- const searchTextObservable = knockout.getObservable(this, "_searchText");
- searchTextObservable.extend({ rateLimit: { timeout: 500 } });
- this._suggestionSubscription = searchTextObservable.subscribe(function () {
- GeocoderViewModel._updateSearchSuggestions(that);
- });
- /**
- * Gets a value indicating whether a search is currently in progress. This property is observable.
- *
- * @type {Boolean}
- */
- this.isSearchInProgress = undefined;
- knockout.defineProperty(this, "isSearchInProgress", {
- get: function () {
- return this._isSearchInProgress;
- },
- });
- /**
- * Gets or sets the text to search for. The text can be an address, or longitude, latitude,
- * and optional height, where longitude and latitude are in degrees and height is in meters.
- *
- * @type {String}
- */
- this.searchText = undefined;
- knockout.defineProperty(this, "searchText", {
- get: function () {
- if (this.isSearchInProgress) {
- return "Searching...";
- }
- return this._searchText;
- },
- set: function (value) {
- //>>includeStart('debug', pragmas.debug);
- if (typeof value !== "string") {
- throw new DeveloperError("value must be a valid string.");
- }
- //>>includeEnd('debug');
- this._searchText = value;
- },
- });
- /**
- * Gets or sets the the duration of the camera flight in seconds.
- * A value of zero causes the camera to instantly switch to the geocoding location.
- * The duration will be computed based on the distance when undefined.
- *
- * @type {Number|undefined}
- * @default undefined
- */
- this.flightDuration = undefined;
- knockout.defineProperty(this, "flightDuration", {
- get: function () {
- return this._flightDuration;
- },
- set: function (value) {
- //>>includeStart('debug', pragmas.debug);
- if (defined(value) && value < 0) {
- throw new DeveloperError("value must be positive.");
- }
- //>>includeEnd('debug');
- this._flightDuration = value;
- },
- });
- }
- Object.defineProperties(GeocoderViewModel.prototype, {
- /**
- * Gets the event triggered on flight completion.
- * @memberof GeocoderViewModel.prototype
- *
- * @type {Event}
- */
- complete: {
- get: function () {
- return this._complete;
- },
- },
- /**
- * Gets the scene to control.
- * @memberof GeocoderViewModel.prototype
- *
- * @type {Scene}
- */
- scene: {
- get: function () {
- return this._scene;
- },
- },
- /**
- * Gets the Command that is executed when the button is clicked.
- * @memberof GeocoderViewModel.prototype
- *
- * @type {Command}
- */
- search: {
- get: function () {
- return this._searchCommand;
- },
- },
- /**
- * Gets the currently selected geocoder search suggestion
- * @memberof GeocoderViewModel.prototype
- *
- * @type {Object}
- */
- selectedSuggestion: {
- get: function () {
- return this._selectedSuggestion;
- },
- },
- /**
- * Gets the list of geocoder search suggestions
- * @memberof GeocoderViewModel.prototype
- *
- * @type {Object[]}
- */
- suggestions: {
- get: function () {
- return this._suggestions;
- },
- },
- });
- /**
- * Destroys the widget. Should be called if permanently
- * removing the widget from layout.
- */
- GeocoderViewModel.prototype.destroy = function () {
- this._suggestionSubscription.dispose();
- };
- function handleArrowUp(viewModel) {
- if (viewModel._suggestions.length === 0) {
- return;
- }
- const currentIndex = viewModel._suggestions.indexOf(
- viewModel._selectedSuggestion
- );
- if (currentIndex === -1 || currentIndex === 0) {
- viewModel._selectedSuggestion = undefined;
- return;
- }
- const next = currentIndex - 1;
- viewModel._selectedSuggestion = viewModel._suggestions[next];
- GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
- }
- function handleArrowDown(viewModel) {
- if (viewModel._suggestions.length === 0) {
- return;
- }
- const numberOfSuggestions = viewModel._suggestions.length;
- const currentIndex = viewModel._suggestions.indexOf(
- viewModel._selectedSuggestion
- );
- const next = (currentIndex + 1) % numberOfSuggestions;
- viewModel._selectedSuggestion = viewModel._suggestions[next];
- GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
- }
- function computeFlyToLocationForCartographic(cartographic, terrainProvider) {
- const availability = defined(terrainProvider)
- ? terrainProvider.availability
- : undefined;
- if (!defined(availability)) {
- cartographic.height += DEFAULT_HEIGHT;
- return Promise.resolve(cartographic);
- }
- return sampleTerrainMostDetailed(terrainProvider, [cartographic]).then(
- function (positionOnTerrain) {
- cartographic = positionOnTerrain[0];
- cartographic.height += DEFAULT_HEIGHT;
- return cartographic;
- }
- );
- }
- function flyToDestination(viewModel, destination) {
- const scene = viewModel._scene;
- const mapProjection = scene.mapProjection;
- const ellipsoid = mapProjection.ellipsoid;
- const camera = scene.camera;
- const terrainProvider = scene.terrainProvider;
- let finalDestination = destination;
- let promise;
- if (destination instanceof Rectangle) {
- // Some geocoders return a Rectangle of zero width/height, treat it like a point instead.
- if (
- CesiumMath.equalsEpsilon(
- destination.south,
- destination.north,
- CesiumMath.EPSILON7
- ) &&
- CesiumMath.equalsEpsilon(
- destination.east,
- destination.west,
- CesiumMath.EPSILON7
- )
- ) {
- // destination is now a Cartographic
- destination = Rectangle.center(destination);
- } else {
- promise = computeFlyToLocationForRectangle(destination, scene);
- }
- } else {
- // destination is a Cartesian3
- destination = ellipsoid.cartesianToCartographic(destination);
- }
- if (!defined(promise)) {
- promise = computeFlyToLocationForCartographic(destination, terrainProvider);
- }
- return promise
- .then(function (result) {
- finalDestination = ellipsoid.cartographicToCartesian(result);
- })
- .finally(function () {
- // Whether terrain querying succeeded or not, fly to the destination.
- camera.flyTo({
- destination: finalDestination,
- complete: function () {
- viewModel._complete.raiseEvent();
- },
- duration: viewModel._flightDuration,
- endTransform: Matrix4.IDENTITY,
- });
- });
- }
- function chainPromise(promise, geocoderService, query, geocodeType) {
- return promise.then(function (result) {
- if (
- defined(result) &&
- result.state === "fulfilled" &&
- result.value.length > 0
- ) {
- return result;
- }
- const nextPromise = geocoderService
- .geocode(query, geocodeType)
- .then(function (result) {
- return { state: "fulfilled", value: result };
- })
- .catch(function (err) {
- return { state: "rejected", reason: err };
- });
- return nextPromise;
- });
- }
- function geocode(viewModel, geocoderServices, geocodeType) {
- const query = viewModel._searchText;
- if (hasOnlyWhitespace(query)) {
- viewModel.showSuggestions();
- return;
- }
- viewModel._isSearchInProgress = true;
- let promise = Promise.resolve();
- for (let i = 0; i < geocoderServices.length; i++) {
- promise = chainPromise(promise, geocoderServices[i], query, geocodeType);
- }
- viewModel._geocodePromise = promise;
- promise.then(function (result) {
- if (promise.cancel) {
- return;
- }
- viewModel._isSearchInProgress = false;
- const geocoderResults = result.value;
- if (
- result.state === "fulfilled" &&
- defined(geocoderResults) &&
- geocoderResults.length > 0
- ) {
- viewModel._searchText = geocoderResults[0].displayName;
- viewModel.destinationFound(viewModel, geocoderResults[0].destination);
- return;
- }
- viewModel._searchText = `${query} (not found)`;
- });
- }
- function adjustSuggestionsScroll(viewModel, focusedItemIndex) {
- const container = getElement(viewModel._viewContainer);
- const searchResults = container.getElementsByClassName("search-results")[0];
- const listItems = container.getElementsByTagName("li");
- const element = listItems[focusedItemIndex];
- if (focusedItemIndex === 0) {
- searchResults.scrollTop = 0;
- return;
- }
- const offsetTop = element.offsetTop;
- if (offsetTop + element.clientHeight > searchResults.clientHeight) {
- searchResults.scrollTop = offsetTop + element.clientHeight;
- } else if (offsetTop < searchResults.scrollTop) {
- searchResults.scrollTop = offsetTop;
- }
- }
- function cancelGeocode(viewModel) {
- viewModel._isSearchInProgress = false;
- if (defined(viewModel._geocodePromise)) {
- viewModel._geocodePromise.cancel = true;
- viewModel._geocodePromise = undefined;
- }
- }
- function hasOnlyWhitespace(string) {
- return /^\s*$/.test(string);
- }
- function clearSuggestions(viewModel) {
- knockout.getObservable(viewModel, "_suggestions").removeAll();
- }
- function updateSearchSuggestions(viewModel) {
- if (!viewModel.autoComplete) {
- return;
- }
- const query = viewModel._searchText;
- clearSuggestions(viewModel);
- if (hasOnlyWhitespace(query)) {
- return;
- }
- let promise = Promise.resolve([]);
- viewModel._geocoderServices.forEach(function (service) {
- promise = promise.then(function (results) {
- if (results.length >= 5) {
- return results;
- }
- return service
- .geocode(query, GeocodeType.AUTOCOMPLETE)
- .then(function (newResults) {
- results = results.concat(newResults);
- return results;
- });
- });
- });
- return promise.then(function (results) {
- const suggestions = viewModel._suggestions;
- for (let i = 0; i < results.length; i++) {
- suggestions.push(results[i]);
- }
- });
- }
- /**
- * A function to fly to the destination found by a successful geocode.
- * @type {Geocoder.DestinationFoundFunction}
- */
- GeocoderViewModel.flyToDestination = flyToDestination;
- //exposed for testing
- GeocoderViewModel._updateSearchSuggestions = updateSearchSuggestions;
- GeocoderViewModel._adjustSuggestionsScroll = adjustSuggestionsScroll;
- export default GeocoderViewModel;
|