diff --git a/crouton.php b/crouton.php index 2e761c9..4a170eb 100644 --- a/crouton.php +++ b/crouton.php @@ -258,7 +258,15 @@ public function enqueueFrontendFiles() wp_enqueue_style("croutoncss", plugin_dir_url(__FILE__) . "croutonjs/dist/crouton.min.css", false, filemtime(plugin_dir_path(__FILE__) . "croutonjs/dist/crouton.min.css"), false); wp_enqueue_script("croutonjs", plugin_dir_url(__FILE__) . "croutonjs/dist/$jsfilename", array('jquery'), filemtime(plugin_dir_path(__FILE__) . "croutonjs/dist/$jsfilename"), true); if ($this->embeddedMap) { - do_action('crouton_map_enqueue_scripts'); + if (has_action('crouton_map_enqueue_scripts')) { + do_action('crouton_map_enqueue_scripts'); + } else { + $jsfilename = (isset($_GET['croutonjsdebug']) ? "crouton-map.js" : "crouton-map.min.js"); + wp_enqueue_script("croutonmapjs", plugin_dir_url(__FILE__) . "croutonjs/dist/$jsfilename", array('croutonjs'), filemtime(plugin_dir_path(__FILE__) . "croutonjs/dist/$jsfilename"), true); + } + } else { + $jsfilename = (isset($_GET['croutonjsdebug']) ? "crouton-map.js" : "crouton-map.min.js"); + wp_enqueue_script("croutonmapjs", plugin_dir_url(__FILE__) . "croutonjs/dist/$jsfilename", array('croutonjs'), filemtime(plugin_dir_path(__FILE__) . "croutonjs/dist/$jsfilename"), true); } } } @@ -379,15 +387,15 @@ public function croutonMap($atts, $content = null) return sprintf('%s
%s
', $this->sharedRender(), $this->renderMap($atts)); } - public function getInitializeCroutonBlock($config = array()) + public function getInitializeCroutonBlock($config, $mapConfig) { if (!$this->croutonBlockInitialized) { $this->croutonBlockInitialized = true; - $externalMap = ""; + $externalMap = "croutonMap = new CroutonMap($mapConfig);"; if ($this->embeddedMap) { $externalMap = apply_filters( "crouton_map_create_control", - "", + $externalMap, isset($config['language']) ? substr($config['language'], 0, 2) : 'en', "crouton_external_map" ); @@ -400,7 +408,7 @@ public function getInitializeCroutonBlock($config = array()) public function renderTable($atts) { - return $this->getInitializeCroutonBlock($this->getCroutonJsConfig($atts)) . ""; + return $this->getInitializeCroutonBlock(...$this->getCroutonJsConfig($atts)) . ""; } public function renderMap($atts) @@ -431,30 +439,30 @@ public function renderMap($atts) ? boolval($atts['map_search_coordinates_search']) : $this->shortCodeOptions['map_search_coordinates_search'] ]; - return $this->getInitializeCroutonBlock($this->getCroutonJsConfig($atts)); + return $this->getInitializeCroutonBlock(...$this->getCroutonJsConfig($atts)); } public function initCrouton($atts) { - return $this->getInitializeCroutonBlock($this->getCroutonJsConfig($atts)); + return $this->getInitializeCroutonBlock(...$this->getCroutonJsConfig($atts)); } public function meetingCount($atts) { $random_id = rand(10000, 99999); - return $this->getInitializeCroutonBlock($this->getCroutonJsConfig($atts)) . ""; + return $this->getInitializeCroutonBlock(...$this->getCroutonJsConfig($atts)) . ""; } public function groupCount($atts) { $random_id = rand(10000, 99999); - return $this->getInitializeCroutonBlock($this->getCroutonJsConfig($atts)) . ""; + return $this->getInitializeCroutonBlock(...$this->getCroutonJsConfig($atts)) . ""; } public function serviceBodyNames($atts) { $random_id = rand(10000, 99999); - return $this->getInitializeCroutonBlock($this->getCroutonJsConfig($atts)) . ""; + return $this->getInitializeCroutonBlock(...$this->getCroutonJsConfig($atts)) . ""; } public function handlebarFooter() { @@ -470,7 +478,7 @@ public function handlebarFooter() var crouton; jQuery(document).ready(function() { - crouton = new Crouton(); + crouton = new Crouton(); crouton.doHandlebars(); }); @@ -588,7 +596,6 @@ public function adminOptionsPage() $this->options['extra_meetings'] = isset($_POST['extra_meetings']) ? $_POST['extra_meetings'] : array(); $this->options['extra_meetings_enabled'] = isset($_POST['extra_meetings_enabled']) ? intval($_POST['extra_meetings_enabled']) : "0"; $this->options['google_api_key'] = $_POST['google_api_key']; - $this->options['google_api_key'] = $_POST['google_api_key']; $this->options['disable_tinyMCE'] = isset($_POST['disable_tinyMCE']); $create_message = $this->createMeetingDetailsPage('meeting_details_href', "Meeting Details"); $create_message .= $this->createMeetingDetailsPage('virtual_meeting_details_href', "Virtual_Meeting Details"); @@ -964,9 +971,12 @@ public function getAllMeetings($root_server) } return $all_meetings; } - public function getCroutonJsConfig($atts) { + $mapParams = []; + if (isset($atts['map_search'])) { + $mapParams['map_search'] = $atts['map_search']; + } // Pulling simple values from options $defaults = $this->shortCodeOptions; foreach ($defaults as $key => $value) { @@ -1072,7 +1082,8 @@ public function getCroutonJsConfig($atts) } $params['metadata_template'] = html_entity_decode($metadata_template); - $params['google_api_key'] = $this->options['google_api_key']; + $mapParams['google_api_key'] = $this->options['google_api_key']; + $mapParams['template_path'] = $params['template_path']; $extra_meetings_array = []; if (isset($this->options['extra_meetings'])) { foreach ($this->options['extra_meetings'] as $value) { @@ -1087,7 +1098,7 @@ public function getCroutonJsConfig($atts) if ($this->embeddedMap) { $params = apply_filters('crouton_configuration', $params); } - return json_encode($params); + return [json_encode($params), json_encode($mapParams)]; } } //End Class Crouton diff --git a/croutonjs/dist/crouton-map.js b/croutonjs/dist/crouton-map.js new file mode 100644 index 0000000..5f26031 --- /dev/null +++ b/croutonjs/dist/crouton-map.js @@ -0,0 +1,2307 @@ +function CroutonMap(config) { + var self = this; + self.config = { + google_api_key: null, // Required if using the show_map option. Be sure to add an HTTP restriction as well. + distance_units: 'mi' + }; + Object.assign(self.config, config); + self.map = null; + self.geocoder = null; + self.map_objects = []; + self.map_clusters = []; + self.oms = null; + self.markerClusterer = null; + self.loadGapi = function(callbackFunctionName) { + var tag = document.createElement('script'); + tag.src = "https://maps.googleapis.com/maps/api/js?key=" + self.config['google_api_key'] + "&callback=" + callbackFunctionName; + tag.defer = true; + tag.async = true; + var firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + }; + self.getCurrentLocation = function(callback) { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(callback, self.errorHandler); + } else { + $('.geo').removeClass("hide").addClass("show").html('

Geolocation is not supported by your browser

'); + } + }; + + self.addCurrentLocationPin = function(latitude, longitude) { + var latlng = new google.maps.LatLng(latitude, longitude); + self.map.setCenter(latlng); + + var currentLocationMarker = new google.maps.Marker({ + "map": self.map, + "position": latlng, + }); + + self.addToMapObjectCollection(currentLocationMarker); + + // TODO: needs to show on click only + /*var infowindow = new google.maps.InfoWindow({ + "content": 'Current Location', + "position": latlng, + }).open(self.map, currentLocationMarker);*/ + }; + + self.findMarkerById = function(id) { + for (var m = 0; m < self.map_objects.length; m++) { + var map_object = self.map_objects[m]; + if (parseInt(map_object['id']) === id) { + return map_object; + } + } + + return null; + }; + + self.rowClick = function(id) { + var map_marker = self.findMarkerById(id); + if (!map_marker) return; + self.map.setCenter(map_marker.getPosition()); + self.map.setZoom(17); + google.maps.event.trigger(map_marker, "click"); + }; + + self.addToMapObjectCollection = function(obj) { + self.map_objects.push(obj); + }; + + self.clearAllMapClusters = function() { + while (self.map_clusters.length > 0) { + self.map_clusters[0].setMap(null); + self.map_clusters.splice(0, 1); + } + + if (self.oms !== null) { + self.oms.removeAllMarkers(); + } + + if (self.markerClusterer !== null) { + self.markerClusterer.clearMarkers(); + } + }; + + self.clearAllMapObjects = function() { + while (self.map_objects.length > 0) { + self.map_objects[0].setMap(null); + self.map_objects.splice(0, 1); + } + + //infoWindow.close(); + }; +} +CroutonMap.prototype.mapSearchClickMode = function() { + var self = this; + self.mapClickSearchMode = true; + self.map.setOptions({ + draggableCursor: 'crosshair', + zoomControl: false, + gestureHandling: 'none' + }); +}; +CroutonMap.prototype.reset = function() { + //self.clearAllMapObjects(); + //self.clearAllMapClusters(); +} +CroutonMap.prototype.mapSearchPanZoomMode = function() { + var self = this; + self.mapClickSearchMode = false; + self.map.setOptions({ + draggableCursor: 'default', + zoomControl: true, + gestureHandling: 'auto' + }); +}; + +CroutonMap.prototype.mapSearchNearMeMode = function() { + var self = this; + self.mapSearchPanZoomMode(); + self.getCurrentLocation(function(position) { + crouton.searchByCoordinates(position.coords.latitude, position.coords.longitude); + }); +}; + +CroutonMap.prototype.mapSearchTextMode = function(location) { + var self = this; + self.mapSearchPanZoomMode(); + if (location !== undefined && location !== null && location !== "") { + self.geocoder.geocode({'address': location}, function (results, status) { + if (status === 'OK') { + crouton.searchByCoordinates(results[0].geometry.location.lat(), results[0].geometry.location.lng()); + } else { + console.log('Geocode was not successful for the following reason: ' + status); + } + }); + } +}; + +Crouton.prototype.renderMap = function() { + var self = this; + jQuery("#bmlt-tabs").before("
"); + self.geocoder = new google.maps.Geocoder(); + jQuery.when(jQuery.getJSON(self.config['template_path'] + "/themes/" + self.config['theme'] + ".json").then( + function (data, textStatus, jqXHR) { + return self.config["theme_js"] = data["google_map_theme"]; + } + )).then(function() { + self.map = new google.maps.Map(document.getElementById('bmlt-map'), { + zoom: self.config['map_search']['zoom'] || 10, + center: { + lat: self.config['map_search']['latitude'], + lng: self.config['map_search']['longitude'], + }, + mapTypeControl: false, + styles: self.config["theme_js"] + }); + + var controlDiv = document.createElement('div'); + + // Set CSS for the control border + var controlUI = document.createElement('div'); + controlUI.className = 'mapcontrolcontainer'; + controlUI.title = 'Click to recenter the map'; + controlDiv.appendChild(controlUI); + + // Set CSS for the control interior + var clickSearch = document.createElement('div'); + clickSearch.className = 'mapcontrols'; + clickSearch.innerHTML = ''; + controlUI.appendChild(clickSearch); + controlDiv.index = 1; + + google.maps.event.addDomListener(clickSearch, 'click', function () { + var controlsButtonSelections = jQuery("input:radio[name='mapcontrols']:checked").attr("id"); + if (controlsButtonSelections === "textsearch") { + self.mapSearchTextMode(prompt("Enter a location or postal code:")); + } else if (controlsButtonSelections === "nearme") { + self.mapSearchNearMeMode(); + } else if (controlsButtonSelections === "clicksearch") { + self.mapSearchClickMode(); + } + }); + + self.map.controls[google.maps.ControlPosition.TOP_LEFT].push(controlDiv); + self.map.addListener('click', function (data) { + if (self.mapClickSearchMode) { + self.mapSearchPanZoomMode(); + crouton.searchByCoordinates(data.latLng.lat(), data.latLng.lng()); + } + }); + + if (self.config['map_search']['auto']) { + self.mapSearchNearMeMode(); + } else if (self.config['map_search']['location'] !== undefined) { + self.mapSearchTextMode(self.config['map_search']['location']); + } else if (self.config['map_search']['coordinates_search']) { + crouton.searchByCoordinates(self.config['map_search']['latitude'], self.config['map_search']['longitude']); + } + }) +}; +CroutonMap.prototype.render = function() { + this.loadGapi('croutonMap.renderMap'); +} +CroutonMap.prototype.initialize = function(meetingData, formatsData, handlebarMapOptions=[]) { + this.meetingData = meetingData; + this.formatsData = formatsData; + this.handlebarMapOptions = handlebarMapOptions; + this.loadGapi('croutonMap.initMap'); +} +CroutonMap.prototype.embed = function(meetingData, formatsData) { + this.meetingData = meetingData; + this.formatsData = formatsData; + this.loadGapi('croutonMap.mapPage'); +} +CroutonMap.prototype.initMap = function(callback) { + var self = this; + if (self.map == null) { + jQuery("#bmlt-tabs").before("
"); + var mapOpt = { zoom: 3 }; + if (self.handlebarMapOptions.length > 0) mapOpt = { + center: new google.maps.LatLng(self.handlebarMapOptions.lat, self.handlebarMapOptions.lng), + zoom: self.handlebarMapOptions.zoom, + mapTypeId:google.maps.MapTypeId.ROADMAP + }; + self.map = new google.maps.Map(document.getElementById('bmlt-map'), mapOpt ); + } + + jQuery("#bmlt-map").removeClass("hide"); + self.fillMap(callback); +} +CroutonMap.prototype.mapPage = function(callback) { + var self = this; + if (self.map == null) { + self.map = new google.maps.Map(document.getElementById('byfield_embeddedMapPage'), { zoom: 3, maxZoom: 17 } ); + } + self.fillMap(callback); +} +CroutonMap.prototype.fillMap = function(callback, filteredIds=null) { + if (typeof crouton_external_map !== 'undefined') { + crouton_external_map.filterFromCrouton(filteredIds); + return; + } + var self = this; + if (self.map == null) return; + self.clearAllMapObjects(); + self.clearAllMapClusters(); + const physicalMeetings = self.meetingData.filter(m => m.venue_type != venueType.VIRTUAL); + const filteredMeetings = (filteredIds===null) ? physicalMeetings + : physicalMeetings.filter((m) => filteredIds.includes(m.id_bigint)); + const bounds = filteredMeetings.reduce( + function (bounds, m) { + return bounds.extend(new google.maps.LatLng(m.latitude, m.longitude)) + }, new google.maps.LatLngBounds()); + // We now have the full rectangle of our meeting search results. Scale the map to fit them. + self.map.fitBounds(bounds); + var infoWindow = new google.maps.InfoWindow(); + + // Create OverlappingMarkerSpiderfier instance + self.oms = new OverlappingMarkerSpiderfier(self.map, { + markersWontMove: true, + markersWontHide: true, + }); + + self.oms.addListener('format', function (marker, status) { + var iconURL; + if (status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED + || status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE + || status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIED) { + iconURL = self.config['template_path'] + '/NAMarkerR.png'; + } else if (status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE) { + iconURL = self.config['template_path'] + '/NAMarkerB.png'; + } else { + iconURL = null; + } + + var iconSize = new google.maps.Size(22, 32); + marker.setIcon({ + url: iconURL, + size: iconSize, + scaledSize: iconSize + }); + }); + + self.map.addListener('zoom_changed', function() { + self.map.addListener('idle', function() { + var spidered = self.oms.markersNearAnyOtherMarker(); + for (var i = 0; i < spidered.length; i ++) { + spidered[i].icon.url = self.config['template_path'] + '/NAMarkerR.png'; + } + }); + }); + + // This is necessary to make the Spiderfy work + self.oms.addListener('click', function (marker) { + marker.zIndex = 999; + infoWindow.setContent(marker.desc); + infoWindow.open(self.map, marker); + }); + // Add some markers to the map. + filteredMeetings.map(function (location, i) { + var marker_html = '
'; + marker_html += location.meeting_name; + marker_html += '
'; + marker_html += '
'; + marker_html += crouton.localization.getDayOfTheWeekWord(location.weekday_tinyint); + var time = location.start_time.toString().split(':'); + var hour = parseInt(time[0]); + var minute = parseInt(time[1]); + var pm = 'AM'; + if (hour >= 12) { + pm = 'PM'; + if (hour > 12) { + hour -= 12; + } + } + hour = hour.toString(); + minute = (minute > 9) ? minute.toString() : ('0' + minute.toString()); + marker_html += ' ' + hour + ':' + minute + ' ' + pm; + marker_html += '
'; + marker_html += location.location_text; + marker_html += '
'; + + if (typeof location.location_street !== "undefined") { + marker_html += location.location_street + '
'; + } + if (typeof location.location_municipality !== "undefined") { + marker_html += location.location_municipality + ' '; + } + if (typeof location.location_province !== "undefined") { + marker_html += location.location_province + ' '; + } + if (typeof location.location_postal_code_1 !== "undefined") { + marker_html += location.location_postal_code_1; + } + + marker_html += '
'; + var url = 'https://maps.google.com/maps?q=' + location.latitude + ',' + location.longitude + '&hl=' + self.config['short_language']; + marker_html += ''; + marker_html += crouton.localization.getWord('map'); + marker_html += ''; + marker_html += '
'; + + var latLng = {"lat": parseFloat(location.latitude), "lng": parseFloat(location.longitude)}; + + var marker = new google.maps.Marker({ + position: latLng + }); + + marker['id'] = location['id_bigint']; + marker['day_id'] = location['weekday_tinyint']; + + self.addToMapObjectCollection(marker); + self.oms.addMarker(marker); + + self.map_clusters.push(marker); + google.maps.event.addListener(marker, 'click', function (evt) { + jQuery(".bmlt-data-row > td").removeClass("rowHighlight"); + jQuery("#meeting-data-row-" + marker['id'] + " > td").addClass("rowHighlight"); + crouton.dayTab(marker['day_id']); + infoWindow.setContent(marker_html); + infoWindow.open(self.map, marker); + }); + return marker; + }); + + // Add a marker clusterer to manage the markers. + self.markerClusterer = new MarkerClusterer(self.map, self.map_clusters, { + imagePath: self.config['template_path'] + '/m', + maxZoom: self.config['map_max_zoom'], + zoomOnClick: false + }); + + if (callback !== undefined && isFunction(callback)) callback(); +}; + +/** + * @name MarkerClusterer for Google Maps v3 + * @version version 1.0.1 + * @author Luke Mahe + * @fileoverview + * The library creates and manages per-zoom-level clusters for large amounts of + * markers. + *
+ * This is a v3 implementation of the + * v2 MarkerClusterer. + */ + +/** + * 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. + */ + + +/** + * A Marker Clusterer that clusters markers. + * + * @param {google.maps.Map} map The Google map to attach to. + * @param {Array.=} opt_markers Optional markers to add to + * the cluster. + * @param {Object=} opt_options support the following options: + * 'gridSize': (number) The grid size of a cluster in pixels. + * 'maxZoom': (number) The maximum zoom level that a marker can be part of a + * cluster. + * 'zoomOnClick': (boolean) Whether the default behaviour of clicking on a + * cluster is to zoom into it. + * 'averageCenter': (boolean) Whether the center of each cluster should be + * the average of all markers in the cluster. + * 'minimumClusterSize': (number) The minimum number of markers to be in a + * cluster before the markers are hidden and a count + * is shown. + * 'styles': (object) An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition': (string) The position of the backgound x, y. + * @constructor + * @extends google.maps.OverlayView + */ +function MarkerClusterer(map, opt_markers, opt_options) { + // MarkerClusterer implements google.maps.OverlayView interface. We use the + // extend function to extend MarkerClusterer with google.maps.OverlayView + // because it might not always be available when the code is defined so we + // look for it at the last possible moment. If it doesn't exist now then + // there is no point going ahead :) + this.extend(MarkerClusterer, google.maps.OverlayView); + this.map_ = map; + + /** + * @type {Array.} + * @private + */ + this.markers_ = []; + + /** + * @type {Array.} + */ + this.clusters_ = []; + + this.sizes = [53, 56, 66, 78, 90]; + + /** + * @private + */ + this.styles_ = []; + + /** + * @type {boolean} + * @private + */ + this.ready_ = false; + + var options = opt_options || {}; + + /** + * @type {number} + * @private + */ + this.gridSize_ = options['gridSize'] || 60; + + /** + * @private + */ + this.minClusterSize_ = options['minimumClusterSize'] || 2; + + + /** + * @type {?number} + * @private + */ + this.maxZoom_ = options['maxZoom'] || null; + + this.styles_ = options['styles'] || []; + + /** + * @type {string} + * @private + */ + this.imagePath_ = options['imagePath'] || + this.MARKER_CLUSTER_IMAGE_PATH_; + + /** + * @type {string} + * @private + */ + this.imageExtension_ = options['imageExtension'] || + this.MARKER_CLUSTER_IMAGE_EXTENSION_; + + /** + * @type {boolean} + * @private + */ + this.zoomOnClick_ = true; + + if (options['zoomOnClick'] != undefined) { + this.zoomOnClick_ = options['zoomOnClick']; + } + + /** + * @type {boolean} + * @private + */ + this.averageCenter_ = false; + + if (options['averageCenter'] != undefined) { + this.averageCenter_ = options['averageCenter']; + } + + this.setupStyles_(); + + this.setMap(map); + + /** + * @type {number} + * @private + */ + this.prevZoom_ = this.map_.getZoom(); + + // Add the map event listeners + var that = this; + google.maps.event.addListener(this.map_, 'zoom_changed', function() { + // Determines map type and prevent illegal zoom levels + var zoom = that.map_.getZoom(); + var minZoom = that.map_.minZoom || 0; + var maxZoom = Math.min(that.map_.maxZoom || 100, + that.map_.mapTypes[that.map_.getMapTypeId()].maxZoom); + zoom = Math.min(Math.max(zoom,minZoom),maxZoom); + + if (that.prevZoom_ != zoom) { + that.prevZoom_ = zoom; + that.resetViewport(); + } + }); + + google.maps.event.addListener(this.map_, 'idle', function() { + that.redraw(); + }); + + // Finally, add the markers + if (opt_markers && (opt_markers.length || Object.keys(opt_markers).length)) { + this.addMarkers(opt_markers, false); + } +} + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_ = '../images/m'; + + +/** + * The marker cluster image path. + * + * @type {string} + * @private + */ +MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_ = 'png'; + + +/** + * Extends a objects prototype by anothers. + * + * @param {Object} obj1 The object to be extended. + * @param {Object} obj2 The object to extend with. + * @return {Object} The new extended object. + * @ignore + */ +MarkerClusterer.prototype.extend = function(obj1, obj2) { + return (function(object) { + for (var property in object.prototype) { + this.prototype[property] = object.prototype[property]; + } + return this; + }).apply(obj1, [obj2]); +}; + + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.onAdd = function() { + this.setReady_(true); +}; + +/** + * Implementaion of the interface method. + * @ignore + */ +MarkerClusterer.prototype.draw = function() {}; + +/** + * Sets up the styles object. + * + * @private + */ +MarkerClusterer.prototype.setupStyles_ = function() { + if (this.styles_.length) { + return; + } + + for (var i = 0, size; size = this.sizes[i]; i++) { + this.styles_.push({ + url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_, + height: size, + width: size + }); + } +}; + +/** + * Fit the map to the bounds of the markers in the clusterer. + */ +MarkerClusterer.prototype.fitMapToMarkers = function() { + var markers = this.getMarkers(); + var bounds = new google.maps.LatLngBounds(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + + this.map_.fitBounds(bounds); +}; + + +/** + * Sets the styles. + * + * @param {Object} styles The style to set. + */ +MarkerClusterer.prototype.setStyles = function(styles) { + this.styles_ = styles; +}; + + +/** + * Gets the styles. + * + * @return {Object} The styles object. + */ +MarkerClusterer.prototype.getStyles = function() { + return this.styles_; +}; + + +/** + * Whether zoom on click is set. + * + * @return {boolean} True if zoomOnClick_ is set. + */ +MarkerClusterer.prototype.isZoomOnClick = function() { + return this.zoomOnClick_; +}; + +/** + * Whether average center is set. + * + * @return {boolean} True if averageCenter_ is set. + */ +MarkerClusterer.prototype.isAverageCenter = function() { + return this.averageCenter_; +}; + + +/** + * Returns the array of markers in the clusterer. + * + * @return {Array.} The markers. + */ +MarkerClusterer.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the number of markers in the clusterer + * + * @return {Number} The number of markers. + */ +MarkerClusterer.prototype.getTotalMarkers = function() { + return this.markers_.length; +}; + + +/** + * Sets the max zoom for the clusterer. + * + * @param {number} maxZoom The max zoom level. + */ +MarkerClusterer.prototype.setMaxZoom = function(maxZoom) { + this.maxZoom_ = maxZoom; +}; + + +/** + * Gets the max zoom for the clusterer. + * + * @return {number} The max zoom level. + */ +MarkerClusterer.prototype.getMaxZoom = function() { + return this.maxZoom_; +}; + + +/** + * The function for calculating the cluster icon image. + * + * @param {Array.} markers The markers in the clusterer. + * @param {number} numStyles The number of styles available. + * @return {Object} A object properties: 'text' (string) and 'index' (number). + * @private + */ +MarkerClusterer.prototype.calculator_ = function(markers, numStyles) { + var index = 0; + var count = markers.length; + var dv = count; + while (dv !== 0) { + dv = parseInt(dv / 10, 10); + index++; + } + + index = Math.min(index, numStyles); + return { + text: count, + index: index + }; +}; + + +/** + * Set the calculator function. + * + * @param {function(Array, number)} calculator The function to set as the + * calculator. The function should return a object properties: + * 'text' (string) and 'index' (number). + * + */ +MarkerClusterer.prototype.setCalculator = function(calculator) { + this.calculator_ = calculator; +}; + + +/** + * Get the calculator function. + * + * @return {function(Array, number)} the calculator function. + */ +MarkerClusterer.prototype.getCalculator = function() { + return this.calculator_; +}; + + +/** + * Add an array of markers to the clusterer. + * + * @param {Array.} markers The markers to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarkers = function(markers, opt_nodraw) { + if (markers.length) { + for (var i = 0, marker; marker = markers[i]; i++) { + this.pushMarkerTo_(marker); + } + } else if (Object.keys(markers).length) { + for (var marker in markers) { + this.pushMarkerTo_(markers[marker]); + } + } + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Pushes a marker to the clusterer. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.pushMarkerTo_ = function(marker) { + marker.isAdded = false; + if (marker['draggable']) { + // If the marker is draggable add a listener so we update the clusters on + // the drag end. + var that = this; + google.maps.event.addListener(marker, 'dragend', function() { + marker.isAdded = false; + that.repaint(); + }); + } + this.markers_.push(marker); +}; + + +/** + * Adds a marker to the clusterer and redraws if needed. + * + * @param {google.maps.Marker} marker The marker to add. + * @param {boolean=} opt_nodraw Whether to redraw the clusters. + */ +MarkerClusterer.prototype.addMarker = function(marker, opt_nodraw) { + this.pushMarkerTo_(marker); + if (!opt_nodraw) { + this.redraw(); + } +}; + + +/** + * Removes a marker and returns true if removed, false if not + * + * @param {google.maps.Marker} marker The marker to remove + * @return {boolean} Whether the marker was removed or not + * @private + */ +MarkerClusterer.prototype.removeMarker_ = function(marker) { + var index = -1; + if (this.markers_.indexOf) { + index = this.markers_.indexOf(marker); + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + index = i; + break; + } + } + } + + if (index == -1) { + // Marker is not in our list of markers. + return false; + } + + marker.setMap(null); + + this.markers_.splice(index, 1); + + return true; +}; + + +/** + * Remove a marker from the cluster. + * + * @param {google.maps.Marker} marker The marker to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + * @return {boolean} True if the marker was removed. + */ +MarkerClusterer.prototype.removeMarker = function(marker, opt_nodraw) { + var removed = this.removeMarker_(marker); + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } else { + return false; + } +}; + + +/** + * Removes an array of markers from the cluster. + * + * @param {Array.} markers The markers to remove. + * @param {boolean=} opt_nodraw Optional boolean to force no redraw. + */ +MarkerClusterer.prototype.removeMarkers = function(markers, opt_nodraw) { + var removed = false; + + for (var i = 0, marker; marker = markers[i]; i++) { + var r = this.removeMarker_(marker); + removed = removed || r; + } + + if (!opt_nodraw && removed) { + this.resetViewport(); + this.redraw(); + return true; + } +}; + + +/** + * Sets the clusterer's ready state. + * + * @param {boolean} ready The state. + * @private + */ +MarkerClusterer.prototype.setReady_ = function(ready) { + if (!this.ready_) { + this.ready_ = ready; + this.createClusters_(); + } +}; + + +/** + * Returns the number of clusters in the clusterer. + * + * @return {number} The number of clusters. + */ +MarkerClusterer.prototype.getTotalClusters = function() { + return this.clusters_.length; +}; + + +/** + * Returns the google map that the clusterer is associated with. + * + * @return {google.maps.Map} The map. + */ +MarkerClusterer.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Sets the google map that the clusterer is associated with. + * + * @param {google.maps.Map} map The map. + */ +MarkerClusterer.prototype.setMap = function(map) { + this.map_ = map; +}; + + +/** + * Returns the size of the grid. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getGridSize = function() { + return this.gridSize_; +}; + + +/** + * Sets the size of the grid. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setGridSize = function(size) { + this.gridSize_ = size; +}; + + +/** + * Returns the min cluster size. + * + * @return {number} The grid size. + */ +MarkerClusterer.prototype.getMinClusterSize = function() { + return this.minClusterSize_; +}; + +/** + * Sets the min cluster size. + * + * @param {number} size The grid size. + */ +MarkerClusterer.prototype.setMinClusterSize = function(size) { + this.minClusterSize_ = size; +}; + + +/** + * Extends a bounds object by the grid size. + * + * @param {google.maps.LatLngBounds} bounds The bounds to extend. + * @return {google.maps.LatLngBounds} The extended bounds. + */ +MarkerClusterer.prototype.getExtendedBounds = function(bounds) { + var projection = this.getProjection(); + + // Turn the bounds into latlng. + var tr = new google.maps.LatLng(bounds.getNorthEast().lat(), + bounds.getNorthEast().lng()); + var bl = new google.maps.LatLng(bounds.getSouthWest().lat(), + bounds.getSouthWest().lng()); + + // Convert the points to pixels and the extend out by the grid size. + var trPix = projection.fromLatLngToDivPixel(tr); + trPix.x += this.gridSize_; + trPix.y -= this.gridSize_; + + var blPix = projection.fromLatLngToDivPixel(bl); + blPix.x -= this.gridSize_; + blPix.y += this.gridSize_; + + // Convert the pixel points back to LatLng + var ne = projection.fromDivPixelToLatLng(trPix); + var sw = projection.fromDivPixelToLatLng(blPix); + + // Extend the bounds to contain the new bounds. + bounds.extend(ne); + bounds.extend(sw); + + return bounds; +}; + + +/** + * Determins if a marker is contained in a bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @param {google.maps.LatLngBounds} bounds The bounds to check against. + * @return {boolean} True if the marker is in the bounds. + * @private + */ +MarkerClusterer.prototype.isMarkerInBounds_ = function(marker, bounds) { + return bounds.contains(marker.getPosition()); +}; + + +/** + * Clears all clusters and markers from the clusterer. + */ +MarkerClusterer.prototype.clearMarkers = function() { + this.resetViewport(true); + + // Set the markers a empty array. + this.markers_ = []; +}; + + +/** + * Clears all existing clusters and recreates them. + * @param {boolean} opt_hide To also hide the marker. + */ +MarkerClusterer.prototype.resetViewport = function(opt_hide) { + // Remove all the clusters + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + cluster.remove(); + } + + // Reset the markers to not be added and to be invisible. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.isAdded = false; + if (opt_hide) { + marker.setMap(null); + } + } + + this.clusters_ = []; +}; + +/** + * + */ +MarkerClusterer.prototype.repaint = function() { + var oldClusters = this.clusters_.slice(); + this.clusters_.length = 0; + this.resetViewport(); + this.redraw(); + + // Remove the old clusters. + // Do it in a timeout so the other clusters have been drawn first. + window.setTimeout(function() { + for (var i = 0, cluster; cluster = oldClusters[i]; i++) { + cluster.remove(); + } + }, 0); +}; + + +/** + * Redraws the clusters. + */ +MarkerClusterer.prototype.redraw = function() { + this.createClusters_(); +}; + + +/** + * Calculates the distance between two latlng locations in km. + * @see http://www.movable-type.co.uk/scripts/latlong.html + * + * @param {google.maps.LatLng} p1 The first lat lng point. + * @param {google.maps.LatLng} p2 The second lat lng point. + * @return {number} The distance between the two points in km. + * @private + */ +MarkerClusterer.prototype.distanceBetweenPoints_ = function(p1, p2) { + if (!p1 || !p2) { + return 0; + } + + var R = 6371; // Radius of the Earth in km + var dLat = (p2.lat() - p1.lat()) * Math.PI / 180; + var dLon = (p2.lng() - p1.lng()) * Math.PI / 180; + var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; +}; + + +/** + * Add a marker to a cluster, or creates a new cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @private + */ +MarkerClusterer.prototype.addToClosestCluster_ = function(marker) { + var distance = 40000; // Some large number + var clusterToAddTo = null; + var pos = marker.getPosition(); + for (var i = 0, cluster; cluster = this.clusters_[i]; i++) { + var center = cluster.getCenter(); + if (center) { + var d = this.distanceBetweenPoints_(center, marker.getPosition()); + if (d < distance) { + distance = d; + clusterToAddTo = cluster; + } + } + } + + if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) { + clusterToAddTo.addMarker(marker); + } else { + var cluster = new Cluster(this); + cluster.addMarker(marker); + this.clusters_.push(cluster); + } +}; + + +/** + * Creates the clusters. + * + * @private + */ +MarkerClusterer.prototype.createClusters_ = function() { + if (!this.ready_) { + return; + } + + // Get our current map view bounds. + // Create a new bounds object so we don't affect the map. + var mapBounds = new google.maps.LatLngBounds(this.map_.getBounds().getSouthWest(), + this.map_.getBounds().getNorthEast()); + var bounds = this.getExtendedBounds(mapBounds); + + for (var i = 0, marker; marker = this.markers_[i]; i++) { + if (!marker.isAdded && this.isMarkerInBounds_(marker, bounds)) { + this.addToClosestCluster_(marker); + } + } +}; + + +/** + * A cluster that contains markers. + * + * @param {MarkerClusterer} markerClusterer The markerclusterer that this + * cluster is associated with. + * @constructor + * @ignore + */ +function Cluster(markerClusterer) { + this.markerClusterer_ = markerClusterer; + this.map_ = markerClusterer.getMap(); + this.gridSize_ = markerClusterer.getGridSize(); + this.minClusterSize_ = markerClusterer.getMinClusterSize(); + this.averageCenter_ = markerClusterer.isAverageCenter(); + this.center_ = null; + this.markers_ = []; + this.bounds_ = null; + this.clusterIcon_ = new ClusterIcon(this, markerClusterer.getStyles(), + markerClusterer.getGridSize()); +} + +/** + * Determins if a marker is already added to the cluster. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker is already added. + */ +Cluster.prototype.isMarkerAlreadyAdded = function(marker) { + if (this.markers_.indexOf) { + return this.markers_.indexOf(marker) != -1; + } else { + for (var i = 0, m; m = this.markers_[i]; i++) { + if (m == marker) { + return true; + } + } + } + return false; +}; + + +/** + * Add a marker the cluster. + * + * @param {google.maps.Marker} marker The marker to add. + * @return {boolean} True if the marker was added. + */ +Cluster.prototype.addMarker = function(marker) { + if (this.isMarkerAlreadyAdded(marker)) { + return false; + } + + if (!this.center_) { + this.center_ = marker.getPosition(); + this.calculateBounds_(); + } else { + if (this.averageCenter_) { + var l = this.markers_.length + 1; + var lat = (this.center_.lat() * (l-1) + marker.getPosition().lat()) / l; + var lng = (this.center_.lng() * (l-1) + marker.getPosition().lng()) / l; + this.center_ = new google.maps.LatLng(lat, lng); + this.calculateBounds_(); + } + } + + marker.isAdded = true; + this.markers_.push(marker); + + var len = this.markers_.length; + if (len < this.minClusterSize_ && marker.getMap() != this.map_) { + // Min cluster size not reached so show the marker. + marker.setMap(this.map_); + } + + if (len == this.minClusterSize_) { + // Hide the markers that were showing. + for (var i = 0; i < len; i++) { + this.markers_[i].setMap(null); + } + } + + if (len >= this.minClusterSize_) { + marker.setMap(null); + } + + this.updateIcon(); + return true; +}; + + +/** + * Returns the marker clusterer that the cluster is associated with. + * + * @return {MarkerClusterer} The associated marker clusterer. + */ +Cluster.prototype.getMarkerClusterer = function() { + return this.markerClusterer_; +}; + + +/** + * Returns the bounds of the cluster. + * + * @return {google.maps.LatLngBounds} the cluster bounds. + */ +Cluster.prototype.getBounds = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + var markers = this.getMarkers(); + for (var i = 0, marker; marker = markers[i]; i++) { + bounds.extend(marker.getPosition()); + } + return bounds; +}; + + +/** + * Removes the cluster + */ +Cluster.prototype.remove = function() { + this.clusterIcon_.remove(); + this.markers_.length = 0; + delete this.markers_; +}; + + +/** + * Returns the center of the cluster. + * + * @return {number} The cluster center. + */ +Cluster.prototype.getSize = function() { + return this.markers_.length; +}; + + +/** + * Returns the center of the cluster. + * + * @return {Array.} The cluster center. + */ +Cluster.prototype.getMarkers = function() { + return this.markers_; +}; + + +/** + * Returns the center of the cluster. + * + * @return {google.maps.LatLng} The cluster center. + */ +Cluster.prototype.getCenter = function() { + return this.center_; +}; + + +/** + * Calculated the extended bounds of the cluster with the grid. + * + * @private + */ +Cluster.prototype.calculateBounds_ = function() { + var bounds = new google.maps.LatLngBounds(this.center_, this.center_); + this.bounds_ = this.markerClusterer_.getExtendedBounds(bounds); +}; + + +/** + * Determines if a marker lies in the clusters bounds. + * + * @param {google.maps.Marker} marker The marker to check. + * @return {boolean} True if the marker lies in the bounds. + */ +Cluster.prototype.isMarkerInClusterBounds = function(marker) { + return this.bounds_.contains(marker.getPosition()); +}; + + +/** + * Returns the map that the cluster is associated with. + * + * @return {google.maps.Map} The map. + */ +Cluster.prototype.getMap = function() { + return this.map_; +}; + + +/** + * Updates the cluster icon + */ +Cluster.prototype.updateIcon = function() { + var zoom = this.map_.getZoom(); + var mz = this.markerClusterer_.getMaxZoom(); + + if (mz && zoom > mz) { + // The zoom is greater than our max zoom so show all the markers in cluster. + for (var i = 0, marker; marker = this.markers_[i]; i++) { + marker.setMap(this.map_); + } + return; + } + + if (this.markers_.length < this.minClusterSize_) { + // Min cluster size not yet reached. + this.clusterIcon_.hide(); + return; + } + + var numStyles = this.markerClusterer_.getStyles().length; + var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles); + this.clusterIcon_.setCenter(this.center_); + this.clusterIcon_.setSums(sums); + this.clusterIcon_.show(); +}; + + +/** + * A cluster icon + * + * @param {Cluster} cluster The cluster to be associated with. + * @param {Object} styles An object that has style properties: + * 'url': (string) The image url. + * 'height': (number) The image height. + * 'width': (number) The image width. + * 'anchor': (Array) The anchor position of the label text. + * 'textColor': (string) The text color. + * 'textSize': (number) The text size. + * 'backgroundPosition: (string) The background postition x, y. + * @param {number=} opt_padding Optional padding to apply to the cluster icon. + * @constructor + * @extends google.maps.OverlayView + * @ignore + */ +function ClusterIcon(cluster, styles, opt_padding) { + cluster.getMarkerClusterer().extend(ClusterIcon, google.maps.OverlayView); + + this.styles_ = styles; + this.padding_ = opt_padding || 0; + this.cluster_ = cluster; + this.center_ = null; + this.map_ = cluster.getMap(); + this.div_ = null; + this.sums_ = null; + this.visible_ = false; + + this.setMap(this.map_); +} + + +/** + * Triggers the clusterclick event and zoom's if the option is set. + */ +ClusterIcon.prototype.triggerClusterClick = function() { + var markerClusterer = this.cluster_.getMarkerClusterer(); + + // Trigger the clusterclick event. + google.maps.event.trigger(markerClusterer, 'clusterclick', this.cluster_); + + if (markerClusterer.isZoomOnClick()) { + // Zoom into the cluster. + this.map_.fitBounds(this.cluster_.getBounds()); + } +}; + + +/** + * Adding the cluster icon to the dom. + * @ignore + */ +ClusterIcon.prototype.onAdd = function() { + this.div_ = document.createElement('DIV'); + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.innerHTML = this.sums_.text; + } + + var panes = this.getPanes(); + panes.overlayMouseTarget.appendChild(this.div_); + + var that = this; + google.maps.event.addDomListener(this.div_, 'click', function() { + that.triggerClusterClick(); + }); +}; + + +/** + * Returns the position to place the div dending on the latlng. + * + * @param {google.maps.LatLng} latlng The position in latlng. + * @return {google.maps.Point} The position in pixels. + * @private + */ +ClusterIcon.prototype.getPosFromLatLng_ = function(latlng) { + var pos = this.getProjection().fromLatLngToDivPixel(latlng); + pos.x -= parseInt(this.width_ / 2, 10); + pos.y -= parseInt(this.height_ / 2, 10); + return pos; +}; + + +/** + * Draw the icon. + * @ignore + */ +ClusterIcon.prototype.draw = function() { + if (this.visible_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.top = pos.y + 'px'; + this.div_.style.left = pos.x + 'px'; + } +}; + + +/** + * Hide the icon. + */ +ClusterIcon.prototype.hide = function() { + if (this.div_) { + this.div_.style.display = 'none'; + } + this.visible_ = false; +}; + + +/** + * Position and show the icon. + */ +ClusterIcon.prototype.show = function() { + if (this.div_) { + var pos = this.getPosFromLatLng_(this.center_); + this.div_.style.cssText = this.createCss(pos); + this.div_.style.display = ''; + } + this.visible_ = true; +}; + + +/** + * Remove the icon from the map + */ +ClusterIcon.prototype.remove = function() { + this.setMap(null); +}; + + +/** + * Implementation of the onRemove interface. + * @ignore + */ +ClusterIcon.prototype.onRemove = function() { + if (this.div_ && this.div_.parentNode) { + this.hide(); + this.div_.parentNode.removeChild(this.div_); + this.div_ = null; + } +}; + + +/** + * Set the sums of the icon. + * + * @param {Object} sums The sums containing: + * 'text': (string) The text to display in the icon. + * 'index': (number) The style index of the icon. + */ +ClusterIcon.prototype.setSums = function(sums) { + this.sums_ = sums; + this.text_ = sums.text; + this.index_ = sums.index; + if (this.div_) { + this.div_.innerHTML = sums.text; + } + + this.useStyle(); +}; + + +/** + * Sets the icon to the the styles. + */ +ClusterIcon.prototype.useStyle = function() { + var index = Math.max(0, this.sums_.index - 1); + index = Math.min(this.styles_.length - 1, index); + var style = this.styles_[index]; + this.url_ = style['url']; + this.height_ = style['height']; + this.width_ = style['width']; + this.textColor_ = style['textColor']; + this.anchor_ = style['anchor']; + this.textSize_ = style['textSize']; + this.backgroundPosition_ = style['backgroundPosition']; +}; + + +/** + * Sets the center of the icon. + * + * @param {google.maps.LatLng} center The latlng to set as the center. + */ +ClusterIcon.prototype.setCenter = function(center) { + this.center_ = center; +}; + + +/** + * Create the css text based on the position of the icon. + * + * @param {google.maps.Point} pos The position. + * @return {string} The css style text. + */ +ClusterIcon.prototype.createCss = function(pos) { + var style = []; + style.push('background-image:url(' + this.url_ + ');'); + var backgroundPosition = this.backgroundPosition_ ? this.backgroundPosition_ : '0 0'; + style.push('background-position:' + backgroundPosition + ';'); + + if (typeof this.anchor_ === 'object') { + if (typeof this.anchor_[0] === 'number' && this.anchor_[0] > 0 && + this.anchor_[0] < this.height_) { + style.push('height:' + (this.height_ - this.anchor_[0]) + + 'px; padding-top:' + this.anchor_[0] + 'px;'); + } else { + style.push('height:' + this.height_ + 'px; line-height:' + this.height_ + + 'px;'); + } + if (typeof this.anchor_[1] === 'number' && this.anchor_[1] > 0 && + this.anchor_[1] < this.width_) { + style.push('width:' + (this.width_ - this.anchor_[1]) + + 'px; padding-left:' + this.anchor_[1] + 'px;'); + } else { + style.push('width:' + this.width_ + 'px; text-align:center;'); + } + } else { + style.push('height:' + this.height_ + 'px; line-height:' + + this.height_ + 'px; width:' + this.width_ + 'px; text-align:center;'); + } + + var txtColor = this.textColor_ ? this.textColor_ : 'black'; + var txtSize = this.textSize_ ? this.textSize_ : 11; + + style.push('cursor:pointer; top:' + pos.y + 'px; left:' + + pos.x + 'px; color:' + txtColor + '; position:absolute; font-size:' + + txtSize + 'px; font-family:Arial,sans-serif; font-weight:bold'); + return style.join(''); +}; + +// Generated by CoffeeScript 1.12.2 + +/** @preserve OverlappingMarkerSpiderfier +https://github.com/jawj/OverlappingMarkerSpiderfier +Copyright (c) 2011 - 2017 George MacKerron +Released under the MIT licence: http://opensource.org/licenses/mit-license +Note: The Google Maps API v3 must be included *before* this code + */ + +(function() { + var callbackName, callbackRegEx, ref, ref1, scriptTag, tag, + hasProp = {}.hasOwnProperty, + slice = [].slice; + + this['OverlappingMarkerSpiderfier'] = (function() { + var ge, gm, j, len, mt, p, ref, twoPi, x; + + p = _Class.prototype; + + ref = [_Class, p]; + for (j = 0, len = ref.length; j < len; j++) { + x = ref[j]; + x['VERSION'] = '1.0.3'; + } + + twoPi = Math.PI * 2; + + gm = ge = mt = null; + + _Class['markerStatus'] = { + 'SPIDERFIED': 'SPIDERFIED', + 'SPIDERFIABLE': 'SPIDERFIABLE', + 'UNSPIDERFIABLE': 'UNSPIDERFIABLE', + 'UNSPIDERFIED': 'UNSPIDERFIED' + }; + + function _Class(map1, opts) { + var k, lcH, lcU, v; + this.map = map1; + if (opts == null) { + opts = {}; + } + if (this.constructor.hasInitialized == null) { + this.constructor.hasInitialized = true; + gm = google.maps; + ge = gm.event; + mt = gm.MapTypeId; + p['keepSpiderfied'] = false; + p['ignoreMapClick'] = false; + p['markersWontHide'] = false; + p['markersWontMove'] = false; + p['basicFormatEvents'] = false; + p['nearbyDistance'] = 20; + p['circleSpiralSwitchover'] = 9; + p['circleFootSeparation'] = 23; + p['circleStartAngle'] = twoPi / 12; + p['spiralFootSeparation'] = 26; + p['spiralLengthStart'] = 11; + p['spiralLengthFactor'] = 4; + p['spiderfiedZIndex'] = gm.Marker.MAX_ZINDEX + 20000; + p['highlightedLegZIndex'] = gm.Marker.MAX_ZINDEX + 10000; + p['usualLegZIndex'] = gm.Marker.MAX_ZINDEX + 1; + p['legWeight'] = 1.5; + p['legColors'] = { + 'usual': {}, + 'highlighted': {} + }; + lcU = p['legColors']['usual']; + lcH = p['legColors']['highlighted']; + lcU[mt.HYBRID] = lcU[mt.SATELLITE] = '#fff'; + lcH[mt.HYBRID] = lcH[mt.SATELLITE] = '#f00'; + lcU[mt.TERRAIN] = lcU[mt.ROADMAP] = '#444'; + lcH[mt.TERRAIN] = lcH[mt.ROADMAP] = '#f00'; + this.constructor.ProjHelper = function(map) { + return this.setMap(map); + }; + this.constructor.ProjHelper.prototype = new gm.OverlayView(); + this.constructor.ProjHelper.prototype['draw'] = function() {}; + } + for (k in opts) { + if (!hasProp.call(opts, k)) continue; + v = opts[k]; + this[k] = v; + } + this.projHelper = new this.constructor.ProjHelper(this.map); + this.initMarkerArrays(); + this.listeners = {}; + this.formatIdleListener = this.formatTimeoutId = null; + this.addListener('click', function(marker, e) { + return ge.trigger(marker, 'spider_click', e); + }); + this.addListener('format', function(marker, status) { + return ge.trigger(marker, 'spider_format', status); + }); + if (!this['ignoreMapClick']) { + ge.addListener(this.map, 'click', (function(_this) { + return function() { + return _this['unspiderfy'](); + }; + })(this)); + } + ge.addListener(this.map, 'maptypeid_changed', (function(_this) { + return function() { + return _this['unspiderfy'](); + }; + })(this)); + ge.addListener(this.map, 'zoom_changed', (function(_this) { + return function() { + _this['unspiderfy'](); + if (!_this['basicFormatEvents']) { + return _this.formatMarkers(); + } + }; + })(this)); + } + + p.initMarkerArrays = function() { + this.markers = []; + return this.markerListenerRefs = []; + }; + + p['addMarker'] = function(marker, spiderClickHandler) { + marker.setMap(this.map); + return this['trackMarker'](marker, spiderClickHandler); + }; + + p['trackMarker'] = function(marker, spiderClickHandler) { + var listenerRefs; + if (marker['_oms'] != null) { + return this; + } + marker['_oms'] = true; + listenerRefs = [ + ge.addListener(marker, 'click', (function(_this) { + return function(e) { + return _this.spiderListener(marker, e); + }; + })(this)) + ]; + if (!this['markersWontHide']) { + listenerRefs.push(ge.addListener(marker, 'visible_changed', (function(_this) { + return function() { + return _this.markerChangeListener(marker, false); + }; + })(this))); + } + if (!this['markersWontMove']) { + listenerRefs.push(ge.addListener(marker, 'position_changed', (function(_this) { + return function() { + return _this.markerChangeListener(marker, true); + }; + })(this))); + } + if (spiderClickHandler != null) { + listenerRefs.push(ge.addListener(marker, 'spider_click', spiderClickHandler)); + } + this.markerListenerRefs.push(listenerRefs); + this.markers.push(marker); + if (this['basicFormatEvents']) { + this.trigger('format', marker, this.constructor['markerStatus']['UNSPIDERFIED']); + } else { + this.trigger('format', marker, this.constructor['markerStatus']['UNSPIDERFIABLE']); + this.formatMarkers(); + } + return this; + }; + + p.markerChangeListener = function(marker, positionChanged) { + if (this.spiderfying || this.unspiderfying) { + return; + } + if ((marker['_omsData'] != null) && (positionChanged || !marker.getVisible())) { + this['unspiderfy'](positionChanged ? marker : null); + } + return this.formatMarkers(); + }; + + p['getMarkers'] = function() { + return this.markers.slice(0); + }; + + p['removeMarker'] = function(marker) { + this['forgetMarker'](marker); + return marker.setMap(null); + }; + + p['forgetMarker'] = function(marker) { + var i, l, len1, listenerRef, listenerRefs; + if (marker['_omsData'] != null) { + this['unspiderfy'](); + } + i = this.arrIndexOf(this.markers, marker); + if (i < 0) { + return this; + } + listenerRefs = this.markerListenerRefs.splice(i, 1)[0]; + for (l = 0, len1 = listenerRefs.length; l < len1; l++) { + listenerRef = listenerRefs[l]; + ge.removeListener(listenerRef); + } + delete marker['_oms']; + this.markers.splice(i, 1); + this.formatMarkers(); + return this; + }; + + p['removeAllMarkers'] = p['clearMarkers'] = function() { + var l, len1, marker, markers; + markers = this['getMarkers'](); + this['forgetAllMarkers'](); + for (l = 0, len1 = markers.length; l < len1; l++) { + marker = markers[l]; + marker.setMap(null); + } + return this; + }; + + p['forgetAllMarkers'] = function() { + var i, l, len1, len2, listenerRef, listenerRefs, marker, n, ref1; + this['unspiderfy'](); + ref1 = this.markers; + for (i = l = 0, len1 = ref1.length; l < len1; i = ++l) { + marker = ref1[i]; + listenerRefs = this.markerListenerRefs[i]; + for (n = 0, len2 = listenerRefs.length; n < len2; n++) { + listenerRef = listenerRefs[n]; + ge.removeListener(listenerRef); + } + delete marker['_oms']; + } + this.initMarkerArrays(); + return this; + }; + + p['addListener'] = function(eventName, func) { + var base; + ((base = this.listeners)[eventName] != null ? base[eventName] : base[eventName] = []).push(func); + return this; + }; + + p['removeListener'] = function(eventName, func) { + var i; + i = this.arrIndexOf(this.listeners[eventName], func); + if (!(i < 0)) { + this.listeners[eventName].splice(i, 1); + } + return this; + }; + + p['clearListeners'] = function(eventName) { + this.listeners[eventName] = []; + return this; + }; + + p.trigger = function() { + var args, eventName, func, l, len1, ref1, ref2, results; + eventName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + ref2 = (ref1 = this.listeners[eventName]) != null ? ref1 : []; + results = []; + for (l = 0, len1 = ref2.length; l < len1; l++) { + func = ref2[l]; + results.push(func.apply(null, args)); + } + return results; + }; + + p.generatePtsCircle = function(count, centerPt) { + var angle, angleStep, circumference, i, l, legLength, ref1, results; + circumference = this['circleFootSeparation'] * (2 + count); + legLength = circumference / twoPi; + angleStep = twoPi / count; + results = []; + for (i = l = 0, ref1 = count; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) { + angle = this['circleStartAngle'] + i * angleStep; + results.push(new gm.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle))); + } + return results; + }; + + p.generatePtsSpiral = function(count, centerPt) { + var angle, i, l, legLength, pt, ref1, results; + legLength = this['spiralLengthStart']; + angle = 0; + results = []; + for (i = l = 0, ref1 = count; 0 <= ref1 ? l < ref1 : l > ref1; i = 0 <= ref1 ? ++l : --l) { + angle += this['spiralFootSeparation'] / legLength + i * 0.0005; + pt = new gm.Point(centerPt.x + legLength * Math.cos(angle), centerPt.y + legLength * Math.sin(angle)); + legLength += twoPi * this['spiralLengthFactor'] / angle; + results.push(pt); + } + return results; + }; + + p.spiderListener = function(marker, e) { + var l, len1, m, mPt, markerPt, markerSpiderfied, nDist, nearbyMarkerData, nonNearbyMarkers, pxSq, ref1; + markerSpiderfied = marker['_omsData'] != null; + if (!(markerSpiderfied && this['keepSpiderfied'])) { + this['unspiderfy'](); + } + if (markerSpiderfied || this.map.getStreetView().getVisible() || this.map.getMapTypeId() === 'GoogleEarthAPI') { + return this.trigger('click', marker, e); + } else { + nearbyMarkerData = []; + nonNearbyMarkers = []; + nDist = this['nearbyDistance']; + pxSq = nDist * nDist; + markerPt = this.llToPt(marker.position); + ref1 = this.markers; + for (l = 0, len1 = ref1.length; l < len1; l++) { + m = ref1[l]; + if (!((m.map != null) && m.getVisible())) { + continue; + } + mPt = this.llToPt(m.position); + if (this.ptDistanceSq(mPt, markerPt) < pxSq) { + nearbyMarkerData.push({ + marker: m, + markerPt: mPt + }); + } else { + nonNearbyMarkers.push(m); + } + } + if (nearbyMarkerData.length === 1) { + return this.trigger('click', marker, e); + } else { + return this.spiderfy(nearbyMarkerData, nonNearbyMarkers); + } + } + }; + + p['markersNearMarker'] = function(marker, firstOnly) { + var l, len1, m, mPt, markerPt, markers, nDist, pxSq, ref1, ref2, ref3; + if (firstOnly == null) { + firstOnly = false; + } + if (this.projHelper.getProjection() == null) { + throw "Must wait for 'idle' event on map before calling markersNearMarker"; + } + nDist = this['nearbyDistance']; + pxSq = nDist * nDist; + markerPt = this.llToPt(marker.position); + markers = []; + ref1 = this.markers; + for (l = 0, len1 = ref1.length; l < len1; l++) { + m = ref1[l]; + if (m === marker || (m.map == null) || !m.getVisible()) { + continue; + } + mPt = this.llToPt((ref2 = (ref3 = m['_omsData']) != null ? ref3.usualPosition : void 0) != null ? ref2 : m.position); + if (this.ptDistanceSq(mPt, markerPt) < pxSq) { + markers.push(m); + if (firstOnly) { + break; + } + } + } + return markers; + }; + + p.markerProximityData = function() { + var i1, i2, l, len1, len2, m, m1, m1Data, m2, m2Data, mData, n, nDist, pxSq, ref1, ref2; + if (this.projHelper.getProjection() == null) { + throw "Must wait for 'idle' event on map before calling markersNearAnyOtherMarker"; + } + nDist = this['nearbyDistance']; + pxSq = nDist * nDist; + mData = (function() { + var l, len1, ref1, ref2, ref3, results; + ref1 = this.markers; + results = []; + for (l = 0, len1 = ref1.length; l < len1; l++) { + m = ref1[l]; + results.push({ + pt: this.llToPt((ref2 = (ref3 = m['_omsData']) != null ? ref3.usualPosition : void 0) != null ? ref2 : m.position), + willSpiderfy: false + }); + } + return results; + }).call(this); + ref1 = this.markers; + for (i1 = l = 0, len1 = ref1.length; l < len1; i1 = ++l) { + m1 = ref1[i1]; + if (!((m1.getMap() != null) && m1.getVisible())) { + continue; + } + m1Data = mData[i1]; + if (m1Data.willSpiderfy) { + continue; + } + ref2 = this.markers; + for (i2 = n = 0, len2 = ref2.length; n < len2; i2 = ++n) { + m2 = ref2[i2]; + if (i2 === i1) { + continue; + } + if (!((m2.getMap() != null) && m2.getVisible())) { + continue; + } + m2Data = mData[i2]; + if (i2 < i1 && !m2Data.willSpiderfy) { + continue; + } + if (this.ptDistanceSq(m1Data.pt, m2Data.pt) < pxSq) { + m1Data.willSpiderfy = m2Data.willSpiderfy = true; + break; + } + } + } + return mData; + }; + + p['markersNearAnyOtherMarker'] = function() { + var i, l, len1, m, mData, ref1, results; + mData = this.markerProximityData(); + ref1 = this.markers; + results = []; + for (i = l = 0, len1 = ref1.length; l < len1; i = ++l) { + m = ref1[i]; + if (mData[i].willSpiderfy) { + results.push(m); + } + } + return results; + }; + + p.setImmediate = function(func) { + return window.setTimeout(func, 0); + }; + + p.formatMarkers = function() { + if (this['basicFormatEvents']) { + return; + } + if (this.formatTimeoutId != null) { + return; + } + return this.formatTimeoutId = this.setImmediate((function(_this) { + return function() { + _this.formatTimeoutId = null; + if (_this.projHelper.getProjection() != null) { + return _this._formatMarkers(); + } else { + if (_this.formatIdleListener != null) { + return; + } + return _this.formatIdleListener = ge.addListenerOnce(_this.map, 'idle', function() { + return _this._formatMarkers(); + }); + } + }; + })(this)); + }; + + p._formatMarkers = function() { + var i, l, len1, len2, marker, n, proximities, ref1, results, results1, status; + if (this['basicFormatEvents']) { + results = []; + for (l = 0, len1 = markers.length; l < len1; l++) { + marker = markers[l]; + status = marker['_omsData'] != null ? 'SPIDERFIED' : 'UNSPIDERFIED'; + results.push(this.trigger('format', marker, this.constructor['markerStatus'][status])); + } + return results; + } else { + proximities = this.markerProximityData(); + ref1 = this.markers; + results1 = []; + for (i = n = 0, len2 = ref1.length; n < len2; i = ++n) { + marker = ref1[i]; + status = marker['_omsData'] != null ? 'SPIDERFIED' : proximities[i].willSpiderfy ? 'SPIDERFIABLE' : 'UNSPIDERFIABLE'; + results1.push(this.trigger('format', marker, this.constructor['markerStatus'][status])); + } + return results1; + } + }; + + p.makeHighlightListenerFuncs = function(marker) { + return { + highlight: (function(_this) { + return function() { + return marker['_omsData'].leg.setOptions({ + strokeColor: _this['legColors']['highlighted'][_this.map.mapTypeId], + zIndex: _this['highlightedLegZIndex'] + }); + }; + })(this), + unhighlight: (function(_this) { + return function() { + return marker['_omsData'].leg.setOptions({ + strokeColor: _this['legColors']['usual'][_this.map.mapTypeId], + zIndex: _this['usualLegZIndex'] + }); + }; + })(this) + }; + }; + + p.spiderfy = function(markerData, nonNearbyMarkers) { + var bodyPt, footLl, footPt, footPts, highlightListenerFuncs, leg, marker, md, nearestMarkerDatum, numFeet, spiderfiedMarkers; + this.spiderfying = true; + numFeet = markerData.length; + bodyPt = this.ptAverage((function() { + var l, len1, results; + results = []; + for (l = 0, len1 = markerData.length; l < len1; l++) { + md = markerData[l]; + results.push(md.markerPt); + } + return results; + })()); + footPts = numFeet >= this['circleSpiralSwitchover'] ? this.generatePtsSpiral(numFeet, bodyPt).reverse() : this.generatePtsCircle(numFeet, bodyPt); + spiderfiedMarkers = (function() { + var l, len1, results; + results = []; + for (l = 0, len1 = footPts.length; l < len1; l++) { + footPt = footPts[l]; + footLl = this.ptToLl(footPt); + nearestMarkerDatum = this.minExtract(markerData, (function(_this) { + return function(md) { + return _this.ptDistanceSq(md.markerPt, footPt); + }; + })(this)); + marker = nearestMarkerDatum.marker; + leg = new gm.Polyline({ + map: this.map, + path: [marker.position, footLl], + strokeColor: this['legColors']['usual'][this.map.mapTypeId], + strokeWeight: this['legWeight'], + zIndex: this['usualLegZIndex'] + }); + marker['_omsData'] = { + usualPosition: marker.getPosition(), + usualZIndex: marker.getZIndex(), + leg: leg + }; + if (this['legColors']['highlighted'][this.map.mapTypeId] !== this['legColors']['usual'][this.map.mapTypeId]) { + highlightListenerFuncs = this.makeHighlightListenerFuncs(marker); + marker['_omsData'].hightlightListeners = { + highlight: ge.addListener(marker, 'mouseover', highlightListenerFuncs.highlight), + unhighlight: ge.addListener(marker, 'mouseout', highlightListenerFuncs.unhighlight) + }; + } + this.trigger('format', marker, this.constructor['markerStatus']['SPIDERFIED']); + marker.setPosition(footLl); + marker.setZIndex(Math.round(this['spiderfiedZIndex'] + footPt.y)); + results.push(marker); + } + return results; + }).call(this); + delete this.spiderfying; + this.spiderfied = true; + return this.trigger('spiderfy', spiderfiedMarkers, nonNearbyMarkers); + }; + + p['unspiderfy'] = function(markerNotToMove) { + var l, len1, listeners, marker, nonNearbyMarkers, ref1, status, unspiderfiedMarkers; + if (markerNotToMove == null) { + markerNotToMove = null; + } + if (this.spiderfied == null) { + return this; + } + this.unspiderfying = true; + unspiderfiedMarkers = []; + nonNearbyMarkers = []; + ref1 = this.markers; + for (l = 0, len1 = ref1.length; l < len1; l++) { + marker = ref1[l]; + if (marker['_omsData'] != null) { + marker['_omsData'].leg.setMap(null); + if (marker !== markerNotToMove) { + marker.setPosition(marker['_omsData'].usualPosition); + } + marker.setZIndex(marker['_omsData'].usualZIndex); + listeners = marker['_omsData'].hightlightListeners; + if (listeners != null) { + ge.removeListener(listeners.highlight); + ge.removeListener(listeners.unhighlight); + } + delete marker['_omsData']; + if (marker !== markerNotToMove) { + status = this['basicFormatEvents'] ? 'UNSPIDERFIED' : 'SPIDERFIABLE'; + this.trigger('format', marker, this.constructor['markerStatus'][status]); + } + unspiderfiedMarkers.push(marker); + } else { + nonNearbyMarkers.push(marker); + } + } + delete this.unspiderfying; + delete this.spiderfied; + this.trigger('unspiderfy', unspiderfiedMarkers, nonNearbyMarkers); + return this; + }; + + p.ptDistanceSq = function(pt1, pt2) { + var dx, dy; + dx = pt1.x - pt2.x; + dy = pt1.y - pt2.y; + return dx * dx + dy * dy; + }; + + p.ptAverage = function(pts) { + var l, len1, numPts, pt, sumX, sumY; + sumX = sumY = 0; + for (l = 0, len1 = pts.length; l < len1; l++) { + pt = pts[l]; + sumX += pt.x; + sumY += pt.y; + } + numPts = pts.length; + return new gm.Point(sumX / numPts, sumY / numPts); + }; + + p.llToPt = function(ll) { + return this.projHelper.getProjection().fromLatLngToDivPixel(ll); + }; + + p.ptToLl = function(pt) { + return this.projHelper.getProjection().fromDivPixelToLatLng(pt); + }; + + p.minExtract = function(set, func) { + var bestIndex, bestVal, index, item, l, len1, val; + for (index = l = 0, len1 = set.length; l < len1; index = ++l) { + item = set[index]; + val = func(item); + if ((typeof bestIndex === "undefined" || bestIndex === null) || val < bestVal) { + bestVal = val; + bestIndex = index; + } + } + return set.splice(bestIndex, 1)[0]; + }; + + p.arrIndexOf = function(arr, obj) { + var i, l, len1, o; + if (arr.indexOf != null) { + return arr.indexOf(obj); + } + for (i = l = 0, len1 = arr.length; l < len1; i = ++l) { + o = arr[i]; + if (o === obj) { + return i; + } + } + return -1; + }; + + return _Class; + + })(); + + callbackRegEx = /(\?.*(&|&)|\?)spiderfier_callback=(\w+)/; + + scriptTag = document.currentScript; + + if (scriptTag == null) { + scriptTag = ((function() { + var j, len, ref, ref1, results; + ref = document.getElementsByTagName('script'); + results = []; + for (j = 0, len = ref.length; j < len; j++) { + tag = ref[j]; + if ((ref1 = tag.getAttribute('src')) != null ? ref1.match(callbackRegEx) : void 0) { + results.push(tag); + } + } + return results; + })())[0]; + } + + if (scriptTag != null) { + callbackName = (ref = scriptTag.getAttribute('src')) != null ? (ref1 = ref.match(callbackRegEx)) != null ? ref1[3] : void 0 : void 0; + if (callbackName) { + if (typeof window[callbackName] === "function") { + window[callbackName](); + } + } + } + + if (typeof window['spiderfier_callback'] === "function") { + window['spiderfier_callback'](); + } + +}).call(this); diff --git a/croutonjs/dist/crouton-map.min.js b/croutonjs/dist/crouton-map.min.js new file mode 100644 index 0000000..b3f68d0 --- /dev/null +++ b/croutonjs/dist/crouton-map.min.js @@ -0,0 +1 @@ +function CroutonMap(t){var e=this;e.config={google_api_key:null,distance_units:"mi"},Object.assign(e.config,t),e.map=null,e.geocoder=null,e.map_objects=[],e.map_clusters=[],e.oms=null,e.markerClusterer=null,e.loadGapi=function(t){var r=document.createElement("script");r.src="https://maps.googleapis.com/maps/api/js?key="+e.config.google_api_key+"&callback="+t,r.defer=!0,r.async=!0;var i=document.getElementsByTagName("script")[0];i.parentNode.insertBefore(r,i)},e.getCurrentLocation=function(t){navigator.geolocation?navigator.geolocation.getCurrentPosition(t,e.errorHandler):$(".geo").removeClass("hide").addClass("show").html("

Geolocation is not supported by your browser

")},e.addCurrentLocationPin=function(t,r){var i=new google.maps.LatLng(t,r);e.map.setCenter(i);var s=new google.maps.Marker({map:e.map,position:i});e.addToMapObjectCollection(s)},e.findMarkerById=function(t){for(var r=0;r0;)e.map_clusters[0].setMap(null),e.map_clusters.splice(0,1);null!==e.oms&&e.oms.removeAllMarkers(),null!==e.markerClusterer&&e.markerClusterer.clearMarkers()},e.clearAllMapObjects=function(){for(;e.map_objects.length>0;)e.map_objects[0].setMap(null),e.map_objects.splice(0,1)}}function MarkerClusterer(t,e,r){this.extend(MarkerClusterer,google.maps.OverlayView),this.map_=t,this.markers_=[],this.clusters_=[],this.sizes=[53,56,66,78,90],this.styles_=[],this.ready_=!1;var i=r||{};this.gridSize_=i.gridSize||60,this.minClusterSize_=i.minimumClusterSize||2,this.maxZoom_=i.maxZoom||null,this.styles_=i.styles||[],this.imagePath_=i.imagePath||this.MARKER_CLUSTER_IMAGE_PATH_,this.imageExtension_=i.imageExtension||this.MARKER_CLUSTER_IMAGE_EXTENSION_,this.zoomOnClick_=!0,null!=i.zoomOnClick&&(this.zoomOnClick_=i.zoomOnClick),this.averageCenter_=!1,null!=i.averageCenter&&(this.averageCenter_=i.averageCenter),this.setupStyles_(),this.setMap(t),this.prevZoom_=this.map_.getZoom();var s=this;google.maps.event.addListener(this.map_,"zoom_changed",function(){var t=s.map_.getZoom(),e=s.map_.minZoom||0,r=Math.min(s.map_.maxZoom||100,s.map_.mapTypes[s.map_.getMapTypeId()].maxZoom);t=Math.min(Math.max(t,e),r),s.prevZoom_!=t&&(s.prevZoom_=t,s.resetViewport())}),google.maps.event.addListener(this.map_,"idle",function(){s.redraw()}),e&&(e.length||Object.keys(e).length)&&this.addMarkers(e,!1)}function Cluster(t){this.markerClusterer_=t,this.map_=t.getMap(),this.gridSize_=t.getGridSize(),this.minClusterSize_=t.getMinClusterSize(),this.averageCenter_=t.isAverageCenter(),this.center_=null,this.markers_=[],this.bounds_=null,this.clusterIcon_=new ClusterIcon(this,t.getStyles(),t.getGridSize())}function ClusterIcon(t,e,r){t.getMarkerClusterer().extend(ClusterIcon,google.maps.OverlayView),this.styles_=e,this.padding_=r||0,this.cluster_=t,this.center_=null,this.map_=t.getMap(),this.div_=null,this.sums_=null,this.visible_=!1,this.setMap(this.map_)}CroutonMap.prototype.mapSearchClickMode=function(){this.mapClickSearchMode=!0,this.map.setOptions({draggableCursor:"crosshair",zoomControl:!1,gestureHandling:"none"})},CroutonMap.prototype.reset=function(){},CroutonMap.prototype.mapSearchPanZoomMode=function(){this.mapClickSearchMode=!1,this.map.setOptions({draggableCursor:"default",zoomControl:!0,gestureHandling:"auto"})},CroutonMap.prototype.mapSearchNearMeMode=function(){this.mapSearchPanZoomMode(),this.getCurrentLocation(function(t){crouton.searchByCoordinates(t.coords.latitude,t.coords.longitude)})},CroutonMap.prototype.mapSearchTextMode=function(t){this.mapSearchPanZoomMode(),null!=t&&""!==t&&this.geocoder.geocode({address:t},function(t,e){"OK"===e?crouton.searchByCoordinates(t[0].geometry.location.lat(),t[0].geometry.location.lng()):console.log("Geocode was not successful for the following reason: "+e)})},Crouton.prototype.renderMap=function(){var t=this;jQuery("#bmlt-tabs").before("
"),t.geocoder=new google.maps.Geocoder,jQuery.when(jQuery.getJSON(t.config.template_path+"/themes/"+t.config.theme+".json").then(function(e,r,i){return t.config.theme_js=e.google_map_theme})).then(function(){t.map=new google.maps.Map(document.getElementById("bmlt-map"),{zoom:t.config.map_search.zoom||10,center:{lat:t.config.map_search.latitude,lng:t.config.map_search.longitude},mapTypeControl:!1,styles:t.config.theme_js});var e=document.createElement("div"),r=document.createElement("div");r.className="mapcontrolcontainer",r.title="Click to recenter the map",e.appendChild(r);var i=document.createElement("div");i.className="mapcontrols",i.innerHTML='",r.appendChild(i),e.index=1,google.maps.event.addDomListener(i,"click",function(){var e=jQuery("input:radio[name='mapcontrols']:checked").attr("id");"textsearch"===e?t.mapSearchTextMode(prompt("Enter a location or postal code:")):"nearme"===e?t.mapSearchNearMeMode():"clicksearch"===e&&t.mapSearchClickMode()}),t.map.controls[google.maps.ControlPosition.TOP_LEFT].push(e),t.map.addListener("click",function(e){t.mapClickSearchMode&&(t.mapSearchPanZoomMode(),crouton.searchByCoordinates(e.latLng.lat(),e.latLng.lng()))}),t.config.map_search.auto?t.mapSearchNearMeMode():void 0!==t.config.map_search.location?t.mapSearchTextMode(t.config.map_search.location):t.config.map_search.coordinates_search&&crouton.searchByCoordinates(t.config.map_search.latitude,t.config.map_search.longitude)})},CroutonMap.prototype.render=function(){this.loadGapi("croutonMap.renderMap")},CroutonMap.prototype.initialize=function(t,e,r=[]){this.meetingData=t,this.formatsData=e,this.handlebarMapOptions=r,this.loadGapi("croutonMap.initMap")},CroutonMap.prototype.embed=function(t,e){this.meetingData=t,this.formatsData=e,this.loadGapi("croutonMap.mapPage")},CroutonMap.prototype.initMap=function(t){if(null==this.map){jQuery("#bmlt-tabs").before("
");var e={zoom:3};this.handlebarMapOptions.length>0&&(e={center:new google.maps.LatLng(this.handlebarMapOptions.lat,this.handlebarMapOptions.lng),zoom:this.handlebarMapOptions.zoom,mapTypeId:google.maps.MapTypeId.ROADMAP}),this.map=new google.maps.Map(document.getElementById("bmlt-map"),e)}jQuery("#bmlt-map").removeClass("hide"),this.fillMap(t)},CroutonMap.prototype.mapPage=function(t){null==this.map&&(this.map=new google.maps.Map(document.getElementById("byfield_embeddedMapPage"),{zoom:3,maxZoom:17})),this.fillMap(t)},CroutonMap.prototype.fillMap=function(t,e=null){if("undefined"!=typeof crouton_external_map)return void crouton_external_map.filterFromCrouton(e);var r=this;if(null==r.map)return;r.clearAllMapObjects(),r.clearAllMapClusters();const i=r.meetingData.filter(t=>t.venue_type!=venueType.VIRTUAL),s=null===e?i:i.filter(t=>e.includes(t.id_bigint)),o=s.reduce(function(t,e){return t.extend(new google.maps.LatLng(e.latitude,e.longitude))},new google.maps.LatLngBounds);r.map.fitBounds(o);var n=new google.maps.InfoWindow;r.oms=new OverlappingMarkerSpiderfier(r.map,{markersWontMove:!0,markersWontHide:!0}),r.oms.addListener("format",function(t,e){var i;i=e===OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED||e===OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE||e===OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIED?r.config.template_path+"/NAMarkerR.png":e===OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE?r.config.template_path+"/NAMarkerB.png":null;var s=new google.maps.Size(22,32);t.setIcon({url:i,size:s,scaledSize:s})}),r.map.addListener("zoom_changed",function(){r.map.addListener("idle",function(){for(var t=r.oms.markersNearAnyOtherMarker(),e=0;e",i+="
",i+=crouton.localization.getDayOfTheWeekWord(t.weekday_tinyint);var s=t.start_time.toString().split(":"),o=parseInt(s[0]),a=parseInt(s[1]),l="AM";o>=12&&(l="PM",o>12&&(o-=12)),o=o.toString(),a=a>9?a.toString():"0"+a.toString(),i+=" "+o+":"+a+" "+l,i+="
",i+=t.location_text,i+="
",void 0!==t.location_street&&(i+=t.location_street+"
"),void 0!==t.location_municipality&&(i+=t.location_municipality+" "),void 0!==t.location_province&&(i+=t.location_province+" "),void 0!==t.location_postal_code_1&&(i+=t.location_postal_code_1),i+="
";var h="https://maps.google.com/maps?q="+t.latitude+","+t.longitude+"&hl="+r.config.short_language;i+='',i+=crouton.localization.getWord("map"),i+="",i+="
";var u={lat:parseFloat(t.latitude),lng:parseFloat(t.longitude)},p=new google.maps.Marker({position:u});return p.id=t.id_bigint,p.day_id=t.weekday_tinyint,r.addToMapObjectCollection(p),r.oms.addMarker(p),r.map_clusters.push(p),google.maps.event.addListener(p,"click",function(t){jQuery(".bmlt-data-row > td").removeClass("rowHighlight"),jQuery("#meeting-data-row-"+p.id+" > td").addClass("rowHighlight"),crouton.dayTab(p.day_id),n.setContent(i),n.open(r.map,p)}),p}),r.markerClusterer=new MarkerClusterer(r.map,r.map_clusters,{imagePath:r.config.template_path+"/m",maxZoom:r.config.map_max_zoom,zoomOnClick:!1}),void 0!==t&&isFunction(t)&&t()},MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_PATH_="../images/m",MarkerClusterer.prototype.MARKER_CLUSTER_IMAGE_EXTENSION_="png",MarkerClusterer.prototype.extend=function(t,e){return function(t){for(var e in t.prototype)this.prototype[e]=t.prototype[e];return this}.apply(t,[e])},MarkerClusterer.prototype.onAdd=function(){this.setReady_(!0)},MarkerClusterer.prototype.draw=function(){},MarkerClusterer.prototype.setupStyles_=function(){if(!this.styles_.length)for(var t,e=0;t=this.sizes[e];e++)this.styles_.push({url:this.imagePath_+(e+1)+"."+this.imageExtension_,height:t,width:t})},MarkerClusterer.prototype.fitMapToMarkers=function(){for(var t,e=this.getMarkers(),r=new google.maps.LatLngBounds,i=0;t=e[i];i++)r.extend(t.getPosition());this.map_.fitBounds(r)},MarkerClusterer.prototype.setStyles=function(t){this.styles_=t},MarkerClusterer.prototype.getStyles=function(){return this.styles_},MarkerClusterer.prototype.isZoomOnClick=function(){return this.zoomOnClick_},MarkerClusterer.prototype.isAverageCenter=function(){return this.averageCenter_},MarkerClusterer.prototype.getMarkers=function(){return this.markers_},MarkerClusterer.prototype.getTotalMarkers=function(){return this.markers_.length},MarkerClusterer.prototype.setMaxZoom=function(t){this.maxZoom_=t},MarkerClusterer.prototype.getMaxZoom=function(){return this.maxZoom_},MarkerClusterer.prototype.calculator_=function(t,e){for(var r=0,i=t.length,s=i;0!==s;)s=parseInt(s/10,10),r++;return{text:i,index:r=Math.min(r,e)}},MarkerClusterer.prototype.setCalculator=function(t){this.calculator_=t},MarkerClusterer.prototype.getCalculator=function(){return this.calculator_},MarkerClusterer.prototype.addMarkers=function(t,e){if(t.length)for(var r=0;i=t[r];r++)this.pushMarkerTo_(i);else if(Object.keys(t).length)for(var i in t)this.pushMarkerTo_(t[i]);e||this.redraw()},MarkerClusterer.prototype.pushMarkerTo_=function(t){if(t.isAdded=!1,t.draggable){var e=this;google.maps.event.addListener(t,"dragend",function(){t.isAdded=!1,e.repaint()})}this.markers_.push(t)},MarkerClusterer.prototype.addMarker=function(t,e){this.pushMarkerTo_(t),e||this.redraw()},MarkerClusterer.prototype.removeMarker_=function(t){var e=-1;if(this.markers_.indexOf)e=this.markers_.indexOf(t);else for(var r,i=0;r=this.markers_[i];i++)if(r==t){e=i;break}return-1!=e&&(t.setMap(null),this.markers_.splice(e,1),!0)},MarkerClusterer.prototype.removeMarker=function(t,e){var r=this.removeMarker_(t);return!(e||!r)&&(this.resetViewport(),this.redraw(),!0)},MarkerClusterer.prototype.removeMarkers=function(t,e){for(var r,i=!1,s=0;r=t[s];s++){var o=this.removeMarker_(r);i=i||o}if(!e&&i)return this.resetViewport(),this.redraw(),!0},MarkerClusterer.prototype.setReady_=function(t){this.ready_||(this.ready_=t,this.createClusters_())},MarkerClusterer.prototype.getTotalClusters=function(){return this.clusters_.length},MarkerClusterer.prototype.getMap=function(){return this.map_},MarkerClusterer.prototype.setMap=function(t){this.map_=t},MarkerClusterer.prototype.getGridSize=function(){return this.gridSize_},MarkerClusterer.prototype.setGridSize=function(t){this.gridSize_=t},MarkerClusterer.prototype.getMinClusterSize=function(){return this.minClusterSize_},MarkerClusterer.prototype.setMinClusterSize=function(t){this.minClusterSize_=t},MarkerClusterer.prototype.getExtendedBounds=function(t){var e=this.getProjection(),r=new google.maps.LatLng(t.getNorthEast().lat(),t.getNorthEast().lng()),i=new google.maps.LatLng(t.getSouthWest().lat(),t.getSouthWest().lng()),s=e.fromLatLngToDivPixel(r);s.x+=this.gridSize_,s.y-=this.gridSize_;var o=e.fromLatLngToDivPixel(i);o.x-=this.gridSize_,o.y+=this.gridSize_;var n=e.fromDivPixelToLatLng(s),a=e.fromDivPixelToLatLng(o);return t.extend(n),t.extend(a),t},MarkerClusterer.prototype.isMarkerInBounds_=function(t,e){return e.contains(t.getPosition())},MarkerClusterer.prototype.clearMarkers=function(){this.resetViewport(!0),this.markers_=[]},MarkerClusterer.prototype.resetViewport=function(t){for(var e,r=0;e=this.clusters_[r];r++)e.remove();var i;for(r=0;i=this.markers_[r];r++)i.isAdded=!1,t&&i.setMap(null);this.clusters_=[]},MarkerClusterer.prototype.repaint=function(){var t=this.clusters_.slice();this.clusters_.length=0,this.resetViewport(),this.redraw(),window.setTimeout(function(){for(var e,r=0;e=t[r];r++)e.remove()},0)},MarkerClusterer.prototype.redraw=function(){this.createClusters_()},MarkerClusterer.prototype.distanceBetweenPoints_=function(t,e){if(!t||!e)return 0;var r=(e.lat()-t.lat())*Math.PI/180,i=(e.lng()-t.lng())*Math.PI/180,s=Math.sin(r/2)*Math.sin(r/2)+Math.cos(t.lat()*Math.PI/180)*Math.cos(e.lat()*Math.PI/180)*Math.sin(i/2)*Math.sin(i/2);return 6371*(2*Math.atan2(Math.sqrt(s),Math.sqrt(1-s)))},MarkerClusterer.prototype.addToClosestCluster_=function(t){for(var e,r=4e4,i=null,s=(t.getPosition(),0);e=this.clusters_[s];s++){var o=e.getCenter();if(o){var n=this.distanceBetweenPoints_(o,t.getPosition());n=this.minClusterSize_&&t.setMap(null),this.updateIcon(),!0},Cluster.prototype.getMarkerClusterer=function(){return this.markerClusterer_},Cluster.prototype.getBounds=function(){for(var t,e=new google.maps.LatLngBounds(this.center_,this.center_),r=this.getMarkers(),i=0;t=r[i];i++)e.extend(t.getPosition());return e},Cluster.prototype.remove=function(){this.clusterIcon_.remove(),this.markers_.length=0,delete this.markers_},Cluster.prototype.getSize=function(){return this.markers_.length},Cluster.prototype.getMarkers=function(){return this.markers_},Cluster.prototype.getCenter=function(){return this.center_},Cluster.prototype.calculateBounds_=function(){var t=new google.maps.LatLngBounds(this.center_,this.center_);this.bounds_=this.markerClusterer_.getExtendedBounds(t)},Cluster.prototype.isMarkerInClusterBounds=function(t){return this.bounds_.contains(t.getPosition())},Cluster.prototype.getMap=function(){return this.map_},Cluster.prototype.updateIcon=function(){var t=this.map_.getZoom(),e=this.markerClusterer_.getMaxZoom();if(e&&t>e)for(var r,i=0;r=this.markers_[i];i++)r.setMap(this.map_);else if(this.markers_.length0&&this.anchor_[0]0&&this.anchor_[1]l;o=0<=l?++n:--n)i=this.circleStartAngle+o*s,u.push(new e.Point(r.x+a*Math.cos(i),r.y+a*Math.sin(i)));return u},o.generatePtsSpiral=function(t,r){var i,s,o,n,a,l,u;for(n=this.spiralLengthStart,i=0,u=[],s=o=0,l=t;0<=l?ol;s=0<=l?++o:--o)i+=this.spiralFootSeparation/n+5e-4*s,a=new e.Point(r.x+n*Math.cos(i),r.y+n*Math.sin(i)),n+=h*this.spiralLengthFactor/i,u.push(a);return u},o.spiderListener=function(t,e){var r,i,s,o,n,a,l,h,u,p,c;if((a=null!=t._omsData)&&this.keepSpiderfied||this.unspiderfy(),a||this.map.getStreetView().getVisible()||"GoogleEarthAPI"===this.map.getMapTypeId())return this.trigger("click",t,e);for(h=[],u=[],p=(l=this.nearbyDistance)*l,n=this.llToPt(t.position),r=0,i=(c=this.markers).length;r=this.circleSpiralSwitchover?this.generatePtsSpiral(m,s).reverse():this.generatePtsCircle(m,s),d=function(){var i,s,p;for(p=[],i=0,s=a.length;i "observerTemplate"); function Crouton(config) { var self = this; self.mutex = false; - self.map = null; self.filtering = false; - self.geocoder = null; - self.map_objects = []; - self.map_clusters = []; - self.oms = null; - self.markerClusterer = null; self.masterFormatCodes = []; self.max_filters = 10; // TODO: needs to be refactored so that dropdowns are treated dynamically - self.handlebarMapOptions = []; self.config = { on_complete: null, // Javascript function to callback when data querying is completed. root_server: null, // The root server to use. @@ -66,7 +59,6 @@ function Crouton(config) { auto_tz_adjust: false, // Will auto adjust the time zone, by default will assume the timezone is local time base_tz: null, // In conjunction with auto_tz_adjust the timezone to base from. Choices are listed here: https://github.com/bmlt-enabled/crouton/blob/master/croutonjs/src/js/moment-timezone.js#L623 custom_query: null, // Enables overriding the services related queries for a custom one - google_api_key: null, // Required if using the show_map option. Be sure to add an HTTP restriction as well. sort_keys: "start_time", // Controls sort keys on the query int_start_day_id: 1, // Controls the first day of the week sequence. Sunday is 1. view_by: "weekday", // TODO: replace with using the first choice in button_filters as the default view_by. @@ -83,23 +75,7 @@ function Crouton(config) { }; self.setConfig(config); - self.loadGapi = function(callbackFunctionName) { - var tag = document.createElement('script'); - tag.src = "https://maps.googleapis.com/maps/api/js?key=" + self.config['google_api_key'] + "&callback=" + callbackFunctionName; - tag.defer = true; - tag.async = true; - var firstScriptTag = document.getElementsByTagName('script')[0]; - firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); - }; - self.getCurrentLocation = function(callback) { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(callback, self.errorHandler); - } else { - $('.geo').removeClass("hide").addClass("show").html('

Geolocation is not supported by your browser

'); - } - }; - - self.searchByCoordinates = function(latitude, longitude) { + Crouton.prototype.searchByCoordinates = function(latitude, longitude) { var width = self.config['map_search']['width'] || -50; self.config['custom_query'] = (self.config['custom_query'] !== null ? self.config['custom_query'] : "") @@ -114,72 +90,6 @@ function Crouton(config) { }); }); }; - - self.addCurrentLocationPin = function(latitude, longitude) { - var latlng = new google.maps.LatLng(latitude, longitude); - self.map.setCenter(latlng); - - var currentLocationMarker = new google.maps.Marker({ - "map": self.map, - "position": latlng, - }); - - self.addToMapObjectCollection(currentLocationMarker); - - // TODO: needs to show on click only - /*var infowindow = new google.maps.InfoWindow({ - "content": 'Current Location', - "position": latlng, - }).open(self.map, currentLocationMarker);*/ - }; - - self.findMarkerById = function(id) { - for (var m = 0; m < self.map_objects.length; m++) { - var map_object = self.map_objects[m]; - if (parseInt(map_object['id']) === id) { - return map_object; - } - } - - return null; - }; - - self.rowClick = function(id) { - var map_marker = self.findMarkerById(id); - if (!map_marker) return; - self.map.setCenter(map_marker.getPosition()); - self.map.setZoom(17); - google.maps.event.trigger(map_marker, "click"); - }; - - self.addToMapObjectCollection = function(obj) { - self.map_objects.push(obj); - }; - - self.clearAllMapClusters = function() { - while (self.map_clusters.length > 0) { - self.map_clusters[0].setMap(null); - self.map_clusters.splice(0, 1); - } - - if (self.oms !== null) { - self.oms.removeAllMarkers(); - } - - if (self.markerClusterer !== null) { - self.markerClusterer.clearMarkers(); - } - }; - - self.clearAllMapObjects = function() { - while (self.map_objects.length > 0) { - self.map_objects[0].setMap(null); - self.map_objects.splice(0, 1); - } - - //infoWindow.close(); - }; - self.getMeetings = function(url) { var promises = [fetchJsonp(this.config['root_server'] + url).then(function(response) { return response.json(); })]; @@ -354,7 +264,7 @@ function Crouton(config) { }; if (self.config['map_search'] !== null) { - self.loadGapi('crouton.renderMap'); + croutonMap.render(); } else { self.meetingSearch(); } @@ -473,12 +383,12 @@ function Crouton(config) { }); showingNow = [...new Set(showingNow)]; if (self.config.map_page) { - this.fillMap(null, showingNow); + croutonMap.fillMap(null, showingNow); if (!jQuery('#byfield_embeddedMapPage').hasClass('hide')) { jQuery('#displayTypeButton_tablePages').removeClass('hide'); jQuery('#filterButton_embeddedMapPage').addClass('hide'); } - } else if (self.config.show_map) this.fillMap(null, showingNow); + } else if (self.config.show_map) croutonMap.fillMap(null, showingNow); if (!self.config.map_page || jQuery('#byfield_embeddedMapPage').hasClass('hide')) { self.showFilteredMeetingsAsTable(); @@ -503,10 +413,10 @@ function Crouton(config) { } self.resetFilter = function () { if (self.config.map_page) { - if (self.filtering) self.fillMap(null); + if (self.filtering) croutonMap.fillMap(null); jQuery('#displayTypeButton_tablePages').addClass('hide'); jQuery('#filterButton_embeddedMapPage').removeClass('hide'); - } else if (self.config.show_map && self.filtering) self.fillMap(null); + } else if (self.config.show_map && self.filtering) croutonMap.fillMap(null); self.filtering = false; jQuery(".filter-dropdown").val(null).trigger("change"); jQuery(".meeting-header").removeClass("hide"); @@ -795,8 +705,7 @@ Crouton.prototype.reset = function() { var self = this; jQuery("#custom-css").remove(); jQuery("#" + self.config["placeholder_id"]).html(""); - self.clearAllMapObjects(); - self.clearAllMapClusters(); + croutonMap.reset(); }; Crouton.prototype.meetingCount = function(callback) { @@ -885,6 +794,7 @@ Crouton.prototype.doHandlebars = function() { var customEnrichTemplate = crouton_Handlebars.compile('{{enrich this}}'); customEnrichTemplate(enrichedMeetingData[0]); var mustDoMap = false; + handlebarMapOptions = []; crouton_Handlebars.registerHelper('crouton_map', function(options) { mustDoMap = true; self.handlebarMapOptions = options.hash; @@ -945,7 +855,7 @@ Crouton.prototype.doHandlebars = function() { } if (mustDoMap) { self.meetingData = enrichedMeetingData; - self.loadGapi('crouton.initMap'); + croutonMap.initialize(self.meetingData, self.formatsData, self.handlebarMapOptions); } }); }); @@ -1133,7 +1043,7 @@ Crouton.prototype.render = function() { if (self.config['map_search'] != null || self.config['show_map']) { jQuery(".bmlt-data-row").css({cursor: "pointer"}); jQuery(".bmlt-data-row").click(function () { - self.rowClick(parseInt(this.id.replace("meeting-data-row-", ""))); + croutonMap.rowClick(parseInt(this.id.replace("meeting-data-row-", ""))); }); } @@ -1219,12 +1129,12 @@ Crouton.prototype.render = function() { } if (self.config['show_map']) { - self.loadGapi('crouton.initMap'); + croutonMap.initialize(self.meetingData, self.formatsData); } if (self.config['map_page']) { if (typeof crouton_external_map === 'undefined') - self.loadGapi('crouton.mapPage'); + croutonMap.embed(self.meetingData, self.formatsData); else { crouton_external_map.loadMapExt(document.getElementById('byfield_embeddedMapPage'), self.meetingData, self.formatsData); const attrObserver = new MutationObserver((mutations) => { @@ -1246,273 +1156,6 @@ Crouton.prototype.render = function() { }); }; -Crouton.prototype.mapSearchClickMode = function() { - var self = this; - self.mapClickSearchMode = true; - self.map.setOptions({ - draggableCursor: 'crosshair', - zoomControl: false, - gestureHandling: 'none' - }); -}; - -Crouton.prototype.mapSearchPanZoomMode = function() { - var self = this; - self.mapClickSearchMode = false; - self.map.setOptions({ - draggableCursor: 'default', - zoomControl: true, - gestureHandling: 'auto' - }); -}; - -Crouton.prototype.mapSearchNearMeMode = function() { - var self = this; - self.mapSearchPanZoomMode(); - self.getCurrentLocation(function(position) { - self.searchByCoordinates(position.coords.latitude, position.coords.longitude); - }); -}; - -Crouton.prototype.mapSearchTextMode = function(location) { - var self = this; - self.mapSearchPanZoomMode(); - if (location !== undefined && location !== null && location !== "") { - self.geocoder.geocode({'address': location}, function (results, status) { - if (status === 'OK') { - self.searchByCoordinates(results[0].geometry.location.lat(), results[0].geometry.location.lng()); - } else { - console.log('Geocode was not successful for the following reason: ' + status); - } - }); - } -}; - -Crouton.prototype.renderMap = function() { - var self = this; - jQuery("#bmlt-tabs").before("
"); - self.geocoder = new google.maps.Geocoder(); - jQuery.when(jQuery.getJSON(self.config['template_path'] + "/themes/" + self.config['theme'] + ".json").then( - function (data, textStatus, jqXHR) { - return self.config["theme_js"] = data["google_map_theme"]; - } - )).then(function() { - self.map = new google.maps.Map(document.getElementById('bmlt-map'), { - zoom: self.config['map_search']['zoom'] || 10, - center: { - lat: self.config['map_search']['latitude'], - lng: self.config['map_search']['longitude'], - }, - mapTypeControl: false, - styles: self.config["theme_js"] - }); - - var controlDiv = document.createElement('div'); - - // Set CSS for the control border - var controlUI = document.createElement('div'); - controlUI.className = 'mapcontrolcontainer'; - controlUI.title = 'Click to recenter the map'; - controlDiv.appendChild(controlUI); - - // Set CSS for the control interior - var clickSearch = document.createElement('div'); - clickSearch.className = 'mapcontrols'; - clickSearch.innerHTML = ''; - controlUI.appendChild(clickSearch); - controlDiv.index = 1; - - google.maps.event.addDomListener(clickSearch, 'click', function () { - var controlsButtonSelections = jQuery("input:radio[name='mapcontrols']:checked").attr("id"); - if (controlsButtonSelections === "textsearch") { - self.mapSearchTextMode(prompt("Enter a location or postal code:")); - } else if (controlsButtonSelections === "nearme") { - self.mapSearchNearMeMode(); - } else if (controlsButtonSelections === "clicksearch") { - self.mapSearchClickMode(); - } - }); - - self.map.controls[google.maps.ControlPosition.TOP_LEFT].push(controlDiv); - self.map.addListener('click', function (data) { - if (self.mapClickSearchMode) { - self.mapSearchPanZoomMode(); - self.searchByCoordinates(data.latLng.lat(), data.latLng.lng()); - } - }); - - if (self.config['map_search']['auto']) { - self.mapSearchNearMeMode(); - } else if (self.config['map_search']['location'] !== undefined) { - self.mapSearchTextMode(self.config['map_search']['location']); - } else if (self.config['map_search']['coordinates_search']) { - self.searchByCoordinates(self.config['map_search']['latitude'], self.config['map_search']['longitude']); - } - }) -}; -Crouton.prototype.initMap = function(callback) { - var self = this; - if (self.map == null) { - jQuery("#bmlt-tabs").before("
"); - var mapOpt = { zoom: 3 }; - if (self.handlebarMapOptions.length > 0) mapOpt = { - center: new google.maps.LatLng(self.handlebarMapOptions.lat, self.handlebarMapOptions.lng), - zoom: self.handlebarMapOptions.zoom, - mapTypeId:google.maps.MapTypeId.ROADMAP - }; - self.map = new google.maps.Map(document.getElementById('bmlt-map'), mapOpt ); - } - - jQuery("#bmlt-map").removeClass("hide"); - self.fillMap(callback); -} -Crouton.prototype.mapPage = function(callback) { - var self = this; - if (self.map == null) { - self.map = new google.maps.Map(document.getElementById('byfield_embeddedMapPage'), { zoom: 3, maxZoom: 17 } ); - } - self.fillMap(callback); -} -Crouton.prototype.fillMap = function(callback, filteredIds=null) { - if (typeof crouton_external_map !== 'undefined') { - crouton_external_map.filterFromCrouton(filteredIds); - return; - } - var self = this; - if (self.map == null) return; - self.clearAllMapObjects(); - self.clearAllMapClusters(); - const physicalMeetings = self.meetingData.filter(m => m.venue_type != venueType.VIRTUAL); - const filteredMeetings = (filteredIds===null) ? physicalMeetings - : physicalMeetings.filter((m) => filteredIds.includes(m.id_bigint)); - const bounds = filteredMeetings.reduce( - function (bounds, m) { - return bounds.extend(new google.maps.LatLng(m.latitude, m.longitude)) - }, new google.maps.LatLngBounds()); - // We now have the full rectangle of our meeting search results. Scale the map to fit them. - self.map.fitBounds(bounds); - var infoWindow = new google.maps.InfoWindow(); - - // Create OverlappingMarkerSpiderfier instance - self.oms = new OverlappingMarkerSpiderfier(self.map, { - markersWontMove: true, - markersWontHide: true, - }); - - self.oms.addListener('format', function (marker, status) { - var iconURL; - if (status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED - || status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE - || status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIED) { - iconURL = self.config['template_path'] + '/NAMarkerR.png'; - } else if (status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE) { - iconURL = self.config['template_path'] + '/NAMarkerB.png'; - } else { - iconURL = null; - } - - var iconSize = new google.maps.Size(22, 32); - marker.setIcon({ - url: iconURL, - size: iconSize, - scaledSize: iconSize - }); - }); - - self.map.addListener('zoom_changed', function() { - self.map.addListener('idle', function() { - var spidered = self.oms.markersNearAnyOtherMarker(); - for (var i = 0; i < spidered.length; i ++) { - spidered[i].icon.url = self.config['template_path'] + '/NAMarkerR.png'; - } - }); - }); - - // This is necessary to make the Spiderfy work - self.oms.addListener('click', function (marker) { - marker.zIndex = 999; - infoWindow.setContent(marker.desc); - infoWindow.open(self.map, marker); - }); - // Add some markers to the map. - // Note: The code uses the JavaScript Array.prototype.map() method to - // create an array of markers based on a given "locations" array. - // The map() method here has nothing to do with the Google Maps API. - filteredMeetings.map(function (location, i) { - var marker_html = '
'; - marker_html += location.meeting_name; - marker_html += '
'; - marker_html += '
'; - marker_html += self.localization.getDayOfTheWeekWord(location.weekday_tinyint); - var time = location.start_time.toString().split(':'); - var hour = parseInt(time[0]); - var minute = parseInt(time[1]); - var pm = 'AM'; - if (hour >= 12) { - pm = 'PM'; - if (hour > 12) { - hour -= 12; - } - } - hour = hour.toString(); - minute = (minute > 9) ? minute.toString() : ('0' + minute.toString()); - marker_html += ' ' + hour + ':' + minute + ' ' + pm; - marker_html += '
'; - marker_html += location.location_text; - marker_html += '
'; - - if (typeof location.location_street !== "undefined") { - marker_html += location.location_street + '
'; - } - if (typeof location.location_municipality !== "undefined") { - marker_html += location.location_municipality + ' '; - } - if (typeof location.location_province !== "undefined") { - marker_html += location.location_province + ' '; - } - if (typeof location.location_postal_code_1 !== "undefined") { - marker_html += location.location_postal_code_1; - } - - marker_html += '
'; - var url = 'https://maps.google.com/maps?q=' + location.latitude + ',' + location.longitude + '&hl=' + self.config['short_language']; - marker_html += ''; - marker_html += self.localization.getWord('map'); - marker_html += ''; - marker_html += '
'; - - var latLng = {"lat": parseFloat(location.latitude), "lng": parseFloat(location.longitude)}; - - var marker = new google.maps.Marker({ - position: latLng - }); - - marker['id'] = location['id_bigint']; - marker['day_id'] = location['weekday_tinyint']; - - self.addToMapObjectCollection(marker); - self.oms.addMarker(marker); - - self.map_clusters.push(marker); - google.maps.event.addListener(marker, 'click', function (evt) { - jQuery(".bmlt-data-row > td").removeClass("rowHighlight"); - jQuery("#meeting-data-row-" + marker['id'] + " > td").addClass("rowHighlight"); - self.dayTab(marker['day_id']); - infoWindow.setContent(marker_html); - infoWindow.open(self.map, marker); - }); - return marker; - }); - - // Add a marker clusterer to manage the markers. - self.markerClusterer = new MarkerClusterer(self.map, self.map_clusters, { - imagePath: self.config['template_path'] + '/m', - maxZoom: self.config['map_max_zoom'], - zoomOnClick: false - }); - - if (callback !== undefined && isFunction(callback)) callback(); -}; function getTrueResult(options, ctx) { return options.fn !== undefined ? options.fn(ctx) : true; diff --git a/croutonjs/src/js/crouton-map.js b/croutonjs/src/js/crouton-map.js new file mode 100644 index 0000000..0033774 --- /dev/null +++ b/croutonjs/src/js/crouton-map.js @@ -0,0 +1,375 @@ +function CroutonMap(config) { + var self = this; + self.config = { + google_api_key: null, // Required if using the show_map option. Be sure to add an HTTP restriction as well. + distance_units: 'mi' + }; + Object.assign(self.config, config); + self.map = null; + self.geocoder = null; + self.map_objects = []; + self.map_clusters = []; + self.oms = null; + self.markerClusterer = null; + self.loadGapi = function(callbackFunctionName) { + var tag = document.createElement('script'); + tag.src = "https://maps.googleapis.com/maps/api/js?key=" + self.config['google_api_key'] + "&callback=" + callbackFunctionName; + tag.defer = true; + tag.async = true; + var firstScriptTag = document.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + }; + self.getCurrentLocation = function(callback) { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition(callback, self.errorHandler); + } else { + $('.geo').removeClass("hide").addClass("show").html('

Geolocation is not supported by your browser

'); + } + }; + + self.addCurrentLocationPin = function(latitude, longitude) { + var latlng = new google.maps.LatLng(latitude, longitude); + self.map.setCenter(latlng); + + var currentLocationMarker = new google.maps.Marker({ + "map": self.map, + "position": latlng, + }); + + self.addToMapObjectCollection(currentLocationMarker); + + // TODO: needs to show on click only + /*var infowindow = new google.maps.InfoWindow({ + "content": 'Current Location', + "position": latlng, + }).open(self.map, currentLocationMarker);*/ + }; + + self.findMarkerById = function(id) { + for (var m = 0; m < self.map_objects.length; m++) { + var map_object = self.map_objects[m]; + if (parseInt(map_object['id']) === id) { + return map_object; + } + } + + return null; + }; + + self.rowClick = function(id) { + var map_marker = self.findMarkerById(id); + if (!map_marker) return; + self.map.setCenter(map_marker.getPosition()); + self.map.setZoom(17); + google.maps.event.trigger(map_marker, "click"); + }; + + self.addToMapObjectCollection = function(obj) { + self.map_objects.push(obj); + }; + + self.clearAllMapClusters = function() { + while (self.map_clusters.length > 0) { + self.map_clusters[0].setMap(null); + self.map_clusters.splice(0, 1); + } + + if (self.oms !== null) { + self.oms.removeAllMarkers(); + } + + if (self.markerClusterer !== null) { + self.markerClusterer.clearMarkers(); + } + }; + + self.clearAllMapObjects = function() { + while (self.map_objects.length > 0) { + self.map_objects[0].setMap(null); + self.map_objects.splice(0, 1); + } + + //infoWindow.close(); + }; +} +CroutonMap.prototype.mapSearchClickMode = function() { + var self = this; + self.mapClickSearchMode = true; + self.map.setOptions({ + draggableCursor: 'crosshair', + zoomControl: false, + gestureHandling: 'none' + }); +}; +CroutonMap.prototype.reset = function() { + //self.clearAllMapObjects(); + //self.clearAllMapClusters(); +} +CroutonMap.prototype.mapSearchPanZoomMode = function() { + var self = this; + self.mapClickSearchMode = false; + self.map.setOptions({ + draggableCursor: 'default', + zoomControl: true, + gestureHandling: 'auto' + }); +}; + +CroutonMap.prototype.mapSearchNearMeMode = function() { + var self = this; + self.mapSearchPanZoomMode(); + self.getCurrentLocation(function(position) { + crouton.searchByCoordinates(position.coords.latitude, position.coords.longitude); + }); +}; + +CroutonMap.prototype.mapSearchTextMode = function(location) { + var self = this; + self.mapSearchPanZoomMode(); + if (location !== undefined && location !== null && location !== "") { + self.geocoder.geocode({'address': location}, function (results, status) { + if (status === 'OK') { + crouton.searchByCoordinates(results[0].geometry.location.lat(), results[0].geometry.location.lng()); + } else { + console.log('Geocode was not successful for the following reason: ' + status); + } + }); + } +}; + +Crouton.prototype.renderMap = function() { + var self = this; + jQuery("#bmlt-tabs").before("
"); + self.geocoder = new google.maps.Geocoder(); + jQuery.when(jQuery.getJSON(self.config['template_path'] + "/themes/" + self.config['theme'] + ".json").then( + function (data, textStatus, jqXHR) { + return self.config["theme_js"] = data["google_map_theme"]; + } + )).then(function() { + self.map = new google.maps.Map(document.getElementById('bmlt-map'), { + zoom: self.config['map_search']['zoom'] || 10, + center: { + lat: self.config['map_search']['latitude'], + lng: self.config['map_search']['longitude'], + }, + mapTypeControl: false, + styles: self.config["theme_js"] + }); + + var controlDiv = document.createElement('div'); + + // Set CSS for the control border + var controlUI = document.createElement('div'); + controlUI.className = 'mapcontrolcontainer'; + controlUI.title = 'Click to recenter the map'; + controlDiv.appendChild(controlUI); + + // Set CSS for the control interior + var clickSearch = document.createElement('div'); + clickSearch.className = 'mapcontrols'; + clickSearch.innerHTML = ''; + controlUI.appendChild(clickSearch); + controlDiv.index = 1; + + google.maps.event.addDomListener(clickSearch, 'click', function () { + var controlsButtonSelections = jQuery("input:radio[name='mapcontrols']:checked").attr("id"); + if (controlsButtonSelections === "textsearch") { + self.mapSearchTextMode(prompt("Enter a location or postal code:")); + } else if (controlsButtonSelections === "nearme") { + self.mapSearchNearMeMode(); + } else if (controlsButtonSelections === "clicksearch") { + self.mapSearchClickMode(); + } + }); + + self.map.controls[google.maps.ControlPosition.TOP_LEFT].push(controlDiv); + self.map.addListener('click', function (data) { + if (self.mapClickSearchMode) { + self.mapSearchPanZoomMode(); + crouton.searchByCoordinates(data.latLng.lat(), data.latLng.lng()); + } + }); + + if (self.config['map_search']['auto']) { + self.mapSearchNearMeMode(); + } else if (self.config['map_search']['location'] !== undefined) { + self.mapSearchTextMode(self.config['map_search']['location']); + } else if (self.config['map_search']['coordinates_search']) { + crouton.searchByCoordinates(self.config['map_search']['latitude'], self.config['map_search']['longitude']); + } + }) +}; +CroutonMap.prototype.render = function() { + this.loadGapi('croutonMap.renderMap'); +} +CroutonMap.prototype.initialize = function(meetingData, formatsData, handlebarMapOptions=[]) { + this.meetingData = meetingData; + this.formatsData = formatsData; + this.handlebarMapOptions = handlebarMapOptions; + this.loadGapi('croutonMap.initMap'); +} +CroutonMap.prototype.embed = function(meetingData, formatsData) { + this.meetingData = meetingData; + this.formatsData = formatsData; + this.loadGapi('croutonMap.mapPage'); +} +CroutonMap.prototype.initMap = function(callback) { + var self = this; + if (self.map == null) { + jQuery("#bmlt-tabs").before("
"); + var mapOpt = { zoom: 3 }; + if (self.handlebarMapOptions.length > 0) mapOpt = { + center: new google.maps.LatLng(self.handlebarMapOptions.lat, self.handlebarMapOptions.lng), + zoom: self.handlebarMapOptions.zoom, + mapTypeId:google.maps.MapTypeId.ROADMAP + }; + self.map = new google.maps.Map(document.getElementById('bmlt-map'), mapOpt ); + } + + jQuery("#bmlt-map").removeClass("hide"); + self.fillMap(callback); +} +CroutonMap.prototype.mapPage = function(callback) { + var self = this; + if (self.map == null) { + self.map = new google.maps.Map(document.getElementById('byfield_embeddedMapPage'), { zoom: 3, maxZoom: 17 } ); + } + self.fillMap(callback); +} +CroutonMap.prototype.fillMap = function(callback, filteredIds=null) { + if (typeof crouton_external_map !== 'undefined') { + crouton_external_map.filterFromCrouton(filteredIds); + return; + } + var self = this; + if (self.map == null) return; + self.clearAllMapObjects(); + self.clearAllMapClusters(); + const physicalMeetings = self.meetingData.filter(m => m.venue_type != venueType.VIRTUAL); + const filteredMeetings = (filteredIds===null) ? physicalMeetings + : physicalMeetings.filter((m) => filteredIds.includes(m.id_bigint)); + const bounds = filteredMeetings.reduce( + function (bounds, m) { + return bounds.extend(new google.maps.LatLng(m.latitude, m.longitude)) + }, new google.maps.LatLngBounds()); + // We now have the full rectangle of our meeting search results. Scale the map to fit them. + self.map.fitBounds(bounds); + var infoWindow = new google.maps.InfoWindow(); + + // Create OverlappingMarkerSpiderfier instance + self.oms = new OverlappingMarkerSpiderfier(self.map, { + markersWontMove: true, + markersWontHide: true, + }); + + self.oms.addListener('format', function (marker, status) { + var iconURL; + if (status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIED + || status === OverlappingMarkerSpiderfier.markerStatus.SPIDERFIABLE + || status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIED) { + iconURL = self.config['template_path'] + '/NAMarkerR.png'; + } else if (status === OverlappingMarkerSpiderfier.markerStatus.UNSPIDERFIABLE) { + iconURL = self.config['template_path'] + '/NAMarkerB.png'; + } else { + iconURL = null; + } + + var iconSize = new google.maps.Size(22, 32); + marker.setIcon({ + url: iconURL, + size: iconSize, + scaledSize: iconSize + }); + }); + + self.map.addListener('zoom_changed', function() { + self.map.addListener('idle', function() { + var spidered = self.oms.markersNearAnyOtherMarker(); + for (var i = 0; i < spidered.length; i ++) { + spidered[i].icon.url = self.config['template_path'] + '/NAMarkerR.png'; + } + }); + }); + + // This is necessary to make the Spiderfy work + self.oms.addListener('click', function (marker) { + marker.zIndex = 999; + infoWindow.setContent(marker.desc); + infoWindow.open(self.map, marker); + }); + // Add some markers to the map. + filteredMeetings.map(function (location, i) { + var marker_html = '
'; + marker_html += location.meeting_name; + marker_html += '
'; + marker_html += '
'; + marker_html += crouton.localization.getDayOfTheWeekWord(location.weekday_tinyint); + var time = location.start_time.toString().split(':'); + var hour = parseInt(time[0]); + var minute = parseInt(time[1]); + var pm = 'AM'; + if (hour >= 12) { + pm = 'PM'; + if (hour > 12) { + hour -= 12; + } + } + hour = hour.toString(); + minute = (minute > 9) ? minute.toString() : ('0' + minute.toString()); + marker_html += ' ' + hour + ':' + minute + ' ' + pm; + marker_html += '
'; + marker_html += location.location_text; + marker_html += '
'; + + if (typeof location.location_street !== "undefined") { + marker_html += location.location_street + '
'; + } + if (typeof location.location_municipality !== "undefined") { + marker_html += location.location_municipality + ' '; + } + if (typeof location.location_province !== "undefined") { + marker_html += location.location_province + ' '; + } + if (typeof location.location_postal_code_1 !== "undefined") { + marker_html += location.location_postal_code_1; + } + + marker_html += '
'; + var url = 'https://maps.google.com/maps?q=' + location.latitude + ',' + location.longitude + '&hl=' + self.config['short_language']; + marker_html += ''; + marker_html += crouton.localization.getWord('map'); + marker_html += ''; + marker_html += '
'; + + var latLng = {"lat": parseFloat(location.latitude), "lng": parseFloat(location.longitude)}; + + var marker = new google.maps.Marker({ + position: latLng + }); + + marker['id'] = location['id_bigint']; + marker['day_id'] = location['weekday_tinyint']; + + self.addToMapObjectCollection(marker); + self.oms.addMarker(marker); + + self.map_clusters.push(marker); + google.maps.event.addListener(marker, 'click', function (evt) { + jQuery(".bmlt-data-row > td").removeClass("rowHighlight"); + jQuery("#meeting-data-row-" + marker['id'] + " > td").addClass("rowHighlight"); + crouton.dayTab(marker['day_id']); + infoWindow.setContent(marker_html); + infoWindow.open(self.map, marker); + }); + return marker; + }); + + // Add a marker clusterer to manage the markers. + self.markerClusterer = new MarkerClusterer(self.map, self.map_clusters, { + imagePath: self.config['template_path'] + '/m', + maxZoom: self.config['map_max_zoom'], + zoomOnClick: false + }); + + if (callback !== undefined && isFunction(callback)) callback(); +}; diff --git a/gulpfile.js b/gulpfile.js index 853b7fe..02d1413 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -21,14 +21,17 @@ let jsFilesNoJQuery = [ 'crouton-default-templates.js', 'crouton-core.js', 'punycode.1.4.1.js', - 'markerclusterer.js', - 'oms-1.0.3.min.js', 'fetch-jsonp.js', 'promises-polyfill.js', ]; let jsFilesWithJquery = [ 'jquery-3.4.1.min.js', ].concat(jsFilesNoJQuery); +let jsFilesCroutonMap = [ + 'crouton-map.js', + 'markerclusterer.js', + 'oms-1.0.3.min.js', +]; let cssFiles = [ 'select2.min.css', 'bootstrap.min.css', @@ -54,6 +57,24 @@ task('js-files-nojquery', () => { .pipe(notify({message:"js-files-nojquery complete", wait: true})); }); +task('jsFilesCroutonMap', () => { + let jsFilesWithFullPath = []; + for (let jsFile of jsFilesCroutonMap) { + jsFilesWithFullPath.push('croutonjs/src/js/' + jsFile); + } + + return src(jsFilesWithFullPath) + .pipe(concat('crouton-map.js')) + .pipe(dest(distDir)) + .pipe(minify({ + ext: { + min:'.min.js' + }, + })) + .pipe(dest(distDir)) + .pipe(notify({message:"jsFilesCroutonMap complete", wait: true})); +}); + task('js-files', () => { let jsFilesWithFullPath = []; for (let jsFile of jsFilesWithJquery) { @@ -102,7 +123,7 @@ task('css-files', () => { .pipe(notify({message: "css-files complete", wait: true})); }); -task('default', series('templates', 'js-files', 'js-files-nojquery', 'css-files')); +task('default', series('templates', 'js-files', 'js-files-nojquery', 'jsFilesCroutonMap', 'css-files')); task('watch', () => { watch([