/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2015 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.layer.google.gwt.client;
import org.geomajas.annotation.Api;
import org.geomajas.command.dto.TransformGeometryRequest;
import org.geomajas.command.dto.TransformGeometryResponse;
import org.geomajas.geometry.Coordinate;
import org.geomajas.gwt.client.command.AbstractCommandCallback;
import org.geomajas.gwt.client.command.GwtCommand;
import org.geomajas.gwt.client.command.GwtCommandDispatcher;
import org.geomajas.gwt.client.gfx.PainterVisitor;
import org.geomajas.gwt.client.gfx.paintable.mapaddon.MapAddon;
import org.geomajas.gwt.client.spatial.Bbox;
import org.geomajas.gwt.client.widget.MapWidget;
import org.geomajas.gwt.client.widget.MapWidget.RenderGroup;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.dom.client.Node;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.UIObject;
import com.smartgwt.client.types.VerticalAlignment;
/**
* <p>
* Map add-on that displays the Google attribution (copyright + terms of use) on the map. It can also show the actual
* Google map as opposed to the tiles that are calculated on the server. For normal Geomajas use this map should be
* hidden as it does not automatically scale to continuous zoom levels. This add-on requires the Google maps library !
* </p>
* <p>
* This MapAddon does not allow center positioning. Also it takes in the whole width of the map, so setting the
* horizontal alignment is of little use. By default, this MapAddon will be placed 20 pixels from the bottom of the map.
* </p>
*
* @author Jan De Moerloose
* @since 1.8.0
*/
@Api
public class GoogleAddon extends MapAddon {
private static final String EPSG_900913 = "EPSG:900913";
private static final String EPSG_3857 = "EPSG:3857";
private static final String EPSG_4326 = "EPSG:4326";
private static final double HALF_CIRCLE = 180.0;
private static final double MERCATOR_WIDTH = Math.PI * 6378137.0;
private static final int VERTICAL_MARGIN = 20;
private static final double LN2 = Math.log(2.0);
private final MapWidget map;
private JavaScriptObject googleMap;
private MapType type;
private final boolean showMap;
private Element tosGroup;
private boolean visible = true;
/**
* Google map types as defined by the API.
*
* @since 1.9.0
*/
@Api
public enum MapType {
NORMAL, SATELLITE, HYBRID, PHYSICAL
}
// Constructor:
/**
* Create the Google addon to assure the copyright details and ToS are displayed on the map. The map itself is
* displayed in the background, any other raster layers will be displayed in front of it.
*
* @param id element id
* @param map map widget to display copyright on
* @param type map type
* @since 1.9.0
*/
@Api
public GoogleAddon(String id, MapWidget map, MapType type) {
this(id, map, type, true);
}
/**
* Create the Google addon to assure the copyright details are displayed on the map. WARNING: deprecated because of
* change in <a href="https://developers.google.com/maps/terms">Google Maps Terms of Service</a>.
*
* @param id element id
* @param map map widget to display copyright on
* @param type map type
* @param showMap should the map be visible?
* @deprecated use {{@link #GoogleAddon(String, MapWidget, MapType)}
*/
@Api
@Deprecated
public GoogleAddon(String id, MapWidget map, MapType type, boolean showMap) {
super(id, 0, 0);
setRepaintOnMapViewChange(true);
this.map = map;
this.type = type;
this.showMap = showMap;
// this.map.getMapModel().getMapView().addMapViewChangedHandler(this);
// Default placement:
setVerticalAlignment(VerticalAlignment.BOTTOM);
setVerticalMargin(VERTICAL_MARGIN);
}
// MapAddon implementation:
@Override
public void accept(PainterVisitor visitor, Object group, Bbox bounds, boolean recursive) {
if (googleMap != null) {
String sourceCrs = map.getMapModel().getCrs();
if (isGoogleProjection(sourceCrs)) {
int zoomLevel = calcZoomLevel(map.getMapModel().getMapView().getCurrentScale());
Coordinate latLon = convertToLatLon(bounds.getCenterPoint());
fitGoogleMapBounds(googleMap, latLon, zoomLevel);
} else {
// transform on server
TransformGeometryRequest request = new TransformGeometryRequest();
request.setBounds(new org.geomajas.geometry.Bbox(bounds.getX(), bounds.getY(), bounds.getWidth(),
bounds.getHeight()));
request.setSourceCrs(map.getMapModel().getCrs());
request.setTargetCrs(EPSG_3857);
GwtCommand command = new GwtCommand(TransformGeometryRequest.COMMAND);
command.setCommandRequest(request);
GwtCommandDispatcher.getInstance().execute(command,
new AbstractCommandCallback<TransformGeometryResponse>() {
public void execute(TransformGeometryResponse response) {
Bbox google = new Bbox(response.getBounds());
int zoomLevel = calcZoomLevelFromBounds(google);
fitGoogleMapBounds(googleMap, convertToLatLon(google.getCenterPoint()), zoomLevel);
}
});
}
}
}
private int calcZoomLevel(double scale) {
int calc = (int) Math.round(Math.log(scale * MERCATOR_WIDTH / 180) / LN2);
return calc;
}
private int calcZoomLevelFromBounds(Bbox google) {
return calcZoomLevel(map.getWidth() / google.getWidth());
}
private void fitGoogleMapBounds(JavaScriptObject object, Coordinate center, int zoomLevel) {
doFitGoogleMapBounds(object, center.getX(), center.getY(), zoomLevel);
}
@Override
public void setMapSize(int mapWidth, int mapHeight) {
super.setMapSize(mapWidth, mapHeight);
if (googleMap != null) {
triggerResize(googleMap);
}
}
@Override
public void onDraw() {
if (googleMap == null) {
// create as first child of raster group
map.getRasterContext().drawGroup(null, this);
String id = map.getRasterContext().getId(this);
// move to first position
Element mapDiv = DOM.getElementById(id);
Element rasterGroup = DOM.getElementById(map.getRasterContext().getId(map.getGroup(RenderGroup.RASTER)));
DOM.insertBefore(DOM.getParent(rasterGroup), mapDiv, rasterGroup);
String graphicsId = map.getVectorContext().getId();
googleMap = createGoogleMap(id, graphicsId, type.name(), showMap, getVerticalMargin(),
getHorizontalMargin(), getVerticalAlignmentString());
}
}
@Override
public void onRemove() {
String id = map.getRasterContext().getId(this);
// Remove the terms of use:
Element element = DOM.getElementById(id + "-googleAddon");
if (element != null) {
Element parent = DOM.getParent(element);
parent.removeChild(element);
}
// Remove the Google map too:
element = DOM.getElementById(id);
if (element != null) {
Element parent = DOM.getParent(element);
parent.removeChild(element);
}
googleMap = null;
}
private void moveTosCopyRight() {
// move the ToS and copyright to the top
// create a div group in the graphics context
if (tosGroup == null) {
String graphicsId = map.getVectorContext().getId();
Element graphics = DOM.getElementById(graphicsId);
tosGroup = DOM.createDiv();
tosGroup.setId(map.getID() + "-googleAddon");
tosGroup.getStyle().setBottom(VERTICAL_MARGIN, Unit.PX);
graphics.appendChild(tosGroup);
UIObject.setVisible(tosGroup, visible);
}
String mapsId = map.getRasterContext().getId(this);
Element gmap = DOM.getElementById(mapsId);
if (gmap.getChildCount() > 0) {
Node baseMap = gmap.getChild(0);
if (baseMap.getChildCount() > 2) {
Node copyright = baseMap.getChild(1);
Node tos = baseMap.getChild(2);
tosGroup.appendChild(copyright);
tosGroup.appendChild(tos);
}
}
}
/**
* Set the visibility of the Google map.
*
* @param visible
* @since 1.9.0
*/
@Api
public void setVisible(boolean visible) {
this.visible = visible;
if (googleMap != null) {
String mapsId = map.getRasterContext().getId(this);
Element gmap = DOM.getElementById(mapsId);
UIObject.setVisible(gmap, visible);
if (tosGroup != null) {
UIObject.setVisible(tosGroup, visible);
}
if (visible) {
triggerResize(googleMap);
}
}
}
/**
* Set the type of the Google map.
*
* @param type the map type
* @since 1.9.0
*/
@Api
public void setMapType(MapType type) {
this.type = type;
if (googleMap != null) {
setMapType(googleMap, type.toString());
}
}
/**
* Get the type of the Google map.
*
* @return
* @since 1.9.0
*/
@Api
public MapType getMapType() {
return type;
}
// ------------------------------------------------------------------------
// Private methods:
// ------------------------------------------------------------------------
private native void setMapType(JavaScriptObject map, String mapType)
/*-{
if (mapType == "NORMAL") {
map.setMapTypeId($wnd.google.maps.MapTypeId.ROADMAP);
} else if (mapType == "SATELLITE") {
map.setMapTypeId($wnd.google.maps.MapTypeId.SATELLITE);
} else if (mapType == "HYBRID") {
map.setMapTypeId($wnd.google.maps.MapTypeId.HYBRID);
} else if (mapType == "PHYSICAL") {
map.setMapTypeId($wnd.google.maps.MapTypeId.TERRAIN);
}
}-*/;
private boolean isGoogleProjection(String sourceCrs) {
return EPSG_900913.equals(sourceCrs) || EPSG_3857.equals(sourceCrs);
}
private native void triggerResize(JavaScriptObject map)
/*-{
$wnd.google.maps.event.trigger(map, "resize");
}-*/;
private native JavaScriptObject createGoogleMap(String mapId, String graphicsId, String mapType, boolean showMap,
int verticalMargin, int horizontalMargin, String verticalAlignment)
/*-{
var _me = this;
var mapDiv = $doc.getElementById(mapId);
var options = {disableDefaultUI: true};
if (mapType == "NORMAL") {
options.mapTypeId = $wnd.google.maps.MapTypeId.ROADMAP;
var map = new $wnd.google.maps.Map(mapDiv, options)
} else if (mapType == "SATELLITE") {
options.mapTypeId = $wnd.google.maps.MapTypeId.SATELLITE;
var map = new $wnd.google.maps.Map(mapDiv, options)
} else if (mapType == "HYBRID") {
options.mapTypeId = $wnd.google.maps.MapTypeId.HYBRID;
var map = new $wnd.google.maps.Map(mapDiv, options)
} else if (mapType == "PHYSICAL") {
options.mapTypeId = $wnd.google.maps.MapTypeId.TERRAIN;
var map = new $wnd.google.maps.Map(mapDiv, options)
}
if(!showMap) {
mapDiv.style.visibility = "hidden";
}
$wnd.google.maps.event.addListener(map, 'tilesloaded',
function(){
_me.@org.geomajas.layer.google.gwt.client.GoogleAddon::moveTosCopyRight()();
}
);
return map;
}-*/;
private native void doFitGoogleMapBounds(JavaScriptObject map, double xCenter, double yCenter, int zoomLevel)
/*-{
var center = new $wnd.google.maps.LatLng(xCenter, yCenter);
map.setZoom(zoomLevel);
map.setCenter(center);
}-*/;
private Bbox convertToLatLon(Bbox bounds) {
// convert corners
Coordinate orig = convertToLatLon(bounds.getOrigin());
Coordinate end = convertToLatLon(bounds.getEndPoint());
return new Bbox(orig.getX(), orig.getY(), end.getX() - orig.getX(), end.getY() - orig.getY());
}
private Coordinate convertToLatLon(Coordinate coordinate) {
double lat = (coordinate.getY() / MERCATOR_WIDTH) * HALF_CIRCLE;
double lon = (coordinate.getX() / MERCATOR_WIDTH) * HALF_CIRCLE;
lat = HALF_CIRCLE / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / HALF_CIRCLE)) - Math.PI / 2.0);
return new Coordinate(lat, lon);
}
private String getVerticalAlignmentString() {
// No center position, just top and bottom:
VerticalAlignment align = getVerticalAlignment();
if (align != null && align.equals(VerticalAlignment.TOP)) {
return "top";
}
return "bottom";
}
}