Source: Widgets/Geocoder/GeocoderViewModel.js

  1. /*global define*/
  2. define([
  3. '../../Core/BingMapsApi',
  4. '../../Core/Cartesian3',
  5. '../../Core/defaultValue',
  6. '../../Core/defined',
  7. '../../Core/defineProperties',
  8. '../../Core/DeveloperError',
  9. '../../Core/Event',
  10. '../../Core/loadJsonp',
  11. '../../Core/Matrix4',
  12. '../../Core/Rectangle',
  13. '../../Scene/SceneMode',
  14. '../../ThirdParty/knockout',
  15. '../../ThirdParty/when',
  16. '../createCommand'
  17. ], function(
  18. BingMapsApi,
  19. Cartesian3,
  20. defaultValue,
  21. defined,
  22. defineProperties,
  23. DeveloperError,
  24. Event,
  25. loadJsonp,
  26. Matrix4,
  27. Rectangle,
  28. SceneMode,
  29. knockout,
  30. when,
  31. createCommand) {
  32. 'use strict';
  33. /**
  34. * The view model for the {@link Geocoder} widget.
  35. * @alias GeocoderViewModel
  36. * @constructor
  37. *
  38. * @param {Object} options Object with the following properties:
  39. * @param {Scene} options.scene The Scene instance to use.
  40. * @param {String} [options.url='https://dev.virtualearth.net'] The base URL of the Bing Maps API.
  41. * @param {String} [options.key] The Bing Maps key for your application, which can be
  42. * created at {@link https://www.bingmapsportal.com}.
  43. * If this parameter is not provided, {@link BingMapsApi.defaultKey} is used.
  44. * If {@link BingMapsApi.defaultKey} is undefined as well, a message is
  45. * written to the console reminding you that you must create and supply a Bing Maps
  46. * key as soon as possible. Please do not deploy an application that uses
  47. * this widget without creating a separate key for your application.
  48. * @param {Number} [options.flightDuration] The duration of the camera flight to an entered location, in seconds.
  49. */
  50. function GeocoderViewModel(options) {
  51. //>>includeStart('debug', pragmas.debug);
  52. if (!defined(options) || !defined(options.scene)) {
  53. throw new DeveloperError('options.scene is required.');
  54. }
  55. //>>includeEnd('debug');
  56. this._url = defaultValue(options.url, 'https://dev.virtualearth.net/');
  57. if (this._url.length > 0 && this._url[this._url.length - 1] !== '/') {
  58. this._url += '/';
  59. }
  60. this._key = BingMapsApi.getKey(options.key);
  61. var errorCredit = BingMapsApi.getErrorCredit(options.key);
  62. if (defined(errorCredit)) {
  63. options.scene._frameState.creditDisplay.addDefaultCredit(errorCredit);
  64. }
  65. this._scene = options.scene;
  66. this._flightDuration = options.flightDuration;
  67. this._searchText = '';
  68. this._isSearchInProgress = false;
  69. this._geocodeInProgress = undefined;
  70. this._complete = new Event();
  71. var that = this;
  72. this._searchCommand = createCommand(function() {
  73. if (that.isSearchInProgress) {
  74. cancelGeocode(that);
  75. } else {
  76. geocode(that);
  77. }
  78. });
  79. /**
  80. * Gets or sets a value indicating if this instance should always show its text input field.
  81. *
  82. * @type {Boolean}
  83. * @default false
  84. */
  85. this.keepExpanded = false;
  86. knockout.track(this, ['_searchText', '_isSearchInProgress', 'keepExpanded']);
  87. /**
  88. * Gets a value indicating whether a search is currently in progress. This property is observable.
  89. *
  90. * @type {Boolean}
  91. */
  92. this.isSearchInProgress = undefined;
  93. knockout.defineProperty(this, 'isSearchInProgress', {
  94. get : function() {
  95. return this._isSearchInProgress;
  96. }
  97. });
  98. /**
  99. * Gets or sets the text to search for. The text can be an address, or longitude, latitude,
  100. * and optional height, where longitude and latitude are in degrees and height is in meters.
  101. *
  102. * @type {String}
  103. */
  104. this.searchText = undefined;
  105. knockout.defineProperty(this, 'searchText', {
  106. get : function() {
  107. if (this.isSearchInProgress) {
  108. return 'Searching...';
  109. }
  110. return this._searchText;
  111. },
  112. set : function(value) {
  113. //>>includeStart('debug', pragmas.debug);
  114. if (typeof value !== 'string') {
  115. throw new DeveloperError('value must be a valid string.');
  116. }
  117. //>>includeEnd('debug');
  118. this._searchText = value;
  119. }
  120. });
  121. /**
  122. * Gets or sets the the duration of the camera flight in seconds.
  123. * A value of zero causes the camera to instantly switch to the geocoding location.
  124. * The duration will be computed based on the distance when undefined.
  125. *
  126. * @type {Number|undefined}
  127. * @default undefined
  128. */
  129. this.flightDuration = undefined;
  130. knockout.defineProperty(this, 'flightDuration', {
  131. get : function() {
  132. return this._flightDuration;
  133. },
  134. set : function(value) {
  135. //>>includeStart('debug', pragmas.debug);
  136. if (defined(value) && value < 0) {
  137. throw new DeveloperError('value must be positive.');
  138. }
  139. //>>includeEnd('debug');
  140. this._flightDuration = value;
  141. }
  142. });
  143. }
  144. defineProperties(GeocoderViewModel.prototype, {
  145. /**
  146. * Gets the Bing maps url.
  147. * @memberof GeocoderViewModel.prototype
  148. *
  149. * @type {String}
  150. */
  151. url : {
  152. get : function() {
  153. return this._url;
  154. }
  155. },
  156. /**
  157. * Gets the Bing maps key.
  158. * @memberof GeocoderViewModel.prototype
  159. *
  160. * @type {String}
  161. */
  162. key : {
  163. get : function() {
  164. return this._key;
  165. }
  166. },
  167. /**
  168. * Gets the event triggered on flight completion.
  169. * @memberof GeocoderViewModel.prototype
  170. *
  171. * @type {Event}
  172. */
  173. complete : {
  174. get : function() {
  175. return this._complete;
  176. }
  177. },
  178. /**
  179. * Gets the scene to control.
  180. * @memberof GeocoderViewModel.prototype
  181. *
  182. * @type {Scene}
  183. */
  184. scene : {
  185. get : function() {
  186. return this._scene;
  187. }
  188. },
  189. /**
  190. * Gets the Command that is executed when the button is clicked.
  191. * @memberof GeocoderViewModel.prototype
  192. *
  193. * @type {Command}
  194. */
  195. search : {
  196. get : function() {
  197. return this._searchCommand;
  198. }
  199. }
  200. });
  201. function updateCamera(viewModel, destination) {
  202. viewModel._scene.camera.flyTo({
  203. destination : destination,
  204. complete: function() {
  205. viewModel._complete.raiseEvent();
  206. },
  207. duration : viewModel._flightDuration,
  208. endTransform : Matrix4.IDENTITY
  209. });
  210. }
  211. function geocode(viewModel) {
  212. var query = viewModel.searchText;
  213. if (/^\s*$/.test(query)) {
  214. //whitespace string
  215. return;
  216. }
  217. // If the user entered (longitude, latitude, [height]) in degrees/meters,
  218. // fly without calling the geocoder.
  219. var splitQuery = query.match(/[^\s,\n]+/g);
  220. if ((splitQuery.length === 2) || (splitQuery.length === 3)) {
  221. var longitude = +splitQuery[0];
  222. var latitude = +splitQuery[1];
  223. var height = (splitQuery.length === 3) ? +splitQuery[2] : 300.0;
  224. if (!isNaN(longitude) && !isNaN(latitude) && !isNaN(height)) {
  225. updateCamera(viewModel, Cartesian3.fromDegrees(longitude, latitude, height));
  226. return;
  227. }
  228. }
  229. viewModel._isSearchInProgress = true;
  230. var promise = loadJsonp(viewModel._url + 'REST/v1/Locations', {
  231. parameters : {
  232. query : query,
  233. key : viewModel._key
  234. },
  235. callbackParameterName : 'jsonp'
  236. });
  237. var geocodeInProgress = viewModel._geocodeInProgress = when(promise, function(result) {
  238. if (geocodeInProgress.cancel) {
  239. return;
  240. }
  241. viewModel._isSearchInProgress = false;
  242. if (result.resourceSets.length === 0) {
  243. viewModel.searchText = viewModel._searchText + ' (not found)';
  244. return;
  245. }
  246. var resourceSet = result.resourceSets[0];
  247. if (resourceSet.resources.length === 0) {
  248. viewModel.searchText = viewModel._searchText + ' (not found)';
  249. return;
  250. }
  251. var resource = resourceSet.resources[0];
  252. viewModel._searchText = resource.name;
  253. var bbox = resource.bbox;
  254. var south = bbox[0];
  255. var west = bbox[1];
  256. var north = bbox[2];
  257. var east = bbox[3];
  258. updateCamera(viewModel, Rectangle.fromDegrees(west, south, east, north));
  259. }, function() {
  260. if (geocodeInProgress.cancel) {
  261. return;
  262. }
  263. viewModel._isSearchInProgress = false;
  264. viewModel.searchText = viewModel._searchText + ' (error)';
  265. });
  266. }
  267. function cancelGeocode(viewModel) {
  268. viewModel._isSearchInProgress = false;
  269. if (defined(viewModel._geocodeInProgress)) {
  270. viewModel._geocodeInProgress.cancel = true;
  271. viewModel._geocodeInProgress = undefined;
  272. }
  273. }
  274. return GeocoderViewModel;
  275. });