package edu.caltech.csn.gwt.client;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import com.github.gwtbootstrap.client.ui.Button;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.event.logical.shared.AttachEvent.Handler;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.event.MapMoveEndHandler;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.geom.LatLngBounds;
import com.google.gwt.maps.client.overlay.Polygon;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.Widget;
import edu.caltech.csn.geocell.Bounds;
import edu.caltech.csn.geocell.GeocellLibrary;
import edu.caltech.csn.geocell.Point;
/**
* This class is used to generate a Google Map which it layers with geocells
* of the resolution specified by the provided list box. This class is
* intended as a visualization tool for understanding the size of geocells at
* different resolutions and zoom lengths.
*
* @author Michael Olson <michael.olson@gmail.com>
*/
// CSOFF: MagicNumber - Liberal use of formatting / default preference numbers.
public class CellMap extends Composite implements MapMoveEndHandler,
Handler, ResizeHandler {
private static CellMapUiBinder uiBinder = GWT
.create(CellMapUiBinder.class);
interface CellMapUiBinder extends UiBinder<Widget, CellMap> {
}
@UiField(provided = true)
protected MapWidget map;
@UiField
protected ListBox res;
@UiField
protected Button freeze;
@UiField
protected Button unfreeze;
private final List<GwtGeocell> geocells;
private int resolution;
private boolean labelsEnabled = true;
private int freezeZoom;
private Polygon frozenBounds = null;
private boolean disableUpdates = true;
/**
* This class instantiates the map with all of its default parameters.
* Adjustments to these parameters are then made with the use of history
* tokens.
*/
public CellMap() {
// Center new map on Caltech (for now).
// #TODO: Should eventually center on a wider view of SoCal/user region
map = new MapWidget(LatLng.newInstance(34.139719, -118.123847), 13);
map.setSize("100%", "100%");
map.setUIToDefault();
initWidget(uiBinder.createAndBindUi(this));
geocells = new ArrayList<GwtGeocell>();
this.res.setVisibleItemCount(1);
this.res.addItem("Auto Resolution");
for (int i = GeocellLibrary.MIN_RESOLUTION;
i <= GeocellLibrary.MAX_RESOLUTION; i++) {
this.res.addItem("Resolution " + Integer.toString(i));
}
// Set defaults.
resolution = 0;
this.res.setItemSelected(resolution, true);
// TODO: Add status display for number of cells, min res, max res
// bounded area, and total cell area.
// TODO: Make Min/MaxUseful and StepSize modifiable by synchronized
// library function and add boxes for changing those values.
// TODO: Add dropdown to select output format: no labels, binary, hex,
// GeoModel, Geohash (fallback to Geostring), Geostring, long.
// TODO: Add infobox to cells that tells bounds, area, resolution, and
// representations in alternative formats.
setUnfreezeActive();
map.addMapMoveEndHandler(this);
Window.addResizeHandler(this);
this.addAttachHandler(this);
}
/**
* This function is called whenever the map stops moving. We do not
* attempt to keep the geocells updated while the map is moving, but,
* instead, when the map stops moving, clear existing geocells and
* recalculate the geocells for the viewable map area. Because this
* calculation is extremely fast for modest resolutions, we are not
* attempting to preserve previously calculated geocells.
*
* @see com.google.gwt.maps.client.event.MapMoveEndHandler#onMoveEnd
*/
public void onMoveEnd(final MapMoveEndEvent event) {
// Halt event processing while updating member variables.
if (disableUpdates) {
return;
}
if (frozenBounds != null) {
// Recalculate text overlays if zoom was used.
if (freezeZoom != map.getZoomLevel()) {
for (GwtGeocell g: geocells) {
map.removeOverlay(g.getLabel());
map.addOverlay(g.getLabel(true));
}
freezeZoom = map.getZoomLevel();
}
} else {
final LatLngBounds bounds = map.getBounds();
recalculateCells(bounds);
}
updateToken();
}
private void recalculateCells(final LatLngBounds bounds) {
// Start fresh.
clearCells();
if (resolution == 0) {
// Sort getQueryCells result first for predictable insertion pattern.
final Set<GwtGeocell> results = new TreeSet<GwtGeocell>(getQueryCells(
bounds.getSouthWest(), bounds.getNorthEast()));
for (GwtGeocell g : results) {
addCell(g);
}
} else {
final GwtGeocell northEast = new GwtGeocell(bounds.getNorthEast(), resolution);
final GwtGeocell southWest = new GwtGeocell(bounds.getSouthWest(), resolution);
final GwtGeocell northWest = new GwtGeocell(
LatLng.newInstance(bounds.getNorthEast().getLatitude(),
bounds.getSouthWest().getLongitude()), resolution);
// There's always at least one geocell, and if there is only one,
// using the geocell from the northwest corner is as good as the
// geocell from any other corner.
int width = 1;
addCell(northWest);
// If the northwest corner and northeast corner are in separate
// geocells, move east until we enter the geocell that the northeast
// corner occupies. Use this movement to determine the width (in
// geocells) of the grid for the current map view.
if (!northWest.equals(northEast) ||
// If far western and far eastern geocells are both visible,
// they are the same, and no intermediate cells would be
// included without this additional condition.
!containsCenterLongitude(bounds, northWest)) {
final GwtGeocell currCell = new GwtGeocell(northWest);
long target = northEast.getGeocell();
// Reset the target if we wrapped around the world.
if (northWest.equals(northEast)) {
target = GeocellLibrary.getWest(northEast.getGeocell());
}
while (currCell.getGeocell() != target) {
currCell.moveEast();
addCell(new GwtGeocell(currCell));
width++;
// TODO: Make this check more efficient and configurable.
if (width > 15) {
clearCells();
return;
}
}
}
// If the northwest corner and southwest corner are in separate
// geocells, move south until we enter the geocell that the
// southwest corner occupies. Use this movement to determine the
// height (in geocells) of the grid for the current map view.
if (!northWest.equals(southWest)) {
final GwtGeocell currCell = new GwtGeocell(northWest);
while (!currCell.equals(southWest)) {
currCell.moveSouth();
addCell(new GwtGeocell(currCell));
// If we previously determined the width of the grid is greater
// than 1 (that is, the northwest and northeast geocells are
// not the same), then for each row collect all cells in the
// row by moving east width number of times.
if (width > 1) {
final GwtGeocell acrossCell = new GwtGeocell(currCell);
for (int i = 1; i < width; i++) {
acrossCell.moveEast();
addCell(new GwtGeocell(acrossCell));
}
}
}
}
}
}
private static boolean containsCenterLongitude(final LatLngBounds boxBounds,
final GwtGeocell northWest) {
final Bounds bounds = northWest.getBounds();
final Point centerTop = new Point(boxBounds.getNorthEast().getLatitude(),
bounds.getCenter().getLongitude());
return bounds.contains(centerTop);
}
private static Set<GwtGeocell> getQueryCells(final LatLng southWest, final LatLng northEast) {
return GeocellLibrary.getQueryCells(
southWest.getLatitude(), southWest.getLongitude(),
northEast.getLatitude(), northEast.getLongitude(),
new GwtGeocellConv());
}
/**
* Centralize actions to perform when adding a cell, such as keeping the
* array up to date and incrementally adding them to the map (rather than
* all at once at the end).
*
* @param cell the cell that should be added to the map
*/
public void addCell(final GwtGeocell cell) {
geocells.add(cell);
map.addOverlay(cell.getPolygon());
if (labelsEnabled) {
map.addOverlay(cell.getLabel());
}
}
/**
* Operations to perform when clearing the cells from the map.
*/
public void clearCells() {
for (GwtGeocell g : geocells) {
map.removeOverlay(g.getPolygon());
if (g.hasLabel()) {
map.removeOverlay(g.getLabel());
}
}
geocells.clear();
}
/**
* In this class, onChange(ChangeEvent event) is only called when the
* resolution is modified. Check the current value of the resolution and
* call the onMoveEnd handler.
*/
@UiHandler("res")
public void resChange(final ChangeEvent event) {
resolution = res.getSelectedIndex();
// Unfreeze when automatic selection value changes.
if (resolution == 0 && frozenBounds != null) {
handleUnfreeze(null);
}
GWT.log("Res changed to: " + resolution);
onMoveEnd(null);
}
@UiHandler("freeze")
public void handleFreeze(final ClickEvent event) {
updateFreeze(map.getBounds());
setFreezeActive();
}
@UiHandler("unfreeze")
public void handleUnfreeze(final ClickEvent event) {
if (frozenBounds != null) {
map.removeOverlay(frozenBounds);
frozenBounds = null;
}
onMoveEnd(null);
setUnfreezeActive();
}
private void setUnfreezeActive() {
freeze.removeStyleName("active");
unfreeze.addStyleName("active");
}
private void setFreezeActive() {
unfreeze.removeStyleName("active");
freeze.addStyleName("active");
}
private void updateFreeze(final LatLngBounds bounds) {
freezeZoom = map.getZoomLevel();
final LatLng sw = bounds.getSouthWest();
final LatLng ne = bounds.getNorthEast();
final LatLng[] polyPoints = {
LatLng.newInstance(ne.getLatitude(), sw.getLongitude()),
ne,
LatLng.newInstance(sw.getLatitude(), ne.getLongitude()),
sw,
LatLng.newInstance(ne.getLatitude(), sw.getLongitude())
};
frozenBounds = new Polygon(polyPoints, "#0000FF", 3, .75, "#3333cc", 0);
map.addOverlay(frozenBounds);
updateToken();
}
@Override
public void onAttachOrDetach(final AttachEvent event) {
if (event.isAttached()) {
GWT.log("CellMap attached; calculating cells.");
// Force frozen labels to be recalculated if they were added
// before the map was attached.
disableUpdates = false;
freezeZoom = -1;
onMoveEnd(null);
} else {
GWT.log("CellMap detached; disabling updates.");
disableUpdates = true;
}
}
private void updateToken() {
final String currToken = getToken();
if (!History.getToken().equals(currToken)) {
History.newItem(currToken, false);
}
}
protected String getToken() {
final StringBuffer token = new StringBuffer();
token.append("mc=").append(map.getCenter().toUrlValue()).append('&');
token.append("mz=").append(map.getZoomLevel()).append('&');
if (frozenBounds != null) {
token.append("fr=").append(frozenBounds.getVertex(3).toUrlValue());
token.append('+').append(frozenBounds.getVertex(1).toUrlValue()).append('&');
}
token.append("r=").append(res.getSelectedIndex());
return token.toString();
}
protected void fromToken(final Map<String, String> vars) {
LatLng mc = null;
int mz;
LatLng sw = null;
LatLng ne = null;
int r = 0;
try {
mc = LatLng.fromUrlValue(vars.get("mc"));
mz = Integer.parseInt(vars.get("mz"));
final String fr = vars.get("fr");
if (fr != null) {
final String[] coords = fr.split("\\+");
sw = LatLng.fromUrlValue(coords[0]);
ne = LatLng.fromUrlValue(coords[1]);
}
r = Integer.parseInt(vars.get("r"));
} catch (final Exception e) {
return;
}
disableUpdates = true;
map.setCenter(mc);
map.setZoomLevel(mz);
disableUpdates = false;
resolution = r;
res.setItemSelected(r, true);
final LatLngBounds bounds;
if (sw != null) {
bounds = LatLngBounds.newInstance(sw, ne);
freezeZoom = -1;
updateFreeze(bounds);
setFreezeActive();
} else {
bounds = map.getBounds();
handleUnfreeze(null);
}
recalculateCells(bounds);
}
@Override
public void onResize(final ResizeEvent event) {
onMoveEnd(null);
}
}