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:
- 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:
- FitLayout is used a base class for layouts that contain a single item that automatically expands to fill the layout's container
- Panels with ColumLayoutData are used for the Horizontal mode
- 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- 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); - 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); - 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.- Improve the look and feel
- Support to Google street
- Internationalization support
- Printing capabilities
- 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;
}
}