Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature 1796 places autocomplete #2314

Merged
merged 30 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dd28faf
Move lat, long parsing and validations logic into GeoPoint
ianguerin Feb 28, 2024
e7d66ee
Fixup: Undefined checks for GeoPoint static functions
ianguerin Mar 6, 2024
f5c4e6a
Fixup: Migrate fromString static method to Backbone-style parse function
ianguerin Mar 6, 2024
d1344fd
Fixup: Parse always returns Backbone-expected value
ianguerin Mar 11, 2024
b65bb9d
Use Google Maps API Autocomplete and Geocoder
ianguerin Feb 28, 2024
0c54e00
Fixup: Hide Google Maps details from GeocoderSearch
ianguerin Mar 6, 2024
d28a553
Fixup: Use es6 class for GeocoderSearch
ianguerin Mar 7, 2024
817db52
Fixup: Add sinon earlier in PR chain to fix failing unit tests
ianguerin Mar 7, 2024
5a8053f
Fixup: Load mock gmaps module before loading any test code
ianguerin Mar 7, 2024
3296eae
Fixup: remove focus on single describe test block
ianguerin Mar 7, 2024
85b0d4d
Fixup: Migrate geocoder models to es6 classes
ianguerin Mar 8, 2024
65fd724
Fixup: Pass Prediction object to GoogleMapsGeocoder
ianguerin Mar 11, 2024
abe9666
Fixup: Add return types for JSDocs
ianguerin Mar 13, 2024
2a9560e
Update src/js/models/geocoder/GeocodedLocation.js
ianguerin Mar 13, 2024
591eebc
Update src/js/models/geocoder/GeocoderSearch.js
ianguerin Mar 13, 2024
71318e5
Update src/js/models/geocoder/GoogleMapsAutocompleter.js
ianguerin Mar 13, 2024
d9f7a67
Update src/js/models/geocoder/GoogleMapsGeocoder.js
ianguerin Mar 13, 2024
2f384d6
Update src/js/models/geocoder/Prediction.js
ianguerin Mar 13, 2024
1d7f759
Fixup: JSdoc comments and es6 class docs
ianguerin Mar 14, 2024
08838e3
Create child views managing list of Predictions
ianguerin Mar 1, 2024
6c9bc9b
Fixup: Move debounce logic out of ViewfinderModel
ianguerin Mar 9, 2024
d3e15f3
Move viewfinder related files into their own directory
ianguerin Mar 4, 2024
883aff0
Support places search in viewfinder view
ianguerin Mar 5, 2024
57b81d5
Fixup: Simplify showing predictions list logic
ianguerin Mar 11, 2024
c054806
Fixup: Hide ViewfinderView if there is not Google Maps API key
ianguerin Mar 21, 2024
6cd8c18
Fixup: Add comment explaining dependency on Google Maps API for Viewf…
ianguerin Mar 21, 2024
7d77304
Fixup: Show error when Google Maps API key does not support Geocoding…
ianguerin Mar 22, 2024
a2db923
Update src/js/models/geocoder/GoogleMapsAutocompleter.js
ianguerin Mar 25, 2024
e591e93
Update src/js/models/maps/viewfinder/ViewfinderModel.js
ianguerin Mar 25, 2024
e792a65
Fixup: Update error messages in tests
ianguerin Mar 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed docs/screenshots/views/maps/ViewfinderView.png
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 51 additions & 3 deletions src/css/map-view.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
}

/* hide the credits until we can find a better placement for them */
.cesium-widget-credits, .cesium-credit-lightbox-overlay {
.cesium-widget-credits,
.cesium-credit-lightbox-overlay {
display: none !important;
}

Expand Down Expand Up @@ -1121,7 +1122,7 @@ other class: .ui-slider-range */
*
*/

.map-help-panel{
.map-help-panel {
width: 100%;
}

Expand Down Expand Up @@ -1190,7 +1191,7 @@ other class: .ui-slider-range */
align-items: center;
}

.map-help-panel__title{
.map-help-panel__title {
text-transform: uppercase;
font-size: 0.95rem;
font-weight: 600;
Expand All @@ -1203,6 +1204,10 @@ other class: .ui-slider-range */
margin-top: 2.5rem;
}

.viewfinder {
width: 100%;
}

.viewfinder__field {
border-radius: 4px;
border: 1px solid var(--portal-col-bkg-active);
Expand All @@ -1221,6 +1226,7 @@ other class: .ui-slider-range */
flex: 1;
height: 100%;
margin: 0;
height: 48px;

&:focus {
border: none;
Expand All @@ -1244,4 +1250,46 @@ other class: .ui-slider-range */
color: var(--map-col-text);
font-size: .8rem;
padding: 4px 12px;
}

.viewfinder-predictions {
list-style: none;
margin: 0;

.viewfinder-prediction__content {
align-items: center;
background: var(--map-col-bkg);
border: 1px solid var(--portal-col-bkg-active);
border-radius: 4px;
box-sizing: border-box;
color: var(--map-col-text);
cursor: pointer;
display: flex;
height: 48px;
justify-content: flex-start;
margin: 4px 0;
padding: 12px 8px;

>* {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

i {
flex-grow: 0;
min-width: 24px;
max-width: 24px;
text-align: center;
}

&:hover {
background-color: var(--map-col-bkg-lightest);
}

&.viewfinder-prediction__focused {
background-color: var(--map-col-bkg-lighter);
}
}
}
2 changes: 1 addition & 1 deletion src/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

MetacatUI.recaptchaURL = 'https://www.google.com/recaptcha/api/js/recaptcha_ajax';
if( MetacatUI.mapKey ){
var gmapsURL = 'https://maps.googleapis.com/maps/api/js?v=3&key=' + MetacatUI.mapKey;
var gmapsURL = 'https://maps.googleapis.com/maps/api/js?v=3&libraries=places&key=' + MetacatUI.mapKey;
define('gmaps',
['async!' + gmapsURL],
function() {
Expand Down
3 changes: 3 additions & 0 deletions src/js/models/AppModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ define(['jquery', 'underscore', 'backbone'],
* Your Google Maps API key, which is used to display interactive maps on the search
* views and static maps on dataset landing pages.
* If a Google Maps API key is not specified, the maps will be omitted from the interface.
* The Google Maps API key also controls the showViewfinder feature on a Map
* and should have the Geocoding API and Places API enabled in order to
* function properly.
* Sign up for Google Maps services at https://console.developers.google.com/
* @type {string}
* @example "AIzaSyCYyHnbIokUEpMx5M61ButwgNGX8fIHUs"
Expand Down
45 changes: 45 additions & 0 deletions src/js/models/geocoder/GeocodedLocation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use strict';

define(
['backbone', 'models/maps/GeoBoundingBox'],
(Backbone, GeoBoundingBox) => {
/**
* @class GeocodedLocation
* @classdes GeocodedLocation is the representation of a place that has been
* geocoded to provide latitude and longitude bounding coordinates for
* navigating to on a map.
* @classcategory Models/Geocoder
* @since x.x.x
*/
const GeocodedLocation = Backbone.Model.extend({
/**
* Overrides the default Backbone.Model.defaults() function to specify
* default attributes.
* @name GeocodedLocation#defaults
* @type {Object}
* @property {GeoBoundingBox} box Bounding box representing this location
* on a map.
* @property {string} displayName A name that can be displayed to the user
* representing this location.
*/
defaults() {
return {
box: new GeoBoundingBox,
displayName: '',
};
},

/**
* @typedef {Object} GeocodedLocationOptions
* @property {Object} box An object representing a boundary around a
* location on a map.
* @property {string} displayName A display name for the location.
*/
initialize({ box: { north, south, east, west } = {}, displayName = '' } = {}) {
this.set('box', new GeoBoundingBox({ north, south, east, west }));
this.set('displayName', displayName);
},
});

return GeocodedLocation;
});
53 changes: 53 additions & 0 deletions src/js/models/geocoder/GeocoderSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

define(
[
'models/geocoder/GoogleMapsGeocoder',
'models/geocoder/GoogleMapsAutocompleter',
],
(GoogleMapsGeocoder, GoogleMapsAutocompleter) => {
/**
* GeocoderSearch interfaces with various geocoding and location
* searching services.
* @classcategory Models/Geocoder
* @since x.x.x
*/
class GeocoderSearch {
/**
* GoogleMapsAutocompleter model for interacting with Google Maps Places
* Autocomplete APIs.
*/
googleMapsAutocompleter = new GoogleMapsAutocompleter();

/**
* GoogleMapsGeocoder for interacting with Google Maps Geocoder APIs.
*/
googleMapsGeocoder = new GoogleMapsGeocoder();

/**
* Convert a Google Maps Place ID into a list geocoded objects that can be
* displayed in the map widget.
* @param {string} newQuery - The user's input search query.
* @returns {Prediction[]} An array of places that could be the result the
* user is looking for. Most often this comes in five or less results.
*/
async autocomplete(newQuery) {
return this.googleMapsAutocompleter.autocomplete(newQuery);
}

/**
* Convert a Google Maps Place ID into a list geocoded objects that can be
* displayed in the map widget.
* @param {Prediction} prediction An autocomplete prediction that includes
* a unique identifier for geocoding.
* @returns {GeocodedLocation[]} An array of locations with an associated
* bounding box. According to Google Maps API this should most often be a
* single value, but could potentially be many.
*/
async geocode(prediction) {
return this.googleMapsGeocoder.geocode(prediction);
}
}

return GeocoderSearch;
});
48 changes: 48 additions & 0 deletions src/js/models/geocoder/GoogleMapsAutocompleter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

define(
['backbone', 'gmaps', 'models/geocoder/Prediction'],
(Backbone, gmaps, Prediction) => {
/**
* Integrate with the Google Maps Places Autocomplete API using the
* Google Maps AutocompleteService JS library.
* @classcategory Models/Geocoder
* @since x.x.x
*/
class GoogleMapsAutocompleter {
/**
* Google Maps service for interacting with the Places Autocomplete API.
*/
autocompleter = new gmaps.places.AutocompleteService();

/**
* Use the Google Maps Places API to get place predictions based off of a
* user input string as the user types.
* @param {string} input - User input to search for Google Maps places.
* @returns {Prediction[]} An array of places that could be the result the
* user is looking for. Most often this comes in five or less results.
*/
async autocomplete(input) {
ianguerin marked this conversation as resolved.
Show resolved Hide resolved
if (!input) return [];
const response = await this.autocompleter.getPlacePredictions({
input,
});
return this.getPredictionsFromResults(response.predictions);
}

/**
* Helper function that converts a Google Maps Autocomplete API result
* into a useable Prediction model.
* @param {Object[]} List of Google Maps Autocomplete API results.
* @returns {Prediction[]} List of corresponding predictions.
*/
getPredictionsFromResults(results) {
return results.map(result => new Prediction({
description: result.description,
googleMapsPlaceId: result.place_id,
}));
}
}

return GoogleMapsAutocompleter;
});
50 changes: 50 additions & 0 deletions src/js/models/geocoder/GoogleMapsGeocoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';

define(
['backbone', 'gmaps', 'models/geocoder/GeocodedLocation'],
(Backbone, gmaps, GeocodedLocation) => {
/**
* Integrate with the Google Maps Geocoder API using the Google
* Maps Geocoder JS library.
* @classcategory Models/Geocoder
* @since x.x.x
*/
class GoogleMapsGeocoder {
/** Google Maps service for interacting with the Geocoder API. */
geocoder = new gmaps.Geocoder();

/**
* Use the Google Maps Geocoder API to convert a Google Maps Place ID into
* a geocoded object that includes latitude and longitude information
* along with a bound box for viewing the location.
* @param {Prediction} prediction An autocomplete prediction that includes
* a unique identifier for geocoding.
* @returns {GeocodedLocation[]} An array of locations with an associated
* bounding box. According to Google Maps API this should most often be a
* single value, but could potentially be many.
*/
async geocode(prediction) {
const response = await this.geocoder.geocode({
placeId: prediction.get('googleMapsPlaceId')
});
return this.getGeocodedLocationsFromResults(response.results);
}

/**
* Helper function that converts a Google Maps Places API result into a
* useable GeocodedLocation model.
* @param {Object[]} List of Google Maps Places API results.
* @returns {GeocodedLocation[]} List of corresponding geocoded locations.
*/
getGeocodedLocationsFromResults(results) {
return results.map(result => {
return new GeocodedLocation({
box: result.geometry.viewport.toJSON(),
displayName: result.address_components[0].long_name,
});
});
}
}

return GoogleMapsGeocoder;
});
40 changes: 40 additions & 0 deletions src/js/models/geocoder/Prediction.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict';

define(['backbone'], (Backbone) => {
/**
* @class Prediction
* @classdes Prediction represents a value returned from a location
* autocompletion search.
* @classcategory Models/Geocoder
* @since x.x.x
*/
const Prediction = Backbone.Model.extend({
/**
* Overrides the default Backbone.Model.defaults() function to specify
* default attributes for the Map
* @name Prediction#defaults
* @type {Object}
* @property {string} description A user-friendly description of a Google
* Maps Place.
* @property {string} googleMapsPlaceId Unique identifier that can be
* geocoded by the Google Maps Geocoder API.
*/
defaults() {
return { description: '', googleMapsPlaceId: '' };
},

/**
* @typedef {Object} PredictionOptions
* @property {string} description A string describing the location
* represented by the Prediction.
* @property {string} googleMapsPlaceId The place ID that is used to
* uniquely identify a place in Google Maps API.
*/
initialize({ description, googleMapsPlaceId, } = {}) {
this.set('description', description);
this.set('googleMapsPlaceId', googleMapsPlaceId);
},
});

return Prediction;
});
Loading
Loading