DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Coding
  3. Languages
  4. Location-based Application with Play Framework, Scala, Google Maps Clustering, PostgreSQL, Heroku and Anorm

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

Felipe Oliveira user avatar by
Felipe Oliveira
·
Jan. 06, 12 · Interview
Like (0)
Save
Tweet
Share
17.51K Views

Join the DZone community and get the full member experience.

Join For Free


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!

    Google Maps Framework clustering Play Framework application PostgreSQL Google (verb) Scala (programming language)

    Opinions expressed by DZone contributors are their own.

    Popular on DZone

    • [DZone Survey] Share Your Expertise and Take our 2023 Web, Mobile, and Low-Code Apps Survey
    • Public Key and Private Key Pairs: Know the Technical Difference
    • How To Best Use Java Records as DTOs in Spring Boot 3
    • Test Execution Tutorial: A Comprehensive Guide With Examples and Best Practices

    Comments

    Partner Resources

    X

    ABOUT US

    • About DZone
    • Send feedback
    • Careers
    • Sitemap

    ADVERTISE

    • Advertise with DZone

    CONTRIBUTE ON DZONE

    • Article Submission Guidelines
    • Become a Contributor
    • Visit the Writers' Zone

    LEGAL

    • Terms of Service
    • Privacy Policy

    CONTACT US

    • 600 Park Offices Drive
    • Suite 300
    • Durham, NC 27709
    • support@dzone.com
    • +1 (919) 678-0300

    Let's be friends: