123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518 |
- import CartographicGeocoderService from '../../Core/CartographicGeocoderService.js';
- import defaultValue from '../../Core/defaultValue.js';
- import defined from '../../Core/defined.js';
- import defineProperties from '../../Core/defineProperties.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 when from '../../ThirdParty/when.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.
- var 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;
- var that = this;
- this._suggestionsVisible = knockout.pureComputed(function () {
- var suggestions = knockout.getObservable(that, '_suggestions');
- var suggestionsNotEmpty = suggestions().length > 0;
- var 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) {
- var downKey = event.key === 'ArrowDown' || event.key === 'Down' || event.keyCode === 40;
- var upKey = event.key === 'ArrowUp' || event.key === 'Up' || event.keyCode === 38;
- if (downKey || upKey) {
- event.preventDefault();
- }
- return true;
- };
- this.handleKeyUp = function (data, event) {
- var downKey = event.key === 'ArrowDown' || event.key === 'Down' || event.keyCode === 40;
- var upKey = event.key === 'ArrowUp' || event.key === 'Up' || event.keyCode === 38;
- var 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;
- var 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']);
- var 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;
- }
- });
- }
- 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;
- }
- var next;
- var currentIndex = viewModel._suggestions.indexOf(viewModel._selectedSuggestion);
- if (currentIndex === -1 || currentIndex === 0) {
- viewModel._selectedSuggestion = undefined;
- return;
- }
- next = currentIndex - 1;
- viewModel._selectedSuggestion = viewModel._suggestions[next];
- GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
- }
- function handleArrowDown(viewModel) {
- if (viewModel._suggestions.length === 0) {
- return;
- }
- var numberOfSuggestions = viewModel._suggestions.length;
- var currentIndex = viewModel._suggestions.indexOf(viewModel._selectedSuggestion);
- var next = (currentIndex + 1) % numberOfSuggestions;
- viewModel._selectedSuggestion = viewModel._suggestions[next];
- GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
- }
- function computeFlyToLocationForCartographic(cartographic, terrainProvider) {
- var availability = defined(terrainProvider) ? terrainProvider.availability : undefined;
- if (!defined(availability)) {
- cartographic.height += DEFAULT_HEIGHT;
- return when.resolve(cartographic);
- }
- return sampleTerrainMostDetailed(terrainProvider, [cartographic])
- .then(function(positionOnTerrain) {
- cartographic = positionOnTerrain[0];
- cartographic.height += DEFAULT_HEIGHT;
- return cartographic;
- });
- }
- function flyToDestination(viewModel, destination) {
- var scene = viewModel._scene;
- var mapProjection = scene.mapProjection;
- var ellipsoid = mapProjection.ellipsoid;
- var camera = scene.camera;
- var terrainProvider = scene.terrainProvider;
- var finalDestination = destination;
- var 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);
- }
- promise
- .then(function(result) {
- finalDestination = ellipsoid.cartographicToCartesian(result);
- })
- .always(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;
- }
- var nextPromise = geocoderService.geocode(query, geocodeType)
- .then(function (result) {
- return {state: 'fulfilled', value: result};
- })
- .otherwise(function (err) {
- return {state: 'rejected', reason: err};
- });
- return nextPromise;
- });
- }
- function geocode(viewModel, geocoderServices, geocodeType) {
- var query = viewModel._searchText;
- if (hasOnlyWhitespace(query)) {
- viewModel.showSuggestions();
- return;
- }
- viewModel._isSearchInProgress = true;
- var promise = when.resolve();
- for (var 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;
- var 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) {
- var container = getElement(viewModel._viewContainer);
- var searchResults = container.getElementsByClassName('search-results')[0];
- var listItems = container.getElementsByTagName('li');
- var element = listItems[focusedItemIndex];
- if (focusedItemIndex === 0) {
- searchResults.scrollTop = 0;
- return;
- }
- var 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;
- }
- var query = viewModel._searchText;
- clearSuggestions(viewModel);
- if (hasOnlyWhitespace(query)) {
- return;
- }
- var promise = when.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;
- });
- });
- });
- promise
- .then(function (results) {
- var suggestions = viewModel._suggestions;
- for (var 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;
|