/*
* Copyright (c) 2015 Washington State Department of Transportation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
package gov.wa.wsdot.mobile.client.activities.trafficmap;
import com.google.gwt.aria.client.Roles;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.maps.client.MapOptions;
import com.google.gwt.maps.client.MapTypeId;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.base.LatLng;
import com.google.gwt.maps.client.base.LatLngBounds;
import com.google.gwt.maps.client.base.Point;
import com.google.gwt.maps.client.controls.MapTypeStyle;
import com.google.gwt.maps.client.events.MapEventType;
import com.google.gwt.maps.client.events.MapHandlerRegistration;
import com.google.gwt.maps.client.events.click.ClickMapEvent;
import com.google.gwt.maps.client.events.click.ClickMapHandler;
import com.google.gwt.maps.client.events.idle.IdleMapEvent;
import com.google.gwt.maps.client.events.idle.IdleMapHandler;
import com.google.gwt.maps.client.events.resize.ResizeMapEvent;
import com.google.gwt.maps.client.events.resize.ResizeMapHandler;
import com.google.gwt.maps.client.layers.TrafficLayer;
import com.google.gwt.maps.client.maptypes.MapTypeStyleElementType;
import com.google.gwt.maps.client.maptypes.MapTypeStyleFeatureType;
import com.google.gwt.maps.client.maptypes.MapTypeStyler;
import com.google.gwt.maps.client.overlays.*;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.storage.client.Storage;
import com.google.gwt.storage.client.StorageMap;
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.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Widget;
import com.googlecode.gwtphonegap.client.geolocation.Position;
import com.googlecode.mgwt.dom.client.event.tap.TapEvent;
import com.googlecode.mgwt.ui.client.MGWT;
import com.googlecode.mgwt.ui.client.widget.button.image.RefreshImageButton;
import com.googlecode.mgwt.ui.client.widget.buttonbar.ButtonBar;
import com.googlecode.mgwt.ui.client.widget.header.HeaderTitle;
import com.googlecode.mgwt.ui.client.widget.panel.flex.FlexSpacer;
import com.googlecode.mgwt.ui.client.widget.progress.ProgressIndicator;
import gov.wa.wsdot.mobile.client.css.AppBundle;
import gov.wa.wsdot.mobile.client.util.ParserUtils;
import gov.wa.wsdot.mobile.client.widget.button.image.*;
import gov.wa.wsdot.mobile.shared.CalloutItem;
import gov.wa.wsdot.mobile.shared.CameraItem;
import gov.wa.wsdot.mobile.shared.HighwayAlertItem;
import gov.wa.wsdot.mobile.shared.RestAreaItem;
import java.util.*;
import java.util.Map.Entry;
public class TrafficMapViewGwtImpl extends Composite implements TrafficMapView {
/**
* The UiBinder interface.
*/
interface TrafficMapViewGwtImplUiBinder extends
UiBinder<Widget, TrafficMapViewGwtImpl> {
}
/**
* The UiBinder used to generate the view.
*/
private static TrafficMapViewGwtImplUiBinder uiBinder = GWT
.create(TrafficMapViewGwtImplUiBinder.class);
@UiField
HeaderTitle heading;
@UiField
BackImageButton backButton;
@UiField
FlexSpacer leftFlexSpacer;
@UiField
FlowPanel flowPanel;
@UiField
ProgressIndicator progressIndicator;
@UiField
ButtonBar buttonBar;
@UiField
Camera2ImageButton cameraButton;
@UiField
MenuImageButton menuButton;
@UiField
WarningImageButton alertsButton;
@UiField
NavigationImageButton navigationButton;
@UiField
StarImageButton starButton;
@UiField
RefreshImageButton refreshButton;
private Presenter presenter;
private MyMapWidget mapWidget;
private Marker cameraMarker;
private Marker alertMarker;
private Marker restAreaMarker;
private Marker calloutMarker;
private Marker myLocationMarker;
private Circle myLocationError;
private static List<Marker> cameraMarkers = new ArrayList<Marker>();
private static List<Marker> alertMarkers = new ArrayList<Marker>();
private static List<Marker> restAreaMarkers = new ArrayList<Marker>();
private static List<Marker> calloutMarkers = new ArrayList<Marker>();
private static Storage localStorage = Storage.getLocalStorageIfSupported();
private static StorageMap storageMap;
public TrafficMapViewGwtImpl() {
initWidget(uiBinder.createAndBindUi(this));
accessibilityPrepare();
if (MGWT.getOsDetection().isAndroid()) {
leftFlexSpacer.setVisible(false);
}
if (localStorage != null) {
storageMap = new StorageMap(localStorage);
if (!storageMap.containsKey("KEY_SHOW_CAMERAS")) {
localStorage.setItem("KEY_SHOW_CAMERAS", "true"); // Set initial default value
}
if (!storageMap.containsKey("KEY_SHOW_RESTAREAS")) {
localStorage.setItem("KEY_SHOW_RESTAREAS", "false"); // Set initial default value
}
// Set initial default location and zoom to Seattle area.
if (!storageMap.containsKey("KEY_MAP_LAT")) {
localStorage.setItem("KEY_MAP_LAT", "47.5990");
}
if (!storageMap.containsKey("KEY_MAP_LON")) {
localStorage.setItem("KEY_MAP_LON", "-122.3350");
}
if (!storageMap.containsKey("KEY_MAP_ZOOM")) {
localStorage.setItem("KEY_MAP_ZOOM", "12");
}
}
final TrafficLayer trafficLayer = TrafficLayer.newInstance();
LatLng center = LatLng.newInstance(
Double.valueOf(localStorage.getItem("KEY_MAP_LAT")),
Double.valueOf(localStorage.getItem("KEY_MAP_LON")));
MapOptions opts = MapOptions.newInstance();
opts.setMapTypeId(MapTypeId.ROADMAP);
opts.setCenter(center);
opts.setZoom(Integer.valueOf(localStorage.getItem("KEY_MAP_ZOOM"), 10));
opts.setPanControl(false);
opts.setZoomControl(false);
opts.setMapTypeControl(false);
opts.setScaleControl(false);
opts.setStreetViewControl(false);
opts.setOverviewMapControl(false);
// Custom map style to remove all "Points of Interest" labels from map
MapTypeStyle style1 = MapTypeStyle.newInstance();
style1.setFeatureType(MapTypeStyleFeatureType.POI);
style1.setElementType(MapTypeStyleElementType.LABELS);
style1.setStylers(new MapTypeStyler[] {MapTypeStyler.newVisibilityStyler("off")});
MapTypeStyle[] styles = { style1 };
opts.setMapTypeStyles(styles);
mapWidget = new MyMapWidget(opts);
trafficLayer.setMap(mapWidget);
flowPanel.add(mapWidget);
mapWidget.setSize(Window.getClientWidth() + "px",
(Window.getClientHeight() - ParserUtils.windowUI()) + "px");
Window.addResizeHandler(new ResizeHandler() {
@Override
public void onResize(ResizeEvent event) {
MapHandlerRegistration.trigger(mapWidget, MapEventType.RESIZE);
}
});
mapWidget.addResizeHandler(new ResizeMapHandler() {
@Override
public void onEvent(ResizeMapEvent event) {
mapWidget.setSize(Window.getClientWidth() + "px",
(Window.getClientHeight() - ParserUtils.windowUI()) + "px");
}
});
mapWidget.addIdleHandler(new IdleMapHandler() {
@Override
public void onEvent(IdleMapEvent event) {
localStorage.setItem("KEY_MAP_LAT",
String.valueOf(mapWidget.getCenter().getLatitude()));
localStorage.setItem("KEY_MAP_LON",
String.valueOf(mapWidget.getCenter().getLongitude()));
localStorage.setItem("KEY_MAP_ZOOM",
String.valueOf(mapWidget.getZoom()));
if (presenter != null) {
presenter.onMapIsIdle();
}
}
});
}
@UiHandler("backButton")
protected void onBackButtonPressed(TapEvent event) {
if (presenter != null) {
presenter.onBackButtonPressed();
}
}
@UiHandler("cameraButton")
protected void onCameraButtonPressed(TapEvent event) {
if (presenter != null) {
presenter.onCameraButtonPressed(Boolean.valueOf(localStorage
.getItem("KEY_SHOW_CAMERAS")));
}
}
@UiHandler("menuButton")
protected void onMenuButtonPressed(TapEvent event) {
if (presenter != null) {
presenter.onMenuButtonPressed();
}
}
@UiHandler("alertsButton")
protected void onAlertsButtonPressed(TapEvent event) {
if (presenter != null) {
presenter.onTrafficAlertsButtonPressed(getViewportBounds());
}
}
@UiHandler("navigationButton")
protected void onLocateButtonPressed(TapEvent event) {
if (presenter != null) {
presenter.onLocateButtonPressed();
}
}
@UiHandler("starButton")
protected void onStarButtonPressed(TapEvent event) {
if (presenter != null) {
presenter.onStarButtonPressed();
}
}
@UiHandler("refreshButton")
protected void onRefreshMapButtonPressed(TapEvent event) {
if (presenter != null) {
presenter.onRefreshMapButtonPressed();
}
}
@Override
public void showProgressIndicator() {
progressIndicator.setVisible(true);
}
@Override
public void hideProgressIndicator() {
progressIndicator.setVisible(false);
}
@Override
public void setPresenter(Presenter presenter) {
this.presenter = presenter;
}
@Override
public LatLngBounds getViewportBounds() {
return mapWidget.getBounds();
}
@Override
public void drawCameras(final List<CameraItem> cameras) {
deleteCameras();
for (final CameraItem camera : cameras) {
final LatLng loc = LatLng.newInstance(camera.getLatitude(), camera.getLongitude());
MarkerOptions options = MarkerOptions.newInstance();
options.setPosition(loc);
MarkerImage icon;
if (camera.getHasVideo() == 1) {
icon = MarkerImage.newInstance(AppBundle.INSTANCE
.cameraVideoPNG().getSafeUri().asString());
} else {
icon = MarkerImage.newInstance(AppBundle.INSTANCE
.cameraPNG().getSafeUri().asString());
}
options.setIcon(icon);
cameraMarker = Marker.newInstance(options);
cameraMarker.addClickHandler(new ClickMapHandler() {
@Override
public void onEvent(ClickMapEvent event) {
presenter.onCameraSelected(camera.getCameraId());
}
});
cameraMarkers.add(cameraMarker);
}
if (Boolean.valueOf(localStorage.getItem("KEY_SHOW_CAMERAS"))) {
showCameras();
}
}
@Override
public void drawAlerts(List<HighwayAlertItem> alerts) {
// Types of categories which result in one icon or another being displayed.
String[] eventClosure = {"closed", "closure"};
String[] eventConstruction = {"construction", "maintenance", "lane closure"};
HashMap<String, String[]> eventCategories = new HashMap<String, String[]>();
eventCategories.put("closure", eventClosure);
eventCategories.put("construction", eventConstruction);
deleteAlerts();
for (final HighwayAlertItem alert: alerts) {
LatLng loc = LatLng.newInstance(alert.getStartLatitude(), alert.getStartLongitude());
MarkerOptions options = MarkerOptions.newInstance();
options.setPosition(loc);
// Determine correct icon for alert type
ImageResource imageResource = getCategoryIcon(eventCategories,
alert.getEventCategory(), alert.getPriority());
MarkerImage icon = MarkerImage.newInstance(imageResource.getSafeUri().asString());
options.setIcon(icon);
alertMarker = Marker.newInstance(options);
alertMarker.addClickHandler(new ClickMapHandler() {
@Override
public void onEvent(ClickMapEvent event) {
presenter.onAlertSelected(alert.getAlertId());
}
});
alertMarkers.add(alertMarker);
}
showAlerts();
}
@Override
public void drawRestAreas(List<RestAreaItem> restAreas) {
ImageResource noDumpSiteIcon = AppBundle.INSTANCE.restAreaPNG();
ImageResource dumpSiteIcon = AppBundle.INSTANCE.restAreaDumpPNG();
deleteRestAreas();
for (final RestAreaItem restArea: restAreas) {
LatLng loc = LatLng.newInstance(Double.valueOf(restArea.getLatitude()), Double.valueOf(restArea.getLongitude()));
MarkerOptions options = MarkerOptions.newInstance();
options.setPosition(loc);
// Determine correct icon for restarea type
ImageResource imageResource = restArea.hasDump() ? dumpSiteIcon : noDumpSiteIcon;
MarkerImage icon = MarkerImage.newInstance(imageResource.getSafeUri().asString());
options.setIcon(icon);
restAreaMarker = Marker.newInstance(options);
restAreaMarker.addClickHandler(new ClickMapHandler() {
@Override
public void onEvent(ClickMapEvent event) {
presenter.onRestAreaSelected(restArea.getId());
}
});
restAreaMarkers.add(restAreaMarker);
}
if (Boolean.valueOf(localStorage.getItem("KEY_SHOW_RESTAREAS"))) {
showRestAreas();
}
}
@Override
public void drawCallouts(List<CalloutItem> callouts) {
deleteCallouts();
for (final CalloutItem callout: callouts) {
LatLng loc = LatLng.newInstance(callout.getLatitude(), callout.getLongitude());
MarkerOptions options = MarkerOptions.newInstance();
options.setPosition(loc);
MarkerImage icon = MarkerImage.newInstance(AppBundle.INSTANCE
.jblmPNG().getSafeUri().asString());
options.setIcon(icon);
calloutMarker = Marker.newInstance(options);
calloutMarker.addClickHandler(new ClickMapHandler() {
@Override
public void onEvent(ClickMapEvent event) {
presenter.onCalloutSelected(callout.getImageUrl());
}
});
calloutMarkers.add(calloutMarker);
}
showCallouts();
}
@Override
public void hideCameras() {
for (Marker marker: cameraMarkers) {
marker.setMap((MapWidget)null);
}
localStorage.setItem("KEY_SHOW_CAMERAS", "false");
}
@Override
public void showCameras() {
for (Marker marker: cameraMarkers) {
marker.setMap(mapWidget);
}
localStorage.setItem("KEY_SHOW_CAMERAS", "true");
}
@Override
public void hideRestAreas() {
for (Marker marker: restAreaMarkers) {
marker.setMap((MapWidget)null);
}
localStorage.setItem("KEY_SHOW_RESTAREAS", "false");
}
@Override
public void showRestAreas() {
for (Marker marker: restAreaMarkers) {
marker.setMap(mapWidget);
}
localStorage.setItem("KEY_SHOW_RESTAREAS", "true");
}
@Override
public MapWidget getMapWidget() {
return mapWidget;
}
@Override
public void deleteCameras() {
for (Marker marker: cameraMarkers) {
marker.setMap((MapWidget)null);
}
cameraMarkers.clear();
}
@Override
public void setMapLocation(double latitude, double longitude, int zoom) {
LatLng latLng = LatLng.newInstance(latitude, longitude);
mapWidget.panTo(latLng);
mapWidget.setZoom(zoom);
}
@Override
public void setMapLocation() {
LatLng center = LatLng.newInstance(
Double.valueOf(localStorage.getItem("KEY_MAP_LAT")),
Double.valueOf(localStorage.getItem("KEY_MAP_LON")));
int zoom = Integer.valueOf(localStorage.getItem("KEY_MAP_ZOOM"), 10);
mapWidget.panTo(center);
mapWidget.setZoom(zoom);
}
@Override
public void addMapMarker(Position position) {
if (myLocationMarker != null) {
myLocationMarker.setMap((MapWidget) null);
}
if (myLocationError != null){
myLocationError.setMap(null);
}
LatLng center = LatLng.newInstance(position.getCoordinates().getLatitude(), position.getCoordinates().getLongitude());
MarkerOptions options = MarkerOptions.newInstance();
options.setPosition(center);
MarkerImage icon = MarkerImage.newInstance(AppBundle.INSTANCE.myLocationPNG().getSafeUri().asString());
icon.setAnchor(Point.newInstance(11, 11));
options.setOptimized(true);
options.setIcon(icon);
myLocationMarker = Marker.newInstance(options);
myLocationMarker.setMap(mapWidget);
// create a circle the size of the error
CircleOptions circleOptions = CircleOptions.newInstance();
circleOptions.setFillOpacity(0.1);
circleOptions.setFillColor("#1a75ff");
circleOptions.setStrokeOpacity(0.12);
circleOptions.setStrokeWeight(1);
circleOptions.setStrokeColor("#1a75ff");
myLocationError = Circle.newInstance(circleOptions);
myLocationError.setCenter(center);
myLocationError.setRadius(position.getCoordinates().getAccuracy());
myLocationError.setMap(mapWidget);
}
@Override
public void hideAlerts() {
for (Marker marker: alertMarkers) {
marker.setMap((MapWidget)null);
}
}
@Override
public void showAlerts() {
for (Marker marker: alertMarkers) {
marker.setMap(mapWidget);
}
}
@Override
public void deleteAlerts() {
for (Marker marker: alertMarkers) {
marker.setMap((MapWidget)null);
}
alertMarkers.clear();
}
@Override
public void deleteRestAreas() {
for (Marker marker: restAreaMarkers) {
marker.setMap((MapWidget)null);
}
restAreaMarkers.clear();
}
@Override
public void deleteCallouts() {
for (Marker marker: calloutMarkers) {
marker.setMap((MapWidget)null);
}
calloutMarkers.clear();
}
@Override
public void showCallouts() {
for (Marker marker: calloutMarkers) {
marker.setMap(mapWidget);
}
}
/**
* Refresh the map to update the traffic layer.
*
* Force the traffic layer to update by zooming in and then out
* of the map.
*/
@Override
public void refreshMap() {
mapWidget.setZoom(mapWidget.getZoom() + 1);
new Timer() {
@Override
public void run() {
mapWidget.setZoom(mapWidget.getZoom() - 1);
}
}.schedule(1);
}
/**
* Get the correct icon given the priority and category of alert.
*
* @param eventCategories
* @param category
* @param priority
* @return
*/
private ImageResource getCategoryIcon(
HashMap<String, String[]> eventCategories, String category, String priority) {
ImageResource alertClosed = AppBundle.INSTANCE.closedPNG();
ImageResource alertHighest = AppBundle.INSTANCE.alertHighestPNG();
ImageResource alertHigh = AppBundle.INSTANCE.alertHighPNG();
ImageResource alertMedium = AppBundle.INSTANCE.alertModeratePNG();
ImageResource alertLow = AppBundle.INSTANCE.alertLowPNG();
ImageResource constructionHighest = AppBundle.INSTANCE.constructionHighestPNG();
ImageResource constructionHigh = AppBundle.INSTANCE.constructionHighPNG();
ImageResource constructionMedium = AppBundle.INSTANCE.constructionModeratePNG();
ImageResource constructionLow = AppBundle.INSTANCE.constructionLowPNG();
ImageResource defaultAlertImage = alertHighest;
Set<Entry<String, String[]>> set = eventCategories.entrySet();
Iterator<Entry<String, String[]>> i = set.iterator();
if (category.equals("")) return defaultAlertImage;
while(i.hasNext()) {
Entry<String, String[]> me = i.next();
for (String phrase: (String[])me.getValue()) {
String patternStr = phrase;
RegExp pattern = RegExp.compile(patternStr, "i");
MatchResult matcher = pattern.exec(category);
boolean matchFound = matcher != null;
if (matchFound) {
String keyWord = me.getKey();
if (keyWord.equalsIgnoreCase("closure")) {
return alertClosed;
} else if (keyWord.equalsIgnoreCase("construction")) {
if (priority.equalsIgnoreCase("highest")) {
return constructionHighest;
} else if (priority.equalsIgnoreCase("high")) {
return constructionHigh;
} else if (priority.equalsIgnoreCase("medium")) {
return constructionMedium;
} else if (priority.equalsIgnoreCase("low")
|| priority.equalsIgnoreCase("lowest")) {
return constructionLow;
}
}
}
}
}
// If we arrive here, it must be an accident or alert item.
if (priority.equalsIgnoreCase("highest")) {
return alertHighest;
} else if (priority.equalsIgnoreCase("high")) {
return alertHigh;
} else if (priority.equalsIgnoreCase("medium")) {
return alertMedium;
} else if (priority.equalsIgnoreCase("low")
|| priority.equalsIgnoreCase("lowest")) {
return alertLow;
}
return defaultAlertImage;
}
/**
* Custom MapWidget for map not resizing issue.
*
* @author Kostas Patakas https://plus.google.com/112436396950239118116
*
* When the onAttach occurs and animation exists meaning that the div started
* smaller and increases in size, but the map has already attached. At the end
* of the animation, the map doesn't auto resize.
*
*/
private class MyMapWidget extends MapWidget {
public MyMapWidget(MapOptions options) {
super(options);
}
@Override
protected void onAttach() {
super.onAttach();
Timer timer = new Timer() {
@Override
public void run() {
resize();
}
};
timer.schedule(5);
}
/**
* This method is called to fix the Map loading issue when opening
* multiple instances of maps in different tabs
* Triggers a resize event to be consumed by google api in order to resize view
* after attach.
*
*/
public void resize() {
LatLng center = this.getCenter();
MapHandlerRegistration.trigger(this, MapEventType.RESIZE);
this.setCenter(center);
}
}
private void accessibilityPrepare(){
// Add ARIA roles for accessibility
Roles.getButtonRole().set(backButton.getElement());
Roles.getButtonRole().setAriaLabelProperty(backButton.getElement(), "back");
Roles.getButtonRole().set(menuButton.getElement());
Roles.getButtonRole().setAriaLabelProperty(menuButton.getElement(), "more options");
Roles.getButtonRole().set(alertsButton.getElement());
Roles.getButtonRole().setAriaLabelProperty(alertsButton.getElement(), "alerts");
Roles.getHeadingRole().set(heading.getElement());
}
}