Skip to content

Instantly share code, notes, and snippets.

@symonny
Created April 29, 2016 02:15
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save symonny/0fe3c7d6a6a61ee8704410eccbec090b to your computer and use it in GitHub Desktop.
Save symonny/0fe3c7d6a6a61ee8704410eccbec090b to your computer and use it in GitHub Desktop.
/* based on https://github.com/kuhnza/angular-google-places-autocomplete */
angular.module('google.places', [])
/**
* DI wrapper around global google places library.
*
* Note: requires the Google Places API to already be loaded on the page.
*/
.factory('googlePlacesApi', ['$window', function ($window) {
if (!$window.google) throw 'Global `google` var missing. Did you forget to include the places API script?';
return $window.google;
}])
/**
* Autocomplete directive. Use like this:
*
* <textares g-places-autocomplete ng-model="myScopeVar" />
*/
.directive('gPlacesAutocomplete',
[ '$parse', '$compile', '$timeout', '$document', 'googlePlacesApi',
function ($parse, $compile, $timeout, $document, google) {
return {
restrict: 'A',
require: '^ngModel',
scope: {
model: '=ngModel',
options: '=?',
forceSelection:'=?',
nearbyCoordinates: '='
},
controller: ['$scope', function ($scope) {}],
link: function ($scope, element, attrs, controller) {
$scope.forceSelection = false;
//run nearby search if there is one location set
var keymap = {
tab: 9,
enter: 13,
esc: 27,
up: 38,
down: 40
},
hotkeys = [keymap.tab, keymap.enter, keymap.esc, keymap.up, keymap.down],
autocompleteService = new google.maps.places.AutocompleteService(),
placesService = new google.maps.places.PlacesService(element[0]),
geocoder = new google.maps.Geocoder();;
(function init() {
$scope.query = '';
$scope.predictions = [];
$scope.input = element;
$scope.options = $scope.options || {};
initAutocompleteDrawer();
initEvents();
initNgModelController();
$scope.initValue = $scope.model ? getDisplayValue($scope.model): null;
}());
function initEvents() {
element.bind('keydown', onKeydown);
// element.bind('paste', );
element.bind('blur', onBlur);
element.bind('submit', onBlur);
$scope.$watch('selected', function(newValue, oldValue){
$scope.hasToBeSet = false;
if(newValue != oldValue )
select();
});
}
function initAutocompleteDrawer() {
// Drawer element used to display predictions
var drawerElement = angular.element('<div g-places-autocomplete-drawer></div>'),
body = angular.element($document[0].body),
$drawer;
drawerElement.attr({
input: 'input',
query: 'query',
predictions: 'predictions',
active: 'active',
selected: 'selected'
});
$drawer = $compile(drawerElement)($scope);
//body.append($drawer); // Append to DOM
$scope.input.after($drawer);
}
function initNgModelController() {
controller.$parsers.push(parse);
controller.$formatters.push(format);
controller.$render = function(){
return element.val(controller.$viewValue);
};
}
function onKeydown(event) {
if ($scope.predictions.length === 0 || indexOf(hotkeys, event.which) === -1) {
return;
}
event.preventDefault();
switch (event.which) {
case keymap.down:
$scope.active = ($scope.active + 1) % $scope.predictions.length;
$scope.$digest();
break;
case keymap.up:
$scope.active = ($scope.active ? $scope.active : $scope.predictions.length) - 1;
$scope.$digest();
break;
case keymap.enter:
case keymap.tab :
if ($scope.forceSelection) {
$scope.active = ($scope.active === -1) ? 0 : $scope.active;
}
$scope.$apply(function () {
$scope.selected = $scope.active;
if ($scope.selected === -1) {
clearPredictions();
}
});
break;
case keymap.esc:
event.stopPropagation();
clearPredictions();
$scope.$digest();
break;
}
}
function getaddress(){
var newVal = element.val();
if($scope.initValue != newVal) {
var phase = $scope.$root.$$phase;
geolocate(newVal, phase);
}
}
function onBlur(event) {
if ($scope.predictions.length === 0) {
getaddress();
return;
}
if ($scope.forceSelection) {
$scope.selected = ($scope.selected === -1) ? 0 : $scope.selected;
}
$scope.$digest();
$scope.$apply(function () {
if ($scope.selected === -1) {
clearPredictions();
getaddress();
}
});
}
function geolocate(address, phase){
if(address) {
geocoder.geocode({'address': address}, function (results, status) {
if (status == google.maps.GeocoderStatus.OK) {
if (results && results.length > 0) {
var res = results[0];
if (res.place_id) setNewPlace(res.place_id);
else updateModel(res, phase);
}
} else {
console.log("Geocode for '" + address + "' was not successful: " + status);
setValue({formatted_address: address}, phase);
}
});
}else setValue(null, phase);
}
function setNewPlace(placeId, description, onsuccess){
placesService.getDetails({ placeId: placeId }, function (place, status) {
if (status == google.maps.places.PlacesServiceStatus.OK) {
place.description = description;
updateModel(place);
}
if(typeof(onsuccess)=='function')onsuccess();
});
}
function getPlaceInfo (place){
var newAddress = '';
if(place) {
var address = place.formatted_address;
if(place.name && place.formatted_address.indexOf(place.name) == 0){
address = place.formatted_address.substring(place.name.length + 1, place.formatted_address.length)
}
newAddress={
name: place.name,
formatted_address: address,
url: place.url,
phone:place.formatted_phone_number
};
if(place.geometry && place.geometry.location){
newAddress.geoloc = {
lat: place.geometry.location.lat(),
lng: place.geometry.location.lng()
}
}
for(var prop in place.address_components){
var valprop = place.address_components[prop];
var val = valprop.types[0];
var name = valprop.short_name;
if(val == 'postal_code'){
newAddress.zipcode = name;
}else if(val == 'locality'){
newAddress.locality = name;
}else if(val == 'administrative_area_level_1'){
newAddress.state = name;
}else if(val == 'country'){
newAddress.country = name;
}
}
}
return newAddress;
}
function getDisplayValue (val){
var init = null;
if (val){
if(typeof (val) == 'object') {
if (val.name) init = val.name;
if (val.formatted_address) {
init = init ? (init + ', ' + val.formatted_address) : val.formatted_address;
}
}else init = val;
}
return init;
}
function setValue (val, phase){
var setval = function() {
$scope.model = val;
$scope.initValue = getDisplayValue(val);
$scope.$emit('g-places-autocomplete:select', val);
};
if(phase == '$apply' || phase == '$digest') {
setval();
} else {
$scope.$apply(setval);
}
}
function updateModel(val, phase){
var newplace = getPlaceInfo(val);
setValue(newplace, phase);
}
var isSelected = false;
function select() {
var prediction = $scope.predictions[$scope.selected];
if (!prediction) return;
if(!isSelected) {
isSelected = true;
setNewPlace(prediction.place_id, prediction.description, function(){
isSelected = false;
clearPredictions();
});
}
}
function distance(lat1, lon1, lat2, lon2, unit) {
var radlat1 = Math.PI * lat1/180 ;
var radlat2 = Math.PI * lat2/180 ;
var theta = lon1-lon2 ;
var radtheta = Math.PI * theta/180;
var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
dist = Math.acos(dist) ;
dist = dist * 180/Math.PI ;
dist = dist * 60 * 1.1515 ;
if (unit=="K") { dist = dist * 1.609344 }
if (unit=="N") { dist = dist * 0.8684 }
return dist ;
}
function parse(viewValue) {
var request;
if (!(viewValue && isString(viewValue))) return viewValue;
$scope.query = viewValue;
request = angular.extend({ input: viewValue }, $scope.options);
autocompleteService.getPlacePredictions(request, function (predictions, status) {
$scope.$apply(function () {
var customPlacePredictions;
/* if($scope.options.location) {
var lat = $scope.options.location.lat();
var lng = $scope.options.location.lng();
for (var i = 0; i < predictions.length; i++) {
//to do
var lat1 = predictions[i];
var lat1 = predictions[i];
predictions[i].distance = distance(predictions[i], predictions[i], lat, lng, 'N') + 'mi';
}
}*/
clearPredictions();
if (status == google.maps.places.PlacesServiceStatus.OK) {
$scope.predictions.push.apply($scope.predictions, predictions);
}
if ($scope.predictions.length > 5) {
$scope.predictions.length = 5; // trim predictions down to size
}
});
});
return viewValue;
}
function format(modelValue) {
var viewValue = "";
if (isString(modelValue)) {
viewValue = modelValue;
} else if (isObject(modelValue)) {
viewValue = getDisplayValue(modelValue);
}
return viewValue;
}
function clearPredictions() {
$scope.active = -1;
$scope.selected = -1;
$scope.predictions.length = 0;
}
function isString(val) {
return Object.prototype.toString.call(val) == '[object String]';
}
function isObject(val) {
return Object.prototype.toString.call(val) == '[object Object]';
}
function indexOf(array, item) {
var i, length;
if (array == null) return -1;
length = array.length;
for (i = 0; i < length; i++) {
if (array[i] === item) return i;
}
return -1;
}
function startsWith(string1, string2) {
return toLower(string1).lastIndexOf(toLower(string2), 0) === 0;
}
function toLower(string) {
return (string == null) ? "" : string.toLowerCase();
}
}
}
}
])
.directive('gPlacesAutocompleteDrawer', ['$window', '$document', function ($window, $document) {
var TEMPLATE = [
'<div class="pac-container" ng-if="isOpen()" ' +
// 'ng-style="{top: position.top+\'px\', left: position.left+\'px\', width: position.width+\'px\'}" ' +
'style="display: block;" role="listbox" aria-hidden="{{!isOpen()}}">',
' <div class="pac-item" g-places-autocomplete-prediction index="$index" prediction="prediction" query="query"',
' ng-repeat="prediction in predictions track by $index" ng-class="{\'pac-item-selected\': isActive($index) }"',
' ng-mouseenter="selectActive($index)" ng-click="selectPrediction($index)" role="option" id="{{prediction.id}}">',
' </div>',
'</div>'
];
return {
restrict: 'A',
scope:{
input: '=',
query: '=',
predictions: '=',
active: '=',
selected: '='
},
template: TEMPLATE.join(''),
link: function ($scope, element) {
element.bind('mousedown', function (event) {
event.preventDefault(); // prevent blur event from firing when clicking selection
});
$scope.isOpen = function () {
return $scope.predictions.length > 0;
};
$scope.isActive = function (index) {
return $scope.active === index;
};
$scope.selectActive = function (index) {
$scope.active = index;
};
$scope.selectPrediction = function (index) {
$scope.selected = index;
};
$scope.$watch('predictions', function () {
$scope.position = getDrawerPosition($scope.input);
}, true);
function getDrawerPosition(element) {
var domEl = element[0],
rect = domEl.getBoundingClientRect(),
docEl = $document[0].documentElement,
body = $document[0].body,
scrollTop = $window.pageYOffset || docEl.scrollTop || body.scrollTop,
scrollLeft = $window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
return {
width: rect.width,
height: rect.height,
top: rect.top + rect.height + scrollTop,
left: rect.left + scrollLeft
};
}
}
}
}])
.directive('gPlacesAutocompletePrediction', [function () {
var TEMPLATE = [
'<span class="pac-icon pac-icon-marker"></span>',
'<span class="pac-item-query" ng-bind-html="prediction | highlightMatched"></span>',
'<span ng-repeat="term in prediction.terms | unmatchedTermsOnly:prediction">{{term.value | trailingComma:!$last}}&nbsp;</span>',
'<span ng-if="prediction.distance" class="distance">&nbsp;{{prediction.distance}}</span>'
];
return {
restrict: 'A',
scope:{
index:'=',
prediction:'=',
query:'='
},
template: TEMPLATE.join('')
}
}])
.filter('highlightMatched', ['$sce', function ($sce) {
return function (prediction) {
var matchedPortion = '',
unmatchedPortion = '',
matched;
if (prediction.matched_substrings.length > 0 && prediction.terms.length > 0) {
matched = prediction.matched_substrings[0];
matchedPortion = prediction.terms[0].value.substr(matched.offset, matched.length);
unmatchedPortion = prediction.terms[0].value.substr(matched.offset + matched.length);
}
return $sce.trustAsHtml('<span class="pac-matched">' + matchedPortion + '</span>' + unmatchedPortion);
}
}])
.filter('unmatchedTermsOnly', [function () {
return function (terms, prediction) {
var i, term, filtered = [];
for (i = 0; i < terms.length; i++) {
term = terms[i];
if (prediction.matched_substrings.length > 0 && term.offset > prediction.matched_substrings[0].length) {
filtered.push(term);
}
}
return filtered;
}
}])
.filter('trailingComma', [function () {
return function (input, condition) {
return (condition) ? input + ',' : input;
}
}]);
@symonny
Copy link
Author

symonny commented Apr 29, 2016

  • searches for an address in google places
  • fills in an address based on selected google places address and attaches the google places locations to the model
  • if no address is selected from google places on blur it searches if exists any google place matching the address.
    • if any address exists attaches that geolocated address to the model.
    • otherwise attaches the entered text to the model

sample usage:
screen shot 2016-04-28 at 7 18 15 pm

screen shot 2016-04-28 at 7 18 55 pm

- potential improvements: search places nearby a specific location given as param

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment