Created
May 11, 2015 10:48
-
-
Save astrotim/ce7bc2fcc433df4824bf to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Copyright 2012 Google Inc. | |
/** | |
* @author Chris Broadfoot (Google) | |
* @fileoverview | |
* An info panel, which complements the map view of the Store Locator. | |
* Provides a list of stores, location search, feature filter, and directions. | |
*/ | |
/** | |
* Licensed under the Apache License, Version 2.0 (the "License"); | |
* you may not use this file except in compliance with the License. | |
* You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
/** | |
* An info panel, to complement the map view. | |
* Provides a list of stores, location search, feature filter, and directions. | |
* @example <pre> | |
* var container = document.getElementById('panel'); | |
* var panel = new storeLocator.Panel(container, { | |
* view: view, | |
* locationSearchLabel: 'Location:' | |
* }); | |
* google.maps.event.addListener(panel, 'geocode', function(result) { | |
* geocodeMarker.setPosition(result.geometry.location); | |
* }); | |
* </pre> | |
* @extends {google.maps.MVCObject} | |
* @param {!Node} el the element to contain this panel. | |
* @param {storeLocator.PanelOptions} opt_options | |
* @constructor | |
* @implements storeLocator_Panel | |
*/ | |
storeLocator.Panel = function(el, opt_options) { | |
this.el_ = $(el); | |
this.el_.addClass('storelocator-panel'); | |
this.settings_ = $.extend({ | |
'locationSearch': true, | |
'locationSearchLabel': 'Postcode or location name', | |
'featureFilter': true, | |
'directions': true, | |
'view': null | |
}, opt_options); | |
this.directionsRenderer_ = new google.maps.DirectionsRenderer({ | |
draggable: true | |
}); | |
this.directionsService_ = new google.maps.DirectionsService; | |
this.init_(); | |
}; | |
storeLocator['Panel'] = storeLocator.Panel; | |
storeLocator.Panel.prototype = new google.maps.MVCObject; | |
/** | |
* Initialise the info panel | |
* @private | |
*/ | |
storeLocator.Panel.prototype.init_ = function() { | |
var that = this; | |
this.itemCache_ = {}; | |
var headerFormInput = window.headerSearchString; | |
if (this.settings_['view']) { | |
this.set('view', this.settings_['view']); | |
} | |
this.filter_ = $('<form class="storelocator-filter"/>'); | |
this.el_.append(this.filter_); | |
if (this.settings_['locationSearch']) { | |
this.locationSearch_ = $('<div class="locations-list-search"><label for="location-filter">' + | |
this.settings_['locationSearchLabel'] + '</label><input placeholder="Postcode or location name..." id="location-filter" class="locations-input form-control"><button type="submit" class="button btn-blue">Go</button></div>'); | |
this.filter_.append(this.locationSearch_); | |
if (typeof google.maps.places != 'undefined') { | |
this.initAutocomplete_(); | |
console.log("typeof google.maps.places != 'undefined'"); | |
} else { | |
this.filter_.submit(function() { | |
var search = $('input', that.locationSearch_).val(); | |
that.searchPosition(/** @type {string} */(search)); | |
}); | |
} | |
this.filter_.submit(function() { | |
return false; | |
}); | |
google.maps.event.addListener(this, 'geocode', function(place) { | |
if (!place.geometry) { | |
that.searchPosition(place.name); | |
return; | |
} | |
this.directionsFrom_ = place.geometry.location; | |
if (that.directionsVisible_) { | |
that.renderDirections_(); | |
} | |
var sl = that.get('view'); | |
sl.highlight(null); | |
var map = sl.getMap(); | |
if (place.geometry.viewport) { | |
map.fitBounds(place.geometry.viewport); | |
map.setZoom(11) | |
} else { | |
map.setCenter(place.geometry.location); | |
map.setZoom(10); // original 13 | |
console.log("map.setCenter(place.geometry.location)", "map.setZoom(13)") | |
} | |
sl.refreshView(); | |
that.listenForStoresUpdate_(); | |
}); | |
} | |
if(headerFormInput) { | |
// var searchValue; | |
// if( parseInt(headerFormInput) != 'Nan' && headerFormInput.length == 3) { | |
// searchValue = '0' + headerFormInput; | |
// } | |
that.searchPosition(headerFormInput); | |
$('input', that.locationSearch_).val(headerFormInput); | |
} | |
if (this.settings_['featureFilter']) { | |
// TODO(cbro): update this on view_changed | |
this.featureFilter_ = $('<div class="feature-filter"/>'); | |
var allFeatures = this.get('view').getFeatures().asList(); | |
for (var i = 0, ii = allFeatures.length; i < ii; i++) { | |
var feature = allFeatures[i]; | |
var checkbox = $('<input type="checkbox"/>'); | |
checkbox.data('feature', feature); | |
$('<label/>').append(checkbox).append(feature.getDisplayName()) | |
.appendTo(this.featureFilter_); | |
} | |
this.filter_.append(this.featureFilter_); | |
this.featureFilter_.find('input').change(function() { | |
var feature = $(this).data('feature'); | |
that.toggleFeatureFilter_(/** @type {storeLocator.Feature} */(feature)); | |
that.get('view').refreshView(); | |
}); | |
} | |
this.storeList_ = $('<ul class="store-list"/>'); | |
this.el_.append(this.storeList_); | |
if (this.settings_['directions']) { | |
this.directionsPanel_ = $('<div class="directions-panel"><form>' + | |
'<input class="directions-to"/>' + | |
'<input type="submit" value="Find directions"/>' + | |
'<a href="#" class="close-directions">Close</a>' + | |
'</form><div class="rendered-directions"></div></div>'); | |
this.directionsPanel_.find('.directions-to').attr('readonly', 'readonly'); | |
this.directionsPanel_.hide(); | |
this.directionsVisible_ = false; | |
this.directionsPanel_.find('form').submit(function() { | |
that.renderDirections_(); | |
return false; | |
}); | |
this.directionsPanel_.find('.close-directions').click(function() { | |
that.hideDirections(); | |
}); | |
this.el_.append(this.directionsPanel_); | |
} | |
}; | |
/** | |
* Toggles a particular feature on/off in the feature filter. | |
* @param {storeLocator.Feature} feature The feature to toggle. | |
* @private | |
*/ | |
storeLocator.Panel.prototype.toggleFeatureFilter_ = function(feature) { | |
var featureFilter = this.get('featureFilter'); | |
featureFilter.toggle(feature); | |
this.set('featureFilter', featureFilter); | |
}; | |
/** | |
* Global Geocoder instance, for convenience. | |
* @type {google.maps.Geocoder} | |
* @private | |
*/ | |
storeLocator.geocoder_ = new google.maps.Geocoder; | |
/** | |
* Triggers an update for the store list in the Panel. Will wait for stores | |
* to load asynchronously from the data source. | |
* @private | |
*/ | |
storeLocator.Panel.prototype.listenForStoresUpdate_ = function() { | |
var that = this; | |
var view = /** @type storeLocator.View */(this.get('view')); | |
if (this.storesChangedListener_) { | |
google.maps.event.removeListener(this.storesChangedListener_); | |
} | |
this.storesChangedListener_ = google.maps.event.addListenerOnce(view, | |
'stores_changed', function() { | |
that.set('stores', view.get('stores')); | |
}); | |
}; | |
/** | |
* Search and pan to the specified address. | |
* @param {string} searchText the address to pan to. | |
*/ | |
storeLocator.Panel.prototype.searchPosition = function(searchText) { | |
var log = 0; // change to 1 to show console.log messages | |
// use underscore to check searchText against store data | |
var check = function (thelist, props) { | |
var pnames = _.keys(props); | |
return _.find(thelist, function (obj) { | |
return _.every(pnames, function (pname) { | |
return obj[pname] == props[pname]; | |
}); | |
}); | |
}; | |
if(log) console.log('search text:', searchText); | |
// use regex to check for 3 or 4 digit postcode | |
var is3or4DigitNumber = /^[\d]{3,4}$/; | |
var is3DigitNumber = /^[\d]{3}$/; | |
var searchValue = searchText, | |
suburb = null, | |
postcode = null; | |
// is the search input a number? | |
if( is3or4DigitNumber.test(searchText) ) { | |
if(log) console.log('looks like a postcode'); | |
// search store data for postcode | |
var searchPostcode = {"postcode":searchText}; | |
var searchResult = check(ruralcoData.stores, searchPostcode); | |
if(typeof(searchResult) != 'undefined') { | |
if(log) console.log(searchResult); | |
searchValue = searchResult.address + ', ' + searchResult.town + ' ' + searchResult.state + ' ' + searchResult.postcode; | |
// searchValue = searchResult.lat + ',' + searchResult.lng; | |
if(log) console.log('matched address:', searchValue); | |
} else { | |
if(log) console.log('no postcode match was found'); | |
//check if its a 3 digit postcode | |
if(is3DigitNumber.test(searchText)) { | |
if(log) console.log('its a three digit number'); | |
searchValue = '0' + searchText; | |
} | |
} | |
// town name | |
} else { | |
if(log) console.log('could be a town name'); | |
String.prototype.toProperCase = function () { | |
return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); | |
}; | |
// search store data for town | |
var searchTown = {"town":searchText.toProperCase()}; | |
var searchResult = check(ruralcoData.stores, searchTown); | |
if(typeof(searchResult) != 'undefined') { | |
if(log) console.log(searchResult); | |
searchValue = searchResult.address + ', ' + searchResult.town + ' ' + searchResult.state + ' ' + searchResult.postcode; | |
if(log) console.log('matched address:', searchValue); | |
} else { | |
if(log) console.log('no town match was found'); | |
} | |
} | |
var that = this; | |
var request = { | |
address: searchValue, | |
componentRestrictions: { | |
country: 'AU' | |
}, | |
bounds: this.get('view').getMap().getBounds() | |
}; | |
storeLocator.geocoder_.geocode(request, function(result, status) { | |
if (status != google.maps.GeocoderStatus.OK) { | |
//TODO(cbro): proper error handling | |
return; | |
} | |
google.maps.event.trigger(that, 'geocode', result[0]); | |
if(log) console.log('formatted address', result[0].formatted_address); | |
}); | |
}; | |
/** | |
* Sets the associated View. | |
* @param {storeLocator.View} view the view to set. | |
*/ | |
storeLocator.Panel.prototype.setView = function(view) { | |
this.set('view', view); | |
}; | |
/** | |
* view_changed handler. | |
* Sets up additional bindings between the info panel and the map view. | |
*/ | |
storeLocator.Panel.prototype.view_changed = function() { | |
var sl = /** @type {google.maps.MVCObject} */ (this.get('view')); | |
this.bindTo('selectedStore', sl); | |
var that = this; | |
if (this.geolocationListener_) { | |
google.maps.event.removeListener(this.geolocationListener_); | |
} | |
if (this.zoomListener_) { | |
google.maps.event.removeListener(this.zoomListener_); | |
} | |
if (this.idleListener_) { | |
google.maps.event.removeListener(this.idleListener_); | |
} | |
var center = sl.getMap().getCenter(); | |
var updateList = function() { | |
sl.clearMarkers(); | |
that.listenForStoresUpdate_(); | |
}; | |
//TODO(cbro): somehow get the geolocated position and populate the 'from' box. | |
this.geolocationListener_ = google.maps.event.addListener(sl, 'load', | |
updateList); | |
this.zoomListener_ = google.maps.event.addListener(sl.getMap(), | |
'zoom_changed', updateList); | |
this.idleListener_ = google.maps.event.addListener(sl.getMap(), | |
'idle', function() { | |
return that.idle_(sl.getMap()); | |
}); | |
updateList(); | |
this.bindTo('featureFilter', sl); | |
if (this.autoComplete_) { | |
this.autoComplete_.bindTo('bounds', sl.getMap()); | |
} | |
}; | |
/** | |
* Adds autocomplete to the input box. | |
* @private | |
*/ | |
storeLocator.Panel.prototype.initAutocomplete_ = function() { | |
var that = this; | |
var input = $('input', this.locationSearch_)[0]; | |
this.autoComplete_ = new google.maps.places.Autocomplete(input); | |
if (this.get('view')) { | |
this.autoComplete_.bindTo('bounds', this.get('view').getMap()); | |
} | |
google.maps.event.addListener(this.autoComplete_, 'place_changed', | |
function() { | |
google.maps.event.trigger(that, 'geocode', this.getPlace()); | |
}); | |
}; | |
/** | |
* Called on the view's map idle event. Refreshes the store list if the | |
* user has navigated far away enough. | |
* @param {google.maps.Map} map the current view's map. | |
* @private | |
*/ | |
storeLocator.Panel.prototype.idle_ = function(map) { | |
if (!this.center_) { | |
this.center_ = map.getCenter(); | |
} else if (!map.getBounds().contains(this.center_)) { | |
this.center_ = map.getCenter(); | |
this.listenForStoresUpdate_(); | |
} | |
}; | |
/** | |
* @const | |
* @type {string} | |
* @private | |
*/ | |
storeLocator.Panel.NO_STORES_HTML_ = '<li class="no-stores">There are no' + | |
' stores in this area.</li>'; | |
/** | |
* @const | |
* @type {string} | |
* @private | |
*/ | |
storeLocator.Panel.NO_STORES_IN_VIEW_HTML_ = '<li class="no-stores">There are' + | |
' no stores in this area. However, stores closest to you are' + | |
' listed below.</li>'; | |
/** | |
* Handler for stores_changed. Updates the list of stores. | |
* @this storeLocator.Panel | |
*/ | |
storeLocator.Panel.prototype.stores_changed = function() { | |
if (!this.get('stores')) { | |
return; | |
} | |
var view = this.get('view'); | |
var bounds = view && view.getMap().getBounds(); | |
var that = this; | |
var stores = this.get('stores'); | |
var selectedStore = this.get('selectedStore'); | |
this.storeList_.empty(); | |
if (!stores.length) { | |
this.storeList_.append(storeLocator.Panel.NO_STORES_HTML_); | |
} else if (bounds && !bounds.contains(stores[0].getLocation())) { | |
this.storeList_.append(storeLocator.Panel.NO_STORES_IN_VIEW_HTML_); | |
} | |
var clickHandler = function() { | |
view.highlight(this['store'], true); | |
}; | |
// TODO(cbro): change 10 to a setting/option | |
for (var i = 0, ii = Math.min(10, stores.length); i < ii; i++) { | |
var storeLi = stores[i].getInfoPanelItem(); | |
storeLi['store'] = stores[i]; | |
if (selectedStore && stores[i].getId() == selectedStore.getId()) { | |
$(storeLi).addClass('highlighted'); | |
} | |
if (!storeLi.clickHandler_) { | |
storeLi.clickHandler_ = google.maps.event.addDomListener( | |
storeLi, 'click', clickHandler); | |
} | |
that.storeList_.append(storeLi); | |
} | |
}; | |
/** | |
* Handler for selectedStore_changed. Highlights the selected store in the | |
* store list. | |
* @this storeLocator.Panel | |
*/ | |
storeLocator.Panel.prototype.selectedStore_changed = function() { | |
$('.highlighted', this.storeList_).removeClass('highlighted'); | |
var that = this; | |
var store = this.get('selectedStore'); | |
if (!store) { | |
return; | |
} | |
this.directionsTo_ = store; | |
this.storeList_.find('#store-' + store.getId()).addClass('highlighted'); | |
if (this.settings_['directions']) { | |
this.directionsPanel_.find('.directions-to') | |
.val(store.getDetails().title); | |
} | |
var node = that.get('view').getInfoWindow().getContent(); | |
var directionsLink = $('<a/>') | |
.text('Directions') | |
.attr('href', '#') | |
.addClass('action') | |
.addClass('directions'); | |
// TODO(cbro): Make these two permanent fixtures in InfoWindow. | |
// Move out of Panel. | |
var zoomLink = $('<a/>') | |
.text('Zoom here') | |
.attr('href', '#') | |
.addClass('action') | |
.addClass('zoomhere'); | |
var streetViewLink = $('<a/>') | |
.text('Street view') | |
.attr('href', '#') | |
.addClass('action') | |
.addClass('streetview'); | |
directionsLink.click(function() { | |
that.showDirections(); | |
return false; | |
}); | |
zoomLink.click(function(e) { | |
e.preventDefault(); | |
that.get('view').getMap().setOptions({ | |
center: store.getLocation(), | |
zoom: 17 | |
}); | |
}); | |
streetViewLink.click(function() { | |
var streetView = that.get('view').getMap().getStreetView(); | |
streetView.setPosition(store.getLocation()); | |
streetView.setVisible(true); | |
}); | |
$(node).append(directionsLink).append(zoomLink).append(streetViewLink); | |
}; | |
/** | |
* Hides the directions panel. | |
*/ | |
storeLocator.Panel.prototype.hideDirections = function() { | |
this.directionsVisible_ = false; | |
this.directionsPanel_.fadeOut(); | |
this.featureFilter_.fadeIn(); | |
this.storeList_.fadeIn(); | |
this.directionsRenderer_.setMap(null); | |
}; | |
/** | |
* Shows directions to the selected store. | |
*/ | |
storeLocator.Panel.prototype.showDirections = function() { | |
var store = this.get('selectedStore'); | |
this.featureFilter_.fadeOut(); | |
this.storeList_.fadeOut(); | |
this.directionsPanel_.find('.directions-to').val(store.getDetails().title); | |
this.directionsPanel_.fadeIn(); | |
this.renderDirections_(); | |
this.directionsVisible_ = true; | |
}; | |
/** | |
* Renders directions from the location in the input box, to the store that is | |
* pre-filled in the 'to' box. | |
* @private | |
*/ | |
storeLocator.Panel.prototype.renderDirections_ = function() { | |
var that = this; | |
if (!this.directionsFrom_ || !this.directionsTo_) { | |
return; | |
} | |
var rendered = this.directionsPanel_.find('.rendered-directions').empty(); | |
this.directionsService_.route({ | |
origin: this.directionsFrom_, | |
destination: this.directionsTo_.getLocation(), | |
travelMode: google.maps['DirectionsTravelMode'].DRIVING | |
//TODO(cbro): region biasing, waypoints, travelmode | |
}, function(result, status) { | |
if (status != google.maps.DirectionsStatus.OK) { | |
// TODO(cbro): better error handling | |
return; | |
} | |
var renderer = that.directionsRenderer_; | |
renderer.setPanel(rendered[0]); | |
renderer.setMap(that.get('view').getMap()); | |
renderer.setDirections(result); | |
}); | |
}; | |
/** | |
* featureFilter_changed event handler. | |
*/ | |
storeLocator.Panel.prototype.featureFilter_changed = function() { | |
this.listenForStoresUpdate_(); | |
}; | |
/** | |
* Fired when searchPosition has been called. This happens when the user has | |
* searched for a location from the location search box and/or autocomplete. | |
* @name storeLocator.Panel#event:geocode | |
* @param {google.maps.PlaceResult|google.maps.GeocoderResult} result | |
* @event | |
*/ | |
/** | |
* Fired when the <code>Panel</code>'s <code>view</code> property changes. | |
* @name storeLocator.Panel#event:view_changed | |
* @event | |
*/ | |
/** | |
* Fired when the <code>Panel</code>'s <code>featureFilter</code> property | |
* changes. | |
* @name storeLocator.Panel#event:featureFilter_changed | |
* @event | |
*/ | |
/** | |
* Fired when the <code>Panel</code>'s <code>stores</code> property changes. | |
* @name storeLocator.Panel#event:stores_changed | |
* @event | |
*/ | |
/** | |
* Fired when the <code>Panel</code>'s <code>selectedStore</code> property | |
* changes. | |
* @name storeLocator.Panel#event:selectedStore_changed | |
* @event | |
*/ | |
/** | |
* @example see storeLocator.Panel | |
* @interface | |
*/ | |
storeLocator.PanelOptions = function() {}; | |
/** | |
* Whether to show the location search box. Default is true. | |
* @type boolean | |
*/ | |
storeLocator.prototype.locationSearch; | |
/** | |
* The label to show above the location search box. Default is "Where are you | |
* now?". | |
* @type string | |
*/ | |
storeLocator.PanelOptions.prototype.locationSearchLabel; | |
/** | |
* Whether to show the feature filter picker. Default is true. | |
* @type boolean | |
*/ | |
storeLocator.PanelOptions.prototype.featureFilter; | |
/** | |
* Whether to provide directions. Deafult is true. | |
* @type boolean | |
*/ | |
storeLocator.PanelOptions.prototype.directions; | |
/** | |
* The store locator model to bind to. | |
* @type storeLocator.View | |
*/ | |
storeLocator.PanelOptions.prototype.view; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment