| 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 testingGeocoderViewModel._updateSearchSuggestions = updateSearchSuggestions;GeocoderViewModel._adjustSuggestionsScroll = adjustSuggestionsScroll;export default GeocoderViewModel;
 |