diff --git a/front-end/public/img/pie2.png b/front-end/public/img/pie2.png
new file mode 100644
index 0000000..bd6d719
Binary files /dev/null and b/front-end/public/img/pie2.png differ
diff --git a/front-end/public/index.html b/front-end/public/index.html
index 1d3b47f..8325c0c 100644
--- a/front-end/public/index.html
+++ b/front-end/public/index.html
@@ -23,10 +23,11 @@
Learn how to configure a non-root public URL by running `npm run build`.
-->
+
-
React App
+ NYU Shuttle
diff --git a/front-end/public/js/markerclusterer.js b/front-end/public/js/markerclusterer.js
new file mode 100644
index 0000000..2e10887
--- /dev/null
+++ b/front-end/public/js/markerclusterer.js
@@ -0,0 +1,1605 @@
+/**
+ * A Marker Clusterer that clusters markers. Google's origin (c) 2010: http://googlemaps.github.io/js-marker-clusterer/docs/reference.html
+ * modified by Passio Transit and djdance in 2017-2018
+ * compiled from ten forks from 2011-2016 years
+ *
+ *
+ * new features:
+ * pie
+ * each item consists of two: img lies under markers, transparent div for click above markers
+ *
+ *
+ *
+ * HOWTO create a PIE
+ * 0. add pie2,png...pie10.png to img/
+ * 1. create array of stopMarkers
+ * 2. push to cluster:
+ stopMarkerCluster = new MarkerClusterer(map, stopMarkers, {
+ imagePath: 'img/pie', //path to pngs
+ showTitle: false,
+ gridSize: 10, //treshold
+ showMarkerCount: 0, //to hide digits
+ pieView:1, //toggle PIE mode
+ pieSize:15, //diameter
+ });
+ *
+ *
+ * HOTWO get markers inside concrete custer
+ * after creating MarkerClusterer add listener:
+ * google.maps.event.addListener(stopMarkerCluster, "click", onClusterMarkerClick);
+ * then create onClusterMarkerClick:
+ * function onClusterMarkerClick(cluster,event){
+ * var markers=cluster.markers_;//this.getMarkers(); <- this returns all
+ * for(var i = 0; i < markers.length; i++) {....
+ *
+ *
+ *
+ * @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.
+* 'showMarkerCount': (boolean) Whether or not to show a number representing
+* the # of markers in a given cluster on the cluster's icon
+* 'onClusterAdded': (function) Fires whenever a cluster is added to the dom.
+* Passes the Cluster object a parameter
+ * '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.
+ * 'iconAnchor': (Array) The anchor position of the icon x, y.
+ * @constructor
+ * @extends google.maps.OverlayView
+ */
+function MarkerClusterer(map, opt_markers, opt_options) {
+ //console.log("MarkerClusterer called");
+
+ // 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;
+
+ //Add a 'cluster' property to markers so it can access its own cluster
+ google.maps.Marker.prototype.cluster = null;
+
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.markers_ = [];
+
+ /**
+ * @type {Array.}
+ */
+ this.clusters_ = [];
+
+ /**
+ * @type {Object} holding information about every markers cluster
+ */
+ this.markersCluster_ = {};
+
+ /**
+ * @type {Number} Unique markers ID
+ */
+ this.markersUniqueID = 1;
+
+
+ /**
+ * @private
+ */
+ this.styles_ = [];
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ this.ready_ = false;
+
+ var options = opt_options || {};
+
+
+
+ this.pieSize_ = options['pieSize'] || 30;
+ this.pieView_ = options['pieView'] || 0;
+ //console.log("MarkerClusterer: this.pieView_="+this.pieView_);
+ if (this.pieView_)
+ this.sizes = [this.pieSize_];
+ else
+ this.sizes = [53, 56, 66, 78, 90];
+
+ /**
+ * @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_;
+ this.showTitle_ = options['showTitle']||false;
+
+ /**
+ * @type {string}
+ * @private
+ */
+ this.imageExtension_ = options['imageExtension'] ||
+ this.MARKER_CLUSTER_IMAGE_EXTENSION_;
+
+ this.customCount = options['customCount'] || null;
+
+ /**
+ * @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'];
+ }
+
+ /**
+ * @type {boolean}
+ * @private
+ */
+ this.showMarkerCount_ = false;
+
+ if (options['showMarkerCount'] != undefined) {
+ this.showMarkerCount_ = options['showMarkerCount'];
+ }
+
+ /**
+ * @type {function}
+ * @private
+ */
+ this.onClusterAdded_ = null;
+
+ if (options['onClusterAdded'] != undefined) {
+ this.onClusterAdded_ = options['onClusterAdded'];
+ }
+
+ 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 (zoom < 0 || zoom > maxZoom) {
+ return;
+ }*/
+
+ 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;
+ }
+
+ // console.log("setupStyles_: this.pieView_="+this.pieView_);
+ if (this.pieView_)
+ for (var i = 0; i<10; i++) {
+ this.styles_.push({
+ url: this.imagePath_ + (i + 1) + '.' + this.imageExtension_,
+ height: this.sizes[0],
+ width: this.sizes[0]
+ });
+ }
+ else
+ 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_;
+};
+
+/**
+* Whether marker count is to be shown on an icon
+*
+* @return {boolean} True if showMarkerCount_ is true.
+*/
+MarkerClusterer.prototype.showMarkerCount = function() {
+ return this.showMarkerCount_;
+};
+
+MarkerClusterer.prototype.pieView = function() {
+ return this.pieView_;
+};
+
+/**
+* Function to execute when a cluster is added to the map
+*
+* @return {function} with a paramter called cluster of type Cluster
+*/
+MarkerClusterer.prototype.onClusterAdded = function() {
+ return this.onClusterAdded_;
+};
+
+/**
+ * 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_;
+};
+
+
+/**
+ * Gets marker's cluster object based on given marker
+ *
+ * @param {google.maps.Marker} marker
+ *
+ * @return {Cluster}
+ */
+MarkerClusterer.prototype.getMarkersCluster = function(marker) {
+ return this.clusters_[this.markersCluster_[marker.uniqueID]];
+};
+
+
+/**
+ * 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, customCount,pieView,showMarkerCount) {
+ var index = 0;
+ var count = markers.length;
+ var customCountValue = 0;
+ var titles=[];
+
+ if (customCount != null) {
+ markers.forEach(function(itm) {
+ customCountValue = customCountValue + parseInt(itm[customCount], 10);
+ });
+ }
+
+ var colors=[];
+ var firstColor="";
+ var diffColors=false;
+ markers.forEach(function(itm) {
+ //console.log("cluster itm=",itm)
+ if (titles.indexOf(itm.title)<0)
+ titles.push(itm.title);
+
+ var color = itm.icon?itm.icon.strokeColor:"#ffffff";
+ if (color!="") colors.push(color);
+ if (firstColor=="")
+ firstColor=color;
+ if (firstColor!=color){
+ diffColors=true;
+ //return;
+ }
+ });
+
+ //console.log("calculator_: pieView="+pieView+", showMarkerCount="+showMarkerCount+",firstColor="+firstColor);
+ if (pieView==1){
+ index=Math.min(10,count);
+ //console.log("calculator_ pie: index="+index+", count="+count);
+ }else{
+ var dv = count;
+ while (dv !== 0) {
+ dv = parseInt(dv / 10, 10);
+ index++;
+ }
+ //console.log("calculator_ NOT pie: index="+index);
+ }
+ var iconText = (showMarkerCount) ? count : "";
+ index = Math.min(index, numStyles);
+ return {
+ text: iconText,
+ index: index,
+ textCount: customCountValue,
+ color: diffColors?"":firstColor,
+ colors:colors,
+ titles:titles
+ };
+};
+
+
+/**
+ * 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();
+ });
+ }
+ marker.uniqueID = this.markersUniqueID;
+ this.markersUniqueID++;
+ 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);
+ delete this.markersCluster_[marker.uniqueID];
+
+ 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) {
+ // create a local copy of markers if required
+ // (removeMarker_ modifies the getMarkers() array in place)
+ var markersCopy = markers === this.getMarkers() ? markers.slice() : markers;
+ var removed = false;
+
+ for (var i = 0, marker; marker = markersCopy[i]; i++) { //was marker = markers[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_;
+};
+
+/**
+ * Returns custom count parameter.
+ *
+ * @return {string} The custom count parameter.
+ */
+MarkerClusterer.prototype.getCustomCount = function() {
+ return this.customCount;
+};
+
+
+
+/**
+ * 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_ = [];
+ this.markersCluster_ = {};
+ this.markersUniqueID = 1;
+};
+
+
+/**
+ * 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_ = [];
+ this.markersCluster_ = {};
+ this.markersUniqueID = 1;
+};
+
+/**
+ *
+ */
+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();
+ var clusterIndex = null;
+ 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;
+ clusterIndex = i;
+ }
+ }
+ }
+
+ if (clusterToAddTo && clusterToAddTo.isMarkerInClusterBounds(marker)) {
+ clusterToAddTo.addMarker(marker);
+ } else {
+ var cluster = new Cluster(this);
+ cluster.addMarker(marker);
+ this.clusters_.push(cluster);
+ clusterIndex = this.clusters_.length - 1;
+ }
+
+ if (marker.isAdded) {
+ this.markersCluster_[marker.uniqueID] = clusterIndex;
+ }
+};
+
+
+/**
+ * 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)) {
+ if (marker.getVisible()){
+ //console.log("createClusters_["+i+"] added "+marker.title);
+ marker.cluster = null;//???djdance
+ this.addToClosestCluster_(marker);
+ } else{
+ //console.log("createClusters_["+i+"] IGNORED "+marker.title);
+ }
+ }
+ }
+};
+
+
+/**
+ * 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.customCount = markerClusterer.getCustomCount();
+ 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) {
+ //console.log("addMarker: visible?"+marker.getVisible()+", "+marker.title);
+ if (this.isMarkerAlreadyAdded(marker) || !marker.getVisible()) {
+ 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;
+ marker.cluster = this;
+ 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;
+};
+
+/**
+* Whether or not the cluster has multiple markers (and thus is visible
+*
+*/
+Cluster.prototype.hasMultipleMarkers = function() {
+ if (this.markers_ == undefined) {
+ return false;
+ }
+ return (this.markers_.length > 1);
+};
+
+/**
+ * Removes the cluster
+ */
+Cluster.prototype.remove = function() {
+ this.clusterIcon_.remove();
+ this.markers_.length = 0;
+ delete this.markers_;
+};
+
+
+/**
+ * Returns the number of markers in the cluster.
+ *
+ * @return {number} The number of markers in the cluster.
+ */
+Cluster.prototype.getSize = function() {
+ return this.markers_.length;
+};
+
+
+/**
+ * Returns a list of the markers in the cluster.
+ *
+ * @return {Array.} The markers in the cluster.
+ */
+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 customCountParam = this.markerClusterer_.getCustomCount();
+ var sums = this.markerClusterer_.getCalculator()(this.markers_, numStyles, customCountParam,this.markerClusterer_.pieView_,this.markerClusterer_.showMarkerCount_);
+ this.clusterIcon_.setCenter(this.center_);
+ this.clusterIcon_.setSums(sums, customCountParam);
+ this.clusterIcon_.show();
+};
+
+/**
+* Updates the URL of the cluster icon
+*/
+Cluster.prototype.updateIconUrl = function(new_url) {
+ this.clusterIcon_.div_.style["background-image"] = "url(" + new_url + ")";
+};
+
+/**
+ * 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.div2_ = null;
+ this.divTitle_ = null;
+ this.sums_ = null;
+ this.visible_ = false;
+
+ this.setMap(this.map_);
+}
+
+
+/**
+ * Triggers the clusterclick event and zoom's if the option is set.
+ *
+ * @param {google.maps.MouseEvent} event The event to propagate
+ */
+ClusterIcon.prototype.triggerClusterClick = function(event) {
+ var markerClusterer = this.cluster_.getMarkerClusterer();
+
+ // Trigger the clusterclick event.
+ //console.log("triggerClusterClick, ? "+markerClusterer.isZoomOnClick()+", Gx="+Gx);
+ google.maps.event.trigger(markerClusterer, 'click', this.cluster_, event);
+
+ if (markerClusterer.isZoomOnClick()) {
+ // Zoom into the cluster.
+ if (Gx && Gy)
+ this.map_.fitBounds(this.cluster_.getBounds(),Math.min(Gy,Gx)*0.35);
+ else
+ this.map_.fitBounds(this.cluster_.getBounds());
+ }
+};
+
+/**
+* Triggers the clustermouseover event
+*/
+ClusterIcon.prototype.triggerClusterMouseover = function() {
+ var markerClusterer = this.cluster_.getMarkerClusterer();
+ //console.log("triggerClusterMouseover");
+ google.maps.event.trigger(markerClusterer, 'clustermouseover', this.cluster_);
+ google.maps.event.trigger(markerClusterer, 'mouseover', this.cluster_,event);
+}
+
+
+/**
+* Triggers the clustermouseout event
+*/
+ClusterIcon.prototype.triggerClusterMouseout = function() {
+ var markerClusterer = this.cluster_.getMarkerClusterer();
+ //console.log("triggerClusterMouseout");
+ google.maps.event.trigger(markerClusterer, 'clustermouseout', this.cluster_);
+ google.maps.event.trigger(markerClusterer, 'mouseout', this.cluster_);
+}
+/**
+ * Adding the cluster icon to the dom.
+ * @ignore
+ */
+ClusterIcon.prototype.onAdd = function() {
+ this.div_ = document.createElement('DIV');
+ this.div2_ = document.createElement('DIV');
+ this.divTitle_= document.createElement('DIV');
+ if (this.visible_) {
+ var pos = this.getPosFromLatLng_(this.center_);
+ this.div_.style.cssText = this.createCss2(pos)+this.createCss(pos);
+ this.div_.innerHTML = (!this.sums_.textCount) ? this.sums_.text : this.sums_.textCount;
+ this.div2_.style.cssText = this.createCss2(pos);
+ this.div2_.id=Math.round(Math.random()*100000);
+ //this.div2_.dataset.cluster=this.cluster_;
+ //if (debug) console.log("ClusterIcon.prototype.onAdd: showTitle_=",this.cluster_.markerClusterer_.showTitle_);
+ if (this.cluster_.markerClusterer_.showTitle_){
+ this.divTitle_.style.cssText = this.createCss2(pos)+this.createCssTitle(pos);
+ this.divTitle_.innerHTML = this.sums_.titles[0]+(this.sums_.titles.length>1?" +"+(this.sums_.titles.length-1):"");
+ //console.log("ClusterIcon.prototype.onAdd: this.sums_.titles=",this.sums_.titles,this.divTitle_.style.cssText);
+ }
+ }
+
+ var panes = this.getPanes();
+ panes.markerLayer.appendChild(this.div_);//under buses
+ panes.overlayMouseTarget.appendChild(this.div2_);//to test clicks
+ panes.markerLayer.appendChild(this.divTitle_);//titles
+ //panes.markerLayer.style['zIndex'] = parseInt(panes.overlayMouseTarget.style['zIndex'])+2;
+ //console.log("markerLayer.zIndex="+panes.markerLayer.style['zIndex']);//103
+ //console.log("overlayMouseTarget.zIndex="+panes.overlayMouseTarget.style['zIndex']);//106
+
+ var that = this;
+ var isDragging = false;
+ var dragX=0, dragY=0;
+ google.maps.event.addDomListener(this.div2_, 'click', function(event) {
+ //console.log("click. isDragging="+isDragging);
+
+ // Prevent event propagation
+ if (event.stop){
+ event.stop();
+ }
+ event.cancelBubble = true;
+ if (event.stopPropagation){
+ event.stopPropagation();
+ }
+ if (event.preventDefault) {
+ event.preventDefault();
+ } else {
+ event.returnValue = false;
+ }
+
+ // Only perform click when not preceded by a drag
+ if (!isDragging) {
+ that.triggerClusterClick(event);
+ }
+ });
+
+ google.maps.event.addDomListener(this.div2_, 'mouseover', function() {
+ that.triggerClusterMouseover();
+ });
+ google.maps.event.addDomListener(this.div2_, 'mouseout', function() {
+ that.triggerClusterMouseout();
+ });
+ google.maps.event.addDomListener(this.div2_, 'mousedown', function() {
+ isDragging = false;
+ dragX=parseInt(this.style.left);
+ dragY=parseInt(this.style.top);
+ //console.log("mousedown, this.style.left="+this.style.left+", dragX="+dragX);
+ });
+ google.maps.event.addDomListener(this.div2_, 'mousemove', function() {
+ var dx=Math.abs(parseInt(this.style.left)-dragX);
+ var dy=Math.abs(parseInt(this.style.top)-dragY);
+ isDragging = dx>50 || dy>50;
+ //console.log("mousemove, dragX="+dragX+", e="+this.style.left+", dx="+dx+", dy="+dy);
+ });
+ if (this.cluster_.markerClusterer_.onClusterAdded_ != null) {
+ this.cluster_.markerClusterer_.onClusterAdded_(this.cluster_);
+ }
+};
+
+
+/**
+ * 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);
+
+ if (typeof this.iconAnchor_ === 'object' && this.iconAnchor_.length === 2) {
+ pos.x -= this.iconAnchor_[0];
+ pos.y -= this.iconAnchor_[1];
+ } else {
+ 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';
+ this.div2_.style.top = pos.y + 'px';
+ this.div2_.style.left = pos.x + 'px';
+ this.divTitle_.style.top = pos.y + 'px';
+ this.divTitle_.style.left = pos.x + 'px';
+ //this.div_.style.zIndex = google.maps.Marker.MAX_ZINDEX + 1;
+ }
+};
+
+
+/**
+ * Hide the icon.
+ */
+ClusterIcon.prototype.hide = function() {
+ if (this.div_) {
+ this.div_.style.display = 'none';
+ this.div2_.style.display = 'none';
+ this.divTitle_.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.createCss2(pos)+this.createCss(pos);
+ this.div_.style.display = '';
+ this.div2_.style.cssText = this.createCss2(pos);
+ this.div2_.style.display = '';
+ this.divTitle_.style.cssText = this.createCss2(pos)+this.createCssTitle(pos);
+ this.divTitle_.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;
+ this.div2_.parentNode.removeChild(this.div2_);
+ this.div2_ = null;
+ this.divTitle_.parentNode.removeChild(this.divTitle_);
+ this.divTitle_ = 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.
+ * 'style': (number) The style of the icon.
+ */
+ClusterIcon.prototype.setSums = function(sums, customCountParam) {
+ this.sums_ = sums;
+ this.text_ = (!sums.textCount) ? sums.text : sums.textCount;
+ //console.log("ClusterIcon setSums: sums=",sums);
+ this.index_ = sums.index;
+ if (this.div_) {
+ this.div_.innerHTML = (!sums.textCount) ? sums.text : sums.textCount;
+ }
+
+ var style;
+ if (sums.style)
+ style = sums.style
+ else if (sums.index) {
+ var index = Math.max(0, sums.index - 1);
+ index = Math.min(this.styles_.length - 1, index);
+ style = this.styles_[index];
+ }
+ style['color']=sums.color;
+ style['colors']=sums.colors;
+ style['titles']=sums.titles;
+ //console.log("ClusterIcon setSums.colors=",style.colors);
+ this.useStyle(style);
+};
+
+
+/**
+ * Sets the icon to the the styles.
+ */
+ClusterIcon.prototype.useStyle = function(style) {
+ this.url_ = style['url'];
+ this.height_ = style['height'];
+ this.width_ = style['width'];
+ this.color_ = style['color'];
+ this.colors_ = style['colors'];
+ this.textColor_ = style['textColor'];
+ //console.log("useStyle: set this.colors=",this.colors_);
+ this.anchor_ = style['anchor'];
+ this.textSize_ = style['textSize'];
+ this.backgroundPosition_ = style['backgroundPosition'];
+ this.iconAnchor_ = style['iconAnchor'];
+};
+
+
+/**
+ * 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 = [];
+ //console.log("createCss: color="+this.color_+", colors_=",this.colors_);
+ if (this.color_!="" && this.color_!=undefined){
+ style.push('background: url(' + this.url_ + '),#ffffff 100% 100% no-repeat;');
+ style.push('background-size: 100%;');
+ style.push('background-color: '+this.color_+';');
+ style.push('background-blend-mode: overlay;');
+ } else if (this.colors_ && this.colors_.length>0){
+ var p=100/this.colors_.length;
+ var g=[];
+ for(var i=0;i 0 &&
+ this.anchor_[0] < this.height_) {
+ style.push('height:' + (this.height_ - this.anchor_[0]) +
+ 'px; padding-top:' + this.anchor_[0] + 'px;');
+ } else if (typeof this.anchor_[0] === 'number' && this.anchor_[0] < 0 &&
+ -this.anchor_[0] < this.height_) {
+ style.push('height:' + this.height_ + 'px; line-height:' + (this.height_ + 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;');
+ }
+ style.push('cursor:pointer; top:' + pos.y + 'px; left:' + pos.x + 'px; position:absolute;');
+ return style.join('');
+};
+
+ClusterIcon.prototype.createCssTitle = function(pos) {
+ //title css
+ var style = [];
+ var txtColor = this.textColor_ ? this.textColor_ : '#0000ffaa';
+ var txtSize = this.textSize_ ? this.textSize_ : 11;
+ style.push('text-align:left; color:' + txtColor + '; font-size:' +txtSize + 'px;');
+ var s='margin-top:'+(this.height_*0.75)+'px; text-shadow: 1px 1px 0 white, -1px -1px 0 white;white-space:nowrap;';
+ //console.log('createCssTitle: '+s);
+ style.push(s);
+ return style.join('');
+};
diff --git a/front-end/src/App.js b/front-end/src/App.js
index e471379..a33c138 100644
--- a/front-end/src/App.js
+++ b/front-end/src/App.js
@@ -15,10 +15,10 @@ import PrivacyPolicyPage from './components/settings/PrivacyPolicyPage';
import LoadingScreen from './components/LoadingScreen';
import TutorialComponent from './components/TutorialComponent';
-
// Import hooks and utilities
import { registerService } from './utils/serviceRegister';
import { getUserPos, loadGoogleMapsAPI } from './utils/mapUtility';
+import { queryRoutes } from './utils/routes';
// Import CSS
import './index.css';
@@ -37,7 +37,7 @@ function App() {
let key = localStorage.key(i);
let value = localStorage.getItem(key);
localStorageItems[key] = value;
- //console.log(key, value)
+ //console.log(key, value)
}
useEffect(() => {
@@ -46,6 +46,7 @@ function App() {
window.addEventListener('keydown', devTools);
registerService();
getUserPos();
+ queryRoutes(true);
return () => window.removeEventListener('keydown', devTools);
}, []);
diff --git a/front-end/src/components/Map.js b/front-end/src/components/Map.js
index ea0a59d..9834e88 100644
--- a/front-end/src/components/Map.js
+++ b/front-end/src/components/Map.js
@@ -4,6 +4,7 @@ import RealTimeDataWebSocket from '../utils/websocket';
import { loadGoogleMapsAPI, initializeMap, getCoordinates, generateTwoUniqueRandomInts } from '../utils/mapUtility';
import { queryTransportations } from '../utils/transportData';
import { updateTransportMarkers } from '../utils/transportMarker';
+import { queryStops, drawStopMarkers } from '../utils/stops';
function Map({ line, lineColor }) {
const googleMapRef = useRef(null);
@@ -32,19 +33,39 @@ function Map({ line, lineColor }) {
washingtonsquare: [40.7315, -73.9971],
jayst: [40.6922, -73.9864],
dumbo: [40.7037, -73.9886],
- ikea: [40.6726, -74.0100],
+ ikea: [40.6726, -74.01],
test1: [39.7392, -104.9903],
test2: [39.7479, -104.9994],
test3: [39.7398, -104.9892],
- test4: [39.7394, -104.9849]
- }
+ test4: [39.7394, -104.9849],
+ };
const routes = {
- route1: [busStops.washingtonsquare, busStops.unionsquare, busStops.tompkins, busStops.hamilton, busStops.eastbroadway, busStops.chinatown, busStops.financial, busStops.tribeca, busStops.canal, busStops.soho, busStops.greenwich, busStops.washingtonsquare],
- route2: [busStops.jayst, busStops.dumbo, busStops.ikea],
- route3: [busStops.jayst, busStops.eastbroadway, busStops.washingtonsquare, busStops.chinatown, busStops.financial],
- route4: [busStops.eastbroadway, busStops.washingtonsquare, busStops.unionsquare, busStops.tompkins, busStops.hamilton, busStops.eastbroadway],
- route5: [busStops.test1, busStops.test2, busStops.test3, busStops.test4]
- }
+ route1: [
+ busStops.washingtonsquare,
+ busStops.unionsquare,
+ busStops.tompkins,
+ busStops.hamilton,
+ busStops.eastbroadway,
+ busStops.chinatown,
+ busStops.financial,
+ busStops.tribeca,
+ busStops.canal,
+ busStops.soho,
+ busStops.greenwich,
+ busStops.washingtonsquare,
+ ],
+ route2: [busStops.jayst, busStops.dumbo, busStops.ikea],
+ route3: [busStops.jayst, busStops.eastbroadway, busStops.washingtonsquare, busStops.chinatown, busStops.financial],
+ route4: [
+ busStops.eastbroadway,
+ busStops.washingtonsquare,
+ busStops.unionsquare,
+ busStops.tompkins,
+ busStops.hamilton,
+ busStops.eastbroadway,
+ ],
+ route5: [busStops.test1, busStops.test2, busStops.test3, busStops.test4],
+ };
// Load Google Maps API
useEffect(() => {
@@ -73,6 +94,11 @@ function Map({ line, lineColor }) {
useEffect(() => {
if (isMapLoaded) {
fetchTransportData(map);
+ queryStops().then((r) => {
+ if (r) {
+ drawStopMarkers();
+ }
+ });
}
}, [isMapLoaded]);
@@ -96,12 +122,10 @@ function Map({ line, lineColor }) {
console.log('endLoc' + endLoc);
console.log('----------------------------');
-
//set start and end location
- let curline = routes['route'+String(line)];
- console.log('selected route: '+curline);
+ let curline = routes['route' + String(line)];
+ console.log('selected route: ' + curline);
plotRoute(curline, lineColor);
-
}, [map, line, startLoc, endLoc]);
const fetchTransportData = async () => {
@@ -116,30 +140,30 @@ function Map({ line, lineColor }) {
const plotRoute = async (curline, lineColor) => {
let directionsService = new window.google.maps.DirectionsService();
-
+
for (let i = 0; i < curline.length - 1; i++) {
let start = curline[i];
let end = curline[i + 1];
-
+
let request = {
origin: new window.google.maps.LatLng(start[0], start[1]),
destination: new window.google.maps.LatLng(end[0], end[1]),
- travelMode: window.google.maps.TravelMode.DRIVING
+ travelMode: window.google.maps.TravelMode.DRIVING,
};
-
+
directionsService.route(request, function (response, status) {
if (status === 'OK') {
let directionsRenderer = new window.google.maps.DirectionsRenderer({
polylineOptions: new window.google.maps.Polyline({
strokeColor: lineColor,
strokeOpacity: 0.8,
- strokeWeight: 5
+ strokeWeight: 5,
}),
- suppressMarkers: true
+ suppressMarkers: true,
});
directionsRenderer.setDirections(response);
directionsRenderer.setMap(map);
-
+
// Create markers at the start and end of each segment
new window.google.maps.Marker({
position: new window.google.maps.LatLng(start[0], start[1]),
@@ -147,32 +171,29 @@ function Map({ line, lineColor }) {
icon: {
path: window.google.maps.SymbolPath.CIRCLE,
scale: 8,
- fillColor: "#b3b3b3",
+ fillColor: '#b3b3b3',
fillOpacity: 0.8,
- strokeWeight: 0
- }
+ strokeWeight: 0,
+ },
});
-
+
new window.google.maps.Marker({
position: new window.google.maps.LatLng(end[0], end[1]),
map: map,
icon: {
path: window.google.maps.SymbolPath.CIRCLE,
scale: 8,
- fillColor: "#b3b3b3",
+ fillColor: '#b3b3b3',
fillOpacity: 0.8,
- strokeWeight: 0
- }
+ strokeWeight: 0,
+ },
});
-
} else {
console.log('Directions request for segment ' + i + ' failed due to ' + status);
}
});
}
- }
-
-
+ };
return (
<>
diff --git a/front-end/src/utils/mapUtility.js b/front-end/src/utils/mapUtility.js
index afd3c9a..3de9983 100644
--- a/front-end/src/utils/mapUtility.js
+++ b/front-end/src/utils/mapUtility.js
@@ -42,6 +42,11 @@ const API_LIBRARIES = ['geometry', 'places'];
const CALLBACK_NAME = 'gmapAPICallback';
const POS_DEFAULT = [40.716503, -73.976077];
+// Shared variable
+if (typeof window.nyushuttle == 'undefined') {
+ window.nyushuttle = {};
+}
+
export function loadGoogleMapsAPI(callback) {
let c = typeof callback == 'function' ? callback : () => {};
@@ -66,6 +71,7 @@ export function initializeMap(mapRef, setIsMapLoaded, setMap) {
options: MAP_OPTIONS,
});
setMap(googleMap);
+ window.nyushuttle.currentMap = googleMap;
window.google.maps.event.addListenerOnce(googleMap, 'tilesloaded', () => {
setIsMapLoaded(true);
diff --git a/front-end/src/utils/routes.js b/front-end/src/utils/routes.js
new file mode 100644
index 0000000..13d2c26
--- /dev/null
+++ b/front-end/src/utils/routes.js
@@ -0,0 +1,70 @@
+import axios from 'axios';
+
+const MIN_QUERY_DELAY = 60000; // 1 min
+
+let lastQuery = -MIN_QUERY_DELAY;
+let subscribedRoutes = localStorage.subscribedRoutes;
+
+// Shared variable
+if (typeof window.nyushuttle == 'undefined') {
+ window.nyushuttle = {};
+}
+window.nyushuttle.routesFull = [];
+window.nyushuttle.routesData = [];
+window.nyushuttle.routesSelected = subscribedRoutes ? subscribedRoutes.split(',') : [];
+
+export async function queryRoutes(fresh) {
+ // Prevent too frequent requests (rate limiting)
+ if (performance.now() - lastQuery < MIN_QUERY_DELAY) {
+ return true;
+ }
+
+ // JSON
+ const json = {
+ systemSelected0: localStorage.agencyId,
+ amount: 1,
+ };
+
+ const formData = new URLSearchParams({});
+ formData.append('json', JSON.stringify(json));
+
+ // Params for URL
+ const params = {
+ getRoutes: fresh ? 1 : 2,
+ deviceId: localStorage.deviceId,
+ };
+
+ // Optional parameters
+ if (!fresh) {
+ params.sortMode = 1;
+ params.credentials = 1;
+
+ // Currently there are no feature supported based on the following data
+ params.lat = undefined;
+ params.lng = undefined;
+ }
+
+ const urlParams = new URLSearchParams(params);
+ const url = `${localStorage.serviceEndpointHome}/mapGetData.php?${urlParams.toString()}`;
+
+ try {
+ const response = await axios.post(url, formData);
+ const data = response.data;
+ if (!data) {
+ throw new Error('empty response');
+ }
+ if (fresh) {
+ window.nyushuttle.routesFull = data;
+ window.nyushuttle.routesData = data;
+ } else {
+ if (!data.all) {
+ throw new Error('empty response');
+ }
+ window.nyushuttle.routesData = data.all;
+ }
+ return true;
+ } catch (error) {
+ console.log('Transportations query error: ' + error.message);
+ return false;
+ }
+}
diff --git a/front-end/src/utils/stops.js b/front-end/src/utils/stops.js
new file mode 100644
index 0000000..ed82c4e
--- /dev/null
+++ b/front-end/src/utils/stops.js
@@ -0,0 +1,387 @@
+import axios from 'axios';
+// import {} from './routes';
+
+const MIN_QUERY_DELAY = 300000; // 5 min
+
+let lastQuery = -MIN_QUERY_DELAY;
+
+// Shared variable
+if (typeof window.nyushuttle == 'undefined') {
+ window.nyushuttle = {};
+}
+window.nyushuttle.routePoints = {};
+window.nyushuttle.stops = {};
+window.nyushuttle.routes = [];
+let center = {};
+let stopMarkers = [];
+let routePaths = [];
+let stopMarkerCluster = null;
+let groupRoutes = false;
+let stopMarkerZoomVisibilityTreshold = 14;
+let bounds;
+let lastZoom = 99;
+
+export async function queryStops() {
+ // Prevent too frequent requests (rate limiting)
+ if (performance.now() - lastQuery < MIN_QUERY_DELAY) {
+ return false;
+ }
+ const routesData = window.nyushuttle.routesData;
+ if (!routesData || routesData === undefined || routesData == null || Object.keys(routesData).length == 0) {
+ return false;
+ }
+
+ // JSON
+ const json = {
+ s0: localStorage.agencyId,
+ sA: 1,
+ };
+
+ const formData = new URLSearchParams({});
+ formData.append('json', JSON.stringify(json));
+
+ // Params for URL
+ const pos = window.nyushuttle.pos;
+ const params = {
+ getStops: 2,
+ deviceId: localStorage.deviceId,
+ withOutdated: 1,
+ wBounds: 1,
+ buildNo: localStorage.softwareVer,
+ showBusInOos: 0,
+
+ // Currently there are no feature supported based on the following data
+ lat: undefined,
+ lng: undefined,
+ };
+
+ const urlParams = new URLSearchParams(params);
+ const url = `${localStorage.serviceEndpointHome}/mapGetData.php?${urlParams.toString()}`;
+
+ try {
+ const response = await axios.post(url, formData);
+ const data = response.data;
+ if (!data) {
+ throw new Error('empty response');
+ }
+ window.nyushuttle.routes = data.routes ? data.routes : {};
+ window.nyushuttle.routePoints = data.routePoints;
+ window.nyushuttle.stops = data.stops;
+ groupRoutes = data.groupRoutes;
+ center = data.center;
+
+ return true;
+ } catch (error) {
+ console.log('Transportations query error: ' + error.message);
+ return false;
+ }
+}
+
+export function drawStopMarkers() {
+ clearAllMarkers();
+ if (!initializeBounds(center)) return;
+
+ const showStopName = isLocalStorageOptionEnabled('optionShowStopname');
+ const optionHideStops = isLocalStorageOptionEnabled('optionHideStops');
+
+ if (!optionHideStops) {
+ drawRoutes(showStopName);
+ drawStops();
+ }
+
+ panToBoundsIfNeeded(center);
+}
+
+function clearAllMarkers() {
+ if (stopMarkerCluster != null) {
+ stopMarkerCluster.clearMarkers();
+ }
+ clearMarkers(stopMarkers, true);
+ clearMarkers(routePaths, true);
+}
+
+function initializeBounds(center) {
+ if (!center) return false;
+ bounds = createBoundsFromCenter(center);
+ return true;
+}
+
+function createBoundsFromCenter(center) {
+ const maps = window.google.maps;
+ let newBounds = new maps.LatLngBounds();
+ newBounds.extend(new maps.LatLng(center.latitude, center.longitude));
+ newBounds.extend(new maps.LatLng(center.latitudeMin, center.longitudeMin));
+ newBounds.extend(new maps.LatLng(center.latitudeMax, center.longitudeMax));
+ return newBounds;
+}
+
+function isLocalStorageOptionEnabled(optionName) {
+ return localStorage.getItem(optionName) == 1;
+}
+
+function clearMarkers(markers, isArray = false) {
+ if (isArray) {
+ markers.forEach((marker) => marker.setMap(null));
+ } else if (markers != null) {
+ markers.clearMarkers();
+ }
+ markers = [];
+}
+
+function drawRoutes(showStopName) {
+ if (!Object.keys(window.nyushuttle.routes).length) {
+ return;
+ }
+ const routes = window.nyushuttle.routes;
+ Object.keys(routes).forEach((routeId) => {
+ drawRoute(routeId, routes[routeId], showStopName);
+ });
+}
+
+function drawRoute(routeId, route, showStopName) {
+ const routeGroupId = route[2];
+ const routeColor = correctColorFromARGB(route[1]);
+ const routePaths = getRoutePaths(routeId);
+
+ drawRoutePath(routePaths, routeColor, routeId, routeGroupId);
+ // routePaths.forEach((path) => {
+ // updateBoundsWithPoint(path);
+ // });
+
+ if (showStopName) {
+ drawStopNamesForRoute(route);
+ }
+}
+
+function getRoutePaths(routeId) {
+ const points = window.nyushuttle.routePoints;
+ return points[routeId][0].map((point) => new window.google.maps.LatLng(point.lat, point.lng));
+}
+
+function drawRoutePath(path, routeColor, routeId, routeGroupId) {
+ const selected = window.nyushuttle.routesSelected && window.nyushuttle.routesSelected.includes(routeId);
+ const opacity = getOpacity(selected);
+ const polylineOptions = createPolylineOptions(path, routeColor, opacity, routeId, routeGroupId);
+ const polyline = new window.google.maps.Polyline(polylineOptions);
+ polyline.setMap(window.nyushuttle.currentMap);
+}
+
+function getOpacity(selected) {
+ return selected ? 1.0 : 0.5;
+}
+
+function createPolylineOptions(path, routeColor, opacity, routeId, routeGroupId) {
+ return {
+ path: path,
+ visible: true,
+ geodesic: false,
+ clickable: false,
+ strokeColor: routeColor,
+ strokeOpacity: opacity,
+ strokeWeight: opacity == 1 ? 5 : 2,
+ routeId: routeId,
+ routeGroupId: routeGroupId,
+ icons: [
+ {
+ icon: getRouteArrow(routeColor, opacity),
+ repeat: '200px',
+ path: [],
+ },
+ ],
+ };
+}
+
+function getRouteArrow(routeColor, opacity) {
+ return {
+ path: window.google.maps.SymbolPath.FORWARD_CLOSED_ARROW,
+ scale: 3,
+ strokeColor: '#ffffff',
+ strokeWeight: 0.5,
+ fillColor: routeColor,
+ fillOpacity: opacity * 0.6,
+ };
+}
+
+function updateBoundsWithPoint(point) {
+ if (bounds == null) bounds = new window.google.maps.LatLngBounds();
+ bounds.extend(point);
+}
+
+function drawStopNamesForRoute(route) {
+ // Logic to draw stop names for the given route
+}
+
+function drawStops() {
+ const stops = window.nyushuttle.stops;
+ if (!stops) return;
+
+ const showStopName = isLocalStorageOptionEnabled('optionShowStopname');
+ const routes = window.nyushuttle.routes;
+ const map = window.nyushuttle.currentMap;
+
+ Object.keys(routes).forEach((routeId) => {
+ const routestops = routes[routeId];
+ const routeGroupId = routes[routeId][2];
+
+ addRouteMarkersOnMap(routeId, routestops, routeGroupId, showStopName);
+ });
+
+ recreateStopMarkerCluster();
+ if (stopMarkerCluster != null) {
+ stopMarkerCluster.repaint();
+ map.addListener('zoom_changed', () => {
+ const currentZoom = map.getZoom();
+ if (shouldUpdateMarkerVisibility(currentZoom)) {
+ updateMarkerVisibility(currentZoom);
+ lastZoom = currentZoom;
+ stopMarkerCluster.repaint();
+ }
+ });
+ }
+}
+
+function addRouteMarkersOnMap(routeId, routestops, routeGroupId, showStopName) {
+ const zoomLevel = window.nyushuttle.currentMap.getZoom();
+ const routeName = routestops[0];
+ const routeColor = correctColorFromARGB(routestops[1]);
+
+ routestops.slice(3).forEach((stop, idx) => {
+ const sID = `ID${stop[1]}`;
+ const theStop = window.nyushuttle.stops[sID];
+
+ if (isStopValid(theStop)) {
+ updateStopData(theStop, stop, routeId, routeGroupId, routeName, routeColor);
+ const marker = createMarkerForStop(theStop, zoomLevel, routeColor, showStopName, idx);
+ // marker.addListener('click', onMarkerClick);
+ // marker.addListener('mouseover', onMarkerHover);
+ stopMarkers.push(marker);
+ }
+ });
+}
+
+function isStopValid(stop) {
+ return stop && stop.latitude != null && !(stop.latitude === 0.0 && stop.longitude === 0.0);
+}
+
+function updateStopData(stop, stopInfo, routeId, routeGroupId, routeName, routeColor) {
+ if (!stop.routeIDs) stop.routeIDs = [];
+ if (!stop.routeGroupIDs) stop.routeGroupIDs = [];
+
+ if (!isRouteAlreadyRegistered(stop, routeId, routeGroupId)) {
+ stop.routeIDs.push(routeId);
+ stop.routeGroupIDs.push(routeGroupId);
+ Object.assign(stop, {
+ position: stopInfo[0],
+ stopId: stopInfo[1],
+ back: stopInfo[2],
+ routeId,
+ routeName,
+ routeColor,
+ markerId: Math.round(Math.random() * 1000000),
+ });
+ }
+}
+
+function isRouteAlreadyRegistered(stop, routeId, routeGroupId) {
+ return stop.routeIDs.includes(routeId) || (routeGroupId && stop.routeGroupIDs.includes(routeGroupId));
+}
+
+function createMarkerForStop(stop, zoomLevel, routeColor, showStopName, idx) {
+ const position = new window.google.maps.LatLng(stop.latitude, stop.longitude);
+ const markerOptions = {
+ position,
+ icon: createStopIcon(routeColor),
+ visible: zoomLevel >= stopMarkerZoomVisibilityTreshold,
+ zIndex: idx,
+ stopId: stop.stopId,
+ id: Math.round(Math.random() * 1000000),
+ routeId: stop.routeId,
+ title: stop.name,
+ label: createMarkerLabel(stop.name, showStopName),
+ };
+ return new window.google.maps.Marker(markerOptions);
+}
+
+function createStopIcon(routeColor) {
+ return {
+ path: window.google.maps.SymbolPath.CIRCLE,
+ strokeWeight: 3,
+ scale: 7,
+ fillColor: '#FFFFFF',
+ fillOpacity: 1,
+ strokeColor: routeColor,
+ strokeOpacity: 1,
+ labelOrigin: new window.google.maps.Point(0, 2),
+ };
+}
+
+function createMarkerLabel(name, showStopName) {
+ // Define the label for the marker
+ return {
+ text: showStopName ? name : ' ',
+ color: '#0000ffaa',
+ fontSize: '11px',
+ };
+}
+
+function recreateStopMarkerCluster() {
+ // Create a new MarkerClusterer instance
+ stopMarkerCluster = new window.MarkerClusterer(window.nyushuttle.currentMap, stopMarkers, {
+ imagePath: 'img/pie',
+ gridSize: 15,
+ showMarkerCount: 0,
+ pieView: 1,
+ pieSize: 20,
+ averageCenter: true,
+ zoomOnClick: false,
+ showTitle: false,
+ });
+
+ // Add event listeners to the marker cluster
+ // window.google.maps.event.addListener(stopMarkerCluster, 'click', onClusterMarkerClick);
+ // window.google.maps.event.addListener(stopMarkerCluster, 'mouseover', onClusterMarkerHover);
+}
+
+function panToBoundsIfNeeded(center) {
+ if (bounds && !bounds.isEmpty()) {
+ adjustMapZoomAndCenter(center);
+ }
+}
+
+function adjustMapZoomAndCenter(center) {
+ // Logic to adjust map zoom and center
+}
+
+function correctColorFromARGB(color) {
+ if (!color || color === 'null') {
+ return '#000000';
+ }
+
+ const match = color.match(/^#([0-9a-f]{8})$/i);
+ if (match) {
+ return '#' + match[1].substr(2, 6);
+ }
+ return color;
+}
+
+function shouldUpdateMarkerVisibility(currentZoom) {
+ return stopMarkers && stopMarkers.length > 0 && isZoomCrossingThreshold(currentZoom, lastZoom);
+}
+
+function isZoomCrossingThreshold(currentZoom, lastZoom) {
+ return (
+ (currentZoom >= stopMarkerZoomVisibilityTreshold && lastZoom < stopMarkerZoomVisibilityTreshold) ||
+ (currentZoom < stopMarkerZoomVisibilityTreshold && lastZoom >= stopMarkerZoomVisibilityTreshold) ||
+ lastZoom === 99
+ );
+}
+
+function updateMarkerVisibility(zoomLevel) {
+ stopMarkers.forEach((marker) => {
+ marker.setVisible(zoomLevel >= stopMarkerZoomVisibilityTreshold);
+ });
+}
+
+window.debugTools = {};
+window.debugTools.queryStops = queryStops;
+window.debugTools.drawStopMarkers = drawStopMarkers;
diff --git a/front-end/src/utils/transportData.js b/front-end/src/utils/transportData.js
index d0bdf75..2c9fb39 100644
--- a/front-end/src/utils/transportData.js
+++ b/front-end/src/utils/transportData.js
@@ -48,7 +48,7 @@ export async function queryTransportations(refresh) {
}
const urlParams = new URLSearchParams(params);
- const url = `${localStorage.serviceEndpointSub}/mapGetData.php?${urlParams.toString()}`;
+ const url = `${localStorage.serviceEndpointHome}/mapGetData.php?${urlParams.toString()}`;
try {
const response = await axios.post(url, formData);