From 89fcd986af45b5b58bbb37f2ad0786be913e8ccc Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Mon, 5 Dec 2016 15:55:39 -0500 Subject: [PATCH 01/33] Support custom geocoder --- Apps/Sandcastle/gallery/Custom Geocoder.html | 10 ++++ Source/Widgets/Geocoder/GeocoderViewModel.js | 56 +++++++++++++++++++- Source/Widgets/Viewer/Viewer.js | 1 + 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 Apps/Sandcastle/gallery/Custom Geocoder.html diff --git a/Apps/Sandcastle/gallery/Custom Geocoder.html b/Apps/Sandcastle/gallery/Custom Geocoder.html new file mode 100644 index 000000000000..f7e58848298e --- /dev/null +++ b/Apps/Sandcastle/gallery/Custom Geocoder.html @@ -0,0 +1,10 @@ + + + + + $Title$ + + +$END$ + + \ No newline at end of file diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index c7d13cc633b4..163663cb7b4c 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -53,6 +53,14 @@ define([ if (!defined(options) || !defined(options.scene)) { throw new DeveloperError('options.scene is required.'); } + if (defined(options.customGeocoder)) { + if (!defined(options.customGeocoder.getSuggestions)) { + throw new DeveloperError('options.customGeocoder is available but missing a getSuggestions method'); + } + if (!defined(options.customGeocoder.geocode)) { + throw new DeveloperError('options.customGeocoder is available but missing a geocode method'); + } + } //>>includeEnd('debug'); this._url = defaultValue(options.url, 'https://dev.virtualearth.net/'); @@ -78,7 +86,7 @@ define([ if (that.isSearchInProgress) { cancelGeocode(that); } else { - geocode(that); + geocode(that, options.customGeocoder); } }); @@ -227,7 +235,7 @@ define([ }); } - function geocode(viewModel) { + function geocode(viewModel, customGeocoder) { var query = viewModel.searchText; if (/^\s*$/.test(query)) { @@ -235,6 +243,50 @@ define([ return; } + if (defined(customGeocoder)) { + viewModel._isSearchInProgress = true; + customGeocoder.geocode(query, function (err, results) { + if (defined(err)) { + viewModel._isSearchInProgress = false; + return; + + } + if (results.length === 0) { + viewModel.searchText = viewModel._searchText + ' (not found)'; + viewModel._isSearchInProgress = false; + return; + } + + var firstResult = results[0]; + //>>includeStart('debug', pragmas.debug); + if (!defined(firstResult.displayName)) { + throw new DeveloperError('each result must have a displayName'); + } + if (!defined(firstResult.bbox)) { + throw new DeveloperError('each result must have a bbox'); + } + if (!defined(firstResult.bbox.south) || !defined(firstResult.bbox.west) || !defined(firstResult.bbox.north) || !defined(firstResult.bbox.east)) { + throw new DeveloperError('each result must have a bbox where south, west, north and east are defined'); + } + //>>includeEnd('debug'); + + viewModel._searchText = firstResult.displayName; + var bbox = firstResult.bbox; + var south = bbox.south; + var west = bbox.west; + var north = bbox.north; + var east = bbox.east; + + updateCamera(viewModel, Rectangle.fromDegrees(west, south, east, north)); + viewModel._isSearchInProgress = false; + }); + } else { + defaultGeocode(viewModel, query); + } + } + + function defaultGeocode(viewModel, query) { + // If the user entered (longitude, latitude, [height]) in degrees/meters, // fly without calling the geocoder. var splitQuery = query.match(/[^\s,\n]+/g); diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index aa067441d541..e23692e5df52 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -473,6 +473,7 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to toolbar.appendChild(geocoderContainer); geocoder = new Geocoder({ container : geocoderContainer, + customGeocoder: options.geocoder, scene : cesiumWidget.scene }); // Subscribe to search so that we can clear the trackedEntity when it is clicked. From 6bfbc9dc115926fc4d70912c5044c710fe0118ec Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Mon, 5 Dec 2016 15:56:59 -0500 Subject: [PATCH 02/33] Add example of how to create make a custom geocoder --- Apps/Sandcastle/gallery/Custom Geocoder.html | 81 +++++++++++++++++++- 1 file changed, 77 insertions(+), 4 deletions(-) diff --git a/Apps/Sandcastle/gallery/Custom Geocoder.html b/Apps/Sandcastle/gallery/Custom Geocoder.html index f7e58848298e..d8bfe5d5b605 100644 --- a/Apps/Sandcastle/gallery/Custom Geocoder.html +++ b/Apps/Sandcastle/gallery/Custom Geocoder.html @@ -1,10 +1,83 @@ - - $Title$ + + + + + + Cesium Demo + + + - -$END$ + + +
+

Loading...

+ \ No newline at end of file From 60db026a0ebc9a69b307194300305f40341ca331 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Mon, 5 Dec 2016 22:45:42 -0500 Subject: [PATCH 03/33] Add support for geocoder suggestions --- Source/Widgets/Geocoder/Geocoder.css | 23 +++++ Source/Widgets/Geocoder/Geocoder.js | 30 ++++++ Source/Widgets/Geocoder/GeocoderViewModel.js | 99 +++++++++++++++++++- Source/Widgets/Viewer/Viewer.js | 5 + 4 files changed, 155 insertions(+), 2 deletions(-) diff --git a/Source/Widgets/Geocoder/Geocoder.css b/Source/Widgets/Geocoder/Geocoder.css index b8da4c244277..739ed6b14bde 100644 --- a/Source/Widgets/Geocoder/Geocoder.css +++ b/Source/Widgets/Geocoder/Geocoder.css @@ -37,6 +37,29 @@ width: 250px; } +.cesium-viewer-geocoderContainer .search-results { + position: absolute; + background-color: black; + overflow-y: auto; + opacity: 0.8; + width: 100%; +} + +.cesium-viewer-geocoderContainer .search-results ul { + list-style-type: none; + margin: 0; + padding: 4px 0px; +} + +.cesium-viewer-geocoderContainer .search-results ul li { + font-size: 14px; + padding: 3px 10px; +} +.cesium-viewer-geocoderContainer .search-results ul li:hover { + text-decoration: underline; + cursor: pointer; +} + .cesium-geocoder-searchButton { background-color: #303336; display: inline-block; diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index 6ed87298029e..8bd17d670bab 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -69,6 +69,7 @@ define([ value: searchText,\ valueUpdate: "afterkeydown",\ disable: isSearchInProgress,\ +event: { keyup: handleKeyUp },\ css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 }'); this._onTextBoxFocus = function() { @@ -92,9 +93,26 @@ cesiumSvgPath: { path: isSearchInProgress ? _stopSearchPath : _startSearchPath, container.appendChild(form); + var searchSuggestionsContainer = document.createElement('div'); + searchSuggestionsContainer.className = 'search-results'; + searchSuggestionsContainer.setAttribute('data-bind', 'visible: suggestionsVisible'); + + var suggestionsList = document.createElement('ul'); + suggestionsList.setAttribute('data-bind', 'foreach: suggestions'); + var suggestions = document.createElement('li'); + suggestionsList.appendChild(suggestions); + suggestions.setAttribute('data-bind', 'text: $data.displayName, \ +click: $parent.activateSuggestion');//,\ +//css: { active: $data === $parent.selectedSuggestion }'); + + searchSuggestionsContainer.appendChild(suggestionsList); + container.appendChild(searchSuggestionsContainer); + knockout.applyBindings(viewModel, form); + knockout.applyBindings(viewModel, searchSuggestionsContainer); this._container = container; + this._searchSuggestionsContainer = searchSuggestionsContainer; this._viewModel = viewModel; this._form = form; @@ -140,6 +158,18 @@ cesiumSvgPath: { path: isSearchInProgress ? _stopSearchPath : _startSearchPath, } }, + /** + * Gets the parent container. + * @memberof Geocoder.prototype + * + * @type {Element} + */ + searchSuggestionsContainer : { + get : function() { + return this._searchSuggestionsContainer; + } + }, + /** * Gets the view model. * @memberof Geocoder.prototype diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 163663cb7b4c..9777747cd916 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -80,8 +80,20 @@ define([ this._isSearchInProgress = false; this._geocodeInProgress = undefined; this._complete = new Event(); + this._suggestions = knockout.observableArray(); + this._selectedSuggestion = knockout.observable(); var that = this; + + /** + * Indicates whether search suggestions should be visible. True if there are at least 1 suggestion. + * + * @type {Boolean} + */ + this.suggestionsVisible = knockout.pureComputed(function () { + return that._suggestions().length > 0; + }); + this._searchCommand = createCommand(function() { if (that.isSearchInProgress) { cancelGeocode(that); @@ -90,6 +102,80 @@ define([ } }); + this.handleArrowDown = function () { + if (that._suggestions().length === 0) { + return; + } + var numberOfSuggestions = that._suggestions().length; + var currentIndex = that._suggestions().indexOf(that._selectedSuggestion()); + var next = (currentIndex + 1) % numberOfSuggestions; + that._selectedSuggestion(that._suggestions()[next]); + }; + this.handleArrowUp = function () { + if (that._suggestions().length === 0) { + return; + } + var numberOfSuggestions = that._suggestions().length; + var next; + var currentIndex = that._suggestions().indexOf(that._selectedSuggestion()); + if (currentIndex === -1 || currentIndex === 0) { + next = numberOfSuggestions - 1; + } else { + next = currentIndex - 1; + } + that._selectedSuggestion(that._suggestions()[next]); + }; + + this.updateSearchSuggestions = function () { + var query = that.searchText; + + if (hasOnlyWhitespace(query)) { + that._suggestions.splice(0, that._suggestions().length); + return; + } + + var customGeocoder = options.customGeocoder; + if (defined(customGeocoder)) { + customGeocoder.geocode(query, function (err, results) { + if (defined(err)) { + return; + } + that._suggestions.splice(0, that._suggestions().length); + if (results.length > 0) { + results.slice(0, 5).forEach(function (result) { + that._suggestions.push(result); + }); + } + }); + } + }; + + this.isSelected = function(data) { + var index = this._suggestions().indexOf(data); + console.log(index); + return index === this._selectedSuggestion(); + }; + + this.handleKeyUp = function(data, event) { + var key = event.which; + if (key === 38) { + that.handleArrowUp(); + return; + } else if (key === 40) { + that.handleArrowDown(); + return; + } + that.updateSearchSuggestions(); + return true; + }; + + this.activateSuggestion = function (data) { + that._searchText = data.displayName; + var bbox = data.bbox; + that._suggestions.splice(0, that._suggestions().length); + updateCamera(that, Rectangle.fromDegrees(bbox.west, bbox.south, bbox.east, bbox.north)); + }; + /** * Gets or sets a value indicating if this instance should always show its text input field. * @@ -221,6 +307,12 @@ define([ get : function() { return this._searchCommand; } + }, + + suggestions : { + get : function() { + return this._suggestions; + } } }); @@ -238,8 +330,7 @@ define([ function geocode(viewModel, customGeocoder) { var query = viewModel.searchText; - if (/^\s*$/.test(query)) { - //whitespace string + if (hasOnlyWhitespace(query)) { return; } @@ -356,5 +447,9 @@ define([ } } + function hasOnlyWhitespace(string) { + return /^\s*$/.test(string); + } + return GeocoderViewModel; }); diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index e23692e5df52..dcf0c865bc1a 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -1270,6 +1270,11 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to baseLayerPickerDropDown.style.maxHeight = panelMaxHeight + 'px'; } + if (defined(this._geocoder)) { + var geocoderSuggestions = this._geocoder.searchSuggestionsContainer; + geocoderSuggestions.style.maxHeight = panelMaxHeight + 'px'; + } + if (defined(this._infoBox)) { this._infoBox.viewModel.maxHeight = panelMaxHeight; } From fb193463c1c940816587b156d44edd2e5b0651a1 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 6 Dec 2016 10:28:40 -0500 Subject: [PATCH 04/33] Enable keyboard navigation for geocoder suggestions --- Source/Widgets/Geocoder/Geocoder.css | 7 ++- Source/Widgets/Geocoder/Geocoder.js | 7 ++- Source/Widgets/Geocoder/GeocoderViewModel.js | 39 +++++++++++- .../Widgets/Geocoder/GeocoderViewModelSpec.js | 63 +++++++++++++++++++ 4 files changed, 108 insertions(+), 8 deletions(-) diff --git a/Source/Widgets/Geocoder/Geocoder.css b/Source/Widgets/Geocoder/Geocoder.css index 739ed6b14bde..f77696d693a8 100644 --- a/Source/Widgets/Geocoder/Geocoder.css +++ b/Source/Widgets/Geocoder/Geocoder.css @@ -48,7 +48,7 @@ .cesium-viewer-geocoderContainer .search-results ul { list-style-type: none; margin: 0; - padding: 4px 0px; + padding: 0; } .cesium-viewer-geocoderContainer .search-results ul li { @@ -56,10 +56,13 @@ padding: 3px 10px; } .cesium-viewer-geocoderContainer .search-results ul li:hover { - text-decoration: underline; cursor: pointer; } +.cesium-viewer-geocoderContainer .search-results ul li.active { + background: #48b; +} + .cesium-geocoder-searchButton { background-color: #303336; display: inline-block; diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index 8bd17d670bab..48d6504d447d 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -69,7 +69,7 @@ define([ value: searchText,\ valueUpdate: "afterkeydown",\ disable: isSearchInProgress,\ -event: { keyup: handleKeyUp },\ +event: { keyup: handleKeyUp, keydown: handleKeyDown },\ css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 }'); this._onTextBoxFocus = function() { @@ -102,8 +102,9 @@ cesiumSvgPath: { path: isSearchInProgress ? _stopSearchPath : _startSearchPath, var suggestions = document.createElement('li'); suggestionsList.appendChild(suggestions); suggestions.setAttribute('data-bind', 'text: $data.displayName, \ -click: $parent.activateSuggestion');//,\ -//css: { active: $data === $parent.selectedSuggestion }'); +click: $parent.activateSuggestion, \ +event: { mouseover: $parent.handleMouseover }, \ +css: { active: $data === $parent.selectedSuggestion() }'); searchSuggestionsContainer.appendChild(suggestionsList); container.appendChild(searchSuggestionsContainer); diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 9777747cd916..f7333d00a85f 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -95,6 +95,10 @@ define([ }); this._searchCommand = createCommand(function() { + if (defined(that._selectedSuggestion())) { + that.activateSuggestion(that._selectedSuggestion()); + return false; + } if (that.isSearchInProgress) { cancelGeocode(that); } else { @@ -156,14 +160,27 @@ define([ return index === this._selectedSuggestion(); }; - this.handleKeyUp = function(data, event) { + this.handleKeyDown = function (data, event) { + var key = event.which; + if (key === 38) { + event.preventDefault(); + } else if (key === 40) { + event.preventDefault(); + } + return true; + }; + + this.handleKeyUp = function (data, event) { var key = event.which; if (key === 38) { that.handleArrowUp(); - return; + return true; } else if (key === 40) { that.handleArrowDown(); - return; + return true; + } else if (key === 13) { + that.activateSuggestion(that._selectedSuggestion()); + return false; } that.updateSearchSuggestions(); return true; @@ -176,6 +193,12 @@ define([ updateCamera(that, Rectangle.fromDegrees(bbox.west, bbox.south, bbox.east, bbox.north)); }; + 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. * @@ -249,6 +272,10 @@ define([ } defineProperties(GeocoderViewModel.prototype, { + /** + * Gets the currently selected geocoder suggestion + * @memberof GeocoderViewModel.prototype + */ /** * Gets the Bing maps url. * @memberof GeocoderViewModel.prototype @@ -309,6 +336,12 @@ define([ } }, + selectedSuggestion : { + get : function() { + return this._selectedSuggestion; + } + }, + suggestions : { get : function() { return this._suggestions; diff --git a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js index 3df6917aad7d..a15d7670659b 100644 --- a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js @@ -128,4 +128,67 @@ defineSuite([ return spyListener.calls.count() === 2; }); }); + + it('can be created with a custom geocoder', function() { + expect(function() { + return new GeocoderViewModel({ + scene : scene, + customGeocoder : { + geocode : function (input) { + return 'fake'; + }, + getSuggestions : function (input) { + return []; + } + } + }); + }).not.toThrowDeveloperError(); + }); + + fit('automatic suggestions can be retrieved', function() { + var geocoder = new GeocoderViewModel({ + scene : scene, + customGeocoder : { + geocode : function (input, callback) { + callback(undefined, ['a', 'b', 'c']); + }, + getSuggestions : function (input) { + return ['a', 'b', 'c']; + } + } + }); + geocoder._searchText = 'some_text'; + geocoder.updateSearchSuggestions(); + expect(geocoder._suggestions().length).toEqual(3); + }); + + fit('automatic suggestions can be navigated by arrow up/down keys', function() { + var geocoder = new GeocoderViewModel({ + scene : scene, + customGeocoder : { + geocode : function (input, callback) { + callback(undefined, ['a', 'b', 'c']); + }, + getSuggestions : function (input) { + return ['a', 'b', 'c']; + } + } + }); + geocoder._searchText = 'some_text'; + geocoder.updateSearchSuggestions(); + + expect(geocoder._selectedSuggestion()).toEqual(undefined); + geocoder.handleArrowDown(); + expect(geocoder._selectedSuggestion()).toEqual('a'); + geocoder.handleArrowDown(); + geocoder.handleArrowDown(); + expect(geocoder._selectedSuggestion()).toEqual('c'); + geocoder.handleArrowDown(); + expect(geocoder._selectedSuggestion()).toEqual('a'); + geocoder.handleArrowUp(); + expect(geocoder._selectedSuggestion()).toEqual('c'); + geocoder.handleArrowUp(); + expect(geocoder._selectedSuggestion()).toEqual('b'); + }); + }, 'WebGL'); From 9f84da3966e9acb0417a4fd126f7f7d131c5c5a7 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 6 Dec 2016 11:04:30 -0500 Subject: [PATCH 05/33] Hide/show geocoder suggestions --- Source/Widgets/Geocoder/Geocoder.js | 4 ++- Source/Widgets/Geocoder/GeocoderViewModel.js | 27 +++++++++++++------- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index 48d6504d447d..ef6cc207c75d 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -69,7 +69,7 @@ define([ value: searchText,\ valueUpdate: "afterkeydown",\ disable: isSearchInProgress,\ -event: { keyup: handleKeyUp, keydown: handleKeyDown },\ +event: { keyup: handleKeyUp, keydown: handleKeyDown, mouseover: deselectSuggestion },\ css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 }'); this._onTextBoxFocus = function() { @@ -120,12 +120,14 @@ css: { active: $data === $parent.selectedSuggestion() }'); this._onInputBegin = function(e) { if (!container.contains(e.target)) { textBox.blur(); + viewModel.hideSuggestions(); } }; this._onInputEnd = function(e) { if (container.contains(e.target)) { textBox.focus(); + viewModel.showSuggestions(); } }; diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index f7333d00a85f..4d52327d3145 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -82,6 +82,8 @@ define([ this._complete = new Event(); this._suggestions = knockout.observableArray(); this._selectedSuggestion = knockout.observable(); + this._showSuggestions = knockout.observable(true); + var that = this; @@ -91,7 +93,7 @@ define([ * @type {Boolean} */ this.suggestionsVisible = knockout.pureComputed(function () { - return that._suggestions().length > 0; + return that._suggestions().length > 0 && that._showSuggestions(); }); this._searchCommand = createCommand(function() { @@ -130,6 +132,10 @@ define([ that._selectedSuggestion(that._suggestions()[next]); }; + this.deselectSuggestion = function () { + that._selectedSuggestion(undefined); + }; + this.updateSearchSuggestions = function () { var query = that.searchText; @@ -154,12 +160,6 @@ define([ } }; - this.isSelected = function(data) { - var index = this._suggestions().indexOf(data); - console.log(index); - return index === this._selectedSuggestion(); - }; - this.handleKeyDown = function (data, event) { var key = event.which; if (key === 38) { @@ -193,6 +193,15 @@ define([ updateCamera(that, Rectangle.fromDegrees(bbox.west, bbox.south, bbox.east, bbox.north)); }; + 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); @@ -369,14 +378,14 @@ define([ if (defined(customGeocoder)) { viewModel._isSearchInProgress = true; + viewModel._suggestions.splice(0, viewModel._suggestions().length); customGeocoder.geocode(query, function (err, results) { if (defined(err)) { viewModel._isSearchInProgress = false; return; - } if (results.length === 0) { - viewModel.searchText = viewModel._searchText + ' (not found)'; + viewModel.searchText = query + ' (not found)'; viewModel._isSearchInProgress = false; return; } From bc8ed9ab5de4191251021878581d2e0c64405cfe Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 6 Dec 2016 12:26:17 -0500 Subject: [PATCH 06/33] Adjust suggestions container scroll when selecting with arrow keys --- Source/Widgets/Geocoder/GeocoderViewModel.js | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 4d52327d3145..03ea8609cd8e 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -108,6 +108,25 @@ define([ } }); + this._adjustSuggestionsScroll = function (focusedItemIndex) { + var container = document.getElementsByClassName('cesium-viewer-geocoderContainer')[0]; + 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; + } + }; + this.handleArrowDown = function () { if (that._suggestions().length === 0) { return; @@ -116,7 +135,10 @@ define([ var currentIndex = that._suggestions().indexOf(that._selectedSuggestion()); var next = (currentIndex + 1) % numberOfSuggestions; that._selectedSuggestion(that._suggestions()[next]); + + this._adjustSuggestionsScroll(next); }; + this.handleArrowUp = function () { if (that._suggestions().length === 0) { return; @@ -130,6 +152,8 @@ define([ next = currentIndex - 1; } that._selectedSuggestion(that._suggestions()[next]); + + this._adjustSuggestionsScroll(next); }; this.deselectSuggestion = function () { From da2b6cb04269862fbf6993b2aaf97a1680e861b1 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 6 Dec 2016 12:27:02 -0500 Subject: [PATCH 07/33] Use textInput binding instead of onKeyUp for updating search input value --- Source/Widgets/Geocoder/Geocoder.js | 3 +-- Source/Widgets/Geocoder/GeocoderViewModel.js | 6 +----- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index ef6cc207c75d..d7c8a3feba95 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -66,8 +66,7 @@ define([ textBox.className = 'cesium-geocoder-input'; textBox.setAttribute('placeholder', 'Enter an address or landmark...'); textBox.setAttribute('data-bind', '\ -value: searchText,\ -valueUpdate: "afterkeydown",\ +textInput: searchText,\ disable: isSearchInProgress,\ event: { keyup: handleKeyUp, keydown: handleKeyDown, mouseover: deselectSuggestion },\ css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 }'); diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 03ea8609cd8e..367650936673 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -198,15 +198,11 @@ define([ var key = event.which; if (key === 38) { that.handleArrowUp(); - return true; } else if (key === 40) { that.handleArrowDown(); - return true; } else if (key === 13) { that.activateSuggestion(that._selectedSuggestion()); - return false; } - that.updateSearchSuggestions(); return true; }; @@ -274,8 +270,8 @@ define([ throw new DeveloperError('value must be a valid string.'); } //>>includeEnd('debug'); - this._searchText = value; + this.updateSearchSuggestions(); } }); From 202eee3b9c2cb223bf6ef8d5e9e58d17ed8f02a7 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 6 Dec 2016 12:27:27 -0500 Subject: [PATCH 08/33] Clean up tests --- .../Widgets/Geocoder/GeocoderViewModelSpec.js | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js index a15d7670659b..e71f73fe1231 100644 --- a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js @@ -14,6 +14,15 @@ defineSuite([ 'use strict'; var scene; + var customGeocoderOptions = { + geocode : function (input, callback) { + callback(undefined, ['a', 'b', 'c']); + }, + getSuggestions : function (input) { + return ['a', 'b', 'c']; + } + }; + beforeAll(function() { scene = createScene(); }); @@ -133,14 +142,7 @@ defineSuite([ expect(function() { return new GeocoderViewModel({ scene : scene, - customGeocoder : { - geocode : function (input) { - return 'fake'; - }, - getSuggestions : function (input) { - return []; - } - } + customGeocoder : customGeocoderOptions }); }).not.toThrowDeveloperError(); }); @@ -148,14 +150,7 @@ defineSuite([ fit('automatic suggestions can be retrieved', function() { var geocoder = new GeocoderViewModel({ scene : scene, - customGeocoder : { - geocode : function (input, callback) { - callback(undefined, ['a', 'b', 'c']); - }, - getSuggestions : function (input) { - return ['a', 'b', 'c']; - } - } + customGeocoder : customGeocoderOptions }); geocoder._searchText = 'some_text'; geocoder.updateSearchSuggestions(); @@ -165,14 +160,7 @@ defineSuite([ fit('automatic suggestions can be navigated by arrow up/down keys', function() { var geocoder = new GeocoderViewModel({ scene : scene, - customGeocoder : { - geocode : function (input, callback) { - callback(undefined, ['a', 'b', 'c']); - }, - getSuggestions : function (input) { - return ['a', 'b', 'c']; - } - } + customGeocoder : customGeocoderOptions }); geocoder._searchText = 'some_text'; geocoder.updateSearchSuggestions(); From d2ef80c5dd3414abee6be47d36b9ccf641abf8d8 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 6 Dec 2016 14:51:00 -0500 Subject: [PATCH 09/33] Clean up and deprecate unused properties --- Source/Widgets/Geocoder/GeocoderViewModel.js | 57 ++++++++++++++------ 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 367650936673..82562c62e566 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -5,6 +5,7 @@ define([ '../../Core/defaultValue', '../../Core/defined', '../../Core/defineProperties', + '../../Core/deprecationWarning', '../../Core/DeveloperError', '../../Core/Event', '../../Core/loadJsonp', @@ -20,6 +21,7 @@ define([ defaultValue, defined, defineProperties, + deprecationWarning, DeveloperError, Event, loadJsonp, @@ -63,13 +65,21 @@ define([ } //>>includeEnd('debug'); + var errorCredit; this._url = defaultValue(options.url, 'https://dev.virtualearth.net/'); if (this._url.length > 0 && this._url[this._url.length - 1] !== '/') { this._url += '/'; } this._key = BingMapsApi.getKey(options.key); - var errorCredit = BingMapsApi.getErrorCredit(options.key); + this._defaultGeocoderOptions = { + url: this._url, + key: this._key + }; + + if (defined(options.key)) { + errorCredit = BingMapsApi.getErrorCredit(options.key); + } if (defined(errorCredit)) { options.scene._frameState.creditDisplay.addDefaultCredit(errorCredit); } @@ -84,15 +94,9 @@ define([ this._selectedSuggestion = knockout.observable(); this._showSuggestions = knockout.observable(true); - var that = this; - /** - * Indicates whether search suggestions should be visible. True if there are at least 1 suggestion. - * - * @type {Boolean} - */ - this.suggestionsVisible = knockout.pureComputed(function () { + this._suggestionsVisible = knockout.pureComputed(function () { return that._suggestions().length > 0 && that._showSuggestions(); }); @@ -301,30 +305,30 @@ define([ } defineProperties(GeocoderViewModel.prototype, { - /** - * Gets the currently selected geocoder suggestion - * @memberof GeocoderViewModel.prototype - */ /** * Gets the Bing maps url. + * @deprecated * @memberof GeocoderViewModel.prototype * * @type {String} */ url : { get : function() { + deprecationWarning('url is deprecated', 'The url property was deprecated in Cesium 1.29 and will be removed in version 1.30.'); return this._url; } }, /** * Gets the Bing maps key. + * @deprecated * @memberof GeocoderViewModel.prototype * * @type {String} */ key : { get : function() { + deprecationWarning('key is deprecated', 'The key property was deprecated in Cesium 1.29 and will be removed in version 1.30.'); return this._key; } }, @@ -365,16 +369,39 @@ define([ } }, + /** + * 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; } + }, + + /** + * Indicates whether search suggestions should be visible. True if there are at least 1 suggestion. + * + * @type {Boolean} + */ + suggestionsVisible : { + get : function() { + return this._suggestionsVisible; + } } }); @@ -439,6 +466,7 @@ define([ } function defaultGeocode(viewModel, query) { + var defaultOptions = viewModel._defaultGeocoderOptions; // If the user entered (longitude, latitude, [height]) in degrees/meters, // fly without calling the geocoder. @@ -455,11 +483,10 @@ define([ } viewModel._isSearchInProgress = true; - var promise = loadJsonp(viewModel._url + 'REST/v1/Locations', { + var promise = loadJsonp(defaultOptions.url + 'REST/v1/Locations', { parameters : { query : query, - key : viewModel._key - + key : defaultOptions.key }, callbackParameterName : 'jsonp' }); From 7680bdc0f504f36205f68bedc8987395e5074531 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 6 Dec 2016 16:09:32 -0500 Subject: [PATCH 10/33] Fix failing tests --- Source/Widgets/Geocoder/Geocoder.js | 2 ++ Source/Widgets/Geocoder/GeocoderViewModel.js | 12 ++++--- Specs/Widgets/Geocoder/GeocoderSpec.js | 36 +++++++++++++++++++ .../Widgets/Geocoder/GeocoderViewModelSpec.js | 33 +++++++++-------- 4 files changed, 64 insertions(+), 19 deletions(-) diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index d7c8a3feba95..34f1761ff217 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -208,7 +208,9 @@ css: { active: $data === $parent.selectedSuggestion() }'); } knockout.cleanNode(this._form); + knockout.cleanNode(this._searchSuggestionsContainer); this._container.removeChild(this._form); + this._container.removeChild(this._searchSuggestionsContainer); this._textBox.removeEventListener('focus', this._onTextBoxFocus, false); return destroyObject(this); diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 82562c62e566..c956cc3f54e9 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -14,7 +14,8 @@ define([ '../../Scene/SceneMode', '../../ThirdParty/knockout', '../../ThirdParty/when', - '../createCommand' + '../createCommand', + '../getElement' ], function( BingMapsApi, Cartesian3, @@ -30,7 +31,8 @@ define([ SceneMode, knockout, when, - createCommand) { + createCommand, + getElement) { 'use strict'; /** @@ -84,6 +86,7 @@ define([ options.scene._frameState.creditDisplay.addDefaultCredit(errorCredit); } + this._viewContainer = options.container; this._scene = options.scene; this._flightDuration = options.flightDuration; this._searchText = ''; @@ -93,6 +96,7 @@ define([ this._suggestions = knockout.observableArray(); this._selectedSuggestion = knockout.observable(); this._showSuggestions = knockout.observable(true); + this._updateCamera = updateCamera; var that = this; @@ -113,7 +117,7 @@ define([ }); this._adjustSuggestionsScroll = function (focusedItemIndex) { - var container = document.getElementsByClassName('cesium-viewer-geocoderContainer')[0]; + var container = getElement(this._viewContainer); var searchResults = container.getElementsByClassName('search-results')[0]; var listItems = container.getElementsByTagName('li'); var element = listItems[focusedItemIndex]; @@ -180,7 +184,7 @@ define([ } that._suggestions.splice(0, that._suggestions().length); if (results.length > 0) { - results.slice(0, 5).forEach(function (result) { + results.slice(0, Math.min(results.length, 5)).forEach(function (result) { that._suggestions.push(result); }); } diff --git a/Specs/Widgets/Geocoder/GeocoderSpec.js b/Specs/Widgets/Geocoder/GeocoderSpec.js index 484dfb350750..0402dc6fe974 100644 --- a/Specs/Widgets/Geocoder/GeocoderSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderSpec.js @@ -8,6 +8,14 @@ defineSuite([ 'use strict'; var scene; + var customGeocoderOptions = { + geocode : function (input, callback) { + callback(undefined, ['a', 'b', 'c']); + }, + getSuggestions : function (input) { + return ['a', 'b', 'c']; + } + }; beforeEach(function() { scene = createScene(); }); @@ -80,4 +88,32 @@ defineSuite([ }); }).toThrowDeveloperError(); }); + + it('automatic suggestions can be navigated by arrow up/down keys', function() { + var container = document.createElement('div'); + container.id = 'testContainer'; + document.body.appendChild(container); + var geocoder = new Geocoder({ + container : 'testContainer', + scene : scene, + customGeocoder : customGeocoderOptions + }); + var viewModel = geocoder._viewModel; + viewModel._searchText = 'some_text'; + viewModel.updateSearchSuggestions(); + + expect(viewModel._selectedSuggestion()).toEqual(undefined); + viewModel.handleArrowDown(); + expect(viewModel._selectedSuggestion()).toEqual('a'); + viewModel.handleArrowDown(); + viewModel.handleArrowDown(); + expect(viewModel._selectedSuggestion()).toEqual('c'); + viewModel.handleArrowDown(); + expect(viewModel._selectedSuggestion()).toEqual('a'); + viewModel.handleArrowUp(); + expect(viewModel._selectedSuggestion()).toEqual('c'); + viewModel.handleArrowUp(); + expect(viewModel._selectedSuggestion()).toEqual('b'); + }); + }, 'WebGL'); diff --git a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js index e71f73fe1231..e252cc0cc1e5 100644 --- a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js @@ -147,7 +147,7 @@ defineSuite([ }).not.toThrowDeveloperError(); }); - fit('automatic suggestions can be retrieved', function() { + it('automatic suggestions can be retrieved', function() { var geocoder = new GeocoderViewModel({ scene : scene, customGeocoder : customGeocoderOptions @@ -157,26 +157,29 @@ defineSuite([ expect(geocoder._suggestions().length).toEqual(3); }); - fit('automatic suggestions can be navigated by arrow up/down keys', function() { + it('update search suggestions results in empty list if the query is empty', function() { var geocoder = new GeocoderViewModel({ scene : scene, customGeocoder : customGeocoderOptions }); - geocoder._searchText = 'some_text'; + geocoder._searchText = ''; + spyOn(geocoder, '_adjustSuggestionsScroll'); geocoder.updateSearchSuggestions(); + expect(geocoder._suggestions().length).toEqual(0); + }); + + it('can activate selected search suggestion', function () { + var geocoder = new GeocoderViewModel({ + scene : scene, + customGeocoder : customGeocoderOptions + }); + spyOn(geocoder, '_updateCamera'); + spyOn(geocoder, '_adjustSuggestionsScroll'); - expect(geocoder._selectedSuggestion()).toEqual(undefined); - geocoder.handleArrowDown(); - expect(geocoder._selectedSuggestion()).toEqual('a'); - geocoder.handleArrowDown(); - geocoder.handleArrowDown(); - expect(geocoder._selectedSuggestion()).toEqual('c'); - geocoder.handleArrowDown(); - expect(geocoder._selectedSuggestion()).toEqual('a'); - geocoder.handleArrowUp(); - expect(geocoder._selectedSuggestion()).toEqual('c'); - geocoder.handleArrowUp(); - expect(geocoder._selectedSuggestion()).toEqual('b'); + var suggestion = {displayName: 'a', bbox: {west: 0.0, east: 0.1, north: 0.1, south: -0.1}}; + geocoder._selectedSuggestion(suggestion); + geocoder.activateSuggestion(suggestion); + expect(geocoder._searchText).toEqual('a'); }); }, 'WebGL'); From 8d049445202673cb3b41a325299fe96e7bf40425 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 7 Dec 2016 10:52:15 -0500 Subject: [PATCH 11/33] Clean up GeocoderService API --- Apps/Sandcastle/gallery/Custom Geocoder.html | 3 -- Source/Core/GeocoderService.js | 49 ++++++++++++++++++++ Source/Widgets/Geocoder/GeocoderViewModel.js | 23 ++++----- Source/Widgets/Viewer/Viewer.js | 2 +- 4 files changed, 59 insertions(+), 18 deletions(-) create mode 100644 Source/Core/GeocoderService.js diff --git a/Apps/Sandcastle/gallery/Custom Geocoder.html b/Apps/Sandcastle/gallery/Custom Geocoder.html index d8bfe5d5b605..1c818cbeb70b 100644 --- a/Apps/Sandcastle/gallery/Custom Geocoder.html +++ b/Apps/Sandcastle/gallery/Custom Geocoder.html @@ -38,9 +38,6 @@ //Sandcastle_Begin var options = { geocoder: { - getSuggestions: function () { - return []; - }, geocode: function (input, callback) { var endpoint = 'http://nominatim.openstreetmap.org/search?'; var query = 'format=json&q=' + input; diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js new file mode 100644 index 000000000000..f0ca404d8eba --- /dev/null +++ b/Source/Core/GeocoderService.js @@ -0,0 +1,49 @@ +/*global define*/ +define([ + './defineProperties', + './DeveloperError' + ], function( + defineProperties, + DeveloperError) { + 'use strict'; + + /** + * @typedef {Object} GeocoderResult + * @property {String} displayName The display name for a location + * @property {Rectangle} rectangle The bounding box for a location + */ + + /** + * Provides geocoding through an external service. This type describes an interface and + * is not intended to be used. + * @alias GeocoderService + * @constructor + * + * @see BingMapsGeocoderService + */ + function GeocoderService () { + } + + defineProperties(GeocoderService.prototype, { + /** + * The name of this service to be displayed next to suggestions + * in case more than one geocoder is in use + * @type {String} + * + */ + displayName : { + get : DeveloperError.throwInstantiationError + } + }); + + /** + * @function + * + * @param {String} query The query to be sent to the geocoder service + * @returns {GeocoderResult[]} geocoderResults An array containing the results from the + * geocoder service + */ + GeocoderService.prototype.geocode = DeveloperError.throwInstantiationError; + + return GeocoderService; +}); diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index c956cc3f54e9..dee1ec700ce3 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -57,13 +57,8 @@ define([ if (!defined(options) || !defined(options.scene)) { throw new DeveloperError('options.scene is required.'); } - if (defined(options.customGeocoder)) { - if (!defined(options.customGeocoder.getSuggestions)) { - throw new DeveloperError('options.customGeocoder is available but missing a getSuggestions method'); - } - if (!defined(options.customGeocoder.geocode)) { - throw new DeveloperError('options.customGeocoder is available but missing a geocode method'); - } + if (defined(options.geocoderService) && !defined(options.geocoderService.geocode)) { + throw new DeveloperError('options.geocoderService is available but missing a geocode method'); } //>>includeEnd('debug'); @@ -112,7 +107,7 @@ define([ if (that.isSearchInProgress) { cancelGeocode(that); } else { - geocode(that, options.customGeocoder); + geocode(that, options.geocoderService); } }); @@ -176,9 +171,9 @@ define([ return; } - var customGeocoder = options.customGeocoder; - if (defined(customGeocoder)) { - customGeocoder.geocode(query, function (err, results) { + var geocoderService = options.geocoderService; + if (defined(geocoderService)) { + geocoderService.geocode(query, function (err, results) { if (defined(err)) { return; } @@ -420,17 +415,17 @@ define([ }); } - function geocode(viewModel, customGeocoder) { + function geocode(viewModel, geocoderService) { var query = viewModel.searchText; if (hasOnlyWhitespace(query)) { return; } - if (defined(customGeocoder)) { + if (defined(geocoderService)) { viewModel._isSearchInProgress = true; viewModel._suggestions.splice(0, viewModel._suggestions().length); - customGeocoder.geocode(query, function (err, results) { + geocoderService.geocode(query, function (err, results) { if (defined(err)) { viewModel._isSearchInProgress = false; return; diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index dcf0c865bc1a..67b21c1cf2aa 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -473,7 +473,7 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to toolbar.appendChild(geocoderContainer); geocoder = new Geocoder({ container : geocoderContainer, - customGeocoder: options.geocoder, + geocoderService: options.geocoder, scene : cesiumWidget.scene }); // Subscribe to search so that we can clear the trackedEntity when it is clicked. From 9124a6bb48e750825170a180fe02442dc15c1625 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 7 Dec 2016 15:03:23 -0500 Subject: [PATCH 12/33] Add geocoder services --- Source/Core/BingMapsGeocoderService.js | 89 +++++++++++++++++++ Source/Core/LongLatGeocoderService.js | 51 +++++++++++ .../OpenStreetMapNominatimGeocoderService.js | 3 + 3 files changed, 143 insertions(+) create mode 100644 Source/Core/BingMapsGeocoderService.js create mode 100644 Source/Core/LongLatGeocoderService.js create mode 100644 Source/Core/OpenStreetMapNominatimGeocoderService.js diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js new file mode 100644 index 000000000000..8ae8deb2a775 --- /dev/null +++ b/Source/Core/BingMapsGeocoderService.js @@ -0,0 +1,89 @@ +/*global define*/ +define([ + './BingMapsApi', + './defaultValue', + './loadJsonp', + './Rectangle', + '../ThirdParty/when', + './DeveloperError' +], function( + BingMapsApi, + defaultValue, + loadJsonp, + Rectangle, + when, + DeveloperError) { + 'use strict'; + + var url = 'https://dev.virtualearth.net/REST/v1/Locations'; + + /** + * Provides geocoding through Bing Maps. + * @alias BingMapsGeocoderService + * + */ + function BingMapsGeocoderService(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this._canceled = false; + this._key = options.key; + + this.autoComplete = defaultValue(options.autoComplete, false); + } + + BingMapsGeocoderService.prototype.cancel = function() { + this._canceled = true; + }; + + /** + * @function + * + * @param {String} query The query to be sent to the geocoder service + * @param {GeocoderCallback} callback Callback to be called with geocoder results + */ + BingMapsGeocoderService.prototype.geocode = function(query, callback) { + this._canceled = false; + + var key = BingMapsApi.getKey(this._key); + var promise = loadJsonp(url, { + parameters : { + query : query, + key : key + }, + callbackParameterName : 'jsonp' + }); + + var that = this; + + when(promise, function(result) { + if (that._canceled) { + return; + } + if (result.resourceSets.length === 0) { + callback(undefined, []); + return; + } + + var results = result.resourceSets[0].resources; + + callback(undefined, results.map(function (resource) { + var bbox = resource.bbox; + var south = bbox[0]; + var west = bbox[1]; + var north = bbox[2]; + var east = bbox[3]; + return { + displayName: resource.name, + rectangle: Rectangle.fromDegrees(west, south, east, north) + }; + })); + + }, function() { + if (that._canceled) { + return; + } + callback(new Error('unknown error when geocoding')); + }); + }; + + return BingMapsGeocoderService; +}); diff --git a/Source/Core/LongLatGeocoderService.js b/Source/Core/LongLatGeocoderService.js new file mode 100644 index 000000000000..b38cacc6b734 --- /dev/null +++ b/Source/Core/LongLatGeocoderService.js @@ -0,0 +1,51 @@ +/*global define*/ +define([ + './Cartesian3', + './defaultValue' +], function( + Cartesian3, + defaultValue) { + 'use strict'; + + /** + * Provides geocoding through Bing Maps. + * @alias LongLatGeocoderService + * + */ + function LongLatGeocoderService(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this.autoComplete = false; + } + + LongLatGeocoderService.prototype.cancel = function() { + }; + + /** + * @function + * + * @param {String} query The query to be sent to the geocoder service + * @param {GeocoderCallback} callback Callback to be called with geocoder results + */ + LongLatGeocoderService.prototype.geocode = function(query, callback) { + var splitQuery = query.match(/[^\s,\n]+/g); + if ((splitQuery.length === 2) || (splitQuery.length === 3)) { + var longitude = +splitQuery[0]; + var latitude = +splitQuery[1]; + var height = (splitQuery.length === 3) ? +splitQuery[2] : 300.0; + + if (!isNaN(longitude) && !isNaN(latitude) && !isNaN(height)) { + var result = { + displayName: query, + destination: Cartesian3.fromDegrees(longitude, latitude, height) + }; + callback(undefined, [result]); + return; + } + callback(new Error('invalid coordinates')); + } else { + callback(undefined, []); + } + }; + + return LongLatGeocoderService; +}); diff --git a/Source/Core/OpenStreetMapNominatimGeocoderService.js b/Source/Core/OpenStreetMapNominatimGeocoderService.js new file mode 100644 index 000000000000..7cc24170861d --- /dev/null +++ b/Source/Core/OpenStreetMapNominatimGeocoderService.js @@ -0,0 +1,3 @@ +/** + * Created by erik on 2016-12-07. + */ From ba111133d062a9f3bac44e1fac079abddd5810fb Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 7 Dec 2016 15:07:45 -0500 Subject: [PATCH 13/33] Add support for multiple geocoders --- Apps/Sandcastle/gallery/Custom Geocoder.html | 32 +-- Source/Core/BingMapsGeocoderService.js | 2 +- Source/Core/GeocoderService.js | 16 +- .../OpenStreetMapNominatimGeocoderService.js | 65 ++++- Source/Widgets/Geocoder/GeocoderViewModel.js | 230 ++++++++---------- Source/Widgets/Viewer/Viewer.js | 2 +- 6 files changed, 182 insertions(+), 165 deletions(-) diff --git a/Apps/Sandcastle/gallery/Custom Geocoder.html b/Apps/Sandcastle/gallery/Custom Geocoder.html index 1c818cbeb70b..c75b7329263e 100644 --- a/Apps/Sandcastle/gallery/Custom Geocoder.html +++ b/Apps/Sandcastle/gallery/Custom Geocoder.html @@ -36,36 +36,10 @@ function startup(Cesium) { 'use strict'; //Sandcastle_Begin -var options = { - geocoder: { - geocode: function (input, callback) { - var endpoint = 'http://nominatim.openstreetmap.org/search?'; - var query = 'format=json&q=' + input; - var requestString = endpoint + query; - Cesium.loadJson(requestString) - .then(function (results) { - var bboxDegrees; - callback(undefined, results.map(function (resultObject) { - bboxDegrees = resultObject.boundingbox; - return { - displayName: resultObject.display_name, - bbox: { - south: bboxDegrees[0], - north: bboxDegrees[1], - west: bboxDegrees[2], - east: bboxDegrees[3] - } - }; - })); - }) - .otherwise(function (err) { - callback(err); - }); - } - } -}; -var viewer = new Cesium.Viewer('cesiumContainer', options); +var viewer = new Cesium.Viewer('cesiumContainer', { + geocoder: new Cesium.OpenStreetMapNominatimGeocoderService() +}); //Sandcastle_End Sandcastle.finishedLoading(); diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js index 8ae8deb2a775..38a6b3364557 100644 --- a/Source/Core/BingMapsGeocoderService.js +++ b/Source/Core/BingMapsGeocoderService.js @@ -73,7 +73,7 @@ define([ var east = bbox[3]; return { displayName: resource.name, - rectangle: Rectangle.fromDegrees(west, south, east, north) + destination: Rectangle.fromDegrees(west, south, east, north) }; })); diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js index f0ca404d8eba..41e23b76678b 100644 --- a/Source/Core/GeocoderService.js +++ b/Source/Core/GeocoderService.js @@ -13,6 +13,12 @@ define([ * @property {Rectangle} rectangle The bounding box for a location */ + /** + * @typedef {Function} GeocoderCallback + * @param {Error | undefined} error The error that occurred during geocoding + * @param {GeocoderResult[]} [results] + */ + /** * Provides geocoding through an external service. This type describes an interface and * is not intended to be used. @@ -22,6 +28,13 @@ define([ * @see BingMapsGeocoderService */ function GeocoderService () { + /** + * Indicates whether this geocoding service is to be used for autocomplete. + * + * @type {boolean} + * @default false + */ + this.autoComplete = false; } defineProperties(GeocoderService.prototype, { @@ -40,8 +53,7 @@ define([ * @function * * @param {String} query The query to be sent to the geocoder service - * @returns {GeocoderResult[]} geocoderResults An array containing the results from the - * geocoder service + * @param {GeocoderCallback} callback Callback to be called with geocoder results */ GeocoderService.prototype.geocode = DeveloperError.throwInstantiationError; diff --git a/Source/Core/OpenStreetMapNominatimGeocoderService.js b/Source/Core/OpenStreetMapNominatimGeocoderService.js index 7cc24170861d..d08c904818a7 100644 --- a/Source/Core/OpenStreetMapNominatimGeocoderService.js +++ b/Source/Core/OpenStreetMapNominatimGeocoderService.js @@ -1,3 +1,62 @@ -/** - * Created by erik on 2016-12-07. - */ +/*global define*/ +define([ + './Cartesian3', + './defaultValue', + './loadJson', + './Rectangle' +], function( + Cartesian3, + defaultValue, + loadJson, + Rectangle) { + 'use strict'; + + /** + * Provides geocoding through OpenStreetMap Nominatim. + * @alias OpenStreetMapNominatimGeocoder + * + */ + function OpenStreetMapNominatimGeocoder(options) { + options = defaultValue(options, defaultValue.EMPTY_OBJECT); + this.displayName = defaultValue(options.displayName, 'Nominatim'); + this._canceled = false; + this.autoComplete = defaultValue(options.autoComplete, true); + } + + OpenStreetMapNominatimGeocoder.prototype.cancel = function() { + this._canceled = true; + }; + + /** + * @function + * + * @param {String} query The query to be sent to the geocoder service + * @param {GeocoderCallback} callback Callback to be called with geocoder results + */ + OpenStreetMapNominatimGeocoder.prototype.geocode = function (input, callback) { + var endpoint = 'http://nominatim.openstreetmap.org/search?'; + var query = 'format=json&q=' + input; + var requestString = endpoint + query; + loadJson(requestString) + .then(function (results) { + var bboxDegrees; + callback(undefined, results.map(function (resultObject) { + bboxDegrees = resultObject.boundingbox; + return { + displayName: resultObject.display_name, + destination: Rectangle.fromDegrees( + bboxDegrees[2], + bboxDegrees[0], + bboxDegrees[3], + bboxDegrees[1] + ) + }; + })); + }) + .otherwise(function (err) { + callback(err); + }); + }; + + return OpenStreetMapNominatimGeocoder; +}); \ No newline at end of file diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index dee1ec700ce3..7f6f28b43c6d 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -1,23 +1,27 @@ /*global define*/ define([ - '../../Core/BingMapsApi', - '../../Core/Cartesian3', - '../../Core/defaultValue', - '../../Core/defined', - '../../Core/defineProperties', - '../../Core/deprecationWarning', - '../../Core/DeveloperError', - '../../Core/Event', - '../../Core/loadJsonp', - '../../Core/Matrix4', - '../../Core/Rectangle', - '../../Scene/SceneMode', - '../../ThirdParty/knockout', - '../../ThirdParty/when', - '../createCommand', - '../getElement' + '../../Core/BingMapsApi', + '../../Core/BingMapsGeocoderService', + '../../Core/Cartesian3', + '../../Core/defaultValue', + '../../Core/defined', + '../../Core/defineProperties', + '../../Core/deprecationWarning', + '../../Core/DeveloperError', + '../../Core/Event', + '../../Core/LongLatGeocoderService', + '../../Core/loadJsonp', + '../../Core/Matrix4', + '../../Core/OpenStreetMapNominatimGeocoderService', + '../../Core/Rectangle', + '../../Scene/SceneMode', + '../../ThirdParty/knockout', + '../../ThirdParty/when', + '../createCommand', + '../getElement' ], function( BingMapsApi, + BingMapsGeocoderService, Cartesian3, defaultValue, defined, @@ -25,8 +29,10 @@ define([ deprecationWarning, DeveloperError, Event, + LongLatGeocoderService, loadJsonp, Matrix4, + OpenStreetMapNominatimGeocoderService, Rectangle, SceneMode, knockout, @@ -42,6 +48,9 @@ define([ * * @param {Object} options Object with the following properties: * @param {Scene} options.scene The Scene instance to use. + * @param {GeocoderService[]} [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 {String} [options.url='https://dev.virtualearth.net'] The base URL of the Bing Maps API. * @param {String} [options.key] The Bing Maps key for your application, which can be * created at {@link https://www.bingmapsportal.com}. @@ -57,11 +66,17 @@ define([ if (!defined(options) || !defined(options.scene)) { throw new DeveloperError('options.scene is required.'); } - if (defined(options.geocoderService) && !defined(options.geocoderService.geocode)) { - throw new DeveloperError('options.geocoderService is available but missing a geocode method'); - } //>>includeEnd('debug'); + this._geocoderServices = options.geocoderServices; + if (!defined(options.geocoderServices)) { + this._geocoderServices = [ + new LongLatGeocoderService(), + new BingMapsGeocoderService(), + new OpenStreetMapNominatimGeocoderService() + ]; + } + var errorCredit; this._url = defaultValue(options.url, 'https://dev.virtualearth.net/'); if (this._url.length > 0 && this._url[this._url.length - 1] !== '/') { @@ -107,7 +122,7 @@ define([ if (that.isSearchInProgress) { cancelGeocode(that); } else { - geocode(that, options.geocoderService); + geocode(that, that._geocoderServices); } }); @@ -171,20 +186,21 @@ define([ return; } - var geocoderService = options.geocoderService; - if (defined(geocoderService)) { - geocoderService.geocode(query, function (err, results) { + that._suggestions.splice(0, that._suggestions().length); + var geocoderServices = that._geocoderServices.filter(function (service) { + return service.autoComplete === true; + }); + + geocoderServices.forEach(function (service) { + service.geocode(query, function (err, results) { if (defined(err)) { return; } - that._suggestions.splice(0, that._suggestions().length); - if (results.length > 0) { - results.slice(0, Math.min(results.length, 5)).forEach(function (result) { - that._suggestions.push(result); - }); - } + results.slice(0, 3).forEach(function (result) { + that._suggestions.push(result); + }); }); - } + }); }; this.handleKeyDown = function (data, event) { @@ -204,16 +220,16 @@ define([ } else if (key === 40) { that.handleArrowDown(); } else if (key === 13) { - that.activateSuggestion(that._selectedSuggestion()); + that._searchCommand(); } return true; }; this.activateSuggestion = function (data) { that._searchText = data.displayName; - var bbox = data.bbox; + var destination = data.destination; that._suggestions.splice(0, that._suggestions().length); - updateCamera(that, Rectangle.fromDegrees(bbox.west, bbox.south, bbox.east, bbox.north)); + updateCamera(that, destination); }; this.hideSuggestions = function () { @@ -415,122 +431,78 @@ define([ }); } - function geocode(viewModel, geocoderService) { + function createGeocodeCallback(geocodePromise) { + return function (err, results) { + if (defined(err)) { + geocodePromise.resolve(undefined); + return; + } + if (results.length === 0) { + geocodePromise.resolve(undefined); + return; + } + + var firstResult = results[0]; + //>>includeStart('debug', pragmas.debug); + if (!defined(firstResult.displayName)) { + throw new DeveloperError('each result must have a displayName'); + } + if (!defined(firstResult.destination)) { + throw new DeveloperError('each result must have a rectangle'); + } + //>>includeEnd('debug'); + + geocodePromise.resolve({ + displayName: firstResult.displayName, + destination: firstResult.destination + }); + }; + } + function geocode(viewModel, geocoderServices) { var query = viewModel.searchText; if (hasOnlyWhitespace(query)) { return; } - if (defined(geocoderService)) { - viewModel._isSearchInProgress = true; - viewModel._suggestions.splice(0, viewModel._suggestions().length); - geocoderService.geocode(query, function (err, results) { - if (defined(err)) { - viewModel._isSearchInProgress = false; - return; - } - if (results.length === 0) { - viewModel.searchText = query + ' (not found)'; - viewModel._isSearchInProgress = false; - return; - } - - var firstResult = results[0]; - //>>includeStart('debug', pragmas.debug); - if (!defined(firstResult.displayName)) { - throw new DeveloperError('each result must have a displayName'); - } - if (!defined(firstResult.bbox)) { - throw new DeveloperError('each result must have a bbox'); - } - if (!defined(firstResult.bbox.south) || !defined(firstResult.bbox.west) || !defined(firstResult.bbox.north) || !defined(firstResult.bbox.east)) { - throw new DeveloperError('each result must have a bbox where south, west, north and east are defined'); - } - //>>includeEnd('debug'); - - viewModel._searchText = firstResult.displayName; - var bbox = firstResult.bbox; - var south = bbox.south; - var west = bbox.west; - var north = bbox.north; - var east = bbox.east; - - updateCamera(viewModel, Rectangle.fromDegrees(west, south, east, north)); - viewModel._isSearchInProgress = false; - }); - } else { - defaultGeocode(viewModel, query); - } - } - - function defaultGeocode(viewModel, query) { - var defaultOptions = viewModel._defaultGeocoderOptions; + viewModel._geocodeInProgress = true; - // If the user entered (longitude, latitude, [height]) in degrees/meters, - // fly without calling the geocoder. - var splitQuery = query.match(/[^\s,\n]+/g); - if ((splitQuery.length === 2) || (splitQuery.length === 3)) { - var longitude = +splitQuery[0]; - var latitude = +splitQuery[1]; - var height = (splitQuery.length === 3) ? +splitQuery[2] : 300.0; + var resultPromises = []; + for (var i = 0; i < geocoderServices.length; i++) { + var geocoderService = geocoderServices[i]; - if (!isNaN(longitude) && !isNaN(latitude) && !isNaN(height)) { - updateCamera(viewModel, Cartesian3.fromDegrees(longitude, latitude, height)); - return; - } + viewModel._isSearchInProgress = true; + viewModel._suggestions.splice(0, viewModel._suggestions().length); + var geocodePromise = when.defer(); + resultPromises.push(geocodePromise); + geocoderService.geocode(query, createGeocodeCallback(geocodePromise)); } - viewModel._isSearchInProgress = true; - - var promise = loadJsonp(defaultOptions.url + 'REST/v1/Locations', { - parameters : { - query : query, - key : defaultOptions.key - }, - callbackParameterName : 'jsonp' - }); - - var geocodeInProgress = viewModel._geocodeInProgress = when(promise, function(result) { - if (geocodeInProgress.cancel) { - return; - } + var allReady = when.all(resultPromises); + allReady.then(function (results) { viewModel._isSearchInProgress = false; - - if (result.resourceSets.length === 0) { - viewModel.searchText = viewModel._searchText + ' (not found)'; - return; - } - - var resourceSet = result.resourceSets[0]; - if (resourceSet.resources.length === 0) { - viewModel.searchText = viewModel._searchText + ' (not found)'; + if (viewModel._cancelGeocode) { + viewModel._cancelGeocode = false; return; } - - var resource = resourceSet.resources[0]; - - viewModel._searchText = resource.name; - var bbox = resource.bbox; - var south = bbox[0]; - var west = bbox[1]; - var north = bbox[2]; - var east = bbox[3]; - - updateCamera(viewModel, Rectangle.fromDegrees(west, south, east, north)); - }, function() { - if (geocodeInProgress.cancel) { - return; + for (var j = 0; j < results.length; j++) { + if (defined(results[j])) { + viewModel._searchText = results[j].displayName; + updateCamera(viewModel, results[j].destination); + return; + } } - + viewModel._searchText = query + ' (not found)'; + }) + .otherwise(function (err) { viewModel._isSearchInProgress = false; - viewModel.searchText = viewModel._searchText + ' (error)'; + viewModel._searchText = query + ' (not found)'; }); } function cancelGeocode(viewModel) { viewModel._isSearchInProgress = false; if (defined(viewModel._geocodeInProgress)) { - viewModel._geocodeInProgress.cancel = true; + viewModel._cancelGeocode = true; viewModel._geocodeInProgress = undefined; } } diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index 67b21c1cf2aa..c2b7aac59014 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -473,7 +473,7 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to toolbar.appendChild(geocoderContainer); geocoder = new Geocoder({ container : geocoderContainer, - geocoderService: options.geocoder, + geocoderServices: defined(options.geocoder) ? [options.geocoder] : undefined, scene : cesiumWidget.scene }); // Subscribe to search so that we can clear the trackedEntity when it is clicked. From 48111c6fea1dccc17a6f8e914d1e84ccc0ba304f Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 7 Dec 2016 16:37:06 -0500 Subject: [PATCH 14/33] Update tests --- Source/Widgets/Viewer/Viewer.js | 2 +- Specs/Core/BingMapsGeocoderServiceSpec.js | 57 ++++++++++ Specs/Core/LongLatGeocoderServiceSpec.js | 47 ++++++++ ...enStreetMapNominatimGeocoderServiceSpec.js | 38 +++++++ .../Widgets/Geocoder/GeocoderViewModelSpec.js | 100 ++++++++++++------ 5 files changed, 209 insertions(+), 35 deletions(-) create mode 100644 Specs/Core/BingMapsGeocoderServiceSpec.js create mode 100644 Specs/Core/LongLatGeocoderServiceSpec.js create mode 100644 Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js diff --git a/Source/Widgets/Viewer/Viewer.js b/Source/Widgets/Viewer/Viewer.js index c2b7aac59014..1d3413031ecd 100644 --- a/Source/Widgets/Viewer/Viewer.js +++ b/Source/Widgets/Viewer/Viewer.js @@ -473,7 +473,7 @@ Either specify options.terrainProvider instead or set options.baseLayerPicker to toolbar.appendChild(geocoderContainer); geocoder = new Geocoder({ container : geocoderContainer, - geocoderServices: defined(options.geocoder) ? [options.geocoder] : undefined, + geocoderServices: defined(options.geocoder) ? (isArray(options.geocoder) ? options.geocoder : [options.geocoder]) : undefined, scene : cesiumWidget.scene }); // Subscribe to search so that we can clear the trackedEntity when it is clicked. diff --git a/Specs/Core/BingMapsGeocoderServiceSpec.js b/Specs/Core/BingMapsGeocoderServiceSpec.js new file mode 100644 index 000000000000..5c68a7804d1c --- /dev/null +++ b/Specs/Core/BingMapsGeocoderServiceSpec.js @@ -0,0 +1,57 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/BingMapsGeocoderService', + 'Core/Cartesian3', + 'Core/loadJsonp', + 'Core/Rectangle' +], function( + BingMapsGeocoderService, + Cartesian3, + loadJsonp, + Rectangle) { + 'use strict'; + + var service = new BingMapsGeocoderService(); + + it('returns geocoder results', function (done) { + var query = 'some query'; + jasmine.createSpy('testSpy', loadJsonp).and.returnValue({ + resourceSets: [{ + resources : [{ + name : 'a', + bbox : [32.0, 3.0, 3.0, 4.0] + }] + }] + }); + service.geocode(query, function(err, results) { + expect(results.length).toEqual(1); + expect(results[0].displayName).toEqual('a'); + expect(results[0].destination).toBeInstanceOf(Rectangle); + done(); + }); + }); + + it('returns no geocoder results if Bing has no results', function (done) { + var query = 'some query'; + jasmine.createSpy('testSpy', loadJsonp).and.returnValue({ + resourceSets: [] + }); + service.geocode(query, function(err, results) { + expect(results.length).toEqual(0); + done(); + }); + }); + + it('returns no geocoder results if Bing has results but no resources', function (done) { + var query = 'some query'; + jasmine.createSpy('testSpy', loadJsonp).and.returnValue({ + resourceSets: [{ + resources: [] + }] + }); + service.geocode(query, function(err, results) { + expect(results.length).toEqual(0); + done(); + }); + }); +}); diff --git a/Specs/Core/LongLatGeocoderServiceSpec.js b/Specs/Core/LongLatGeocoderServiceSpec.js new file mode 100644 index 000000000000..005603c4853b --- /dev/null +++ b/Specs/Core/LongLatGeocoderServiceSpec.js @@ -0,0 +1,47 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/Cartesian3', + 'Core/LongLatGeocoderService' +], function( + Cartesian3, + LongLatGeocoderService) { + 'use strict'; + + var service = new LongLatGeocoderService(); + + it('returns cartesian with matching coordinates for long/lat/height input', function (done) { + var query = ' 1.0, 2.0, 3.0 '; + service.geocode(query, function(err, results) { + expect(results.length).toEqual(1); + expect(results[0]).toEqual(Cartesian3.fromDegrees(1.0, 2.0, 3.0)); + done(); + }); + }); + + it('returns cartesian with matching coordinates for long/lat input', function (done) { + var query = ' 1.0, 2.0 '; + var defaultHeight = 300.0; + service.geocode(query, function(err, results) { + expect(results.length).toEqual(1); + expect(results[0]).toEqual(Cartesian3.fromDegrees(1.0, 2.0, defaultHeight)); + done(); + }); + }); + + it('returns empty array for input with only one number', function (done) { + var query = ' 2.0 '; + service.geocode(query, function(err, results) { + expect(results.length).toEqual(0); + done(); + }); + }); + + it('returns empty array for with string', function (done) { + var query = ' aoeu '; + service.geocode(query, function(err, results) { + expect(results.length).toEqual(0); + done(); + }); + }); + +}); diff --git a/Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js b/Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js new file mode 100644 index 000000000000..f04086580be1 --- /dev/null +++ b/Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js @@ -0,0 +1,38 @@ +/*global defineSuite*/ +defineSuite([ + 'Core/OpenStreetMapNominatimGeocoderService', + 'Core/Cartesian3', + 'Core/loadJsonp', + 'Core/Rectangle' +], function( + OpenStreetMapNominatimGeocoderService, + Cartesian3, + loadJsonp, + Rectangle) { + 'use strict'; + + var service = new OpenStreetMapNominatimGeocoderService(); + + it('returns geocoder results', function (done) { + var query = 'some query'; + jasmine.createSpy('testSpy', loadJsonp).and.returnValue([{ + displayName: 'a', + boundingbox: [10, 20, 0, 20] + }]); + service.geocode(query, function(err, results) { + expect(results.length).toEqual(1); + expect(results[0].displayName).toEqual('a'); + expect(results[0].destination).toBeInstanceOf(Rectangle); + done(); + }); + }); + + it('returns no geocoder results if OSM Nominatim has no results', function (done) { + var query = 'some query'; + jasmine.createSpy('testSpy', loadJsonp).and.returnValue([]); + service.geocode(query, function(err, results) { + expect(results.length).toEqual(0); + done(); + }); + }); +}); \ No newline at end of file diff --git a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js index e252cc0cc1e5..4884916e9922 100644 --- a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js @@ -14,12 +14,40 @@ defineSuite([ 'use strict'; var scene; + var mockDestination = new Cartesian3(1.0, 2.0, 3.0); + + var geocoderResults1 = [{ + displayName: 'a', + destination: mockDestination + }, { + displayName: 'b', + destination: mockDestination + }]; var customGeocoderOptions = { - geocode : function (input, callback) { - callback(undefined, ['a', 'b', 'c']); - }, - getSuggestions : function (input) { - return ['a', 'b', 'c']; + autoComplete: true, + geocode: function (input, callback) { + callback(undefined, geocoderResults1); + } + }; + + var geocoderResults2 = [{ + displayName: '1', + destination: mockDestination + }, { + displayName: '2', + destination: mockDestination + }]; + var customGeocoderOptions2 = { + autoComplete: true, + geocode: function (input, callback) { + callback(undefined, geocoderResults2); + } + }; + + var noResultsGeocoder = { + autoComplete: true, + geocode: function (input, callback) { + callback(undefined, []); } }; @@ -87,27 +115,6 @@ defineSuite([ }); }); - it('Zooms to longitude, latitude, height', function() { - var viewModel = new GeocoderViewModel({ - scene : scene - }); - - spyOn(Camera.prototype, 'flyTo'); - - viewModel.searchText = ' 1.0, 2.0, 3.0 '; - viewModel.search(); - expect(Camera.prototype.flyTo).toHaveBeenCalled(); - expect(Camera.prototype.flyTo.calls.mostRecent().args[0].destination).toEqual(Cartesian3.fromDegrees(1.0, 2.0, 3.0)); - - viewModel.searchText = '1.0 2.0 3.0'; - viewModel.search(); - expect(Camera.prototype.flyTo.calls.mostRecent().args[0].destination).toEqual(Cartesian3.fromDegrees(1.0, 2.0, 3.0)); - - viewModel.searchText = '-1.0, -2.0'; - viewModel.search(); - expect(Camera.prototype.flyTo.calls.mostRecent().args[0].destination).toEqual(Cartesian3.fromDegrees(-1.0, -2.0, 300.0)); - }); - it('constructor throws without scene', function() { expect(function() { return new GeocoderViewModel(); @@ -117,7 +124,8 @@ defineSuite([ it('raises the complete event camera finished', function() { var viewModel = new GeocoderViewModel({ scene : scene, - flightDuration : 0 + flightDuration : 0, + geocoderServices : [customGeocoderOptions] }); var spyListener = jasmine.createSpy('listener'); @@ -129,7 +137,7 @@ defineSuite([ expect(spyListener.calls.count()).toBe(1); viewModel.flightDuration = 1.5; - viewModel.serachText = '2.0, 2.0'; + viewModel.searchText = '2.0, 2.0'; viewModel.search(); return pollToPromise(function() { @@ -142,7 +150,7 @@ defineSuite([ expect(function() { return new GeocoderViewModel({ scene : scene, - customGeocoder : customGeocoderOptions + geocoderServices : [customGeocoderOptions] }); }).not.toThrowDeveloperError(); }); @@ -150,17 +158,17 @@ defineSuite([ it('automatic suggestions can be retrieved', function() { var geocoder = new GeocoderViewModel({ scene : scene, - customGeocoder : customGeocoderOptions + geocoderServices : [customGeocoderOptions] }); geocoder._searchText = 'some_text'; geocoder.updateSearchSuggestions(); - expect(geocoder._suggestions().length).toEqual(3); + expect(geocoder._suggestions().length).toEqual(2); }); it('update search suggestions results in empty list if the query is empty', function() { var geocoder = new GeocoderViewModel({ scene : scene, - customGeocoder : customGeocoderOptions + geocoderServices : [customGeocoderOptions] }); geocoder._searchText = ''; spyOn(geocoder, '_adjustSuggestionsScroll'); @@ -171,15 +179,39 @@ defineSuite([ it('can activate selected search suggestion', function () { var geocoder = new GeocoderViewModel({ scene : scene, - customGeocoder : customGeocoderOptions + geocoderServices : [customGeocoderOptions] }); spyOn(geocoder, '_updateCamera'); spyOn(geocoder, '_adjustSuggestionsScroll'); - var suggestion = {displayName: 'a', bbox: {west: 0.0, east: 0.1, north: 0.1, south: -0.1}}; + var suggestion = {displayName: 'a', destination: {west: 0.0, east: 0.1, north: 0.1, south: -0.1}}; geocoder._selectedSuggestion(suggestion); geocoder.activateSuggestion(suggestion); expect(geocoder._searchText).toEqual('a'); }); + it('if more than one geocoder service is provided, use first result from first geocode in array order', function () { + var geocoder = new GeocoderViewModel({ + scene : scene, + geocoderServices : [noResultsGeocoder, customGeocoderOptions2] + }); + geocoder._searchText = 'sthsnth'; // an empty query will prevent geocoding + spyOn(geocoder, '_updateCamera'); + spyOn(geocoder, '_adjustSuggestionsScroll'); + geocoder.search(); + expect(geocoder._searchText).toEqual(geocoderResults2[0].displayName); + }); + + it('can update autoComplete suggestions list using multiple geocoders', function () { + var geocoder = new GeocoderViewModel({ + scene : scene, + geocoderServices : [customGeocoderOptions, customGeocoderOptions2] + }); + geocoder._searchText = 'sthsnth'; // an empty query will prevent geocoding + spyOn(geocoder, '_updateCamera'); + spyOn(geocoder, '_adjustSuggestionsScroll'); + geocoder.updateSearchSuggestions(); + expect(geocoder._suggestions().length).toEqual(geocoderResults1.length + geocoderResults2.length); + }); + }, 'WebGL'); From 3d4071fd05ae5fd24aabe53309e4b202678751ed Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 7 Dec 2016 18:35:17 -0500 Subject: [PATCH 15/33] Use promises instead of callbacks in geocoder service API --- Source/Core/BingMapsGeocoderService.js | 55 ++++++++------ Source/Core/GeocoderService.js | 8 +-- Source/Core/LongLatGeocoderService.js | 15 ++-- .../OpenStreetMapNominatimGeocoderService.js | 13 ++-- Source/Widgets/Geocoder/GeocoderViewModel.js | 72 ++++++++++--------- Specs/Core/LongLatGeocoderServiceSpec.js | 8 +-- Specs/Widgets/Geocoder/GeocoderSpec.js | 41 +++++++---- .../Widgets/Geocoder/GeocoderViewModelSpec.js | 24 ++++--- 8 files changed, 131 insertions(+), 105 deletions(-) diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js index 38a6b3364557..31785cc1c6a7 100644 --- a/Source/Core/BingMapsGeocoderService.js +++ b/Source/Core/BingMapsGeocoderService.js @@ -2,17 +2,15 @@ define([ './BingMapsApi', './defaultValue', + './defineProperties', './loadJsonp', './Rectangle', - '../ThirdParty/when', - './DeveloperError' ], function( BingMapsApi, defaultValue, + defineProperties, loadJsonp, - Rectangle, - when, - DeveloperError) { + Rectangle) { 'use strict'; var url = 'https://dev.virtualearth.net/REST/v1/Locations'; @@ -25,11 +23,35 @@ define([ function BingMapsGeocoderService(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); this._canceled = false; - this._key = options.key; + + this._url = 'https://dev.virtualearth.net/REST/v1/Locations'; + this._key = BingMapsApi.getKey(options.key); this.autoComplete = defaultValue(options.autoComplete, false); } + defineProperties(BingMapsGeocoderService.prototype, { + /** + * The URL endpoint for the Bing geocoder service + * @type {String} + */ + url : { + get : function () { + return this._url; + } + }, + + /** + * The key for the Bing geocoder service + * @type {String} + */ + key : { + get : function () { + return this._key; + } + } + }); + BingMapsGeocoderService.prototype.cancel = function() { this._canceled = true; }; @@ -38,12 +60,12 @@ define([ * @function * * @param {String} query The query to be sent to the geocoder service - * @param {GeocoderCallback} callback Callback to be called with geocoder results + * @returns {Promise} */ - BingMapsGeocoderService.prototype.geocode = function(query, callback) { + BingMapsGeocoderService.prototype.geocode = function(query) { this._canceled = false; - var key = BingMapsApi.getKey(this._key); + var key = this.key; var promise = loadJsonp(url, { parameters : { query : query, @@ -54,18 +76,17 @@ define([ var that = this; - when(promise, function(result) { + return promise.then(function(result) { if (that._canceled) { return; } if (result.resourceSets.length === 0) { - callback(undefined, []); - return; + return []; } var results = result.resourceSets[0].resources; - callback(undefined, results.map(function (resource) { + return results.map(function (resource) { var bbox = resource.bbox; var south = bbox[0]; var west = bbox[1]; @@ -75,13 +96,7 @@ define([ displayName: resource.name, destination: Rectangle.fromDegrees(west, south, east, north) }; - })); - - }, function() { - if (that._canceled) { - return; - } - callback(new Error('unknown error when geocoding')); + }); }); }; diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js index 41e23b76678b..ba96aae633c8 100644 --- a/Source/Core/GeocoderService.js +++ b/Source/Core/GeocoderService.js @@ -13,12 +13,6 @@ define([ * @property {Rectangle} rectangle The bounding box for a location */ - /** - * @typedef {Function} GeocoderCallback - * @param {Error | undefined} error The error that occurred during geocoding - * @param {GeocoderResult[]} [results] - */ - /** * Provides geocoding through an external service. This type describes an interface and * is not intended to be used. @@ -53,7 +47,7 @@ define([ * @function * * @param {String} query The query to be sent to the geocoder service - * @param {GeocoderCallback} callback Callback to be called with geocoder results + * @returns {Promise} */ GeocoderService.prototype.geocode = DeveloperError.throwInstantiationError; diff --git a/Source/Core/LongLatGeocoderService.js b/Source/Core/LongLatGeocoderService.js index b38cacc6b734..d46bd7a2bcfe 100644 --- a/Source/Core/LongLatGeocoderService.js +++ b/Source/Core/LongLatGeocoderService.js @@ -1,10 +1,12 @@ /*global define*/ define([ './Cartesian3', - './defaultValue' + './defaultValue', + '../ThirdParty/when' ], function( Cartesian3, - defaultValue) { + defaultValue, + when) { 'use strict'; /** @@ -24,7 +26,7 @@ define([ * @function * * @param {String} query The query to be sent to the geocoder service - * @param {GeocoderCallback} callback Callback to be called with geocoder results + * @returns {Promise} */ LongLatGeocoderService.prototype.geocode = function(query, callback) { var splitQuery = query.match(/[^\s,\n]+/g); @@ -38,13 +40,10 @@ define([ displayName: query, destination: Cartesian3.fromDegrees(longitude, latitude, height) }; - callback(undefined, [result]); - return; + return when.resolve([result]); } - callback(new Error('invalid coordinates')); - } else { - callback(undefined, []); } + return when.resolve([]); }; return LongLatGeocoderService; diff --git a/Source/Core/OpenStreetMapNominatimGeocoderService.js b/Source/Core/OpenStreetMapNominatimGeocoderService.js index d08c904818a7..749bc9aa8e0e 100644 --- a/Source/Core/OpenStreetMapNominatimGeocoderService.js +++ b/Source/Core/OpenStreetMapNominatimGeocoderService.js @@ -31,16 +31,16 @@ define([ * @function * * @param {String} query The query to be sent to the geocoder service - * @param {GeocoderCallback} callback Callback to be called with geocoder results + * @returns {Promise} */ - OpenStreetMapNominatimGeocoder.prototype.geocode = function (input, callback) { + OpenStreetMapNominatimGeocoder.prototype.geocode = function (input) { var endpoint = 'http://nominatim.openstreetmap.org/search?'; var query = 'format=json&q=' + input; var requestString = endpoint + query; - loadJson(requestString) + return loadJson(requestString) .then(function (results) { var bboxDegrees; - callback(undefined, results.map(function (resultObject) { + return results.map(function (resultObject) { bboxDegrees = resultObject.boundingbox; return { displayName: resultObject.display_name, @@ -51,11 +51,8 @@ define([ bboxDegrees[1] ) }; - })); + }); }) - .otherwise(function (err) { - callback(err); - }); }; return OpenStreetMapNominatimGeocoder; diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 7f6f28b43c6d..003f7498e730 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -192,15 +192,13 @@ define([ }); geocoderServices.forEach(function (service) { - service.geocode(query, function (err, results) { - if (defined(err)) { - return; - } - results.slice(0, 3).forEach(function (result) { - that._suggestions.push(result); + service.geocode(query) + .then(function (results) { + results.slice(0, 3).forEach(function(result) { + that._suggestions.push(result); + }); }); }); - }); }; this.handleKeyDown = function (data, event) { @@ -431,33 +429,27 @@ define([ }); } - function createGeocodeCallback(geocodePromise) { - return function (err, results) { - if (defined(err)) { - geocodePromise.resolve(undefined); - return; - } - if (results.length === 0) { - geocodePromise.resolve(undefined); - return; - } + function getFirstResult(results) { + if (results.length === 0) { + return undefined; + } - var firstResult = results[0]; - //>>includeStart('debug', pragmas.debug); - if (!defined(firstResult.displayName)) { - throw new DeveloperError('each result must have a displayName'); - } - if (!defined(firstResult.destination)) { - throw new DeveloperError('each result must have a rectangle'); - } - //>>includeEnd('debug'); + var firstResult = results[0]; + //>>includeStart('debug', pragmas.debug); + if (!defined(firstResult.displayName)) { + throw new DeveloperError('each result must have a displayName'); + } + if (!defined(firstResult.destination)) { + throw new DeveloperError('each result must have a rectangle'); + } + //>>includeEnd('debug'); - geocodePromise.resolve({ - displayName: firstResult.displayName, - destination: firstResult.destination - }); + return { + displayName: firstResult.displayName, + destination: firstResult.destination }; } + function geocode(viewModel, geocoderServices) { var query = viewModel.searchText; @@ -473,9 +465,19 @@ define([ viewModel._isSearchInProgress = true; viewModel._suggestions.splice(0, viewModel._suggestions().length); - var geocodePromise = when.defer(); + var geocodePromise = geocoderService.geocode(query); resultPromises.push(geocodePromise); - geocoderService.geocode(query, createGeocodeCallback(geocodePromise)); + geocodePromise.then(getFirstResult); + + if (typeof geocodePromise.otherwise === 'function') { + geocodePromise.otherwise(function (err) { + console.log('otherwise err: ' + err); + }); + } else if (typeof geocodePromise.catch === 'function') { + geocodePromise.catch(function (err) { + console.log('catch err: ' + err); + }); + } } var allReady = when.all(resultPromises); allReady.then(function (results) { @@ -485,9 +487,9 @@ define([ return; } for (var j = 0; j < results.length; j++) { - if (defined(results[j])) { - viewModel._searchText = results[j].displayName; - updateCamera(viewModel, results[j].destination); + if (defined(results[j]) && results[j].length > 0) { + viewModel._searchText = results[j][0].displayName; + updateCamera(viewModel, results[j][0].destination); return; } } diff --git a/Specs/Core/LongLatGeocoderServiceSpec.js b/Specs/Core/LongLatGeocoderServiceSpec.js index 005603c4853b..c2cd515a2a6d 100644 --- a/Specs/Core/LongLatGeocoderServiceSpec.js +++ b/Specs/Core/LongLatGeocoderServiceSpec.js @@ -1,10 +1,10 @@ /*global defineSuite*/ defineSuite([ - 'Core/Cartesian3', - 'Core/LongLatGeocoderService' + 'Core/LongLatGeocoderService', + 'Core/Cartesian3' ], function( - Cartesian3, - LongLatGeocoderService) { + LongLatGeocoderService, + Cartesian3) { 'use strict'; var service = new LongLatGeocoderService(); diff --git a/Specs/Widgets/Geocoder/GeocoderSpec.js b/Specs/Widgets/Geocoder/GeocoderSpec.js index 0402dc6fe974..cbe522c08808 100644 --- a/Specs/Widgets/Geocoder/GeocoderSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderSpec.js @@ -1,19 +1,34 @@ /*global defineSuite*/ defineSuite([ 'Widgets/Geocoder/Geocoder', - 'Specs/createScene' + 'Core/Cartesian3', + 'Specs/createScene', + 'ThirdParty/when' ], function( Geocoder, - createScene) { + Cartesian3, + createScene, + when) { 'use strict'; var scene; + + var mockDestination = new Cartesian3(1.0, 2.0, 3.0); + var geocoderResults = [{ + displayName: 'a', + destination: mockDestination + }, { + displayName: 'b', + destination: mockDestination + }, { + displayName: 'c', + destination: mockDestination + }]; + var customGeocoderOptions = { - geocode : function (input, callback) { - callback(undefined, ['a', 'b', 'c']); - }, - getSuggestions : function (input) { - return ['a', 'b', 'c']; + autoComplete : true, + geocode : function (input) { + return when.resolve(geocoderResults); } }; beforeEach(function() { @@ -96,7 +111,7 @@ defineSuite([ var geocoder = new Geocoder({ container : 'testContainer', scene : scene, - customGeocoder : customGeocoderOptions + geocoderServices : [customGeocoderOptions] }); var viewModel = geocoder._viewModel; viewModel._searchText = 'some_text'; @@ -104,16 +119,16 @@ defineSuite([ expect(viewModel._selectedSuggestion()).toEqual(undefined); viewModel.handleArrowDown(); - expect(viewModel._selectedSuggestion()).toEqual('a'); + expect(viewModel._selectedSuggestion().displayName).toEqual('a'); viewModel.handleArrowDown(); viewModel.handleArrowDown(); - expect(viewModel._selectedSuggestion()).toEqual('c'); + expect(viewModel._selectedSuggestion().displayName).toEqual('c'); viewModel.handleArrowDown(); - expect(viewModel._selectedSuggestion()).toEqual('a'); + expect(viewModel._selectedSuggestion().displayName).toEqual('a'); viewModel.handleArrowUp(); - expect(viewModel._selectedSuggestion()).toEqual('c'); + expect(viewModel._selectedSuggestion().displayName).toEqual('c'); viewModel.handleArrowUp(); - expect(viewModel._selectedSuggestion()).toEqual('b'); + expect(viewModel._selectedSuggestion().displayName).toEqual('b'); }); }, 'WebGL'); diff --git a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js index 4884916e9922..d3a270f2d030 100644 --- a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js @@ -4,13 +4,15 @@ defineSuite([ 'Core/Cartesian3', 'Scene/Camera', 'Specs/createScene', - 'Specs/pollToPromise' + 'Specs/pollToPromise', + 'ThirdParty/when', ], function( GeocoderViewModel, Cartesian3, Camera, createScene, - pollToPromise) { + pollToPromise, + when) { 'use strict'; var scene; @@ -25,8 +27,8 @@ defineSuite([ }]; var customGeocoderOptions = { autoComplete: true, - geocode: function (input, callback) { - callback(undefined, geocoderResults1); + geocode: function (input) { + return when.resolve(geocoderResults1); } }; @@ -39,15 +41,15 @@ defineSuite([ }]; var customGeocoderOptions2 = { autoComplete: true, - geocode: function (input, callback) { - callback(undefined, geocoderResults2); + geocode: function (input) { + return when.resolve(geocoderResults2); } }; var noResultsGeocoder = { autoComplete: true, - geocode: function (input, callback) { - callback(undefined, []); + geocode: function (input) { + return when.resolve([]); } }; @@ -92,7 +94,8 @@ defineSuite([ it('throws is searchText is not a string', function() { var viewModel = new GeocoderViewModel({ - scene : scene + scene : scene, + geocoderServices : [customGeocoderOptions] }); expect(function() { viewModel.searchText = undefined; @@ -101,7 +104,8 @@ defineSuite([ it('moves camera when search command invoked', function() { var viewModel = new GeocoderViewModel({ - scene : scene + scene : scene, + geocoderServices : [customGeocoderOptions] }); var cameraPosition = Cartesian3.clone(scene.camera.position); From b94d7be278f42a5fba68bc115e1d69b5fcb33299 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 7 Dec 2016 18:51:23 -0500 Subject: [PATCH 16/33] Update geocoder interface to reflect that geocoders can send back results that destinations of type Rectangle or Cartesian3 --- Source/Core/GeocoderService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js index ba96aae633c8..3fa0664abc27 100644 --- a/Source/Core/GeocoderService.js +++ b/Source/Core/GeocoderService.js @@ -10,7 +10,7 @@ define([ /** * @typedef {Object} GeocoderResult * @property {String} displayName The display name for a location - * @property {Rectangle} rectangle The bounding box for a location + * @property {Rectangle|Cartesian3} destination The bounding box for a location */ /** From 4e58f4b2cae90253b2f9886c618567349f330b73 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 7 Dec 2016 19:44:04 -0500 Subject: [PATCH 17/33] Add deprecation/change notes --- CHANGES.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f7659ea2524a..5dde6d39e540 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,7 +2,9 @@ Change Log ========== ### 1.29 - 2017-01-02 - +* Deprecated + * The properties `url` and `key` will be removed from `GeocoderViewModel` in 1.30. This properties will be available on geocoder services that support them, like `BingMapsGeocoderService`; +* Added support for custom geocoder services [4723](https://github.com/AnalyticalGraphicsInc/cesium/pull/4723). * Added the ability to blend a `Model` with a color/translucency. Added `color`, `colorBlendMode`, and `colorBlendAmount` properties to `Model`, `ModelGraphics`, and CZML. Added `ColorBlendMode` enum. [#4547](https://github.com/AnalyticalGraphicsInc/cesium/pull/4547) * Fixed tooltips for gallery thumbnails in Sandcastle [#4702](https://github.com/AnalyticalGraphicsInc/cesium/pull/4702) * Fixed texture rotation for `RectangleGeometry` [#2737](https://github.com/AnalyticalGraphicsInc/cesium/issues/2737) From a68cbc5f5bfd1fb8f11c427b51efbe3ea224424e Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Thu, 8 Dec 2016 10:35:22 -0500 Subject: [PATCH 18/33] Reject promises when geocoding fails --- Source/Core/LongLatGeocoderService.js | 28 ++++---- Source/Widgets/Geocoder/GeocoderViewModel.js | 76 ++++++++++++-------- 2 files changed, 64 insertions(+), 40 deletions(-) diff --git a/Source/Core/LongLatGeocoderService.js b/Source/Core/LongLatGeocoderService.js index d46bd7a2bcfe..c07c682a90a3 100644 --- a/Source/Core/LongLatGeocoderService.js +++ b/Source/Core/LongLatGeocoderService.js @@ -29,21 +29,25 @@ define([ * @returns {Promise} */ LongLatGeocoderService.prototype.geocode = function(query, callback) { - var splitQuery = query.match(/[^\s,\n]+/g); - if ((splitQuery.length === 2) || (splitQuery.length === 3)) { - var longitude = +splitQuery[0]; - var latitude = +splitQuery[1]; - var height = (splitQuery.length === 3) ? +splitQuery[2] : 300.0; + try { + var splitQuery = query.match(/[^\s,\n]+/g); + if ((splitQuery.length === 2) || (splitQuery.length === 3)) { + var longitude = +splitQuery[0]; + var latitude = +splitQuery[1]; + var height = (splitQuery.length === 3) ? +splitQuery[2] : 300.0; - if (!isNaN(longitude) && !isNaN(latitude) && !isNaN(height)) { - var result = { - displayName: query, - destination: Cartesian3.fromDegrees(longitude, latitude, height) - }; - return when.resolve([result]); + if (!isNaN(longitude) && !isNaN(latitude) && !isNaN(height)) { + var result = { + displayName: query, + destination: Cartesian3.fromDegrees(longitude, latitude, height) + }; + return when.resolve([result]); + } } + return when.resolve([]); + } catch (e) { + when.reject(e); } - return when.resolve([]); }; return LongLatGeocoderService; diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 759106889eb1..b8440ba086a0 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -448,6 +448,29 @@ define([ }; } + function promisesSettled(promises) { + var settledPromises = []; + promises.forEach(function (promise) { + var settled = when.defer(); + settledPromises.push(settled); + promise.then(function (result) { + return settled.resolve({state: 'fulfilled', value: result}); + }); + + if (defined(promise.otherwise)) { + promise.otherwise(function (err) { + return settled.resolve({state: 'rejected', reason: err}); + }); + return; + } + promise.catch(function (err) { + return settled.resolve({state: 'rejected', reason: err}); + }); + }); + + return when.all(settledPromises); + } + function geocode(viewModel, geocoderServices) { var query = viewModel.searchText; @@ -466,37 +489,34 @@ define([ var geocodePromise = geocoderService.geocode(query); resultPromises.push(geocodePromise); geocodePromise.then(getFirstResult); - - if (typeof geocodePromise.otherwise === 'function') { - geocodePromise.otherwise(function (err) { - console.log('otherwise err: ' + err); - }); - } else if (typeof geocodePromise.catch === 'function') { - geocodePromise.catch(function (err) { - console.log('catch err: ' + err); - }); - } } - var allReady = when.all(resultPromises); - allReady.then(function (results) { - viewModel._isSearchInProgress = false; - if (viewModel._cancelGeocode) { - viewModel._cancelGeocode = false; - return; - } - for (var j = 0; j < results.length; j++) { - if (defined(results[j]) && results[j].length > 0) { - viewModel._searchText = results[j][0].displayName; - updateCamera(viewModel, results[j][0].destination); + + promisesSettled(resultPromises) + .then(function (descriptors) { + viewModel._isSearchInProgress = false; + if (viewModel._cancelGeocode) { + viewModel._cancelGeocode = false; return; } - } - viewModel._searchText = query + ' (not found)'; - }) - .otherwise(function (err) { - viewModel._isSearchInProgress = false; - viewModel._searchText = query + ' (not found)'; - }); + var allFailed = true; + for (var j = 0; j < descriptors.length; j++) { + if (descriptors[j].state === 'rejected') { + continue; + } + allFailed = false; + var geocoderResults = descriptors[j].value; + if (defined(geocoderResults) && geocoderResults.length > 0) { + viewModel._searchText = geocoderResults[0].displayName; + updateCamera(viewModel, geocoderResults[0].destination); + return; + } + } + if (allFailed) { + viewModel._searchText = query + ' (geocoders failed)'; + return; + } + viewModel._searchText = query + ' (not found)'; + }); } function cancelGeocode(viewModel) { From 93876bc69ec51d8a5369c62dc7c533664442de59 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Thu, 8 Dec 2016 14:11:19 -0500 Subject: [PATCH 19/33] Address review comments --- CHANGES.md | 2 +- Source/Core/BingMapsGeocoderService.js | 15 ++++++ Source/Core/GeocoderService.js | 10 ++++ Source/Core/LongLatGeocoderService.js | 12 +++-- .../OpenStreetMapNominatimGeocoderService.js | 14 ++++-- Source/Widgets/Geocoder/Geocoder.js | 1 + Source/Widgets/Geocoder/GeocoderViewModel.js | 46 +++++++++---------- 7 files changed, 69 insertions(+), 31 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5dde6d39e540..fed3ac1f38d4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,7 @@ Change Log ### 1.29 - 2017-01-02 * Deprecated * The properties `url` and `key` will be removed from `GeocoderViewModel` in 1.30. This properties will be available on geocoder services that support them, like `BingMapsGeocoderService`; -* Added support for custom geocoder services [4723](https://github.com/AnalyticalGraphicsInc/cesium/pull/4723). +* Added support for custom geocoder services [#4723](https://github.com/AnalyticalGraphicsInc/cesium/pull/4723). * Added the ability to blend a `Model` with a color/translucency. Added `color`, `colorBlendMode`, and `colorBlendAmount` properties to `Model`, `ModelGraphics`, and CZML. Added `ColorBlendMode` enum. [#4547](https://github.com/AnalyticalGraphicsInc/cesium/pull/4547) * Fixed tooltips for gallery thumbnails in Sandcastle [#4702](https://github.com/AnalyticalGraphicsInc/cesium/pull/4702) * Fixed texture rotation for `RectangleGeometry` [#2737](https://github.com/AnalyticalGraphicsInc/cesium/issues/2737) diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js index 31785cc1c6a7..2aec91277620 100644 --- a/Source/Core/BingMapsGeocoderService.js +++ b/Source/Core/BingMapsGeocoderService.js @@ -18,11 +18,16 @@ define([ /** * Provides geocoding through Bing Maps. * @alias BingMapsGeocoderService + * @constructor * + * @param {Object} options Object with the following properties: + * @param {String} [key] A key to use with the Bing Maps geocoding service + * @param {Boolean} autoComplete Indicates whether this service shall be used to fetch auto-complete suggestions */ function BingMapsGeocoderService(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); this._canceled = false; + this._displayName = 'Bing Maps Geocoder Service'; this._url = 'https://dev.virtualearth.net/REST/v1/Locations'; this._key = BingMapsApi.getKey(options.key); @@ -31,6 +36,16 @@ define([ } defineProperties(BingMapsGeocoderService.prototype, { + /** + * The display name of the geocoder service + * @type {String} + */ + displayName : { + get : function () { + return this._displayName; + } + }, + /** * The URL endpoint for the Bing geocoder service * @type {String} diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js index 3fa0664abc27..14965256fa01 100644 --- a/Source/Core/GeocoderService.js +++ b/Source/Core/GeocoderService.js @@ -20,6 +20,7 @@ define([ * @constructor * * @see BingMapsGeocoderService + * @see OpenStreetMapNominatimGeocoderService */ function GeocoderService () { /** @@ -51,5 +52,14 @@ define([ */ GeocoderService.prototype.geocode = DeveloperError.throwInstantiationError; + /** + * A function that is called when geocoding is canceled by the user, so that the + * geocoding service can stop processing current requests. + * @function + * + * @returns {undefined} + */ + GeocoderService.prototype.cancel = DeveloperError.throwInstantiationError; + return GeocoderService; }); diff --git a/Source/Core/LongLatGeocoderService.js b/Source/Core/LongLatGeocoderService.js index c07c682a90a3..be29f6f890be 100644 --- a/Source/Core/LongLatGeocoderService.js +++ b/Source/Core/LongLatGeocoderService.js @@ -11,14 +11,18 @@ define([ /** * Provides geocoding through Bing Maps. - * @alias LongLatGeocoderService * + * @alias LongLatGeocoderService + * @constructor */ - function LongLatGeocoderService(options) { - options = defaultValue(options, defaultValue.EMPTY_OBJECT); + function LongLatGeocoderService() { this.autoComplete = false; } + /** + * This service completes geocoding synchronously and therefore does not + * need to handle canceled requests that have not finished yet. + */ LongLatGeocoderService.prototype.cancel = function() { }; @@ -28,7 +32,7 @@ define([ * @param {String} query The query to be sent to the geocoder service * @returns {Promise} */ - LongLatGeocoderService.prototype.geocode = function(query, callback) { + LongLatGeocoderService.prototype.geocode = function(query) { try { var splitQuery = query.match(/[^\s,\n]+/g); if ((splitQuery.length === 2) || (splitQuery.length === 3)) { diff --git a/Source/Core/OpenStreetMapNominatimGeocoderService.js b/Source/Core/OpenStreetMapNominatimGeocoderService.js index 749bc9aa8e0e..9c5386fb3458 100644 --- a/Source/Core/OpenStreetMapNominatimGeocoderService.js +++ b/Source/Core/OpenStreetMapNominatimGeocoderService.js @@ -14,21 +14,29 @@ define([ /** * Provides geocoding through OpenStreetMap Nominatim. * @alias OpenStreetMapNominatimGeocoder + * @constructor * + * @param {Object} options Object with the following properties: + * @param {Boolean} autoComplete Indicates whether this service shall be used to fetch auto-complete suggestions */ function OpenStreetMapNominatimGeocoder(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); - this.displayName = defaultValue(options.displayName, 'Nominatim'); + this.displayName = 'Nominatim'; this._canceled = false; this.autoComplete = defaultValue(options.autoComplete, true); } + /** + * The function called when a user cancels geocoding. + * + * @returns {undefined} + */ OpenStreetMapNominatimGeocoder.prototype.cancel = function() { this._canceled = true; }; /** - * @function + * The function called to geocode using this geocoder service. * * @param {String} query The query to be sent to the geocoder service * @returns {Promise} @@ -52,7 +60,7 @@ define([ ) }; }); - }) + }); }; return OpenStreetMapNominatimGeocoder; diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index 34f1761ff217..0d2ba2e53522 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -32,6 +32,7 @@ define([ * @param {Object} options Object with the following properties: * @param {Element|String} options.container The DOM element or ID that will contain the widget. * @param {Scene} options.scene The Scene instance to use. + * @param {Object} [options.geocoderServices] The geocoder services to be used * @param {String} [options.url='https://dev.virtualearth.net'] The base URL of the Bing Maps API. * @param {String} [options.key] The Bing Maps key for your application, which can be * created at {@link https://www.bingmapsportal.com}. diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index b8440ba086a0..ac884f5d6636 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -70,8 +70,7 @@ define([ if (!defined(options.geocoderServices)) { this._geocoderServices = [ new LongLatGeocoderService(), - new BingMapsGeocoderService(), - new OpenStreetMapNominatimGeocoderService() + new BingMapsGeocoderService() ]; } @@ -105,6 +104,7 @@ define([ this._selectedSuggestion = knockout.observable(); this._showSuggestions = knockout.observable(true); this._updateCamera = updateCamera; + this._adjustSuggestionsScroll = adjustSuggestionsScroll; var that = this; @@ -124,25 +124,6 @@ define([ } }); - this._adjustSuggestionsScroll = function (focusedItemIndex) { - var container = getElement(this._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; - } - }; - this.handleArrowDown = function () { if (that._suggestions().length === 0) { return; @@ -152,7 +133,7 @@ define([ var next = (currentIndex + 1) % numberOfSuggestions; that._selectedSuggestion(that._suggestions()[next]); - this._adjustSuggestionsScroll(next); + adjustSuggestionsScroll(this, next); }; this.handleArrowUp = function () { @@ -169,7 +150,7 @@ define([ } that._selectedSuggestion(that._suggestions()[next]); - this._adjustSuggestionsScroll(next); + adjustSuggestionsScroll(this, next); }; this.deselectSuggestion = function () { @@ -519,6 +500,25 @@ define([ }); } + 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._geocodeInProgress)) { From 2849a18227c6ab5a16393291395dd5f26c2571d9 Mon Sep 17 00:00:00 2001 From: Patrick Cozzi Date: Thu, 8 Dec 2016 15:05:34 -0500 Subject: [PATCH 20/33] Fix CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index fed3ac1f38d4..b98ea0837e15 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ Change Log ### 1.29 - 2017-01-02 * Deprecated - * The properties `url` and `key` will be removed from `GeocoderViewModel` in 1.30. This properties will be available on geocoder services that support them, like `BingMapsGeocoderService`; + * The properties `url` and `key` will be removed from `GeocoderViewModel` in 1.30. These properties will be available on geocoder services that support them, like `BingMapsGeocoderService`. * Added support for custom geocoder services [#4723](https://github.com/AnalyticalGraphicsInc/cesium/pull/4723). * Added the ability to blend a `Model` with a color/translucency. Added `color`, `colorBlendMode`, and `colorBlendAmount` properties to `Model`, `ModelGraphics`, and CZML. Added `ColorBlendMode` enum. [#4547](https://github.com/AnalyticalGraphicsInc/cesium/pull/4547) * Fixed tooltips for gallery thumbnails in Sandcastle [#4702](https://github.com/AnalyticalGraphicsInc/cesium/pull/4702) From 1fc73d420ad50ac2274a804ab3350c5a437a1160 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 13 Dec 2016 11:00:38 +0100 Subject: [PATCH 21/33] Rename LongLatGeocoderService to CartographicGeocoderService --- ...ocoderService.js => CartographicGeocoderService.js} | 10 +++++----- Source/Widgets/Geocoder/GeocoderViewModel.js | 6 +++--- ...rviceSpec.js => CartographicGeocoderServiceSpec.js} | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) rename Source/Core/{LongLatGeocoderService.js => CartographicGeocoderService.js} (84%) rename Specs/Core/{LongLatGeocoderServiceSpec.js => CartographicGeocoderServiceSpec.js} (91%) diff --git a/Source/Core/LongLatGeocoderService.js b/Source/Core/CartographicGeocoderService.js similarity index 84% rename from Source/Core/LongLatGeocoderService.js rename to Source/Core/CartographicGeocoderService.js index be29f6f890be..efa0961dbb38 100644 --- a/Source/Core/LongLatGeocoderService.js +++ b/Source/Core/CartographicGeocoderService.js @@ -12,10 +12,10 @@ define([ /** * Provides geocoding through Bing Maps. * - * @alias LongLatGeocoderService + * @alias CartographicGeocoderService * @constructor */ - function LongLatGeocoderService() { + function CartographicGeocoderService() { this.autoComplete = false; } @@ -23,7 +23,7 @@ define([ * This service completes geocoding synchronously and therefore does not * need to handle canceled requests that have not finished yet. */ - LongLatGeocoderService.prototype.cancel = function() { + CartographicGeocoderService.prototype.cancel = function() { }; /** @@ -32,7 +32,7 @@ define([ * @param {String} query The query to be sent to the geocoder service * @returns {Promise} */ - LongLatGeocoderService.prototype.geocode = function(query) { + CartographicGeocoderService.prototype.geocode = function(query) { try { var splitQuery = query.match(/[^\s,\n]+/g); if ((splitQuery.length === 2) || (splitQuery.length === 3)) { @@ -54,5 +54,5 @@ define([ } }; - return LongLatGeocoderService; + return CartographicGeocoderService; }); diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index ac884f5d6636..32a8ec9cf3fc 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -9,7 +9,7 @@ define([ '../../Core/deprecationWarning', '../../Core/DeveloperError', '../../Core/Event', - '../../Core/LongLatGeocoderService', + '../../Core/CartographicGeocoderService', '../../Core/loadJsonp', '../../Core/Matrix4', '../../Core/OpenStreetMapNominatimGeocoderService', @@ -28,7 +28,7 @@ define([ deprecationWarning, DeveloperError, Event, - LongLatGeocoderService, + CartographicGeocoderService, loadJsonp, Matrix4, OpenStreetMapNominatimGeocoderService, @@ -69,7 +69,7 @@ define([ this._geocoderServices = options.geocoderServices; if (!defined(options.geocoderServices)) { this._geocoderServices = [ - new LongLatGeocoderService(), + new CartographicGeocoderService(), new BingMapsGeocoderService() ]; } diff --git a/Specs/Core/LongLatGeocoderServiceSpec.js b/Specs/Core/CartographicGeocoderServiceSpec.js similarity index 91% rename from Specs/Core/LongLatGeocoderServiceSpec.js rename to Specs/Core/CartographicGeocoderServiceSpec.js index c2cd515a2a6d..5ecf68dbca37 100644 --- a/Specs/Core/LongLatGeocoderServiceSpec.js +++ b/Specs/Core/CartographicGeocoderServiceSpec.js @@ -1,13 +1,13 @@ /*global defineSuite*/ defineSuite([ - 'Core/LongLatGeocoderService', + 'Core/CartographicGeocoderService', 'Core/Cartesian3' ], function( - LongLatGeocoderService, + CartographicGeocoderService, Cartesian3) { 'use strict'; - var service = new LongLatGeocoderService(); + var service = new CartographicGeocoderService(); it('returns cartesian with matching coordinates for long/lat/height input', function (done) { var query = ' 1.0, 2.0, 3.0 '; From b06cfa18500fde7bfe1459ea53a47f9dca20d473 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 13 Dec 2016 11:18:47 +0100 Subject: [PATCH 22/33] Update doc for cartographic geocoder --- Source/Core/CartographicGeocoderService.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Core/CartographicGeocoderService.js b/Source/Core/CartographicGeocoderService.js index efa0961dbb38..0298626bb44b 100644 --- a/Source/Core/CartographicGeocoderService.js +++ b/Source/Core/CartographicGeocoderService.js @@ -10,7 +10,8 @@ define([ 'use strict'; /** - * Provides geocoding through Bing Maps. + * Geocodes queries containing longitude and latitude coordinates and an optional height. + * Query format: `longitude latitude (height)` with longitude/latitude in degrees and height in meters. * * @alias CartographicGeocoderService * @constructor From e99f83812e460c32a9ae7a87f4eed50983712591 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 13 Dec 2016 11:58:28 +0100 Subject: [PATCH 23/33] Remove Nominatim geocoder from core and show only as a Sandcastle example --- Apps/Sandcastle/gallery/Custom Geocoder.html | 54 ++++++++++++++- .../OpenStreetMapNominatimGeocoderService.js | 67 ------------------- Source/Widgets/Geocoder/GeocoderViewModel.js | 2 - ...enStreetMapNominatimGeocoderServiceSpec.js | 38 ----------- 4 files changed, 53 insertions(+), 108 deletions(-) delete mode 100644 Source/Core/OpenStreetMapNominatimGeocoderService.js delete mode 100644 Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js diff --git a/Apps/Sandcastle/gallery/Custom Geocoder.html b/Apps/Sandcastle/gallery/Custom Geocoder.html index c75b7329263e..c41a144245ba 100644 --- a/Apps/Sandcastle/gallery/Custom Geocoder.html +++ b/Apps/Sandcastle/gallery/Custom Geocoder.html @@ -37,8 +37,60 @@ 'use strict'; //Sandcastle_Begin +/** + * This class is an example of a custom geocoder. It provides geocoding through the OpenStreetMap Nominatim service. + * @alias OpenStreetMapNominatimGeocoder + * @constructor + * + * @param {Object} options Object with the following properties: + * @param {Boolean} autoComplete Indicates whether this service shall be used to fetch auto-complete suggestions + */ +function OpenStreetMapNominatimGeocoder(options) { + options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT); + this.displayName = 'Nominatim'; + this._canceled = false; + this.autoComplete = Cesium.defaultValue(options.autoComplete, true); +} + +/** + * The function called when a user cancels geocoding. + * + * @returns {undefined} + */ +OpenStreetMapNominatimGeocoder.prototype.cancel = function() { + this._canceled = true; +}; + +/** + * The function called to geocode using this geocoder service. + * + * @param {String} query The query to be sent to the geocoder service + * @returns {Promise} + */ +OpenStreetMapNominatimGeocoder.prototype.geocode = function (input) { + var endpoint = 'http://nominatim.openstreetmap.org/search?'; + var query = 'format=json&q=' + input; + var requestString = endpoint + query; + return Cesium.loadJson(requestString) + .then(function (results) { + var bboxDegrees; + return results.map(function (resultObject) { + bboxDegrees = resultObject.boundingbox; + return { + displayName: resultObject.display_name, + destination: Cesium.Rectangle.fromDegrees( + bboxDegrees[2], + bboxDegrees[0], + bboxDegrees[3], + bboxDegrees[1] + ) + }; + }); + }); +}; + var viewer = new Cesium.Viewer('cesiumContainer', { - geocoder: new Cesium.OpenStreetMapNominatimGeocoderService() + geocoder: new OpenStreetMapNominatimGeocoder() }); //Sandcastle_End diff --git a/Source/Core/OpenStreetMapNominatimGeocoderService.js b/Source/Core/OpenStreetMapNominatimGeocoderService.js deleted file mode 100644 index 9c5386fb3458..000000000000 --- a/Source/Core/OpenStreetMapNominatimGeocoderService.js +++ /dev/null @@ -1,67 +0,0 @@ -/*global define*/ -define([ - './Cartesian3', - './defaultValue', - './loadJson', - './Rectangle' -], function( - Cartesian3, - defaultValue, - loadJson, - Rectangle) { - 'use strict'; - - /** - * Provides geocoding through OpenStreetMap Nominatim. - * @alias OpenStreetMapNominatimGeocoder - * @constructor - * - * @param {Object} options Object with the following properties: - * @param {Boolean} autoComplete Indicates whether this service shall be used to fetch auto-complete suggestions - */ - function OpenStreetMapNominatimGeocoder(options) { - options = defaultValue(options, defaultValue.EMPTY_OBJECT); - this.displayName = 'Nominatim'; - this._canceled = false; - this.autoComplete = defaultValue(options.autoComplete, true); - } - - /** - * The function called when a user cancels geocoding. - * - * @returns {undefined} - */ - OpenStreetMapNominatimGeocoder.prototype.cancel = function() { - this._canceled = true; - }; - - /** - * The function called to geocode using this geocoder service. - * - * @param {String} query The query to be sent to the geocoder service - * @returns {Promise} - */ - OpenStreetMapNominatimGeocoder.prototype.geocode = function (input) { - var endpoint = 'http://nominatim.openstreetmap.org/search?'; - var query = 'format=json&q=' + input; - var requestString = endpoint + query; - return loadJson(requestString) - .then(function (results) { - var bboxDegrees; - return results.map(function (resultObject) { - bboxDegrees = resultObject.boundingbox; - return { - displayName: resultObject.display_name, - destination: Rectangle.fromDegrees( - bboxDegrees[2], - bboxDegrees[0], - bboxDegrees[3], - bboxDegrees[1] - ) - }; - }); - }); - }; - - return OpenStreetMapNominatimGeocoder; -}); \ No newline at end of file diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 32a8ec9cf3fc..24497a643100 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -12,7 +12,6 @@ define([ '../../Core/CartographicGeocoderService', '../../Core/loadJsonp', '../../Core/Matrix4', - '../../Core/OpenStreetMapNominatimGeocoderService', '../../Core/Rectangle', '../../ThirdParty/knockout', '../../ThirdParty/when', @@ -31,7 +30,6 @@ define([ CartographicGeocoderService, loadJsonp, Matrix4, - OpenStreetMapNominatimGeocoderService, Rectangle, knockout, when, diff --git a/Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js b/Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js deleted file mode 100644 index f04086580be1..000000000000 --- a/Specs/Core/OpenStreetMapNominatimGeocoderServiceSpec.js +++ /dev/null @@ -1,38 +0,0 @@ -/*global defineSuite*/ -defineSuite([ - 'Core/OpenStreetMapNominatimGeocoderService', - 'Core/Cartesian3', - 'Core/loadJsonp', - 'Core/Rectangle' -], function( - OpenStreetMapNominatimGeocoderService, - Cartesian3, - loadJsonp, - Rectangle) { - 'use strict'; - - var service = new OpenStreetMapNominatimGeocoderService(); - - it('returns geocoder results', function (done) { - var query = 'some query'; - jasmine.createSpy('testSpy', loadJsonp).and.returnValue([{ - displayName: 'a', - boundingbox: [10, 20, 0, 20] - }]); - service.geocode(query, function(err, results) { - expect(results.length).toEqual(1); - expect(results[0].displayName).toEqual('a'); - expect(results[0].destination).toBeInstanceOf(Rectangle); - done(); - }); - }); - - it('returns no geocoder results if OSM Nominatim has no results', function (done) { - var query = 'some query'; - jasmine.createSpy('testSpy', loadJsonp).and.returnValue([]); - service.geocode(query, function(err, results) { - expect(results.length).toEqual(0); - done(); - }); - }); -}); \ No newline at end of file From 0846a8948e1dc70df0572d4d0b6e8172fc257f98 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 13 Dec 2016 12:15:30 +0100 Subject: [PATCH 24/33] Add geocoder-related classes, interface and example to changes list --- CHANGES.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b98ea0837e15..396ab05e0430 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,11 @@ Change Log ### 1.29 - 2017-01-02 * Deprecated * The properties `url` and `key` will be removed from `GeocoderViewModel` in 1.30. These properties will be available on geocoder services that support them, like `BingMapsGeocoderService`. -* Added support for custom geocoder services [#4723](https://github.com/AnalyticalGraphicsInc/cesium/pull/4723). +* Added support for custom geocoder services and autocomplete [#4723](https://github.com/AnalyticalGraphicsInc/cesium/pull/4723). + * Added [Custom Geocoder Sandcastle example](http://localhost:8080/Apps/Sandcastle/index.html?src=Custom%20Geocoder.html) +* Added `GeocoderService`, an interface for geocoders. +* Added `BingMapsGeocoderService` implementing the `GeocoderService` interface. +* Added `CartographicGeocoderService` implementing the `GeocoderService` interface. * Added the ability to blend a `Model` with a color/translucency. Added `color`, `colorBlendMode`, and `colorBlendAmount` properties to `Model`, `ModelGraphics`, and CZML. Added `ColorBlendMode` enum. [#4547](https://github.com/AnalyticalGraphicsInc/cesium/pull/4547) * Fixed tooltips for gallery thumbnails in Sandcastle [#4702](https://github.com/AnalyticalGraphicsInc/cesium/pull/4702) * Fixed texture rotation for `RectangleGeometry` [#2737](https://github.com/AnalyticalGraphicsInc/cesium/issues/2737) From b72ae904721beda0319699eb5565704416a35ca9 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Tue, 13 Dec 2016 12:40:46 +0100 Subject: [PATCH 25/33] Clean up documentation --- Source/Core/BingMapsGeocoderService.js | 6 ++++++ Source/Core/CartographicGeocoderService.js | 17 ++++++++++++++++- Source/Core/GeocoderService.js | 3 +-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js index 2aec91277620..0c85dd6755ef 100644 --- a/Source/Core/BingMapsGeocoderService.js +++ b/Source/Core/BingMapsGeocoderService.js @@ -32,6 +32,12 @@ define([ this._url = 'https://dev.virtualearth.net/REST/v1/Locations'; this._key = BingMapsApi.getKey(options.key); + /** + * Indicates whether this geocoding service is to be used for autocomplete. + * + * @type {boolean} + * @default false + */ this.autoComplete = defaultValue(options.autoComplete, false); } diff --git a/Source/Core/CartographicGeocoderService.js b/Source/Core/CartographicGeocoderService.js index 0298626bb44b..48ea09cc6af9 100644 --- a/Source/Core/CartographicGeocoderService.js +++ b/Source/Core/CartographicGeocoderService.js @@ -2,10 +2,12 @@ define([ './Cartesian3', './defaultValue', + './defineProperties', '../ThirdParty/when' ], function( Cartesian3, defaultValue, + defineProperties, when) { 'use strict'; @@ -17,9 +19,22 @@ define([ * @constructor */ function CartographicGeocoderService() { - this.autoComplete = false; + this._autoComplete = false; } + defineProperties(CartographicGeocoderService.prototype, { + /** + * This geocoder does not support autocomplete, so this property will always be false. + * + * @type {boolean} + */ + autoComplete : { + get : function () { + return this._autoComplete; + } + } + }); + /** * This service completes geocoding synchronously and therefore does not * need to handle canceled requests that have not finished yet. diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js index 14965256fa01..9721c86a17bc 100644 --- a/Source/Core/GeocoderService.js +++ b/Source/Core/GeocoderService.js @@ -20,9 +20,8 @@ define([ * @constructor * * @see BingMapsGeocoderService - * @see OpenStreetMapNominatimGeocoderService */ - function GeocoderService () { + function GeocoderService() { /** * Indicates whether this geocoding service is to be used for autocomplete. * From 94eecbfc1adf0689210d84ebf83ee88547d46b0f Mon Sep 17 00:00:00 2001 From: hpinkos Date: Tue, 13 Dec 2016 14:56:23 -0500 Subject: [PATCH 26/33] knockout es5 makes everything difficult --- Source/Core/BingMapsGeocoderService.js | 10 - Source/Core/GeocoderService.js | 12 -- Source/Widgets/Geocoder/Geocoder.css | 2 +- Source/Widgets/Geocoder/Geocoder.js | 17 +- Source/Widgets/Geocoder/GeocoderViewModel.js | 194 ++++++++++--------- 5 files changed, 111 insertions(+), 124 deletions(-) diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js index 0c85dd6755ef..5e0ee46e2f58 100644 --- a/Source/Core/BingMapsGeocoderService.js +++ b/Source/Core/BingMapsGeocoderService.js @@ -42,16 +42,6 @@ define([ } defineProperties(BingMapsGeocoderService.prototype, { - /** - * The display name of the geocoder service - * @type {String} - */ - displayName : { - get : function () { - return this._displayName; - } - }, - /** * The URL endpoint for the Bing geocoder service * @type {String} diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js index 9721c86a17bc..fbf80f2ce6ea 100644 --- a/Source/Core/GeocoderService.js +++ b/Source/Core/GeocoderService.js @@ -31,18 +31,6 @@ define([ this.autoComplete = false; } - defineProperties(GeocoderService.prototype, { - /** - * The name of this service to be displayed next to suggestions - * in case more than one geocoder is in use - * @type {String} - * - */ - displayName : { - get : DeveloperError.throwInstantiationError - } - }); - /** * @function * diff --git a/Source/Widgets/Geocoder/Geocoder.css b/Source/Widgets/Geocoder/Geocoder.css index f77696d693a8..b97c06f55f1d 100644 --- a/Source/Widgets/Geocoder/Geocoder.css +++ b/Source/Widgets/Geocoder/Geocoder.css @@ -39,7 +39,7 @@ .cesium-viewer-geocoderContainer .search-results { position: absolute; - background-color: black; + background-color: #000; overflow-y: auto; opacity: 0.8; width: 100%; diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index 0d2ba2e53522..e2bf4a7c63fc 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -70,7 +70,8 @@ define([ textInput: searchText,\ disable: isSearchInProgress,\ event: { keyup: handleKeyUp, keydown: handleKeyDown, mouseover: deselectSuggestion },\ -css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 }'); +css: { "cesium-geocoder-input-wide" : keepExpanded || searchText.length > 0 },\ +hasFocus: _focusTextbox'); this._onTextBoxFocus = function() { // as of 2016-10-19, setTimeout is required to ensure that the @@ -95,16 +96,16 @@ cesiumSvgPath: { path: isSearchInProgress ? _stopSearchPath : _startSearchPath, var searchSuggestionsContainer = document.createElement('div'); searchSuggestionsContainer.className = 'search-results'; - searchSuggestionsContainer.setAttribute('data-bind', 'visible: suggestionsVisible'); + searchSuggestionsContainer.setAttribute('data-bind', 'visible: _suggestionsVisible'); var suggestionsList = document.createElement('ul'); - suggestionsList.setAttribute('data-bind', 'foreach: suggestions'); + suggestionsList.setAttribute('data-bind', 'foreach: _suggestions'); var suggestions = document.createElement('li'); suggestionsList.appendChild(suggestions); suggestions.setAttribute('data-bind', 'text: $data.displayName, \ click: $parent.activateSuggestion, \ -event: { mouseover: $parent.handleMouseover }, \ -css: { active: $data === $parent.selectedSuggestion() }'); +event: { mouseover: $parent.handleMouseover}, \ +css: { active: $data === $parent._selectedSuggestion }'); searchSuggestionsContainer.appendChild(suggestionsList); container.appendChild(searchSuggestionsContainer); @@ -119,14 +120,14 @@ css: { active: $data === $parent.selectedSuggestion() }'); this._onInputBegin = function(e) { if (!container.contains(e.target)) { - textBox.blur(); + viewModel._focusTextbox = false; viewModel.hideSuggestions(); } }; this._onInputEnd = function(e) { if (container.contains(e.target)) { - textBox.focus(); + viewModel._focusTextbox = true; viewModel.showSuggestions(); } }; @@ -207,7 +208,7 @@ css: { active: $data === $parent.selectedSuggestion() }'); document.removeEventListener('touchstart', this._onInputBegin, true); document.removeEventListener('touchend', this._onInputEnd, true); } - + this._viewModel.destory(); knockout.cleanNode(this._form); knockout.cleanNode(this._searchSuggestionsContainer); this._container.removeChild(this._form); diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 24497a643100..3a0fd488a301 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -64,8 +64,9 @@ define([ } //>>includeEnd('debug'); - this._geocoderServices = options.geocoderServices; - if (!defined(options.geocoderServices)) { + if (defined(options.geocoderServices)) { + this._geocoderServices = options.geocoderServices; + } else { this._geocoderServices = [ new CartographicGeocoderService(), new BingMapsGeocoderService() @@ -98,21 +99,25 @@ define([ this._isSearchInProgress = false; this._geocodeInProgress = undefined; this._complete = new Event(); - this._suggestions = knockout.observableArray(); - this._selectedSuggestion = knockout.observable(); - this._showSuggestions = knockout.observable(true); + this._suggestions = []; + this._selectedSuggestion = undefined; + this._showSuggestions = true; this._updateCamera = updateCamera; this._adjustSuggestionsScroll = adjustSuggestionsScroll; var that = this; this._suggestionsVisible = knockout.pureComputed(function () { - return that._suggestions().length > 0 && that._showSuggestions(); + var suggestions = knockout.getObservable(that, '_suggestions'); + var suggestionsNotEmpty = suggestions().length > 0; + var showSuggestions = knockout.getObservable(that, '_showSuggestions')(); + return suggestionsNotEmpty && showSuggestions; }); this._searchCommand = createCommand(function() { - if (defined(that._selectedSuggestion())) { - that.activateSuggestion(that._selectedSuggestion()); + that.hideSuggestions(); + if (defined(that._selectedSuggestion)) { + that.activateSuggestion(that._selectedSuggestion); return false; } if (that.isSearchInProgress) { @@ -122,79 +127,29 @@ define([ } }); - this.handleArrowDown = function () { - if (that._suggestions().length === 0) { - return; - } - var numberOfSuggestions = that._suggestions().length; - var currentIndex = that._suggestions().indexOf(that._selectedSuggestion()); - var next = (currentIndex + 1) % numberOfSuggestions; - that._selectedSuggestion(that._suggestions()[next]); - - adjustSuggestionsScroll(this, next); - }; - - this.handleArrowUp = function () { - if (that._suggestions().length === 0) { - return; - } - var numberOfSuggestions = that._suggestions().length; - var next; - var currentIndex = that._suggestions().indexOf(that._selectedSuggestion()); - if (currentIndex === -1 || currentIndex === 0) { - next = numberOfSuggestions - 1; - } else { - next = currentIndex - 1; - } - that._selectedSuggestion(that._suggestions()[next]); - - adjustSuggestionsScroll(this, next); - }; - this.deselectSuggestion = function () { - that._selectedSuggestion(undefined); + that._selectedSuggestion = undefined; }; - this.updateSearchSuggestions = function () { - var query = that.searchText; - - if (hasOnlyWhitespace(query)) { - that._suggestions.splice(0, that._suggestions().length); - return; - } - - that._suggestions.splice(0, that._suggestions().length); - var geocoderServices = that._geocoderServices.filter(function (service) { - return service.autoComplete === true; - }); - - geocoderServices.forEach(function (service) { - service.geocode(query) - .then(function (results) { - results.slice(0, 3).forEach(function(result) { - that._suggestions.push(result); - }); - }); - }); - }; - - this.handleKeyDown = function (data, event) { - var key = event.which; - if (key === 38) { - event.preventDefault(); - } else if (key === 40) { + 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 key = event.which; - if (key === 38) { - that.handleArrowUp(); - } else if (key === 40) { - that.handleArrowDown(); - } else if (key === 13) { + 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; @@ -203,22 +158,22 @@ define([ this.activateSuggestion = function (data) { that._searchText = data.displayName; var destination = data.destination; - that._suggestions.splice(0, that._suggestions().length); + clearSuggestions(that); updateCamera(that, destination); }; this.hideSuggestions = function () { - that._showSuggestions(false); - that._selectedSuggestion(undefined); + that._showSuggestions = false; + that._selectedSuggestion = undefined; }; this.showSuggestions = function () { - that._showSuggestions(true); + that._showSuggestions = true; }; this.handleMouseover = function (data, event) { - if (data !== that._selectedSuggestion()) { - that._selectedSuggestion(data); + if (data !== that._selectedSuggestion) { + that._selectedSuggestion = data; } }; @@ -230,8 +185,15 @@ define([ */ this.keepExpanded = false; - knockout.track(this, ['_searchText', '_isSearchInProgress', 'keepExpanded']); + this._focusTextbox = false; + knockout.track(this, ['_searchText', '_isSearchInProgress', 'keepExpanded', '_suggestions', '_selectedSuggestion', '_showSuggestions', '_focusTextbox']); + + var searchTextObservable = knockout.getObservable(this, '_searchText'); + searchTextObservable.extend({ rateLimit: { timeout: 500, method: "notifyWhenChangesStop" } }); + this._suggestionSubscription = searchTextObservable.subscribe(function() { + updateSearchSuggestions(that); + }); /** * Gets a value indicating whether a search is currently in progress. This property is observable. * @@ -256,6 +218,7 @@ define([ if (this.isSearchInProgress) { return 'Searching...'; } + return this._searchText; }, set : function(value) { @@ -265,7 +228,6 @@ define([ } //>>includeEnd('debug'); this._searchText = value; - this.updateSearchSuggestions(); } }); @@ -381,20 +343,40 @@ define([ get : function() { return this._suggestions; } - }, - - /** - * Indicates whether search suggestions should be visible. True if there are at least 1 suggestion. - * - * @type {Boolean} - */ - suggestionsVisible : { - get : function() { - return this._suggestionsVisible; - } } }); + 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]; + 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]; + + adjustSuggestionsScroll(viewModel, next); + } + function updateCamera(viewModel, destination) { viewModel._scene.camera.flyTo({ destination : destination, @@ -451,7 +433,7 @@ define([ } function geocode(viewModel, geocoderServices) { - var query = viewModel.searchText; + var query = viewModel._searchText; if (hasOnlyWhitespace(query)) { return; @@ -464,7 +446,7 @@ define([ var geocoderService = geocoderServices[i]; viewModel._isSearchInProgress = true; - viewModel._suggestions.splice(0, viewModel._suggestions().length); + clearSuggestions(viewModel); var geocodePromise = geocoderService.geocode(query); resultPromises.push(geocodePromise); geocodePromise.then(getFirstResult); @@ -529,5 +511,31 @@ define([ return /^\s*$/.test(string); } + function clearSuggestions(viewModel) { + knockout.getObservable(viewModel, '_suggestions').removeAll(); + } + + function updateSearchSuggestions(viewModel) { + var query = viewModel._searchText; + + clearSuggestions(viewModel); + if (hasOnlyWhitespace(query)) { + return; + } + + var geocoderServices = viewModel._geocoderServices.filter(function (service) { + return service.autoComplete === true; + }); + + geocoderServices.forEach(function (service) { + service.geocode(query) + .then(function (results) { + results.slice(0, 3).forEach(function(result) { + viewModel._suggestions.push(result); + }); + }); + }); + } + return GeocoderViewModel; }); From 1b66af9caa7746c12fab4ecbcdd4c38cb5bdc5dc Mon Sep 17 00:00:00 2001 From: hpinkos Date: Tue, 13 Dec 2016 15:13:40 -0500 Subject: [PATCH 27/33] fix specs --- Source/Widgets/Geocoder/Geocoder.js | 2 +- Source/Widgets/Geocoder/GeocoderViewModel.js | 3 ++ Specs/Widgets/Geocoder/GeocoderSpec.js | 29 ++++++++++--------- .../Widgets/Geocoder/GeocoderViewModelSpec.js | 16 +++++----- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index e2bf4a7c63fc..e96346785ec7 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -208,7 +208,7 @@ css: { active: $data === $parent._selectedSuggestion }'); document.removeEventListener('touchstart', this._onInputBegin, true); document.removeEventListener('touchend', this._onInputEnd, true); } - this._viewModel.destory(); + this._viewModel.destroy(); knockout.cleanNode(this._form); knockout.cleanNode(this._searchSuggestionsContainer); this._container.removeChild(this._form); diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 3a0fd488a301..d25d847572a3 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -104,6 +104,9 @@ define([ this._showSuggestions = true; this._updateCamera = updateCamera; this._adjustSuggestionsScroll = adjustSuggestionsScroll; + this._updateSearchSuggestions = updateSearchSuggestions; + this._handleArrowDown = handleArrowDown; + this._handleArrowUp = handleArrowUp; var that = this; diff --git a/Specs/Widgets/Geocoder/GeocoderSpec.js b/Specs/Widgets/Geocoder/GeocoderSpec.js index cbe522c08808..0b2bde8b24ad 100644 --- a/Specs/Widgets/Geocoder/GeocoderSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderSpec.js @@ -115,20 +115,21 @@ defineSuite([ }); var viewModel = geocoder._viewModel; viewModel._searchText = 'some_text'; - viewModel.updateSearchSuggestions(); - - expect(viewModel._selectedSuggestion()).toEqual(undefined); - viewModel.handleArrowDown(); - expect(viewModel._selectedSuggestion().displayName).toEqual('a'); - viewModel.handleArrowDown(); - viewModel.handleArrowDown(); - expect(viewModel._selectedSuggestion().displayName).toEqual('c'); - viewModel.handleArrowDown(); - expect(viewModel._selectedSuggestion().displayName).toEqual('a'); - viewModel.handleArrowUp(); - expect(viewModel._selectedSuggestion().displayName).toEqual('c'); - viewModel.handleArrowUp(); - expect(viewModel._selectedSuggestion().displayName).toEqual('b'); + viewModel._updateSearchSuggestions(viewModel); + + expect(viewModel._selectedSuggestion).toEqual(undefined); + viewModel._handleArrowDown(viewModel); + expect(viewModel._selectedSuggestion.displayName).toEqual('a'); + viewModel._handleArrowDown(viewModel); + viewModel._handleArrowDown(viewModel); + expect(viewModel._selectedSuggestion.displayName).toEqual('c'); + viewModel._handleArrowDown(viewModel); + expect(viewModel._selectedSuggestion.displayName).toEqual('a'); + viewModel._handleArrowDown(viewModel); + viewModel._handleArrowUp(viewModel); + expect(viewModel._selectedSuggestion.displayName).toEqual('a'); + viewModel._handleArrowUp(viewModel); + expect(viewModel._selectedSuggestion).toBeUndefined(); }); }, 'WebGL'); diff --git a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js index d3a270f2d030..442e7dd9ded2 100644 --- a/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js +++ b/Specs/Widgets/Geocoder/GeocoderViewModelSpec.js @@ -5,7 +5,7 @@ defineSuite([ 'Scene/Camera', 'Specs/createScene', 'Specs/pollToPromise', - 'ThirdParty/when', + 'ThirdParty/when' ], function( GeocoderViewModel, Cartesian3, @@ -165,8 +165,8 @@ defineSuite([ geocoderServices : [customGeocoderOptions] }); geocoder._searchText = 'some_text'; - geocoder.updateSearchSuggestions(); - expect(geocoder._suggestions().length).toEqual(2); + geocoder._updateSearchSuggestions(geocoder); + expect(geocoder._suggestions.length).toEqual(2); }); it('update search suggestions results in empty list if the query is empty', function() { @@ -176,8 +176,8 @@ defineSuite([ }); geocoder._searchText = ''; spyOn(geocoder, '_adjustSuggestionsScroll'); - geocoder.updateSearchSuggestions(); - expect(geocoder._suggestions().length).toEqual(0); + geocoder._updateSearchSuggestions(geocoder); + expect(geocoder._suggestions.length).toEqual(0); }); it('can activate selected search suggestion', function () { @@ -189,7 +189,7 @@ defineSuite([ spyOn(geocoder, '_adjustSuggestionsScroll'); var suggestion = {displayName: 'a', destination: {west: 0.0, east: 0.1, north: 0.1, south: -0.1}}; - geocoder._selectedSuggestion(suggestion); + geocoder._selectedSuggestion = suggestion; geocoder.activateSuggestion(suggestion); expect(geocoder._searchText).toEqual('a'); }); @@ -214,8 +214,8 @@ defineSuite([ geocoder._searchText = 'sthsnth'; // an empty query will prevent geocoding spyOn(geocoder, '_updateCamera'); spyOn(geocoder, '_adjustSuggestionsScroll'); - geocoder.updateSearchSuggestions(); - expect(geocoder._suggestions().length).toEqual(geocoderResults1.length + geocoderResults2.length); + geocoder._updateSearchSuggestions(geocoder); + expect(geocoder._suggestions.length).toEqual(geocoderResults1.length + geocoderResults2.length); }); }, 'WebGL'); From 2659c7f9e310df9ab9075e65d78e4d521277747e Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Wed, 14 Dec 2016 13:15:14 +0100 Subject: [PATCH 28/33] Remove unused property displayName --- Apps/Sandcastle/gallery/Custom Geocoder.html | 1 - Source/Core/BingMapsGeocoderService.js | 2 -- 2 files changed, 3 deletions(-) diff --git a/Apps/Sandcastle/gallery/Custom Geocoder.html b/Apps/Sandcastle/gallery/Custom Geocoder.html index c41a144245ba..1431907308eb 100644 --- a/Apps/Sandcastle/gallery/Custom Geocoder.html +++ b/Apps/Sandcastle/gallery/Custom Geocoder.html @@ -47,7 +47,6 @@ */ function OpenStreetMapNominatimGeocoder(options) { options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT); - this.displayName = 'Nominatim'; this._canceled = false; this.autoComplete = Cesium.defaultValue(options.autoComplete, true); } diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js index 5e0ee46e2f58..1ff29ec6b2ba 100644 --- a/Source/Core/BingMapsGeocoderService.js +++ b/Source/Core/BingMapsGeocoderService.js @@ -27,8 +27,6 @@ define([ function BingMapsGeocoderService(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); this._canceled = false; - this._displayName = 'Bing Maps Geocoder Service'; - this._url = 'https://dev.virtualearth.net/REST/v1/Locations'; this._key = BingMapsApi.getKey(options.key); From f10c75f3d99d5a6966e3bb4ccca379fcb0c703ea Mon Sep 17 00:00:00 2001 From: hpinkos Date: Fri, 16 Dec 2016 17:29:40 -0500 Subject: [PATCH 29/33] chain geocoder promises --- Source/Core/BingMapsGeocoderService.js | 34 ++--- Source/Core/CartographicGeocoderService.js | 31 ++-- Source/Core/GeocoderService.js | 16 -- Source/Widgets/Geocoder/Geocoder.css | 1 + Source/Widgets/Geocoder/Geocoder.js | 3 +- Source/Widgets/Geocoder/GeocoderViewModel.js | 153 +++++++++---------- 6 files changed, 95 insertions(+), 143 deletions(-) diff --git a/Source/Core/BingMapsGeocoderService.js b/Source/Core/BingMapsGeocoderService.js index 1ff29ec6b2ba..52f73196cf6e 100644 --- a/Source/Core/BingMapsGeocoderService.js +++ b/Source/Core/BingMapsGeocoderService.js @@ -2,13 +2,17 @@ define([ './BingMapsApi', './defaultValue', + './defined', './defineProperties', + './DeveloperError', './loadJsonp', - './Rectangle', + './Rectangle' ], function( BingMapsApi, defaultValue, + defined, defineProperties, + DeveloperError, loadJsonp, Rectangle) { 'use strict'; @@ -26,23 +30,16 @@ define([ */ function BingMapsGeocoderService(options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); - this._canceled = false; this._url = 'https://dev.virtualearth.net/REST/v1/Locations'; this._key = BingMapsApi.getKey(options.key); - - /** - * Indicates whether this geocoding service is to be used for autocomplete. - * - * @type {boolean} - * @default false - */ - this.autoComplete = defaultValue(options.autoComplete, false); } defineProperties(BingMapsGeocoderService.prototype, { /** * The URL endpoint for the Bing geocoder service * @type {String} + * @memberof {BingMapsGeocoderService.prototype} + * @readonly */ url : { get : function () { @@ -53,6 +50,8 @@ define([ /** * The key for the Bing geocoder service * @type {String} + * @memberof {BingMapsGeocoderService.prototype} + * @readonly */ key : { get : function () { @@ -61,10 +60,6 @@ define([ } }); - BingMapsGeocoderService.prototype.cancel = function() { - this._canceled = true; - }; - /** * @function * @@ -72,7 +67,11 @@ define([ * @returns {Promise} */ BingMapsGeocoderService.prototype.geocode = function(query) { - this._canceled = false; + //>>includeStart('debug', pragmas.debug); + if (!defined(query)) { + throw new DeveloperError('query must be defined'); + } + //>>includeEnd('debug'); var key = this.key; var promise = loadJsonp(url, { @@ -83,12 +82,7 @@ define([ callbackParameterName : 'jsonp' }); - var that = this; - return promise.then(function(result) { - if (that._canceled) { - return; - } if (result.resourceSets.length === 0) { return []; } diff --git a/Source/Core/CartographicGeocoderService.js b/Source/Core/CartographicGeocoderService.js index 48ea09cc6af9..19beb25f0dbf 100644 --- a/Source/Core/CartographicGeocoderService.js +++ b/Source/Core/CartographicGeocoderService.js @@ -3,11 +3,15 @@ define([ './Cartesian3', './defaultValue', './defineProperties', + './defined', + './DeveloperError', '../ThirdParty/when' ], function( Cartesian3, defaultValue, defineProperties, + defined, + DeveloperError, when) { 'use strict'; @@ -19,29 +23,8 @@ define([ * @constructor */ function CartographicGeocoderService() { - this._autoComplete = false; } - defineProperties(CartographicGeocoderService.prototype, { - /** - * This geocoder does not support autocomplete, so this property will always be false. - * - * @type {boolean} - */ - autoComplete : { - get : function () { - return this._autoComplete; - } - } - }); - - /** - * This service completes geocoding synchronously and therefore does not - * need to handle canceled requests that have not finished yet. - */ - CartographicGeocoderService.prototype.cancel = function() { - }; - /** * @function * @@ -49,6 +32,12 @@ define([ * @returns {Promise} */ CartographicGeocoderService.prototype.geocode = function(query) { + //>>includeStart('debug', pragmas.debug); + if (!defined(query)) { + throw new DeveloperError('query must be defined'); + } + //>>includeEnd('debug'); + try { var splitQuery = query.match(/[^\s,\n]+/g); if ((splitQuery.length === 2) || (splitQuery.length === 3)) { diff --git a/Source/Core/GeocoderService.js b/Source/Core/GeocoderService.js index fbf80f2ce6ea..c15982213a0f 100644 --- a/Source/Core/GeocoderService.js +++ b/Source/Core/GeocoderService.js @@ -22,13 +22,6 @@ define([ * @see BingMapsGeocoderService */ function GeocoderService() { - /** - * Indicates whether this geocoding service is to be used for autocomplete. - * - * @type {boolean} - * @default false - */ - this.autoComplete = false; } /** @@ -39,14 +32,5 @@ define([ */ GeocoderService.prototype.geocode = DeveloperError.throwInstantiationError; - /** - * A function that is called when geocoding is canceled by the user, so that the - * geocoding service can stop processing current requests. - * @function - * - * @returns {undefined} - */ - GeocoderService.prototype.cancel = DeveloperError.throwInstantiationError; - return GeocoderService; }); diff --git a/Source/Widgets/Geocoder/Geocoder.css b/Source/Widgets/Geocoder/Geocoder.css index b97c06f55f1d..244e41de2c8d 100644 --- a/Source/Widgets/Geocoder/Geocoder.css +++ b/Source/Widgets/Geocoder/Geocoder.css @@ -40,6 +40,7 @@ .cesium-viewer-geocoderContainer .search-results { position: absolute; background-color: #000; + color: #eee; overflow-y: auto; opacity: 0.8; width: 100%; diff --git a/Source/Widgets/Geocoder/Geocoder.js b/Source/Widgets/Geocoder/Geocoder.js index e96346785ec7..2b42e86a2ccb 100644 --- a/Source/Widgets/Geocoder/Geocoder.js +++ b/Source/Widgets/Geocoder/Geocoder.js @@ -32,7 +32,8 @@ define([ * @param {Object} options Object with the following properties: * @param {Element|String} options.container The DOM element or ID that will contain the widget. * @param {Scene} options.scene The Scene instance to use. - * @param {Object} [options.geocoderServices] The geocoder services to be used + * @param {GeocoderService[]} [options.geocoderServices] The geocoder services to be used + * @param {Boolean} [options.autoComplete = true] True if the geocoder should query as the user types to autocomplete * @param {String} [options.url='https://dev.virtualearth.net'] The base URL of the Bing Maps API. * @param {String} [options.key] The Bing Maps key for your application, which can be * created at {@link https://www.bingmapsportal.com}. diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index d25d847572a3..a45e3a82cb28 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -97,7 +97,7 @@ define([ this._flightDuration = options.flightDuration; this._searchText = ''; this._isSearchInProgress = false; - this._geocodeInProgress = undefined; + this._geocodePromise = undefined; this._complete = new Event(); this._suggestions = []; this._selectedSuggestion = undefined; @@ -159,6 +159,7 @@ define([ }; this.activateSuggestion = function (data) { + that.hideSuggestions(); that._searchText = data.displayName; var destination = data.destination; clearSuggestions(that); @@ -188,12 +189,19 @@ define([ */ this.keepExpanded = false; + /** + * True if the geocoder should query as the user types to autocomplete + * @type {Booelan} + * @default true + */ + this.autoComplete = defaultValue(options.autocomplete, true); + this._focusTextbox = false; knockout.track(this, ['_searchText', '_isSearchInProgress', 'keepExpanded', '_suggestions', '_selectedSuggestion', '_showSuggestions', '_focusTextbox']); var searchTextObservable = knockout.getObservable(this, '_searchText'); - searchTextObservable.extend({ rateLimit: { timeout: 500, method: "notifyWhenChangesStop" } }); + searchTextObservable.extend({ rateLimit: { timeout: 500 } }); this._suggestionSubscription = searchTextObservable.subscribe(function() { updateSearchSuggestions(that); }); @@ -391,48 +399,27 @@ define([ }); } - function getFirstResult(results) { - if (results.length === 0) { - return undefined; - } - - var firstResult = results[0]; - //>>includeStart('debug', pragmas.debug); - if (!defined(firstResult.displayName)) { - throw new DeveloperError('each result must have a displayName'); - } - if (!defined(firstResult.destination)) { - throw new DeveloperError('each result must have a rectangle'); - } - //>>includeEnd('debug'); - - return { - displayName: firstResult.displayName, - destination: firstResult.destination - }; - } - - function promisesSettled(promises) { - var settledPromises = []; - promises.forEach(function (promise) { - var settled = when.defer(); - settledPromises.push(settled); - promise.then(function (result) { - return settled.resolve({state: 'fulfilled', value: result}); - }); - - if (defined(promise.otherwise)) { - promise.otherwise(function (err) { - return settled.resolve({state: 'rejected', reason: err}); - }); - return; - } - promise.catch(function (err) { - return settled.resolve({state: 'rejected', reason: err}); + function chainPromise(promise, geocoderService, query) { + return promise + .then(function(result) { + if (defined(result) && result.state === 'fulfilled' && result.value.length > 0){ + return result; + } + var nextPromise = geocoderService.geocode(query) + .then(function (result) { + return {state: 'fulfilled', value: result}; + }); + if (defined(nextPromise.otherwise)) { + nextPromise.otherwise(function (err) { + return {state: 'rejected', reason: err}; + }); + } else if (defined(nextPromise.catch)) { + nextPromise.catch(function (err) { + return {state: 'rejected', reason: err}; + }); + } + return nextPromise; }); - }); - - return when.all(settledPromises); } function geocode(viewModel, geocoderServices) { @@ -442,41 +429,25 @@ define([ return; } - viewModel._geocodeInProgress = true; + viewModel._isSearchInProgress = true; - var resultPromises = []; + var promise = when.resolve(); for (var i = 0; i < geocoderServices.length; i++) { - var geocoderService = geocoderServices[i]; - - viewModel._isSearchInProgress = true; - clearSuggestions(viewModel); - var geocodePromise = geocoderService.geocode(query); - resultPromises.push(geocodePromise); - geocodePromise.then(getFirstResult); + promise = chainPromise(promise, geocoderServices[i], query); } - promisesSettled(resultPromises) - .then(function (descriptors) { - viewModel._isSearchInProgress = false; - if (viewModel._cancelGeocode) { - viewModel._cancelGeocode = false; + viewModel._geocodePromise = promise; + promise + .then(function (result) { + if (promise.cancel || promise !== viewModel._geocodePromise) { return; } - var allFailed = true; - for (var j = 0; j < descriptors.length; j++) { - if (descriptors[j].state === 'rejected') { - continue; - } - allFailed = false; - var geocoderResults = descriptors[j].value; - if (defined(geocoderResults) && geocoderResults.length > 0) { - viewModel._searchText = geocoderResults[0].displayName; - updateCamera(viewModel, geocoderResults[0].destination); - return; - } - } - if (allFailed) { - viewModel._searchText = query + ' (geocoders failed)'; + viewModel._isSearchInProgress = false; + + var geocoderResults = result.value; + if (result.state === 'fulfilled' && defined(geocoderResults) && geocoderResults.length > 0) { + viewModel._searchText = geocoderResults[0].displayName; + updateCamera(viewModel, geocoderResults[0].destination); return; } viewModel._searchText = query + ' (not found)'; @@ -504,9 +475,9 @@ define([ function cancelGeocode(viewModel) { viewModel._isSearchInProgress = false; - if (defined(viewModel._geocodeInProgress)) { - viewModel._cancelGeocode = true; - viewModel._geocodeInProgress = undefined; + if (defined(viewModel._geocodePromise)) { + viewModel._geocodePromise.cancel = true; + viewModel._geocodePromise = undefined; } } @@ -519,6 +490,10 @@ define([ } function updateSearchSuggestions(viewModel) { + if (!viewModel.autoComplete) { + return; + } + var query = viewModel._searchText; clearSuggestions(viewModel); @@ -526,18 +501,26 @@ define([ return; } - var geocoderServices = viewModel._geocoderServices.filter(function (service) { - return service.autoComplete === true; - }); - - geocoderServices.forEach(function (service) { - service.geocode(query) - .then(function (results) { - results.slice(0, 3).forEach(function(result) { - viewModel._suggestions.push(result); + var promise = when.resolve([]); + viewModel._geocoderServices.forEach(function (service) { + promise = promise.then(function(results) { + if (results.length >= 5) { + return results; + } + return service.geocode(query) + .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]); + } + }); } return GeocoderViewModel; From ff5b0eae3da7f1e78d02aa6a364c7d3384f67fa9 Mon Sep 17 00:00:00 2001 From: hpinkos Date: Mon, 19 Dec 2016 13:00:32 -0500 Subject: [PATCH 30/33] update sandcastle example --- Apps/Sandcastle/gallery/Custom Geocoder.html | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/Apps/Sandcastle/gallery/Custom Geocoder.html b/Apps/Sandcastle/gallery/Custom Geocoder.html index 1431907308eb..b1f388c92db4 100644 --- a/Apps/Sandcastle/gallery/Custom Geocoder.html +++ b/Apps/Sandcastle/gallery/Custom Geocoder.html @@ -41,29 +41,14 @@ * This class is an example of a custom geocoder. It provides geocoding through the OpenStreetMap Nominatim service. * @alias OpenStreetMapNominatimGeocoder * @constructor - * - * @param {Object} options Object with the following properties: - * @param {Boolean} autoComplete Indicates whether this service shall be used to fetch auto-complete suggestions */ -function OpenStreetMapNominatimGeocoder(options) { - options = Cesium.defaultValue(options, Cesium.defaultValue.EMPTY_OBJECT); - this._canceled = false; - this.autoComplete = Cesium.defaultValue(options.autoComplete, true); +function OpenStreetMapNominatimGeocoder() { } -/** - * The function called when a user cancels geocoding. - * - * @returns {undefined} - */ -OpenStreetMapNominatimGeocoder.prototype.cancel = function() { - this._canceled = true; -}; - /** * The function called to geocode using this geocoder service. * - * @param {String} query The query to be sent to the geocoder service + * @param {String} input The query to be sent to the geocoder service * @returns {Promise} */ OpenStreetMapNominatimGeocoder.prototype.geocode = function (input) { From 7d52c003b79e3c9d39cfc2237fcfa7926fe10884 Mon Sep 17 00:00:00 2001 From: hpinkos Date: Wed, 4 Jan 2017 17:43:04 -0500 Subject: [PATCH 31/33] cleanup --- Source/Core/CartographicGeocoderService.js | 28 +++++++++----------- Source/Widgets/Geocoder/GeocoderViewModel.js | 19 ++++++------- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/Source/Core/CartographicGeocoderService.js b/Source/Core/CartographicGeocoderService.js index 19beb25f0dbf..e7475fcdcca1 100644 --- a/Source/Core/CartographicGeocoderService.js +++ b/Source/Core/CartographicGeocoderService.js @@ -38,25 +38,21 @@ define([ } //>>includeEnd('debug'); - try { - var splitQuery = query.match(/[^\s,\n]+/g); - if ((splitQuery.length === 2) || (splitQuery.length === 3)) { - var longitude = +splitQuery[0]; - var latitude = +splitQuery[1]; - var height = (splitQuery.length === 3) ? +splitQuery[2] : 300.0; + var splitQuery = query.match(/[^\s,\n]+/g); + if ((splitQuery.length === 2) || (splitQuery.length === 3)) { + var longitude = +splitQuery[0]; + var latitude = +splitQuery[1]; + var height = (splitQuery.length === 3) ? +splitQuery[2] : 300.0; - if (!isNaN(longitude) && !isNaN(latitude) && !isNaN(height)) { - var result = { - displayName: query, - destination: Cartesian3.fromDegrees(longitude, latitude, height) - }; - return when.resolve([result]); - } + if (!isNaN(longitude) && !isNaN(latitude) && !isNaN(height)) { + var result = { + displayName: query, + destination: Cartesian3.fromDegrees(longitude, latitude, height) + }; + return when.resolve([result]); } - return when.resolve([]); - } catch (e) { - when.reject(e); } + return when.resolve([]); }; return CartographicGeocoderService; diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index a45e3a82cb28..4a1b55581530 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -10,7 +10,6 @@ define([ '../../Core/DeveloperError', '../../Core/Event', '../../Core/CartographicGeocoderService', - '../../Core/loadJsonp', '../../Core/Matrix4', '../../Core/Rectangle', '../../ThirdParty/knockout', @@ -28,7 +27,6 @@ define([ DeveloperError, Event, CartographicGeocoderService, - loadJsonp, Matrix4, Rectangle, knockout, @@ -44,7 +42,7 @@ define([ * * @param {Object} options Object with the following properties: * @param {Scene} options.scene The Scene instance to use. - * @param {GeocoderService[]} [geocoderServices] Geocoder services to use for geocoding queries. + * @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 {String} [options.url='https://dev.virtualearth.net'] The base URL of the Bing Maps API. @@ -357,6 +355,10 @@ define([ } }); + /** + * Destroys the widget. Should be called if permanently + * removing the widget from layout. + */ GeocoderViewModel.prototype.destroy = function() { this._suggestionSubscription.dispose(); }; @@ -408,16 +410,11 @@ define([ var nextPromise = geocoderService.geocode(query) .then(function (result) { return {state: 'fulfilled', value: result}; - }); - if (defined(nextPromise.otherwise)) { - nextPromise.otherwise(function (err) { + }) + .otherwise(function (err) { return {state: 'rejected', reason: err}; }); - } else if (defined(nextPromise.catch)) { - nextPromise.catch(function (err) { - return {state: 'rejected', reason: err}; - }); - } + return nextPromise; }); } From 97f10a76a57d86723548061458f3391e468f2009 Mon Sep 17 00:00:00 2001 From: hpinkos Date: Wed, 4 Jan 2017 18:05:58 -0500 Subject: [PATCH 32/33] fix search dropdown --- Source/Widgets/Geocoder/GeocoderViewModel.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 4a1b55581530..88058f912319 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -423,6 +423,7 @@ define([ var query = viewModel._searchText; if (hasOnlyWhitespace(query)) { + viewModel.showSuggestions(); return; } @@ -436,7 +437,7 @@ define([ viewModel._geocodePromise = promise; promise .then(function (result) { - if (promise.cancel || promise !== viewModel._geocodePromise) { + if (promise.cancel) { return; } viewModel._isSearchInProgress = false; From 7c8f5f4ebbaabec23dd0d764700e1b7d868b32c2 Mon Sep 17 00:00:00 2001 From: Erik Andersson Date: Thu, 5 Jan 2017 10:32:07 +0100 Subject: [PATCH 33/33] Update release numbers since this was delayed --- CHANGES.md | 2 +- Source/Widgets/Geocoder/GeocoderViewModel.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index caaf5980ecf8..0feb998afd60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,7 @@ Change Log ### 1.30 - 2017-02-01 * Deprecated - * The properties `url` and `key` will be removed from `GeocoderViewModel` in 1.30. These properties will be available on geocoder services that support them, like `BingMapsGeocoderService`. + * The properties `url` and `key` will be removed from `GeocoderViewModel` in 1.31. These properties will be available on geocoder services that support them, like `BingMapsGeocoderService`. * Added support for custom geocoder services and autocomplete [#4723](https://github.com/AnalyticalGraphicsInc/cesium/pull/4723). * Added [Custom Geocoder Sandcastle example](http://localhost:8080/Apps/Sandcastle/index.html?src=Custom%20Geocoder.html) * Added `GeocoderService`, an interface for geocoders. diff --git a/Source/Widgets/Geocoder/GeocoderViewModel.js b/Source/Widgets/Geocoder/GeocoderViewModel.js index 88058f912319..e1ebc2e3068d 100644 --- a/Source/Widgets/Geocoder/GeocoderViewModel.js +++ b/Source/Widgets/Geocoder/GeocoderViewModel.js @@ -275,7 +275,7 @@ define([ */ url : { get : function() { - deprecationWarning('url is deprecated', 'The url property was deprecated in Cesium 1.29 and will be removed in version 1.30.'); + deprecationWarning('url is deprecated', 'The url property was deprecated in Cesium 1.30 and will be removed in version 1.31.'); return this._url; } }, @@ -289,7 +289,7 @@ define([ */ key : { get : function() { - deprecationWarning('key is deprecated', 'The key property was deprecated in Cesium 1.29 and will be removed in version 1.30.'); + deprecationWarning('key is deprecated', 'The key property was deprecated in Cesium 1.30 and will be removed in version 1.31.'); return this._key; } },