Wednesday, February 24, 2010

ADF Faces RC: Building a Reusable Google Map Viewer that Supports both Geocoding and Reverse Geocoding (Applying Frank Nimphius' Declarative Lightweight Popup)

In this post, I will show you how to build a reusable Google Map Viewer that supports both Geocoding and Reverse Geocoding. This map viewer is implemented based on Frank Nimphius' declarative lightweight popup pattern. This can be incorporated from anywhere in your application that has address information.

Our steps to recreate this would be as follows:
  1. Sign up for the Google Maps API
  2. Create a new Task Flow named "google-map-viewer-task-flow" with a single view and a task-flow-return activity.
  3. Define task-flow input and return parameters
  4. Create and design the google_map.jspx
  5. Copy my javascript code
  6. Create the GoogleMapViewerForm (the backing bean of google_map.jspx)
  7. Integrate the google-map-viewer-task-flow as a task-flow-call activity into your existing task flows

Sign up for the Google Maps API

Go to the following site: http://code.google.com/apis/maps/signup.html and secure an API Key. A single Maps API key is valid for a single directory or domain. If you are testing your app using http://localhost:7101 or http://127.0.0.1:7101, then you should input that in the "My web site URL" textbox when generating the key. You must have a Google Account to get a Maps API key, and your API key will be connected to your Google Account. Keep the API Key for later use.

Create a new Task Flow named "google-map-viewer-task-flow" with a single view and a task-flow-return activity

Create the task flow with a single view named "google_map", a task-flow-return named "exit", and a control-flow-case named "return" as depicted below:

Define taskflow input and return parameters

Below are the optional input parameters and there definitions:
  • address - if a valid latitude or longitude parameter is not available, the map viewer will try to geocode the address;
  • latitude - both latitude and longitude should be provided so that the map viewer will process the coordinates and center it on map, otherwise the map viewer will just geocode for the address;
  • longitude - same as latitude info above;
  • countryCode - to prefer results to this country (but not to restrict the results); stated as a two-letter ISO compliant code;
Below are the defined output parameters which could return null values if no point was clicked on the map for reverse geocoding:
  • returnAddress - reverse geocode resulting address;
  • returnLatitude - selected point latitude;
  • returnLongitude - selected point longitude, selected coordinates are mark with an arrow marker;
  • returnDetails - if a point was selected in the map for reverse geocoding, the selected coordinates and the resulting details (all information from the client including the address above) will be returned here, otherwise null;

Create and design the google_map.jspx

Double click the google_map view in the task flow to invoke the "create page" wizard. You could put the page under ".../public_html/pages". Basically here are the items that we need to put on our page:
  • a clientListener that upon page load invokes the initialize javascript which inturn call a serverListener;
  • a serverListener mentioned above that invokes a server side code;
  • a <af:resource> component that will import our external javascript;
  • a panelHeader where the map will be placed;
  • an inputTextBox to input address to search.
  • a button to invoke search;
  • a button to return to the calling task-flow;
<?xml version="1.0" encoding="UTF-8"?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page" version="2.1"
          xmlns:f="http://java.sun.com/jsf/core"
          xmlns:h="http://java.sun.com/jsf/html"
          xmlns:af="http://xmlns.oracle.com/adf/faces/rich"
          xmlns:v="urn:schemas-microsoft-com:vml">
  <jsp:directive.page contentType="text/html;charset=UTF-8"/>
  <f:view>
    <af:document id="d1" clientComponent="true">
        <af:clientListener method="initialize" type="load" />
        <af:serverListener type="loadGoogleMap"
                           method="#{backingBeanScope.googleMapViewer.loadGoogleMap}" />
        <af:serverListener type="submitInfoToServer"
                           method="#{backingBeanScope.googleMapViewer.setReturnValues}" />
      
      <af:form id="f1" defaultCommand="cb1">
       
        <af:panelStretchLayout id="psl1" topHeight="auto">
          <f:facet name="center">
            <af:panelHeader id="mapPH" text=" "
                            inlineStyle="width:700PX; height: 400px">
              <f:facet name="toolbar"/>
            </af:panelHeader>
          </f:facet>
          <f:facet name="top">
            <af:panelBorderLayout id="pbl1">
              <f:facet name="start">
                <af:panelGroupLayout id="pgl2" layout="horizontal">
                  <af:inputText id="searchField" clientComponent="true"
                                columns="100"/>
                  <af:commandButton id="cb1" text="Search"
                                    clientComponent="true" partialSubmit="true">
                    <af:clientListener type="click" method="goFindAddress"/>
                  </af:commandButton>
                </af:panelGroupLayout>
              </f:facet>
              <f:facet name="end">
                <af:panelGroupLayout id="pgl1">
                  <af:commandButton text="Return" id="cb2" action="return"/>
                </af:panelGroupLayout>
              </f:facet>
            </af:panelBorderLayout>
          </f:facet>
        </af:panelStretchLayout>
      </af:form>
      <f:facet name="metaContainer">
        <af:group>
          <af:resource type="javascript" source="http://www.google.com/jsapi?key=YOUR_KEY_FROM_STEP_1"></af:resource>      
           <af:resource type="javascript">
    google.load("maps", "2.x");
   </af:resource>
   <af:resource type="javascript" source="/js/google_map_viewer.js"/>
   <af:resource type="javascript">
          window.onunload = GUnload;
          </af:resource>      
         </af:group>
      </f:facet>
    </af:document>
  </f:view>
</jsp:root>
Find the snippet below in the above code and replace the key with the key you generated in step 1:
<af:resource type="javascript" source="http://www.google.com/jsapi?key=YOUR_KEY_FROM_STEP_1"></af:resource>   

Copy my javascript code

This the bonus part. You'll just have to copy my code below for now and understand it later. Put the google_map_viewer.js under ".../public_html/js" folder.
/**http://soadev.blogspot.com/
*/

var map;
var geocoder;
var orig_coordinates;
var orig_marker;

function initialize(event) {
    var source = event.getSource();
    AdfCustomEvent.queue(source, "loadGoogleMap", 
    {
    },
false);
}

function initializeMap(clientId, latitude, longitude, address, countryCode) {
    if (GBrowserIsCompatible()) {
        map = new google.maps.Map2(document.getElementById(clientId));
        map.addControl(new GLargeMapControl());
        map.addControl(new GMapTypeControl());
        GEvent.addListener(map, 'click', getAddress);
        geocoder = new GClientGeocoder();
        //tailor to a particular domain (country)     
        if (countryCode) {
            geocoder.setBaseCountryCode(countryCode);
        }
        if (latitude != null && longitude != null) {
            //valid coordinates
            //set center of map to the coordinates and add a marker 
            orig_coordinates = new GLatLng(latitude, longitude);
            map.setCenter(orig_coordinates, 15);
            orig_marker = new GMarker(orig_coordinates);
            map.addOverlay(orig_marker);
            var info = "Original Coordinates: " + latitude + "," + longitude;
            addClickListener(orig_marker, info);
        }
        else {
            //none valid coordinates so try to find the address instead
            if (address != null) {
                findAddress(address);
            }
            else {
                //address is null so set center to default
                map.setCenter(new GLatLng(0, 0), 1);
            }
        }
    }
}

function getAddress(overlay, latlng) {
    //if far enough return
    if (map.getZoom() < 14) {
        return;
    }
    if (latlng != null) {
        var icon = createArrowIcon();
        arrow_marker = new GMarker(latlng, 
        {
            icon : icon
        });
        map.clearOverlays();
        map.addOverlay(arrow_marker);
        addClickListener(arrow_marker, latlng.toUrlValue());
        if (orig_marker) {
            map.addOverlay(orig_marker);
            var info = "Original Coordinates: " + orig_coordinates.toUrlValue();
            addClickListener(orig_marker, info);
        }
        //Reverse GeoCode
        geocoder.getLocations(latlng, showAddress);
    }
}

function createArrowIcon() {
    var icon = new GIcon();
    var url = "http://maps.google.com/mapfiles/";
    icon.image = url + "arrow.png";
    icon.shadow = url + "arrowshadow.png";
    icon.iconSize = new GSize(39, 34);
    icon.shadowSize = new GSize(39, 34);
    icon.iconAnchor = new GPoint(20, 34);
    icon.infoWindowAnchor = new GPoint(20, 0);
    return icon;
}

function showAddress(response) {
    if (!response || response.Status.code != 200) {
        alert("Status Code:" + response.Status.code);
    }
    else {
        submitInfoToServer(response);
        place = response.Placemark[0];
        plotPlace(place);
    }
}

function goFindAddress(event) {
    var searchField = event.getSource().findComponent("searchField");
    var input = searchField.getValue();
    findAddress(input);
    event.cancel();
}

function findAddress(input) {
    var address = input;
    //GeoCode
    if (geocoder) {
        geocoder.getLocations(input, function (response) {
            if (!response || response.Status.code != 200) {
                var index = address.indexOf(",");
                if (index ==  - 1) {
                    processPointNotFound();
                    return;
                }
                //simplify address by removing the details separated by comma
                address = address.substring(index + 1);
                //recurse until a point is found
                findAddress(address);
            }
            else {
                place = response.Placemark[0];
                plotPlace(place);
            }
        });
    }
}

function processPointNotFound() {
    alert("address not found");
    map.setCenter(new GLatlng(0, 0), 1);
}

function plotPlace(place) {
    var point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);
    var marker = new GMarker(point);
    map.addOverlay(marker);
    map.setCenter(point, 14);
    var info = "<b>Result Coordinates:</b>" + point.toUrlValue() + "<br/>" + "<b>Address:</b>" + place.address + "<br/>" + "<b>Accuracy:</b>" + place.AddressDetails.Accuracy + "<br/>" + "<b>Country code:</b> " + place.AddressDetails.Country.CountryNameCode;
    addClickListener(marker, info);

}

function addClickListener(marker, info) {
    GEvent.addListener(marker, "click", function () {
        marker.openInfoWindowHtml(info);
    });
    marker.openInfoWindowHtml(info);
}

function submitInfoToServer(response) {
    var place = response.Placemark[0];
    var result_point = new GLatLng(place.Point.coordinates[1], place.Point.coordinates[0]);
    var address = place.address;
    var addressDetails = place.AddressDetails;
    var accuracy = addressDetails.Accuracy;
    var country = addressDetails.Country;
    var countryNameCode = country.CountryNameCode;
    var adminArea = country.AdministrativeArea;
    var adminAreaName;
    var locality;
    var localityName;
    var thoroughfare;
    var postalCode;
    if (adminArea != null) {
        adminAreaName = adminArea.AdministrativeAreaName;
        locality = adminArea.Locality;
        if (locality != null) {
            localityName = locality.LocalityName;
            thoroughfare = locality.Thoroughfare;
            postalCode = locality.PostalCode;
            if (thoroughfare != null) {
                thoroughfare = thoroughfare.ThoroughfareName;
            }
            if (postalCode != null) {
                postalCode = postalCode.PostalCodeNumber;
            }
        }
    }
    var source = AdfPage.PAGE.findComponent("d1");
    AdfCustomEvent.queue(source, "submitInfoToServer", 
    {
        selected_point : response.name, result_point:result_point , address : address, country : countryNameCode, area : adminAreaName, locality : localityName, thoroughfare : thoroughfare, postalCode : postalCode, accuracy : accuracy
    },
false);
}

Create the GoogleMapViewerForm (the backing bean of google_map.jspx)

Create a new class GoogleMapViewerForm and add a "googleMapViewer" managed bean with backingBean scope in google-map-viewer-task-flow.
package blogspot.soadev.view.backing;

import java.util.Map;
import javax.faces.context.FacesContext;
import oracle.adf.view.rich.context.AdfFacesContext;
import oracle.adf.view.rich.render.ClientEvent;
import org.apache.myfaces.trinidad.render.ExtendedRenderKitService;
import org.apache.myfaces.trinidad.util.Service;

/**@author pino
 */
public class GoogleMapViewerForm {

    public void loadGoogleMap(ClientEvent clientEvent){
        FacesContext context = FacesContext.getCurrentInstance();
        Map<String, Object> pageFlowScope = AdfFacesContext.getCurrentInstance().getPageFlowScope();
        Object obj = null;
        String address = null;
        String countryCode = null;
        Double latitude = null;
        Double longitude = null;
        
        address = (String)pageFlowScope.get("address");
        countryCode = (String) pageFlowScope.get("countryCode");
        obj = pageFlowScope.get("latitude");
        if (obj != null && obj instanceof Double){
            latitude = (Double)obj;
        }
        obj = pageFlowScope.get("longitude");
        if (obj != null && obj instanceof Double){
            longitude = (Double)obj;
        }
        //build javascript
        StringBuilder script = new StringBuilder();
        script
            .append("initializeMap(")
            .append("'mapPH',")
            .append(latitude)
            .append(",")
            .append(longitude)
            .append(",'")
            .append(address)
            .append("','")
            .append(countryCode)
            .append("');");
       
        ExtendedRenderKitService erks = 
            Service.getService(context.getRenderKit(), ExtendedRenderKitService.class);
          erks.addScript(context, script.toString());
    }

   public void setReturnValues(ClientEvent event) {
        Map<String, Object> pageFlowScope =AdfFacesContext.getCurrentInstance().getPageFlowScope();
        Map<String, Object> eventParams = event.getParameters();
        Object selectedCoordinates = eventParams.get("selected_point");
        if (selectedCoordinates != null){
            String coordinates[] = ((String)selectedCoordinates).split(",");
            Double latitude = Double.parseDouble(coordinates[0]);
            Double longitude = Double.parseDouble(coordinates[1]);
            pageFlowScope.put("returnLatitude", latitude);
            pageFlowScope.put("returnLongitude", longitude);
        }
        pageFlowScope.put("returnAddress", eventParams.get("address"));
        pageFlowScope.put("returnDetails", eventParams);
    }
}

Integrate the google-map-viewer-task-flow as a task-flow-call activity into your existing task flows

Steps to integrate the Google Map viewer into your application:
  1. Open your existing task flow in diagram view and drag a task-flow-call activity from the component pallete.
  2. Drag the google-map-viewer-task-flow from the project explorer into the task-flow-call activity created above.
  3. Add a control-flow-case and extend it from one of your page that has address information to our google-map-viewer-task-flow activity.
  4. Add a button on your page that will have an
A picture paints a thousand words. Please see screen shot below:
Screen shot of the sample page that invokes the map viewer:
The action listener is bound to the class SamplePageForm declared as "samplePage" managed bean with backingBean scope. Below is the illustrative code:
package soadev.blogspot.view.backing;

import java.util.Iterator;
import java.util.Map;
import javax.faces.event.ActionEvent;
import oracle.adf.view.rich.context.AdfFacesContext;
import org.apache.myfaces.trinidad.event.ReturnEvent;

public class SamplePageForm {
    public void setMapInputParams(ActionEvent event) {
        Map pageFlowScope = AdfFacesContext.getCurrentInstance().getPageFlowScope();
        //You will get the information to put to the pageFlowScope below
        //from your iterator binding objects. 
        //sample inputs only, replace Jeddah
        pageFlowScope.put("address", "Jeddah");
        //replace null with your own data;
        pageFlowScope.put("latitude", null);
        pageFlowScope.put("longitude", null);
        pageFlowScope.put("countryCode", null);
        
    }

    public void handleMapDialogReturn(ReturnEvent event) {
        System.out.println("handling dialog return...");
        Map eventReturnParams = event.getReturnParameters();
        Iterator iterator = eventReturnParams.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry)iterator.next();
            Object key = entry.getKey();
            Object value = entry.getValue();
            System.out.println(key + " ..... " + value);
        }
        Object obj = null;
        Double latitude = null;
        Double longitude = null;
        obj = eventReturnParams.get("latitude");
        if (obj != null && obj instanceof Double) {
            latitude = (Double)obj;
        }
        obj = eventReturnParams.get("longitude");
        if (obj != null && obj instanceof Double) {
            longitude = (Double)obj;
        }
        if (latitude != null && longitude != null) {
            //TODO create popup confirmation to apply new selected coordinates
            //showDialog(popup);
        }
    }
}
Below is a sample result of the handleMapDialogReturn() method above:
handling dialog return...
returnDetails ..... {postalCode=null, area=null, address=7901 Hail, Jeddah 23325, Saudi Arabia, locality=null, thoroughfare=null, selected_point=21.540356,39.160337, accuracy=8.0, country=SA, result_point={of=21.5404074, y=21.5404074, x=39.1601585, $a=39.1601585}}
returnLatitude ..... 21.540356
returnAddress ..... 7901 Hail, Jeddah 23325, Saudi Arabia
returnLongitude ..... 39.160337

Summary

This blogpost illustrate the following "How Tos":
  • How to integrate the Google Maps API to an Oracle ADF application.
  • How to run a backing bean code upon page load.
  • How to pass parameters from the server to the client and vice-versa.
  • How to do geocoding and reverse geocoding in Google Maps.
  • How to apply Frank Nimphius declarative lightweight popup pattern.

Related Posts

This post is long one (and it sure made me exhausted). I hope that you learn something from here.

Cheers!

5 comments:

  1. magnificent work . I am really surprized to see such an advanced level work in ADF .

    ReplyDelete
  2. great!
    works immediately!
    and: includes a puzzle for free: Why does it start at "Result Coordinates: 26.360111,-98.782501"

    ReplyDelete
  3. Hello! Can you please share full project (code)?

    ReplyDelete
  4. an you please share full project (code)?

    ReplyDelete
  5. Can u please share ur code...?

    ReplyDelete