Saturday, July 26, 2008

GWT-Ext: Wrapping a Google Map component

The aim of this tutorial is to present how to wrap a Google Map component with GWT-Ext.

GWT-Ext proposes a lot of Google Map wrappers. The tutorial proposes to demonstrate how to wrap the “Direction” and “Map” components. Thanks this component it’s now easy to reuse a Direction in a web page.

Direction widget features:

directions

  • Map and direction in the same component
  • Map and directions size are automaticaly adjusted depending component size
  • Optional form to define origin and destination
  • Horizontal or vertical layout

 

Main steps to create the widget:

Overloading the Panel component
Creating the “<Div>” elements used by the Google Map API
Those 2 elements are used by the Google API (next step) to display the map and the directions components.
        String mapDiv = "
";
String directionsDiv = "
";
_mapHTMLPanel = new HTMLPanel(mapDiv);
_directionsHTMLPanel = new HTMLPanel(directionsDiv);



Initializing the Google Map API
GMap2 and GDirections javascript objects are initialized. $wnd is used by GWT API to access javascript objects.
    private native JavaScriptObject initializeMap() /*-{
var map = new $wnd.GMap2($doc.getElementById('widget_map_canvas'));
map.addControl(new $wnd.GSmallMapControl());
map.addControl(new $wnd.GMapTypeControl());
return map;
}-*/;

private native JavaScriptObject initializeDirections(Directions thisModule) /*-{
var map = this.@com.benevolarc.gwt.client.Directions::_map;
var gdir = new $wnd.GDirections(map, $doc.getElementById('widget_directions'));
$wnd.GEvent.addListener(gdir, "load", function(response) {
if (response && response.getStatus().code == 200) {
thisModule.@com.benevolarc.gwt.client.Directions::directionsOK()();
}
});



Creating the layout (horizontal or vertical) to store the Map and the Direction components
The layout allows to display the map and the directions with 2 options (left-rigth or top-down)
Muliple GWT-Ext layouts are used for that purpose:

  1. FitLayout is used a base class for layouts that contain a single item that automatically expands to fill the layout's container

  2. Panels with ColumLayoutData are used for the Horizontal mode

  3. AnchorLayoutData with PanelListenerAdapter is used for the Horizontal mode


The difficulty was to display an horizontal panel with a height expressed in percentage instead of pixel. The solution was to use a PanelListenerAdapter.
    private void init() {
setLayout(new FitLayout());
setBorder(false);
String mapDiv = "
";
String directionsDiv = "
";
_mapHTMLPanel = new HTMLPanel(mapDiv);
_directionsHTMLPanel = new HTMLPanel(directionsDiv);

//_panel.add(new HTMLPanel("
"));

if (_displayMode == DisplayMode.HORIZONTAL) {
Log.debug("Horizontal mode");
Panel columnLayoutPanel = new Panel();
columnLayoutPanel.setBorder(false);
columnLayoutPanel.setLayout(new ColumnLayout());

col1Panel = new Panel();
col1Panel.setBorder(false);
col1Panel.setLayout(new FitLayout());
Panel panelMap = new Panel();
panelMap.setBorder(false);
_mapHTMLPanel.setMargins(10, 10, 10, 10);
panelMap.setLayout(new AnchorLayout());
if (_displayForm) {
panelMap.add(createFromToForm());
}
panelMap.add(_mapHTMLPanel, new AnchorLayoutData("100% 100%"));
col1Panel.add(panelMap);
col2Panel = new Panel();
col2Panel.setBorder(false);
col2Panel.setLayout(new FitLayout());
col2Panel.add(_directionsHTMLPanel);

columnLayoutPanel.add(col1Panel, new ColumnLayoutData(.50));
columnLayoutPanel.add(col2Panel, new ColumnLayoutData(.50));
columnLayoutPanel.addListener(new PanelListenerAdapter() {
public void onResize(BoxComponent component, int adjWidth, int adjHeight, int rawWidth, int rawHeight) {
col1Panel.setHeight(rawHeight);
col2Panel.setHeight(rawHeight);
}
});


add(columnLayoutPanel);

} else {
Log.debug("Vertical mode");
Panel panel = new Panel();
panel.setLayout(new AnchorLayout());
if (_displayForm) {
FormPanel fPanel = createFromToForm();
panel.add(fPanel);
}
panel.add(_mapHTMLPanel, new AnchorLayoutData("100% 50%"));
_mapHTMLPanel.collapse();
panel.add(_directionsHTMLPanel, new AnchorLayoutData("100% 50%"));
add(panel);
}
}



Invoking the directions
When the user clicks on the "get directions" button a call to the Google API has to be done. A listener is attached on the button and a call to the JSNI method is done
        
goButton.addListener(new ButtonListenerAdapter() {
public void onClick(Button button, EventObject e) {
lauchDirections();
}
});

launchDirections checks if origin and destination are correctly filled and call the setDirections native method.
    private void lauchDirections() {
Log.debug("Getting direction" + _map);
if (_toTextField.getText().length() > 0 && _fromTextField.getText().length() > 0) {
setDirections(_fromTextField.getValueAsString(), _toTextField.getValueAsString(), _locale);
clearErrorMessage();
} else {
Log.warn("From and to must be filled");
setErrorMessage("From and to must be filled");
}
}

The setDirections method calls the Google API method gdir.load. In case of success, the Map and the directions components are updated. In case of error, a message is displayed to the user.
  public native void setDirections(String fromAddress, String toAddress, String locale) /*-{
var gdir = this.@com.benevolarc.gwt.client.Directions::_gdir;
gdir.clear();
gdir.load("from: " + fromAddress + " to: " + toAddress,
{ "locale": locale });
}-*/;


Using the Directions component

Using the component is easy

  1. Displaying the Directions and Map component with origin and destination in the constructor (horizontal layout)
    Default component size is 600px*600px

    directions = new Directions("paris","bordeaux", Directions.DisplayMode.HORIZONTAL);


  2. Displaying the Form allowing the user to enter origin and destination (horizontal layout)

    //directions with Horizontal layout
    directions = new Directions(true, Directions.DisplayMode.HORIZONTAL);
    directions.setWidth("1000px");
    directions.setHeight("800px");
    directions.setBorder(true);



  3. Displaying the Form allowing the user to enter origin and destination vertical layout)

    //directions with Horizontal layout
    directions = new Directions(true, Directions.DisplayMode.VERTICAL);
    directions.setBorder(true);




Conclusion

The tutorial demonstrates how to wrap a Google component. Of course this component is not perfect and a lot of improvement needs to be done.

  1. Improve the look and feel

  2. Support to Google street

  3. Internationalization support

  4. Printing capabilities

  5. And more...


Feel free to reuse it and please don't hesitate to give your opinion. If you find this component usefull, the source can be use for GWT-EXT extenstion...
Thanks for GWT-Ext forum for the support regarding layout management.


Complete Code


 
package com.benevolarc.gwt.client;

import com.allen_sauer.gwt.log.client.Log;

import com.google.gwt.core.client.JavaScriptObject;

import com.google.gwt.user.client.ui.HTML;

import com.gwtext.client.core.EventObject;
import com.gwtext.client.widgets.BoxComponent;
import com.gwtext.client.widgets.Button;
import com.gwtext.client.widgets.HTMLPanel;
import com.gwtext.client.widgets.Panel;
import com.gwtext.client.widgets.event.ButtonListenerAdapter;
import com.gwtext.client.widgets.event.PanelListenerAdapter;
import com.gwtext.client.widgets.form.Field;
import com.gwtext.client.widgets.form.FormPanel;
import com.gwtext.client.widgets.form.MultiFieldPanel;
import com.gwtext.client.widgets.form.TextField;
import com.gwtext.client.widgets.form.event.FieldListenerAdapter;
import com.gwtext.client.widgets.layout.AnchorLayout;
import com.gwtext.client.widgets.layout.AnchorLayoutData;
import com.gwtext.client.widgets.layout.ColumnLayout;
import com.gwtext.client.widgets.layout.ColumnLayoutData;
import com.gwtext.client.widgets.layout.FitLayout;


public class Directions extends Panel{
//private Panel _panel;
private TextField _fromTextField;
private TextField _toTextField;
private HTMLPanel _mapHTMLPanel;
private HTMLPanel _directionsHTMLPanel;
private JavaScriptObject _map;
private JavaScriptObject _gdir;
public enum DisplayMode {HORIZONTAL, VERTICAL};
private DisplayMode _displayMode = DisplayMode.VERTICAL;
private boolean _displayForm = true;
private Panel col1Panel = null;
private Panel col2Panel = null;
private HTML _errorMessage = new HTML();
private String _from = null;
private String _to = null;
private String _locale = "EN";

public Directions(boolean displayForm) {
_displayForm = displayForm;
init();
}

public Directions(boolean displayForm, DisplayMode displayMode) {
_displayForm = displayForm;
_displayMode = displayMode;
init();
}

public Directions(String from, String to, DisplayMode displayMode) {
_displayMode = displayMode;
_displayForm = false;
_from = from;
_to = to;
init();
}

private FormPanel createFromToForm() {
_fromTextField = new TextField("From", "from", 250);
_toTextField = new TextField("To", "to", 250);
Button goButton = new Button("Get directions");

MultiFieldPanel directionsFormPanel = new MultiFieldPanel();

directionsFormPanel.setBorder(false);
directionsFormPanel.setPaddings(10, 10, 10, 10);
directionsFormPanel.addToRow(_fromTextField, 300);
directionsFormPanel.addToRow(_toTextField, 300);
directionsFormPanel.addToRow(goButton, 100);

FormPanel fPanel = new FormPanel();
fPanel.setBorder(false);
fPanel.setLabelWidth(30);
fPanel.add(_errorMessage);
fPanel.add(directionsFormPanel);
_fromTextField.addListener(new FieldListenerAdapter() {
public void onSpecialKey(Field field, EventObject e) {
if (e.getKey() == EventObject.ENTER) {
lauchDirections();
}
}
});
_toTextField.addListener(new FieldListenerAdapter() {
public void onSpecialKey(Field field, EventObject e) {
if (e.getKey() == EventObject.ENTER) {
lauchDirections();
}
}
});

goButton.addListener(new ButtonListenerAdapter() {
public void onClick(Button button, EventObject e) {
lauchDirections();
}
});
return fPanel;
}

private void setErrorMessage(String message) {
_errorMessage.setHTML("
Error: "+ message + "
");
}

private void clearErrorMessage() {
_errorMessage.setHTML("
");
}

private void lauchDirections() {
Log.debug("Getting direction" + _map);
if (_toTextField.getText().length() > 0 && _fromTextField.getText().length() > 0) {
setDirections(_fromTextField.getValueAsString(), _toTextField.getValueAsString(), _locale);
clearErrorMessage();
} else {
Log.warn("From and to must be filled");
setErrorMessage("From and to must be filled");
}
}

private void init() {
Log.debug("Setting default width and heigth");
setWidth(600);
setHeight(600);
setLayout(new FitLayout());
setBorder(false);
String mapDiv = "
";
String directionsDiv = "
";
_mapHTMLPanel = new HTMLPanel(mapDiv);
_directionsHTMLPanel = new HTMLPanel(directionsDiv);

//_panel.add(new HTMLPanel("
"));

if (_displayMode == DisplayMode.HORIZONTAL) {
Log.debug("Horizontal mode");
Panel columnLayoutPanel = new Panel();
columnLayoutPanel.setBorder(false);
columnLayoutPanel.setLayout(new ColumnLayout());

col1Panel = new Panel();
col1Panel.setBorder(false);
col1Panel.setLayout(new FitLayout());
Panel panelMap = new Panel();
panelMap.setBorder(false);
_mapHTMLPanel.setMargins(10, 10, 10, 10);
panelMap.setLayout(new AnchorLayout());
if (_displayForm) {
panelMap.add(createFromToForm());
}
panelMap.add(_mapHTMLPanel, new AnchorLayoutData("100% 100%"));
col1Panel.add(panelMap);
col2Panel = new Panel();
col2Panel.setBorder(false);
col2Panel.setLayout(new FitLayout());
col2Panel.add(_directionsHTMLPanel);

columnLayoutPanel.add(col1Panel, new ColumnLayoutData(.50));
columnLayoutPanel.add(col2Panel, new ColumnLayoutData(.50));
columnLayoutPanel.addListener(new PanelListenerAdapter() {
public void onResize(BoxComponent component, int adjWidth, int adjHeight, int rawWidth, int rawHeight) {
col1Panel.setHeight(rawHeight);
col2Panel.setHeight(rawHeight);
}
});


add(columnLayoutPanel);

} else {
Log.debug("Vertical mode");
Panel panel = new Panel();
panel.setLayout(new AnchorLayout());
if (_displayForm) {
FormPanel fPanel = createFromToForm();
panel.add(fPanel);
}
panel.add(_mapHTMLPanel, new AnchorLayoutData("100% 50%"));
_mapHTMLPanel.collapse();
panel.add(_directionsHTMLPanel, new AnchorLayoutData("100% 50%"));
add(panel);
}
}

protected void onAttach() {
Log.debug("Component attached");
_map = initializeMap();
_gdir = initializeDirections(this);
if (_from != null && _to != null) {
setDirections(_from, _to, _locale);
} else {
_mapHTMLPanel.collapse();
}
super.onAttach();
}

private native JavaScriptObject initializeMap() /*-{
var map = new $wnd.GMap2($doc.getElementById('widget_map_canvas'));
map.addControl(new $wnd.GSmallMapControl());
map.addControl(new $wnd.GMapTypeControl());
return map;
}-*/;

private native JavaScriptObject initializeDirections(Directions thisModule) /*-{
var map = this.@com.benevolarc.gwt.client.Directions::_map;
var gdir = new $wnd.GDirections(map, $doc.getElementById('widget_directions'));
$wnd.GEvent.addListener(gdir, "load", function(response) {
if (response && response.getStatus().code == 200) {
thisModule.@com.benevolarc.gwt.client.Directions::directionsOK()();
}
});

$wnd.GEvent.addListener(gdir, "error", function(response) {
if (!response || response.getStatus().code != 200) {
thisModule.@com.benevolarc.gwt.client.Directions::directionsError(I)(response.getStatus().code);
}
});
return gdir;
}-*/;

public void directionsError(int error) {
Log.debug("Error");
String errorMessage = "Unknown error";
switch (error) {
case 601: errorMessage = "Missing query";break;
case 602: errorMessage = "Unknown address";break;
case 603: errorMessage = "Unavailable address";break;
case 604: errorMessage = "Unknown direction";break;
default: errorMessage ="Unknown error";
}
setErrorMessage(errorMessage);
_mapHTMLPanel.collapse();
_directionsHTMLPanel.collapse();
}

public void directionsOK() {
Log.debug("Directions OK");
_mapHTMLPanel.expand(true);
_directionsHTMLPanel.expand(true);
}


public native void setDirections(String fromAddress, String toAddress, String locale) /*-{
var gdir = this.@com.benevolarc.gwt.client.Directions::_gdir;
gdir.clear();
gdir.load("from: " + fromAddress + " to: " + toAddress,
{ "locale": locale });
}-*/;

public void setLocale(String locale) {
this._locale = locale;
}

public String getLocale() {
return _locale;
}



}

3 comments:

Anonymous said...

Great job!
It would be better to provide test html.

Anonymous said...

Great! It would be better to provide test html.

Anonymous said...

Hey can put in your blog content of the file Log (com.allen_sauer.gwt.log.client.Log) in order to better understand the example, please

Frederic shared items