Geocoder.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import {
  2. defined,
  3. destroyObject,
  4. DeveloperError,
  5. FeatureDetection,
  6. getElement,
  7. } from "@cesium/engine";
  8. import knockout from "../ThirdParty/knockout.js";
  9. import GeocoderViewModel from "./GeocoderViewModel.js";
  10. const startSearchPath =
  11. "M29.772,26.433l-7.126-7.126c0.96-1.583,1.523-3.435,1.524-5.421C24.169,8.093,19.478,3.401,13.688,3.399C7.897,3.401,3.204,8.093,3.204,13.885c0,5.789,4.693,10.481,10.484,10.481c1.987,0,3.839-0.563,5.422-1.523l7.128,7.127L29.772,26.433zM7.203,13.885c0.006-3.582,2.903-6.478,6.484-6.486c3.579,0.008,6.478,2.904,6.484,6.486c-0.007,3.58-2.905,6.476-6.484,6.484C10.106,20.361,7.209,17.465,7.203,13.885z";
  12. const stopSearchPath =
  13. "M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z";
  14. /**
  15. * A widget for finding addresses and landmarks, and flying the camera to them. Geocoding is
  16. * performed using {@link https://cesium.com/cesium-ion/|Cesium ion}.
  17. *
  18. * @alias Geocoder
  19. * @constructor
  20. *
  21. * @param {object} options Object with the following properties:
  22. * @param {Element|string} options.container The DOM element or ID that will contain the widget.
  23. * @param {Scene} options.scene The Scene instance to use.
  24. * @param {GeocoderService[]} [options.geocoderServices] The geocoder services to be used
  25. * @param {boolean} [options.autoComplete = true] True if the geocoder should query as the user types to autocomplete
  26. * @param {number} [options.flightDuration=1.5] The duration of the camera flight to an entered location, in seconds.
  27. * @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.
  28. */
  29. function Geocoder(options) {
  30. //>>includeStart('debug', pragmas.debug);
  31. if (!defined(options) || !defined(options.container)) {
  32. throw new DeveloperError("options.container is required.");
  33. }
  34. if (!defined(options.scene)) {
  35. throw new DeveloperError("options.scene is required.");
  36. }
  37. //>>includeEnd('debug');
  38. const container = getElement(options.container);
  39. const viewModel = new GeocoderViewModel(options);
  40. viewModel._startSearchPath = startSearchPath;
  41. viewModel._stopSearchPath = stopSearchPath;
  42. const form = document.createElement("form");
  43. form.setAttribute("data-bind", "submit: search");
  44. const textBox = document.createElement("input");
  45. textBox.type = "search";
  46. textBox.className = "cesium-geocoder-input";
  47. textBox.setAttribute("placeholder", "Enter an address or landmark...");
  48. textBox.setAttribute(
  49. "data-bind",
  50. '\
  51. textInput: searchText,\
  52. disable: isSearchInProgress,\
  53. event: { keyup: handleKeyUp, keydown: handleKeyDown, mouseover: deselectSuggestion },\
  54. css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 },\
  55. hasFocus: _focusTextbox'
  56. );
  57. this._onTextBoxFocus = function () {
  58. // as of 2016-10-19, setTimeout is required to ensure that the
  59. // text is focused on Safari 10
  60. setTimeout(function () {
  61. textBox.select();
  62. }, 0);
  63. };
  64. textBox.addEventListener("focus", this._onTextBoxFocus, false);
  65. form.appendChild(textBox);
  66. this._textBox = textBox;
  67. const searchButton = document.createElement("span");
  68. searchButton.className = "cesium-geocoder-searchButton";
  69. searchButton.setAttribute(
  70. "data-bind",
  71. "\
  72. click: search,\
  73. cesiumSvgPath: { path: isSearchInProgress ? _stopSearchPath : _startSearchPath, width: 32, height: 32 }"
  74. );
  75. form.appendChild(searchButton);
  76. container.appendChild(form);
  77. const searchSuggestionsContainer = document.createElement("div");
  78. searchSuggestionsContainer.className = "search-results";
  79. searchSuggestionsContainer.setAttribute(
  80. "data-bind",
  81. "visible: _suggestionsVisible"
  82. );
  83. const suggestionsList = document.createElement("ul");
  84. suggestionsList.setAttribute("data-bind", "foreach: _suggestions");
  85. const suggestions = document.createElement("li");
  86. suggestionsList.appendChild(suggestions);
  87. suggestions.setAttribute(
  88. "data-bind",
  89. "text: $data.displayName, \
  90. click: $parent.activateSuggestion, \
  91. event: { mouseover: $parent.handleMouseover}, \
  92. css: { active: $data === $parent._selectedSuggestion }"
  93. );
  94. searchSuggestionsContainer.appendChild(suggestionsList);
  95. container.appendChild(searchSuggestionsContainer);
  96. knockout.applyBindings(viewModel, form);
  97. knockout.applyBindings(viewModel, searchSuggestionsContainer);
  98. this._container = container;
  99. this._searchSuggestionsContainer = searchSuggestionsContainer;
  100. this._viewModel = viewModel;
  101. this._form = form;
  102. this._onInputBegin = function (e) {
  103. // e.target will not be correct if we are inside of the Shadow DOM
  104. // and contains will always fail. To retrieve the correct target,
  105. // we need to access the first element of the composedPath.
  106. // This allows us to use shadow DOM if it exists and fall
  107. // back to legacy behavior if its not being used.
  108. let target = e.target;
  109. if (typeof e.composedPath === "function") {
  110. target = e.composedPath()[0];
  111. }
  112. if (!container.contains(target)) {
  113. viewModel._focusTextbox = false;
  114. viewModel.hideSuggestions();
  115. }
  116. };
  117. this._onInputEnd = function (e) {
  118. viewModel._focusTextbox = true;
  119. viewModel.showSuggestions();
  120. };
  121. //We subscribe to both begin and end events in order to give the text box
  122. //focus no matter where on the widget is clicked.
  123. if (FeatureDetection.supportsPointerEvents()) {
  124. document.addEventListener("pointerdown", this._onInputBegin, true);
  125. container.addEventListener("pointerup", this._onInputEnd, true);
  126. container.addEventListener("pointercancel", this._onInputEnd, true);
  127. } else {
  128. document.addEventListener("mousedown", this._onInputBegin, true);
  129. container.addEventListener("mouseup", this._onInputEnd, true);
  130. document.addEventListener("touchstart", this._onInputBegin, true);
  131. container.addEventListener("touchend", this._onInputEnd, true);
  132. container.addEventListener("touchcancel", this._onInputEnd, true);
  133. }
  134. }
  135. Object.defineProperties(Geocoder.prototype, {
  136. /**
  137. * Gets the parent container.
  138. * @memberof Geocoder.prototype
  139. *
  140. * @type {Element}
  141. */
  142. container: {
  143. get: function () {
  144. return this._container;
  145. },
  146. },
  147. /**
  148. * Gets the parent container.
  149. * @memberof Geocoder.prototype
  150. *
  151. * @type {Element}
  152. */
  153. searchSuggestionsContainer: {
  154. get: function () {
  155. return this._searchSuggestionsContainer;
  156. },
  157. },
  158. /**
  159. * Gets the view model.
  160. * @memberof Geocoder.prototype
  161. *
  162. * @type {GeocoderViewModel}
  163. */
  164. viewModel: {
  165. get: function () {
  166. return this._viewModel;
  167. },
  168. },
  169. });
  170. /**
  171. * @returns {boolean} true if the object has been destroyed, false otherwise.
  172. */
  173. Geocoder.prototype.isDestroyed = function () {
  174. return false;
  175. };
  176. /**
  177. * Destroys the widget. Should be called if permanently
  178. * removing the widget from layout.
  179. */
  180. Geocoder.prototype.destroy = function () {
  181. const container = this._container;
  182. if (FeatureDetection.supportsPointerEvents()) {
  183. document.removeEventListener("pointerdown", this._onInputBegin, true);
  184. container.removeEventListener("pointerup", this._onInputEnd, true);
  185. } else {
  186. document.removeEventListener("mousedown", this._onInputBegin, true);
  187. container.removeEventListener("mouseup", this._onInputEnd, true);
  188. document.removeEventListener("touchstart", this._onInputBegin, true);
  189. container.removeEventListener("touchend", this._onInputEnd, true);
  190. }
  191. this._viewModel.destroy();
  192. knockout.cleanNode(this._form);
  193. knockout.cleanNode(this._searchSuggestionsContainer);
  194. container.removeChild(this._form);
  195. container.removeChild(this._searchSuggestionsContainer);
  196. this._textBox.removeEventListener("focus", this._onTextBoxFocus, false);
  197. return destroyObject(this);
  198. };
  199. /**
  200. * A function that handles the result of a successful geocode.
  201. * @callback Geocoder.DestinationFoundFunction
  202. * @param {GeocoderViewModel} viewModel The view model.
  203. * @param {Cartesian3|Rectangle} destination The destination result of the geocode.
  204. */
  205. export default Geocoder;