Binding.scala - Google Maps - Step by Step Tutorial
Clone or download
Pull request Compare This branch is 13 commits ahead, 1 commit behind Algomancer:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
ci
client/src/main/scala/example
project
screenshots
server
shared/src/main/scala/shared
.gitignore
.travis.yml
LICENSE
Procfile
README.md
activator.properties
app.json
build.sbt

README.md

Play with Scala.js, Binding.scala & Google Maps API

See the general setup on the original: Full-Stack-Scala-Starter This project is inspired by Binding.scala with Semantic-UI to get a step by step tutorial.

On top of the Full-Stack-Scala-Starter project with Binding.scala you will get an integration with Google Maps and its Scala JS implementation. Here a screenshot of how the result will look like: result screenshot

I created a branch for each step - so if you want to skip some of the setup steps - use them. Play and the server is not needed for this example.

Dependencies

Git Commit Git Branch If my Pull request was accepted this step is not needed.

I upgraded to new versions:

  • Scala: 2.12
  • Play: 2.6
  • Bindings: 11.0.0-M4

Verify the setup with sbt run: On http://localhost:9000 you should get a working page.

adding Google Maps

Git Commit - Git Branch

Next we add the ScalaJS facade for the Google Map API. We use this Scala JS implementation. Here the important steps from that project:

Include google maps on your page

 <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=API_KEY"></script>

This goes to the head section of server/app/view/main.scala.html. Make sure you add there your key!

Build.sbt

Add the following dependency to your porject.

resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"

"io.surfkit" %%% "scalajs-google-maps" % "0.0.3-SNAPSHOT",

I had troubles with the resolvers. It only worked when I added the resolver to USER_HOME/.sbt/repositories.

Add the map to the page

Add a div to main.scala.html: <div id="map-canvas"></div>

Add the style to public/stylesheets/main.css (otherwise you don't see the map):

#map-canvas {
       width: 100%;
       height: 800px;
   }

And replace the main method from the ScalaJSExample:

  def main(): Unit = {
    val initialize = js.Function {
      val opts = google.maps.MapOptions(
        center = new LatLng(51.201203, -1.724370),
        zoom = 8,
        panControl = false,
        streetViewControl = false,
        mapTypeControl = false)
      new google.maps.Map(document.getElementById("map-canvas"), opts)
      "" // this function needs a String as return type?!
    }

    google.maps.event.addDomListener(window, "load", initialize)
  }

Now you should see the map on localhost:9000

Add Bindings.scala

Git Commit

The dependency is already there, so no work there. So first we add a textfield and a button:

  @dom def render(): Binding[HTMLElement] = {
    <div>
      <input class="prompt" type="text" placeholder="Address..." />
      <button class="ui primary button">
        Search Address
      </button>
    </div>
  }

In Intellij you will get a compile exception even though you imported all needed dependencies. This is because the macro will transform the XML elements to Binding[HTMLElement]. You can fix that by adding this line: implicit def makeIntellijHappy(x: scala.xml.Elem): Binding[HTMLElement] = ???.

next we need to add this to the page. Add the following line to the main method: dom.render(document.getElementById("map-control"), render) And in the index.scala.html add <div id="map-control"></div> as first div.

Now check localhost:9000 if everything works as expected.

Putting everything together

Git Commit - Git Branch

We would like to search for an address and see it on the map. So first let us prepare the needed Google map code. To make the map available, provide it and its options as a variables:

  private lazy val opts = google.maps.MapOptions(
    center = new LatLng(51.201203, -1.724370),
    zoom = 8,
    panControl = false,
    streetViewControl = false,
    mapTypeControl = false)

  private lazy val gmap = new google.maps.Map(document.getElementById("map-canvas"), opts)

Provide a function that:

  1. takes the address (String) from the input
  2. gets a GeocoderResult (Position) from the Google map API
  3. centers the the map to the position
  4. sets a marker to the position
   private def geocodeAddress(address: String) { // 1
     val geocoder = new Geocoder()
     val callback = (results: js.Array[GeocoderResult], status: GeocoderStatus) =>
       if (status == GeocoderStatus.OK) {
         gmap.setCenter(results(0).geometry.location) // 3
         val marker = new google.maps.Marker(
           google.maps.MarkerOptions(map = gmap
             , position = results(0).geometry.location)) // 4
       } else {
         window.alert("Geocode was not successful for the following reason: " + status)
       }

     geocoder.geocode(GeocoderRequest(address), callback) // 2
   }

The initialize function is now as simple as:

  private lazy val initialize = js.Function {
    gmap // the map must be initialized in this function
    "" // this function needs a String as return type?!
  }

Extend the Bindings.scala code

  • oninput sets the value of the 'search-Var' on each input character (searchInput is a compile exception on Intellij)
  • onclick calls the geocodeAddress function with the current 'search-Var' value
  • for demonstration only I added the 'search-Var' bind example that automatically displayes the search value
  @dom private lazy val render: Binding[HTMLElement] = {
    val search: Var[String] = Var("")

    <div>
      <input id="searchInput" class="prompt" type="text" placeholder="Address..." oninput={event: Event => search.value = searchInput.value}/>
      <button class="ui primary button" onclick={event: Event =>
        geocodeAddress(search.value)}>
        Search Address
      </button>
      <div>Your input is {search.bind}</div>
    </div>
  }

Now the main function looks as simple as:

  def main(): Unit = {
    dom.render(document.getElementById("map-control"), render)
    google.maps.event.addDomListener(window, "load", initialize)
  }

Dive a bit deeper

Git Commit - Git Branch

Ok lets add a list that shows possible Addresses for our input, from where we can select one, or just take the first. First we need another datatype where we can pass around the possible addresses:

 private val possibleAddrs: Var[Seq[GeocoderResult]] = Var(Seq())

We need to redo our Address fetching function a bit:

  private def possibleAddresses(address: String) {

    val callback = (results: js.Array[GeocoderResult], status: GeocoderStatus) =>
      if (status == GeocoderStatus.OK) {
        possibleAddrs.value = results.to[Seq].take(5)
      } else {
        window.alert("Geocode was not successful for the following reason: " + status)
      }

    new Geocoder().geocode(GeocoderRequest(address), callback)
  }

We provide two helper function that will display the Address on the map

  private def selectAddress() {
    val value = possibleAddrs.value
    if(value.nonEmpty)
      selectAddress(value.head)
    else
      window.alert("There is no Address for your input")
  }

  private def selectAddress(address: GeocoderResult) {
    gmap.setCenter(address.geometry.location)
    val marker = new google.maps.Marker(
      google.maps.MarkerOptions(map = gmap
        , position = address.geometry.location))
  }

We adjust our render function:

  @dom private lazy val render: Binding[HTMLElement] = {
    <div>
      <input id="searchInput" class="prompt" type="text" placeholder="Address..." oninput={event: Event =>
      val value: String = searchInput.value
      if (value.length > 2)
        possibleAddresses(value)}/>
      <button class="ui primary button" onclick={event: Event =>
        selectAddress()}>
        Search Address
      </button>
      <div>
        <ol>
          {for (addr <- possibleAddrs.bind) yield
          <li>
            {addr.formatted_address}<button onclick={event: Event =>
            selectAddress(addr)}>select</button>
          </li>}
        </ol>
      </div>
    </div>
  }

Now it gets tricky. If you refresh localhost:9000 you will get a Compile Exception: 'each' instructions must be inside a SDE block. Ok, that suggest to extract the <li> part:

 ...
    <ol>
      {for (addr <- possibleAddrs.bind) yield
      renderPosAddr(addr: GeocoderResult).bind}
    </ol>
 ...

  @dom private def renderPosAddr(addr: GeocoderResult): Binding[HTMLElement] = {
    <li>
      {addr.formatted_address}<button onclick={event: Event =>
      selectAddress(addr)}>select</button>
    </li>
  }

That was not enough - still the same exception! Now we need to do it like this (explained here: Stackoverflow )

 ...
    <ol>
      {Constants(possibleAddrs.bind.map(addr =>
                renderPosAddr(addr)): _*).map(_.bind)}
    </ol>
 ...

Now everything compiles and we can search our Addresses!

Conclusion

It's quite interesting to see a stream based Framework (Binding.scala) next to the callback based API (Google maps).

  • The Binding.scala solution is really elegant.
  • I had, still have some problems that there are compile time exceptions shown by the IDE (Intellij). Some I could get rid of by adding implicit conversions.
  • The usage of scala XML to declare the HTML-DOM is really nice. You literally can copy your HTML code directly, just adding the dynamic parts.

Improvements

Please let me know if there are things:

  • that could be done better
  • there are errors
  • you like to extend

Just create an issue on that repo.