智慧水务管理系统 - 精河县供水工程综合管理平台

GeocoderViewModel.js 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614
  1. import {
  2. computeFlyToLocationForRectangle,
  3. defined,
  4. DeveloperError,
  5. destroyObject,
  6. Event,
  7. GeocoderService,
  8. GeocodeType,
  9. getElement,
  10. IonGeocoderService,
  11. Math as CesiumMath,
  12. Matrix4,
  13. Rectangle,
  14. sampleTerrainMostDetailed,
  15. } from "@cesium/engine";
  16. import knockout from "../ThirdParty/knockout.js";
  17. import createCommand from "../createCommand.js";
  18. // The height we use if geocoding to a specific point instead of an rectangle.
  19. const DEFAULT_HEIGHT = 1000;
  20. /**
  21. * The view model for the {@link Geocoder} widget.
  22. * @alias GeocoderViewModel
  23. * @constructor
  24. *
  25. * @param {object} options Object with the following properties:
  26. * @param {Scene} options.scene The Scene instance to use.
  27. * @param {GeocoderService[]} [options.geocoderServices] Geocoder services to use for geocoding queries.
  28. * If more than one are supplied, suggestions will be gathered for the geocoders that support it,
  29. * and if no suggestion is selected the result from the first geocoder service wil be used.
  30. * @param {number} [options.flightDuration] The duration of the camera flight to an entered location, in seconds.
  31. * @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.
  32. */
  33. function GeocoderViewModel(options) {
  34. //>>includeStart('debug', pragmas.debug);
  35. if (!defined(options) || !defined(options.scene)) {
  36. throw new DeveloperError("options.scene is required.");
  37. }
  38. //>>includeEnd('debug');
  39. if (defined(options.geocoderServices)) {
  40. this._geocoderServices = options.geocoderServices;
  41. } else {
  42. this._geocoderServices = [new IonGeocoderService({ scene: options.scene })];
  43. }
  44. this._viewContainer = options.container;
  45. this._scene = options.scene;
  46. this._flightDuration = options.flightDuration;
  47. this._searchText = "";
  48. this._isSearchInProgress = false;
  49. this._wasGeocodeCancelled = false;
  50. this._previousCredits = [];
  51. this._complete = new Event();
  52. this._suggestions = [];
  53. this._selectedSuggestion = undefined;
  54. this._showSuggestions = true;
  55. this._handleArrowDown = handleArrowDown;
  56. this._handleArrowUp = handleArrowUp;
  57. const that = this;
  58. this._suggestionsVisible = knockout.pureComputed(function () {
  59. const suggestions = knockout.getObservable(that, "_suggestions");
  60. const suggestionsNotEmpty = suggestions().length > 0;
  61. const showSuggestions = knockout.getObservable(that, "_showSuggestions")();
  62. return suggestionsNotEmpty && showSuggestions;
  63. });
  64. this._searchCommand = createCommand(function (geocodeType) {
  65. geocodeType = geocodeType ?? GeocodeType.SEARCH;
  66. that._focusTextbox = false;
  67. if (defined(that._selectedSuggestion)) {
  68. that.activateSuggestion(that._selectedSuggestion);
  69. return false;
  70. }
  71. that.hideSuggestions();
  72. if (that.isSearchInProgress) {
  73. cancelGeocode(that);
  74. } else {
  75. return geocode(that, that._geocoderServices, geocodeType);
  76. }
  77. });
  78. this.deselectSuggestion = function () {
  79. that._selectedSuggestion = undefined;
  80. };
  81. this.handleKeyDown = function (data, event) {
  82. const downKey =
  83. event.key === "ArrowDown" || event.key === "Down" || event.keyCode === 40;
  84. const upKey =
  85. event.key === "ArrowUp" || event.key === "Up" || event.keyCode === 38;
  86. if (downKey || upKey) {
  87. event.preventDefault();
  88. }
  89. return true;
  90. };
  91. this.handleKeyUp = function (data, event) {
  92. const downKey =
  93. event.key === "ArrowDown" || event.key === "Down" || event.keyCode === 40;
  94. const upKey =
  95. event.key === "ArrowUp" || event.key === "Up" || event.keyCode === 38;
  96. const enterKey = event.key === "Enter" || event.keyCode === 13;
  97. if (upKey) {
  98. handleArrowUp(that);
  99. } else if (downKey) {
  100. handleArrowDown(that);
  101. } else if (enterKey) {
  102. that._searchCommand();
  103. }
  104. return true;
  105. };
  106. this.activateSuggestion = function (data) {
  107. that.hideSuggestions();
  108. that._searchText = data.displayName;
  109. const destination = data.destination;
  110. clearSuggestions(that);
  111. that.destinationFound(that, destination);
  112. };
  113. this.hideSuggestions = function () {
  114. that._showSuggestions = false;
  115. that._selectedSuggestion = undefined;
  116. };
  117. this.showSuggestions = function () {
  118. that._showSuggestions = true;
  119. };
  120. this.handleMouseover = function (data, event) {
  121. if (data !== that._selectedSuggestion) {
  122. that._selectedSuggestion = data;
  123. }
  124. };
  125. /**
  126. * Gets or sets a value indicating if this instance should always show its text input field.
  127. *
  128. * @type {boolean}
  129. * @default false
  130. */
  131. this.keepExpanded = false;
  132. /**
  133. * True if the geocoder should query as the user types to autocomplete
  134. * @type {boolean}
  135. * @default true
  136. */
  137. this.autoComplete = options.autocomplete ?? true;
  138. /**
  139. * Gets and sets the command called when a geocode destination is found
  140. * @type {Geocoder.DestinationFoundFunction}
  141. */
  142. this.destinationFound =
  143. options.destinationFound ?? GeocoderViewModel.flyToDestination;
  144. this._focusTextbox = false;
  145. knockout.track(this, [
  146. "_searchText",
  147. "_isSearchInProgress",
  148. "keepExpanded",
  149. "_suggestions",
  150. "_selectedSuggestion",
  151. "_showSuggestions",
  152. "_focusTextbox",
  153. ]);
  154. const searchTextObservable = knockout.getObservable(this, "_searchText");
  155. searchTextObservable.extend({ rateLimit: { timeout: 500 } });
  156. this._suggestionSubscription = searchTextObservable.subscribe(function () {
  157. GeocoderViewModel._updateSearchSuggestions(that);
  158. });
  159. /**
  160. * Gets a value indicating whether a search is currently in progress. This property is observable.
  161. *
  162. * @type {boolean}
  163. */
  164. this.isSearchInProgress = undefined;
  165. knockout.defineProperty(this, "isSearchInProgress", {
  166. get: function () {
  167. return this._isSearchInProgress;
  168. },
  169. });
  170. /**
  171. * Gets or sets the text to search for. The text can be an address, or longitude, latitude,
  172. * and optional height, where longitude and latitude are in degrees and height is in meters.
  173. *
  174. * @type {string}
  175. */
  176. this.searchText = undefined;
  177. knockout.defineProperty(this, "searchText", {
  178. get: function () {
  179. if (this.isSearchInProgress) {
  180. return "Searching...";
  181. }
  182. return this._searchText;
  183. },
  184. set: function (value) {
  185. //>>includeStart('debug', pragmas.debug);
  186. if (typeof value !== "string") {
  187. throw new DeveloperError("value must be a valid string.");
  188. }
  189. //>>includeEnd('debug');
  190. this._searchText = value;
  191. },
  192. });
  193. /**
  194. * Gets or sets the the duration of the camera flight in seconds.
  195. * A value of zero causes the camera to instantly switch to the geocoding location.
  196. * The duration will be computed based on the distance when undefined.
  197. *
  198. * @type {number|undefined}
  199. * @default undefined
  200. */
  201. this.flightDuration = undefined;
  202. knockout.defineProperty(this, "flightDuration", {
  203. get: function () {
  204. return this._flightDuration;
  205. },
  206. set: function (value) {
  207. //>>includeStart('debug', pragmas.debug);
  208. if (defined(value) && value < 0) {
  209. throw new DeveloperError("value must be positive.");
  210. }
  211. //>>includeEnd('debug');
  212. this._flightDuration = value;
  213. },
  214. });
  215. }
  216. Object.defineProperties(GeocoderViewModel.prototype, {
  217. /**
  218. * Gets the event triggered on flight completion.
  219. * @memberof GeocoderViewModel.prototype
  220. *
  221. * @type {Event}
  222. */
  223. complete: {
  224. get: function () {
  225. return this._complete;
  226. },
  227. },
  228. /**
  229. * Gets the scene to control.
  230. * @memberof GeocoderViewModel.prototype
  231. *
  232. * @type {Scene}
  233. */
  234. scene: {
  235. get: function () {
  236. return this._scene;
  237. },
  238. },
  239. /**
  240. * Gets the Command that is executed when the button is clicked.
  241. * @memberof GeocoderViewModel.prototype
  242. *
  243. * @type {Command}
  244. */
  245. search: {
  246. get: function () {
  247. return this._searchCommand;
  248. },
  249. },
  250. /**
  251. * Gets the currently selected geocoder search suggestion
  252. * @memberof GeocoderViewModel.prototype
  253. *
  254. * @type {object}
  255. */
  256. selectedSuggestion: {
  257. get: function () {
  258. return this._selectedSuggestion;
  259. },
  260. },
  261. /**
  262. * Gets the list of geocoder search suggestions
  263. * @memberof GeocoderViewModel.prototype
  264. *
  265. * @type {object[]}
  266. */
  267. suggestions: {
  268. get: function () {
  269. return this._suggestions;
  270. },
  271. },
  272. });
  273. /**
  274. * Destroys the widget. Should be called if permanently
  275. * removing the widget from layout.
  276. */
  277. GeocoderViewModel.prototype.destroy = function () {
  278. this._suggestionSubscription.dispose();
  279. };
  280. function handleArrowUp(viewModel) {
  281. if (viewModel._suggestions.length === 0) {
  282. return;
  283. }
  284. const currentIndex = viewModel._suggestions.indexOf(
  285. viewModel._selectedSuggestion,
  286. );
  287. if (currentIndex === -1 || currentIndex === 0) {
  288. viewModel._selectedSuggestion = undefined;
  289. return;
  290. }
  291. const next = currentIndex - 1;
  292. viewModel._selectedSuggestion = viewModel._suggestions[next];
  293. GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
  294. }
  295. function handleArrowDown(viewModel) {
  296. if (viewModel._suggestions.length === 0) {
  297. return;
  298. }
  299. const numberOfSuggestions = viewModel._suggestions.length;
  300. const currentIndex = viewModel._suggestions.indexOf(
  301. viewModel._selectedSuggestion,
  302. );
  303. const next = (currentIndex + 1) % numberOfSuggestions;
  304. viewModel._selectedSuggestion = viewModel._suggestions[next];
  305. GeocoderViewModel._adjustSuggestionsScroll(viewModel, next);
  306. }
  307. function computeFlyToLocationForCartographic(cartographic, terrainProvider) {
  308. const availability = defined(terrainProvider)
  309. ? terrainProvider.availability
  310. : undefined;
  311. if (!defined(availability)) {
  312. cartographic.height += DEFAULT_HEIGHT;
  313. return Promise.resolve(cartographic);
  314. }
  315. return sampleTerrainMostDetailed(terrainProvider, [cartographic]).then(
  316. function (positionOnTerrain) {
  317. cartographic = positionOnTerrain[0];
  318. cartographic.height += DEFAULT_HEIGHT;
  319. return cartographic;
  320. },
  321. );
  322. }
  323. function flyToDestination(viewModel, destination) {
  324. const scene = viewModel._scene;
  325. const ellipsoid = scene.ellipsoid;
  326. const camera = scene.camera;
  327. const terrainProvider = scene.terrainProvider;
  328. let finalDestination = destination;
  329. let promise;
  330. if (destination instanceof Rectangle) {
  331. // Some geocoders return a Rectangle of zero width/height, treat it like a point instead.
  332. if (
  333. CesiumMath.equalsEpsilon(
  334. destination.south,
  335. destination.north,
  336. CesiumMath.EPSILON7,
  337. ) &&
  338. CesiumMath.equalsEpsilon(
  339. destination.east,
  340. destination.west,
  341. CesiumMath.EPSILON7,
  342. )
  343. ) {
  344. // destination is now a Cartographic
  345. destination = Rectangle.center(destination);
  346. } else {
  347. promise = computeFlyToLocationForRectangle(destination, scene);
  348. }
  349. } else {
  350. // destination is a Cartesian3
  351. destination = ellipsoid.cartesianToCartographic(destination);
  352. }
  353. if (!defined(promise)) {
  354. promise = computeFlyToLocationForCartographic(destination, terrainProvider);
  355. }
  356. return promise
  357. .then(function (result) {
  358. finalDestination = ellipsoid.cartographicToCartesian(result);
  359. })
  360. .finally(function () {
  361. // Whether terrain querying succeeded or not, fly to the destination.
  362. camera.flyTo({
  363. destination: finalDestination,
  364. complete: function () {
  365. viewModel._complete.raiseEvent();
  366. },
  367. duration: viewModel._flightDuration,
  368. endTransform: Matrix4.IDENTITY,
  369. });
  370. });
  371. }
  372. async function attemptGeocode(geocoderService, query, geocodeType) {
  373. try {
  374. const result = await geocoderService.geocode(query, geocodeType);
  375. return {
  376. state: "fulfilled",
  377. value: result,
  378. credits: geocoderService.credit,
  379. };
  380. } catch (error) {
  381. return { state: "rejected", reason: error };
  382. }
  383. }
  384. async function geocode(viewModel, geocoderServices, geocodeType) {
  385. const query = viewModel._searchText;
  386. if (hasOnlyWhitespace(query)) {
  387. viewModel.showSuggestions();
  388. return;
  389. }
  390. viewModel._isSearchInProgress = true;
  391. viewModel._wasGeocodeCancelled = false;
  392. let i;
  393. let result;
  394. for (i = 0; i < geocoderServices.length; i++) {
  395. if (viewModel._wasGeocodeCancelled) {
  396. return;
  397. }
  398. result = await attemptGeocode(geocoderServices[i], query, geocodeType);
  399. if (
  400. defined(result) &&
  401. result.state === "fulfilled" &&
  402. result.value.length > 0
  403. ) {
  404. break;
  405. }
  406. }
  407. if (viewModel._wasGeocodeCancelled) {
  408. return;
  409. }
  410. viewModel._isSearchInProgress = false;
  411. clearCredits(viewModel);
  412. const geocoderResults = result.value;
  413. if (
  414. result.state === "fulfilled" &&
  415. defined(geocoderResults) &&
  416. geocoderResults.length > 0
  417. ) {
  418. viewModel._searchText = geocoderResults[0].displayName;
  419. viewModel.destinationFound(viewModel, geocoderResults[0].destination);
  420. const credits = updateCredits(
  421. viewModel,
  422. GeocoderService.getCreditsFromResult(geocoderResults[0]),
  423. );
  424. // If the result does not contain any credits, default to the service credit.
  425. if (!defined(credits)) {
  426. updateCredit(viewModel, geocoderServices[i].credit);
  427. }
  428. return;
  429. }
  430. viewModel._searchText = `${query} (not found)`;
  431. }
  432. function updateCredit(viewModel, credit) {
  433. if (
  434. defined(credit) &&
  435. !viewModel._scene.isDestroyed() &&
  436. !viewModel._scene.frameState.creditDisplay.isDestroyed()
  437. ) {
  438. viewModel._scene.frameState.creditDisplay.addStaticCredit(credit);
  439. viewModel._previousCredits.push(credit);
  440. }
  441. }
  442. function updateCredits(viewModel, credits) {
  443. if (defined(credits)) {
  444. credits.forEach((credit) => updateCredit(viewModel, credit));
  445. }
  446. return credits;
  447. }
  448. function clearCredits(viewModel) {
  449. if (
  450. !viewModel._scene.isDestroyed() &&
  451. !viewModel._scene.frameState.creditDisplay.isDestroyed()
  452. ) {
  453. viewModel._previousCredits.forEach((credit) => {
  454. viewModel._scene.frameState.creditDisplay.removeStaticCredit(credit);
  455. });
  456. }
  457. viewModel._previousCredits.length = 0;
  458. }
  459. function adjustSuggestionsScroll(viewModel, focusedItemIndex) {
  460. const container = getElement(viewModel._viewContainer);
  461. const searchResults = container.getElementsByClassName("search-results")[0];
  462. const listItems = container.getElementsByTagName("li");
  463. const element = listItems[focusedItemIndex];
  464. if (focusedItemIndex === 0) {
  465. searchResults.scrollTop = 0;
  466. return;
  467. }
  468. const offsetTop = element.offsetTop;
  469. if (offsetTop + element.clientHeight > searchResults.clientHeight) {
  470. searchResults.scrollTop = offsetTop + element.clientHeight;
  471. } else if (offsetTop < searchResults.scrollTop) {
  472. searchResults.scrollTop = offsetTop;
  473. }
  474. }
  475. function cancelGeocode(viewModel) {
  476. if (viewModel._isSearchInProgress) {
  477. viewModel._isSearchInProgress = false;
  478. viewModel._wasGeocodeCancelled = true;
  479. }
  480. }
  481. function hasOnlyWhitespace(string) {
  482. return /^\s*$/.test(string);
  483. }
  484. function clearSuggestions(viewModel) {
  485. knockout.getObservable(viewModel, "_suggestions").removeAll();
  486. }
  487. async function updateSearchSuggestions(viewModel) {
  488. if (!viewModel.autoComplete) {
  489. return;
  490. }
  491. const query = viewModel._searchText;
  492. clearSuggestions(viewModel);
  493. clearCredits(viewModel);
  494. if (hasOnlyWhitespace(query)) {
  495. return;
  496. }
  497. for (const service of viewModel._geocoderServices) {
  498. const newResults = await service.geocode(query, GeocodeType.AUTOCOMPLETE);
  499. viewModel._suggestions = viewModel._suggestions.concat(newResults);
  500. if (newResults.length > 0) {
  501. let useDefaultCredit = true;
  502. newResults.forEach((result) => {
  503. const credits = GeocoderService.getCreditsFromResult(result);
  504. useDefaultCredit = useDefaultCredit && !defined(credits);
  505. updateCredits(viewModel, credits);
  506. });
  507. // Use the service credit if there were no attributions in the results
  508. if (useDefaultCredit) {
  509. updateCredit(viewModel, service.credit);
  510. }
  511. }
  512. if (viewModel._suggestions.length >= 5) {
  513. return;
  514. }
  515. }
  516. }
  517. /**
  518. * A function to fly to the destination found by a successful geocode.
  519. * @type {Geocoder.DestinationFoundFunction}
  520. */
  521. GeocoderViewModel.flyToDestination = flyToDestination;
  522. //exposed for testing
  523. GeocoderViewModel._updateSearchSuggestions = updateSearchSuggestions;
  524. GeocoderViewModel._adjustSuggestionsScroll = adjustSuggestionsScroll;
  525. /**
  526. * @returns {boolean} true if the object has been destroyed, false otherwise.
  527. */
  528. GeocoderViewModel.prototype.isDestroyed = function () {
  529. return false;
  530. };
  531. /**
  532. * Destroys the widget. Should be called if permanently
  533. * removing the widget from layout.
  534. */
  535. GeocoderViewModel.prototype.destroy = function () {
  536. clearCredits(this);
  537. return destroyObject(this);
  538. };
  539. export default GeocoderViewModel;