Skip to content

Commit

Permalink
Merge pull request #2314 from NCEAS/feature-1796-places-autocomplete
Browse files Browse the repository at this point in the history
Feature 1796 places autocomplete
  • Loading branch information
robyngit authored Mar 25, 2024
2 parents 697db60 + e792a65 commit e3f0efa
Show file tree
Hide file tree
Showing 43 changed files with 2,333 additions and 495 deletions.
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 @@ -98,7 +98,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 @@ -1213,7 +1214,7 @@ other class: .ui-slider-range */
*
*/

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

Expand Down Expand Up @@ -1294,7 +1295,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 @@ -1307,6 +1308,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 @@ -1325,6 +1330,7 @@ other class: .ui-slider-range */
flex: 1;
height: 100%;
margin: 0;
height: 48px;

&:focus {
border: none;
Expand All @@ -1348,4 +1354,46 @@ other class: .ui-slider-range */
color: var(--map-col-text__deprecate);
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) {
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

0 comments on commit e3f0efa

Please sign in to comment.