Cloud Zone is brought to you in partnership with:

The American Dream Realized: NYC-based Java Consultant. Author of Play Framework Mods Elastic Search, RabbitMQ. JavaLobby Featured MVB Writer. I blog at http://geeks.aretotally.in and tweet at http://twitter.com/_felipera. Felipe is a DZone MVB and is not an employee of DZone and has posted 13 posts at DZone. View Full User Profile

Location-based Application with Play Framework, Scala, Google Maps Clustering, PostgreSQL, Heroku and Anorm

01.06.2012
| 12014 views |
  • submit to reddit


As I mentioned in a past article, Why Did I Fall in Love with Play! Framework?, I was the tech lead for a few real estate deployments for Fannie Mae, HUD, Foreclosure.com, etc; as you know, real estate is all about LOCATION! LOCATION! LOCATION!

I’m on vacation, but I am still a geek (and my wife is still sleeping!) so I decided to create a location-based application using the all-awesomeness of Play Framework, Scala, Google Maps v3, PostgreSQL and Anorm; you know, for ol’ times’ sake! On the map-side I will show you how to solve the very common “too many markers” problem (see image below) using clustering to group the markers.

See a live demo running on Heroku right here! The complete source code is available at https://github.com/feliperazeek/where-is-felipe. Thank you my crazy French friend Pascal (author of Siena) for the review.

  • First let’s create the data transfer classes returned by the Controller in JSON form and used by Google Maps (markers and clusters). These are very simple classes which will just hold data used by the thin RESTful layer.
  • /**
     * Map Marker
     *
     * @author Felipe Oliveira [@_felipera]
     */
    case class MapMarker(id: String, latitude: Double, longitude: Double, address: Option[String], city: Option[String], state: Option[String], zip: Option[String], county: Option[String])
    
    /**
     * Map Cluster
     *
     * @author Felipe Oliveira [@_felipera]
     */
    case class MapCluster(geohash: String, count: Long, latitude: Double, longitude: Double)
    
    /**
     * Map Cluster Companion
     *
     * @author Felipe Oliveira [@_felipera]
     */
    object MapCluster {
    
        /**
         * Constructor
         */
        def apply(geohash: String, count: Long): MapCluster = {
            val coords = GeoHashUtils decode geohash
            new MapCluster(geohash, count, coords(0), coords(1))
        }
    
    }
    
    /**
     * Map Overlay
     *
     * @author Felipe Oliveira [@_felipera]
     */
    case class MapOverlay(markers: List[MapMarker], clusters: List[MapCluster])
  • Second let’s define the model classes using Play Framework’s Anorm. There’s only one model defined—it’s called Site. That’s a very simple model which contains only basic fields, such as id, address, city, state, city, zip, county, latitude, longitude and geohash. Geohash uniquely identifies geographical areas, a system created by Gustavo Niemyer, and it has many limitations in comparison to true GIS tools, such as PostgreSQL’s PostGIS or search indexes with geo-features (like my personal favorite ElasticSearch). Although geohash has its share of limitations, it’s a very easy way to add geographical capabilities using data stores without GeoSpatial capabilities such as MySQL. In this case, I am using geohashes to group markers, the so-called marker clusters (punches in bunches, baby!). Click here to find a good representation on how geohashes work. I am using the geohash encoder and decoder from Lucene Spatial, very easy to use.
    /**
     * To String Trait
     *
     * @author Felipe Oliveira [@_felipera]
     */
    trait ToString {
        self: Entity =>
    
        /**
         * Reflection-Based ToString
         */
        override def toString = ToStringBuilder.reflectionToString(this)
    
    }
    
    /**
     * Entity Base Class
     *
     * @author Felipe Oliveira [@_felipera]
     */
    trait Entity extends ToString
    
    /**
     * Site Model
     *
     * @author Felipe Oliveira [@_felipera]
     */
    case class Site(
            var id: Pk[Long],
            @Required var name: Option[String],
            @Required var address: Option[String],
            @Required var city: Option[String],
            @Required var state: Option[String],
            @Required var zipcode: Option[String],
            @Required var county: Option[String],
            @Required var latitude: Option[Double],
            @Required var longitude: Option[Double]) extends Entity {
    
        /**
         * Constructor
         */
        def this(address: Option[String], city: Option[String], state: Option[String], zipcode: Option[String], county: Option[String], latitude: Option[Double], longitude: Option[Double]) {
            this(NotAssigned, None, address, city, state, zipcode, county, latitude, longitude)
        }
    
    }
    
    /**
     * Site Companion Object
     *
     * @author Felipe Oliveira [@_felipera]
     */
    object Site extends Magic[Site] {
    
        /**
         * Count
         */
        def count(implicit filters: SearchFilters): Long = statement("select count(1) as count from Site").as(scalar[Long])
    
        /**
         * Map Overlay
         */
        def mapOverlay(implicit filters: SearchFilters): MapOverlay = {
            // Get Map Clusters
            val clusters = mapClusters
    
            // Get Markers
            filters.geohashes = Option(clusters.filter(_.count == 1).map(_.geohash).toList)
            val markers = mapMarkers
    
            // Define Map Overlay
            new MapOverlay(markers, clusters.filter(_.count > 1))
        }
    
        /**
         * Map Clusters
         */
        def mapClusters(implicit filters: SearchFilters): List[MapCluster] = {
            // Get Query
            val query = statement("select " + geohashExpression + " as geohash, count(1) as count from Site", "group by " + geohashExpression + " order by count desc")
    
            // Get Results
            val list: List[MapCluster] = query().filter(_.data(0) != null).map {
                row =>
                    {
                        // Get Fields
                        val fields = row.data
    
                        // Geohash
                        Option(fields(0)) match {
                            case Some(geohash: String) => {
                                // Count
                                val count: Long = fields(1).toString.toLong
    
                                // Map Cluster
                                MapCluster(geohash, count)
                            }
                            case _ => null
                        }
                    }
            } toList
    
            // Log Debug
            please log "Map Clusters: " + list.size
    
            // Return List
            list.filter(_ != null)
        }
    
        /**
         * Map Markers
         */
        def mapMarkers(implicit filters: SearchFilters): List[MapMarker] = {
            // Get Query
            val query = statement("select site.* from Site site", "order by id")
    
            // Get Results
            val list: List[MapMarker] = query().map {
                row =>
                    try {
                        // Id
                        val id = row[String]("id")
    
                        // Fields
                        val address = row[Option[String]]("address")
                        val city = row[Option[String]]("city")
                        val state = row[Option[String]]("state")
                        val zip = row[Option[String]]("zip")
                        val county = row[Option[String]]("county")
                        val latitude = row[Option[Double]]("latitude")
                        val longitude = row[Option[Double]]("longitude")
    
                        // Map Marker (coord required)
                        (latitude, longitude) match {
                            case (lat: Some[Double], lng: Some[Double]) => new MapMarker(id, lat.get, lng.get, address, city, state, zip, county)
                            case _ => null
                        }
    
                    } catch {
                        case error: Throwable => {
                            please report error
                            null
                        }
                    }
            } toList
    
            // Log Debug
            please log "Map Markers: " + list.size
    
            // Return List
            list
        }
    
        /**
         * Statement
         */
        def statement(prefix: String, suffix: Option[String] = None)(implicit filterBy: SearchFilters) = {
            // Params
            val geohashes = filterBy geohashes
            val zoom = filterBy zoom
            val geohashPrecision = filterBy geohashPrecision
    
            // Params will contain the list of name/value pairs that need to be bound to the query
            val params = new HashMap[String, Any]
    
            // This is gonna define the statement that we'll use on the find method
            val terms = new StringBuffer
            terms.append(prefix)
            terms.append(" where ")
    
            // Boundary
            filterBy.hasBounds match {
                case true => {
                    params += "nw" -> filterBy.nw.get
                    params += "ne" -> filterBy.ne.get
                    params += "se" -> filterBy.se.get
                    params += "sw" -> filterBy.sw.get
                    terms.append("latitude between {nw} and {ne} and longitude between {sw} and {se} and ")
                }
                case _ => please log "Ignoring Map Bounds!"
            }
    
            // Geohashes
            geohashes match {
                case Some(list) => {
                    if (!list.isEmpty) {
                        val values = list.map(_.substring(0, geohashPrecision)).toSet
                        terms.append(geohashExpression + " in (" + multiValues(values) + ") and ")
                        terms.append("geohash is not null and ")
                    }
                }
                case _ => please log "Not including geohashes in query!"
            }
    
            // Final one just in case
            terms.append("1 = {someNumber} ")
    
            // Suffix
            terms.append(suffix.getOrElse(""))
    
            // Define SQL
            val sql = terms.toString.trim
    
            // Define Query
            var query = SQL(sql).on("someNumber" -> 1)
            for (param <- params) {             please log "Bind - " + param._1 + ": " + param._2             query = query.on(param._1 -> param._2)
            }
    
            // Return Query
            query
        }
    
        /**
         * Geohash Expression
         */
        def geohashExpression(implicit filterBy: SearchFilters): String = filterBy.zoom match {
            case Some(z: Int) if z > 0 => "substring(geohash from 1 for " + filterBy.geohashPrecision.toString + ")"
            case _ => "geohash"
        }
    }
  • Then let’s define the search filters.
    /**
     * Search Filters
     *
     * @author Felipe Oliveira [@_felipera]
     */
    case class SearchFilters(var ne: Option[Double], var sw: Option[Double], var nw: Option[Double], var se: Option[Double], var geohashes: Option[List[String]] = None, var zoom: Option[Int] = None) {
    
        /**
         * Log Debug
         */
        please log "NE: " + ne
        please log "SW: " + sw
        please log "NW: " + nw
        please log "SE: " + se
        please log "Geohashes: " + geohashes
        please log "Zoom: " + zoom
    
        /**
         * Format Date
         */
        def format(date: Date) = dateFormat format date
    
        /**
         * Geohash Precision
         */
        def geohashPrecision: Int = zoom match {
            case Some(z: Int) if z > 0 => 22 - z
            case _ => 1
        }
    
        /**
         * Geohash Suffix
         */
        def geohashSuffix: String = geohashPrecision toString
    
        /**
         * Has Bounds
         */
        def hasBounds: Boolean = {
            please log "Has Bounds? NE: " + ne + ", SW: " + sw + ", NW: " + nw + ", SE: " + se
            if (ne.valid && nw.valid && nw.valid && se.valid) {
                please log "Yes!"
                true
            } else {
                please log "No!"
                false
            }
        }
    
        /**
         * To Query String
         */
        def toQueryString = {
            // Start
            val queryString = new StringBuilder
    
            // Map Bounds
            if (hasBounds) {
                queryString append "ne=" append ne append "&"
                queryString append "sw=" append sw append "&"
                queryString append "nw=" append nw append "&"
                queryString append "se=" append se append "&"
            }
    
            // Zoom
            zoom match {
                case Some(s) => queryString append "zoom=" append s append "&"
                case _ => please log "No zoom level defined!"
            }
    
            // Log Debug
            please log "Query String: " + queryString
            queryString.toString
        }
    
    }
  • And a bunch of sugar!
    package pretty
    
    import play.Logger
    import play.mvc.Controller
    import org.apache.commons.lang.exception.ExceptionUtils
    import net.liftweb.json.Printer._
    import net.liftweb.json.JsonAST._
    import net.liftweb.json.Extraction._
    import java.util.{ Calendar, Date }
    import org.joda.time.Months
    import java.net.URLEncoder
    import java.text.SimpleDateFormat
    import org.apache.commons.lang.StringUtils
    import play.Logger
    import org.joda.time.DateTime
    import java.util.Date
    import java.sql.Timestamp
    
    /**
     * Helper Object
     *
     * @author Felipe Oliveira [@_felipera]
     */
    object please {
    
        /**
         * JSON Formats
         */
        implicit val formats = PlayParameterReader.Formats.formats
    
        /**
         * Compress
         */
        def compress[A](ls: List[A]): List[A] = {
            ls.foldRight(List[A]()) {
                (h, r) =>
                    if (r.isEmpty || r.head != h) {
                        h :: r
                    } else {
                        r
                    }
            }
        }
    
        /**
         * Time Execution
         */
        def time[T](identifier: String = "n/a")(runnable: => T): T = {
            throwOnError {
                // Define Search Start Time
                val started = new Date
    
                // Execute Action
                val results: T = runnable
    
                // Define Duration Time
                logOnError[Unit] {
                    val ended = new Date
                    val duration = ended.getTime - started.getTime
                    Logger.info("Runnable: " + identifier + ", Duration: " + duration + "ms (" + (duration / 1000.0) + "s)")
                }
    
                // Return Results
                results
            }
        }
    
        /**
         * Now
         */
        def now = new Date()
    
        /**
         * Current Time
         */
        def currentTime = now.getTime
    
        /**
         * Log
         */
        def log(any: String) = Logger info any
    
        /**
         * Report Exception
         */
        def report(error: Throwable) = Logger error ExceptionUtils.getStackTrace(error)
    
        /**
         * Dummy Controller
         */
        private val _dummy = new Controller {}
    
        /**
         * Conf
         */
        def conf(name: String) = play.Play.configuration.getProperty(name)
    
        /**
         * Multi Values
         */
        def multiValues(values: Iterable[String]) = values.mkString("'", "','", "'")
    
        /**
         * Log If Error
         */
        def logOnError[T](runnable: => T): Option[T] = {
            try {
                Some(runnable)
            } catch {
                case error: Throwable =>
                    please report error
                    None
            }
        }
    
        /**
         * Throw if Error
         */
        def throwOnError[T](runnable: => T): T = {
            try {
                runnable
            } catch {
                case error: Throwable => {
                    please report error
                    throw new RuntimeException(error fillInStackTrace)
                }
            }
        }
    
        /**
         * URL Encode
         */
        def encode(value: String) = URLEncoder.encode(value, conf("encoding"))
    
        /**
         * Automatically generate a standard perks json response.
         */
        def jsonify(runnable: => Any) = {
            _dummy.Json(pretty(render(decompose(
                try {
                    val data = runnable match {
                        // Lift-json has a cow if you pass it a mutable map to render.
                        case mmapResult: Map[Any, Any] => mmapResult.toMap
                        case result => result
                    }
                    Map("status" -> 200, "data" -> data)
                } catch {
                    case error: Throwable =>
                        please report error
                        Map("status" -> 409, "errors" -> Map(error.hashCode.toString -> error.getMessage))
                }
            ))))
        }
    
        /**
         * Option[Date] to Pimp Date Implicit Conversion
         */
        implicit def date2PimpDate(date: Option[Date]) = new PimpDate(date.getOrElse(new Date))
    
        /**
         * Option[Date] to Date Implicit Conversion
         */
        implicit def optionDate2Date(date: Option[Date]) = date.getOrElse(new Date)
    
        /**
         * Int to Pimp Int
         */
        implicit def int2PimpInt(i: Int) = new PimpInt(i)
    
        /**
         * String to Date
         */
        implicit def stringToDate(string: String): Date = dateFormat parse string
    
        /**
         * Date to String
         */
        implicit def dateToString(date: Date): String = dateFormat format date
    
        /**
         * Option Date to String
         */
        implicit def optionDate2String(date: Option[Date]): String = date match {
            case Some(d: Date) => d
            case _ => "n/a"
        }
    
        /**
         * Option Double to Pimp Option Double
         */
        implicit def optionDoubleToPimpOptionDouble(value: Option[Double]): PimpOptionDouble = new PimpOptionDouble(value)
    
        /**
         * String to Double
         */
        implicit def stringToDouble(str: Option[String]): Double = {
            str match {
                case Some(s) => {
                    try {
                        s.toDouble
                    } catch {
                        case ne: NumberFormatException => 0.0
                        case error: Throwable => 0.0
                    }
                }
                case _ => 0.0
            }
        }
    
        /**
         * Date Format
         */
        def dateFormat = new SimpleDateFormat("MM/dd/yyyy")
    
        /**
         * String to Option String
         */
        implicit def string2OptionString(value: String): Option[String] = Option(value)
    
        /**
         * Option String to String
         */
        implicit def optionString2String(value: Option[String]): String = value.getOrElse("n/a")
    
        /**
         * String to Option Double
         */
        implicit def string2OptionDouble(value: String): Option[Double] = try {
            if (StringUtils.isNotBlank(value)) {
                Option(value.toDouble)
            } else {
                None
            }
        } catch {
            case error: Throwable => {
                please report error
                None
            }
        }
    
        /**
         * Dinossaur Birth Date
         */
        def dinoBirthDate = {
            val c = Calendar.getInstance()
            c.set(0, 0, 0)
            c.getTime
        }
    
    }
    
    /**
     * Pimp Int
     *
     * @author Felipe Oliveira [@_felipera]
     */
    case class PimpInt(value: Int) {
    
        /**
         * Months Ago
         */
        def monthsAgo = {
            val c = Calendar.getInstance()
            c.setTime(new Date)
            c.add(Calendar.MONTH, (value * -1))
            c.getTime
        }
    
    }
    
    /**
     * Pimp Option Double
     *
     * @author Felipe Oliveira [@_felipera
     */
    case class PimpOptionDouble(value: Option[Double]) {
    
        /**
         * Is Valid
         */
        def valid: Boolean = value.getOrElse(0.0) != 0.0
    
    }
    
    /**
     * Pimp Date
     *
     * @author Felipe Oliveira [@_felipera]
     */
    class PimpDate(date: Date) {
    
        /**
         * Is?
         */
        def is = {
            Option(date) match {
                case Some(d) => d
                case _ => new Date
            }
        }
    
        /**
         * Before?
         */
        def before(other: Date) = date.before(other)
    
        /**
         * After?
         */
        def after(other: Date) = date.after(other)
    
        /**
         * Past?
         */
        def past = date.before(new Date)
    
        /**
         * Future?
         */
        def future = date.after(new Date)
    
        /**
         * Subtract
         */
        def -(months: Int) = {
            val c = Calendar.getInstance()
            c.setTime(date)
            c.add(Calendar.MONTH, (months * -1))
            c.getTime
        }
    
    }
    
    /**
     * Clockman Object
     *
     * @author Felipe Oliveira [@_felipera]
     */
    object Clockman {
    
        /**
         * Pimp Date
         */
        def is(date: Date) = new PimpDate(date)
    
    }
  • Then let’s define the controllers.
    /**
     * Filters Trait
     *
     * @author Felipe Oliveira [@_felipera]
     */
    trait Filters {
        self: Controller =>
    
        /**
         * Search Filters
         */
        def filters = new SearchFilters(ne, sw, nw, se, zoom = zoom)
    
        /**
         * Zoom
         */
        def zoom: Option[Int] = Option(params.get("zoom")) match {
            case Some(o: String) => Option(o.toInt)
            case _ => Option(1)
        }
    
        /**
         * NE Bound
         */
        def ne = boundParam("ne")
    
        /**
         * SW Bound
         */
        def sw = boundParam("sw")
    
        /**
         * NW Bound
         */
        def nw = boundParam("nw")
    
        /**
         * SE Bound
         */
        def se = boundParam("se")
    
        /**
         * Bound Param
         */
        def boundParam(name: String): String = Option(params.get(name)).getOrElse("")
    
    }
    
    /**
     * Main Controller
     *
     * @author Felipe Oliveira [@_felipera]
     */
    object Application extends Controller with controllers.Filters {
    
        /**
         * Index Action
         */
        def index = time("index") {
            views.Application.html.index(filters)
        }
    
    }
    
    /**
     * Geo Controller
     *
     * @author Felipe Oliveira [@_felipera]
     */
    object Geo extends Controller with controllers.Filters {
    
        /**
         * Map Overlay
         */
        def mapOverlay = time("mapOverlay") {
            jsonify {
                implicit val searchWith = filters
                Site mapOverlay
            }
        }
    
    }
  • Now it’s time for the frontend code, it’s mostly sugar so I am just gonna show you the JavaScript part of it, the important piece. Basically it defined the map, calls the RESTful MapOverlay API, grabs the markers and clusters and bind them to the map!
    // "Da" Map
    var map;
    
    // Global Markers Array
    var markers = [];
    
    // Global Debug Flag
    var debug = true;
    if (console == null) {
        debug = false;
    }
    
    // Log Function
    function log(msg) {
        if (debug) {
            console.log(msg);
        }
    }
    
    // Initialize Map
    function initialize() {
        // Map Options
        var myOptions = {
              zoom: 8,
              mapTypeId: google.maps.MapTypeId.ROADMAP
        };
    
        // Define Map
        map = new google.maps.Map(document.getElementById('map_canvas'), myOptions);
    
        // Set Map Position
        map.setCenter(new google.maps.LatLng(40.710623, -74.006605));
    
        // Load Markers
        loadMarkers(map, null);
    
        // Listen for Map Movements
        google.maps.event.addListener(map, 'idle', function(ev) {
                log("Idle Listener!");
                var bounds = map.getBounds();
                var ne = bounds.getNorthEast(); // LatLng of the north-east corner
                var sw = bounds.getSouthWest(); // LatLng of the south-west corner
                var nw = new google.maps.LatLng(ne.lat(), sw.lng());
                var se = new google.maps.LatLng(sw.lat(), ne.lng());
                var q = "&ne=" + ne.lat() + "&sw=" + sw.lng() + "&nw=" + sw.lat() + "&se=" + ne.lng();
                log("Map Bounds: " + q);
                clearOverlays();
                loadMarkers(map, q);
            });
    }
    
    // Clear Markers
    function clearOverlays() {
        log("Clear Overlays!");
        if (markers != null) {
            for (i in markers) {
                markers[i].setMap(null);
            }
        }
        markers = new Array();
    }
    
    // Cap Words
    function capWords(str){
        if (str == null) {
            return "";
        }
        str = str.toLowerCase();
       words = str.split(" ");
       for (i=0 ; i < words.length ; i++){
          testwd = words[i];
          firLet = testwd.substr(0,1); //lop off first letter
          rest = testwd.substr(1, testwd.length -1)
          words[i] = firLet.toUpperCase() + rest
       }
       return words.join(" ");
    }
    
    // Load Markers
    function loadMarkers(map, extra) {
        $(document).ready(function() {
            zoomLevel = map.getZoom();
            if (zoomLevel == null) {
                zoomLevel = 1;
            }
            var url = '/mapOverlay';
            if (document.location.search) {
                url = url + document.location.search;
            } else {
                url = url + "?1=1"
            }
            if (extra != null) {
                url = url + extra;
            }
            url = url + "&zoom=" + zoomLevel;
            log('URL: ' + url);
            $.getJSON(url, function(json) {
                markers = new Array(json.data.markers.length);
                for (i = 0; i < json.data.markers.length; i++) {
                      // Get Marker
                      var marker = json.data.markers[i];
    
                      // Customer Logo
                      var icon = "http://geeks.aretotally.in/wp-content/uploads/2011/03/html5_geek_matt_16.png";
                      var logo = "http://geeks.aretotally.in/wp-content/uploads/2011/03/html5_geek_matt_32.png";
                      var width = 32;
                      var height = 32;
                      var customer = marker.customer;
    
                      // console.log('Marker: ' + marker + ', Lat: ' + marker.latitude + ', Lng: ' + marker.longitude);
                      var contentString = marker.address + ' ' + marker.city + ', ' + marker.state + ' ' + marker.zip;
    
                      var position = new google.maps.LatLng(marker.latitude, marker.longitude);
    
                      var m = new google.maps.Marker({
                          position: position,
                          icon: icon,
                          html: contentString
                      });
    
                      markers[i] = m;
                }
    
                // Clusters
                for (i = 0; i < json.data.clusters.length; i++) {
                    var cluster = json.data.clusters[i];
                    log('Cluster: ' + cluster);
    
                    var position = new google.maps.LatLng(cluster.latitude, cluster.longitude);
    
                    for (c = 0; c < cluster.count; c++) {
                      log('Cluster Item: ' + c);
                      var m = new google.maps.Marker({
                          position: position
                      });
    
                      markers[i] = m;
                    }
                }
    
                // Marker Cluster
                var clusterOptions = { zoomOnClick: true }
                var markerCluster = new MarkerClusterer(map, markers, clusterOptions);
    
                // Info Window
                var infowindow = new google.maps.InfoWindow({
                    content: 'Loading...'
                });
    
                // Info Window Listener for Markers
                for (var i = 0; i < markers.length; i++) {
                    var marker = markers[i];
                    google.maps.event.addListener(marker, 'click', function () {
                        // Log Debug
                        log('Marker Click!');
    
                        // Set Info Window Marker Content
                        infowindow.close();
                        infowindow.setContent(this.html);
    
                        // Set Current Marker
                        var currentMarker = this;
    
                        // Get Map Position
                        var mapLatLng = map.getCenter();
                        var markerLatLng = currentMarker.getPosition();
    
                        // Check Coordinate
                        if (!markerLatLng.equals(mapLatLng)) {
                            // Map will need to pan
                            map.panTo(markerLatLng);
                            google.maps.event.addListenerOnce(map, 'idle', function() {
                                // Open Info Window
                                infowindow.open(map, currentMarker);
                                setTimeout(function () { infowindow.close(); }, 5000);
                            });
                        } else {
                            // Map won't be panning, which wouldn't trigger 'idle' listener so just open info window
                            infowindow.open(map, currentMarker);
                            setTimeout(function () { infowindow.close(); }, 5000);
                        }
                    });
                }
    
            });
        });
    }
    
    // Initialize Map
    google.maps.event.addDomListener(window, 'load', initialize);
  • Happy New Year! 2011 was a great year, I can’t wait to see what 2012 has in store!

    Published at DZone with permission of Felipe Oliveira, author and DZone MVB.

    (Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

    Comments

    Anand Narasimhan replied on Sat, 2012/01/07 - 1:46pm

    Great article Felipe. A lot of code to consume. Next time probably break it into multiple parts so that it's easily consumed ;-).

    Felipe Oliveira replied on Sun, 2012/01/08 - 4:43pm in response to: Anand Narasimhan

    Good call thank you for the feedback!

    Tyson Hamilton replied on Tue, 2014/02/04 - 5:11pm



    Tyson Hamilton replied on Tue, 2014/02/04 - 5:10pm

    Excellent post, finally the clutter from too many pins is solved! Some things to note,

    that was a long, long post

    be explicit with return types so I don't need to guess / think real hard =)


    Comment viewing options

    Select your preferred way to display the comments and click "Save settings" to activate your changes.