/* * Copyright (c) 2016 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.ferries.vesselwatch; import com.google.code.gwt.database.client.GenericRow; import com.google.code.gwt.database.client.service.DataServiceException; import com.google.code.gwt.database.client.service.ListCallback; import com.google.code.gwt.database.client.service.RowIdListCallback; import com.google.code.gwt.database.client.service.VoidCallback; import com.google.gwt.event.shared.EventBus; import com.google.gwt.jsonp.client.JsonpRequestBuilder; import com.google.gwt.maps.client.base.LatLng; import com.google.gwt.maps.client.base.LatLngBounds; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.googlecode.gwtphonegap.client.PhoneGap; import com.googlecode.gwtphonegap.client.geolocation.GeolocationCallback; import com.googlecode.gwtphonegap.client.geolocation.Position; import com.googlecode.gwtphonegap.client.geolocation.PositionError; import com.googlecode.gwtphonegap.client.notification.AlertCallback; import com.googlecode.mgwt.mvp.client.MGWTAbstractActivity; import gov.wa.wsdot.mobile.client.ClientFactory; import gov.wa.wsdot.mobile.client.activities.camera.CameraPlace; import gov.wa.wsdot.mobile.client.activities.ferries.vesselwatch.location.GoToFerriesLocationPlace; import gov.wa.wsdot.mobile.client.activities.ferries.vesselwatch.vesseldetails.VesselDetailsPlace; import gov.wa.wsdot.mobile.client.css.AppBundle; import gov.wa.wsdot.mobile.client.event.ActionEvent; import gov.wa.wsdot.mobile.client.event.ActionNames; import gov.wa.wsdot.mobile.client.plugins.accessibility.Accessibility; import gov.wa.wsdot.mobile.client.plugins.analytics.Analytics; import gov.wa.wsdot.mobile.client.service.WSDOTContract.CachesColumns; import gov.wa.wsdot.mobile.client.service.WSDOTContract.CamerasColumns; import gov.wa.wsdot.mobile.client.service.WSDOTDataService; import gov.wa.wsdot.mobile.client.service.WSDOTDataService.Tables; import gov.wa.wsdot.mobile.client.util.Consts; import gov.wa.wsdot.mobile.shared.*; import java.util.ArrayList; import java.util.HashMap; import java.util.List; public class VesselWatchMapActivity extends MGWTAbstractActivity implements VesselWatchMapView.Presenter { private final ClientFactory clientFactory; private VesselWatchMapView view; private EventBus eventBus; private WSDOTDataService dbService; private static final String CAMERAS_URL = Consts.HOST_URL + "/traveler/api/cameras"; private static final String VESSEL_WATCH_URL = Consts.HOST_URL + "/traveler/api/ferries/vessellocations"; private static ArrayList<VesselWatchItem> vesselWatchItems = new ArrayList<VesselWatchItem>(); private static HashMap<Integer, String> ferryIcons; private Timer timer; private PhoneGap phoneGap; private Analytics analytics; private Accessibility accessibility; public VesselWatchMapActivity(ClientFactory clientFactory) { this.clientFactory = clientFactory; } @Override public void start(AcceptsOneWidget panel, final EventBus eventBus) { view = clientFactory.getVesselWatchMapView(); dbService = clientFactory.getDbService(); phoneGap = clientFactory.getPhoneGap(); analytics = clientFactory.getAnalytics(); accessibility = clientFactory.getAccessibility(); this.eventBus = eventBus; view.setPresenter(this); view.setMapLocation(); // Set initial map location. buildFerryIcons(); drawVesselsLayer(); timer = new Timer() { public void run() { drawVesselsLayer(); } }; // Schedule vessels to update every 30 seconds (30000 millseconds). timer.scheduleRepeating(30000); if (Consts.ANALYTICS_ENABLED) { analytics.trackScreen("/Ferries/Vessel Watch"); } panel.setWidget(view); accessibility.postScreenChangeNotification(); } private void buildFerryIcons() { ferryIcons = new HashMap<Integer, String>(); ferryIcons.put(0, AppBundle.INSTANCE.ferry0().getSafeUri().asString()); ferryIcons.put(30, AppBundle.INSTANCE.ferry30().getSafeUri().asString()); ferryIcons.put(60, AppBundle.INSTANCE.ferry60().getSafeUri().asString()); ferryIcons.put(90, AppBundle.INSTANCE.ferry90().getSafeUri().asString()); ferryIcons.put(120, AppBundle.INSTANCE.ferry120().getSafeUri().asString()); ferryIcons.put(150, AppBundle.INSTANCE.ferry150().getSafeUri().asString()); ferryIcons.put(180, AppBundle.INSTANCE.ferry180().getSafeUri().asString()); ferryIcons.put(210, AppBundle.INSTANCE.ferry210().getSafeUri().asString()); ferryIcons.put(240, AppBundle.INSTANCE.ferry240().getSafeUri().asString()); ferryIcons.put(270, AppBundle.INSTANCE.ferry270().getSafeUri().asString()); ferryIcons.put(300, AppBundle.INSTANCE.ferry300().getSafeUri().asString()); ferryIcons.put(330, AppBundle.INSTANCE.ferry330().getSafeUri().asString()); ferryIcons.put(360, AppBundle.INSTANCE.ferry360().getSafeUri().asString()); } private void getCameras() { /** * Check the cache table for the last time data was downloaded. If we are within * the allowed time period, don't sync, otherwise get fresh data from the server. */ dbService.getCacheLastUpdated(Tables.CAMERAS, new ListCallback<GenericRow>() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess(List<GenericRow> result) { boolean shouldUpdate = true; if (!result.isEmpty()) { double now = System.currentTimeMillis(); double lastUpdated = result.get(0).getDouble(CachesColumns.CACHE_LAST_UPDATED); shouldUpdate = (Math.abs(now - lastUpdated) > (7 * 86400000)); // Refresh every 7 days. } view.showProgressIndicator(); if (shouldUpdate) { JsonpRequestBuilder jsonp = new JsonpRequestBuilder(); // Set timeout for 30 seconds (30000 milliseconds) jsonp.setTimeout(30000); jsonp.requestObject(CAMERAS_URL, new AsyncCallback<CamerasFeed>() { @Override public void onFailure(Throwable caught) { view.hideProgressIndicator(); phoneGap.getNotification() .alert("Can't load data. Check your connection.", new AlertCallback() { @Override public void onOkButtonClicked() { // TODO Auto-generated method stub } }, "Connection Error"); } @Override public void onSuccess(final CamerasFeed result) { if (result.getCameras() != null) { final List<Integer> starred = new ArrayList<Integer>(); // Get any starred camera rows. dbService.getStarredCameras(new ListCallback<GenericRow>() { @Override public void onFailure(DataServiceException error) { Window.alert(error.getMessage()); } @Override public void onSuccess(List<GenericRow> rows) { final ArrayList<CameraItem> cameraItems = new ArrayList<CameraItem>(); CameraItem item; if (!rows.isEmpty()) { int numResults = rows.size(); for (int i = 0; i < numResults; i++) { starred.add(rows.get(i).getInt(CamerasColumns.CAMERA_ID)); } } //cameraItems.clear(); int numCameras = result.getCameras().getItems().length(); for (int i = 0; i < numCameras; i++) { item = new CameraItem(); item.setCameraId(result.getCameras().getItems().get(i).getId()); item.setTitle(result.getCameras().getItems().get(i).getTitle()); item.setImageUrl(result.getCameras().getItems().get(i).getUrl()); item.setLatitude(result.getCameras().getItems().get(i).getLat()); item.setLongitude(result.getCameras().getItems().get(i).getLon()); item.setHasVideo(result.getCameras().getItems().get(i).getHasVideo()); item.setRoadName(result.getCameras().getItems().get(i).getRoadName()); if (starred.contains(result.getCameras().getItems().get(i).getId())) { item.setIsStarred(1); } cameraItems.add(item); } // Purge existing cameras covered by incoming data dbService.deleteCameras(new VoidCallback() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess() { // Bulk insert all the new cameras dbService.insertCameras(cameraItems, new RowIdListCallback() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess(List<Integer> rowIds) { // Update the cache table with the time we did the update ArrayList<CacheItem> cacheItems = new ArrayList<CacheItem>(); cacheItems.add(new CacheItem(Tables.CAMERAS, System.currentTimeMillis())); dbService.updateCachesTable(cacheItems, new VoidCallback() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess() { view.hideProgressIndicator(); drawCamerasLayer(); } }); } }); } }); } }); } } }); } else { view.hideProgressIndicator(); drawCamerasLayer(); } } }); } private void drawCamerasLayer() { dbService.getCameras(new ListCallback<GenericRow>() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess(List<GenericRow> result) { LatLngBounds bounds = view.getViewportBounds(); LatLng swPoint = bounds.getSouthWest(); LatLng nePoint = bounds.getNorthEast(); ArrayList<LatLonItem> viewableMapArea = new ArrayList<LatLonItem>(); viewableMapArea.add(new LatLonItem(nePoint.getLatitude(), swPoint.getLongitude())); viewableMapArea.add(new LatLonItem(nePoint.getLatitude(), nePoint.getLongitude())); viewableMapArea.add(new LatLonItem(swPoint.getLatitude(), nePoint.getLongitude())); viewableMapArea.add(new LatLonItem(swPoint.getLatitude(), swPoint.getLongitude())); ArrayList<CameraItem> cameras = new ArrayList<CameraItem>(); int numRows = result.size(); for (int i = 0; i < numRows; i++) { if (inPolygon( viewableMapArea, result.get(i).getDouble(CamerasColumns.CAMERA_LATITUDE), result.get(i).getDouble(CamerasColumns.CAMERA_LONGITUDE)) && result.get(i).getString(CamerasColumns.CAMERA_ROAD_NAME) .equalsIgnoreCase("ferries")) { cameras.add(new CameraItem( result.get(i).getInt(CamerasColumns.CAMERA_ID), result.get(i).getString(CamerasColumns.CAMERA_TITLE), result.get(i).getString(CamerasColumns.CAMERA_URL), result.get(i).getDouble(CamerasColumns.CAMERA_LATITUDE), result.get(i).getDouble(CamerasColumns.CAMERA_LONGITUDE), result.get(i).getInt(CamerasColumns.CAMERA_HAS_VIDEO))); } } view.drawCameras(cameras); } }); } private void drawVesselsLayer() { JsonpRequestBuilder jsonp = new JsonpRequestBuilder(); // Set timeout for 25 seconds (25000 milliseconds) jsonp.setTimeout(25000); jsonp.requestObject(VESSEL_WATCH_URL, new AsyncCallback<VesselWatchFeed>() { @Override public void onFailure(Throwable caught) { view.hideProgressIndicator(); phoneGap.getNotification() .alert("Can't load data. Check your connection.", new AlertCallback() { @Override public void onOkButtonClicked() { // TODO Auto-generated method stub } }, "Connection Error"); } @Override public void onSuccess(VesselWatchFeed result) { vesselWatchItems.clear(); VesselWatchItem item = null; if (result != null) { int numEntries = result.length(); for (int i = 0; i < numEntries; i++) { item = new VesselWatchItem(); if (!result.get(i).getInService()) { continue; } item.setVesselID(result.get(i).getVesselID()); item.setName(result.get(i).getName()); item.setRoute(result.get(i).getRoute()); item.setLastDock(result.get(i).getLastDock()); item.setArrivingTerminal(result.get(i).getATerm()); item.setLeftDock(result.get(i).getLeftDock()); item.setNextDep(result.get(i).getNextDep()); item.setEta(result.get(i).getEta()); item.setHead(result.get(i).getHead()); item.setSpeed(result.get(i).getSpeed()); // round heading to nearest 30 degrees int nearest = (result.get(i) .getHead() + 30 / 2) / 30 * 30; item.setIcon(ferryIcons.get(nearest)); item.setLat(result.get(i).getLat()); item.setLon(result.get(i).getLon()); vesselWatchItems.add(item); } view.drawFerries(vesselWatchItems); } } }); } /** * Iterate through collection of LatLon objects in ArrayList and see if * passed latitude and longitude point is within the collection. * * @param points * @param latitude * @param longitude * @return */ private boolean inPolygon(ArrayList<LatLonItem> points, double latitude, double longitude) { int j = points.size() - 1; double lat = latitude; double lon = longitude; boolean inPoly = false; for (int i = 0; i < points.size(); i++) { if ((points.get(i).getLongitude() < lon && points.get(j).getLongitude() >= lon) || (points.get(j).getLongitude() < lon && points.get(i).getLongitude() >= lon)) { if (points.get(i).getLatitude() + (lon - points.get(i).getLongitude()) / (points.get(j).getLongitude() - points.get(i).getLongitude()) * (points.get(j).getLatitude() - points.get(i).getLatitude()) < lat) { inPoly = !inPoly; } } j = i; } return inPoly; } @Override public void onStop() { view.setPresenter(null); timer.cancel(); } @Override public void onBackButtonPressed() { ActionEvent.fire(eventBus, ActionNames.BACK); } @Override public void onGoToLocationButtonPressed() { clientFactory.getPlaceController().goTo(new GoToFerriesLocationPlace()); if (Consts.ANALYTICS_ENABLED) { analytics.trackScreen("/Ferries/Vessel Watch/Go To Location"); } } @Override public void onCameraButtonPressed(boolean showCameras) { if (showCameras) { view.hideCameras(); } else { view.showCameras(); } } @Override public void onCameraSelected(int cameraId) { clientFactory.getPlaceController().goTo( new CameraPlace(Integer.toString(cameraId))); if (Consts.ANALYTICS_ENABLED) { analytics.trackScreen("/Ferries/Vessel Watch/Cameras"); } } @Override public void onLocateButtonPressed() { phoneGap.getGeolocation().getCurrentPosition(new GeolocationCallback() { @Override public void onSuccess(Position position) { double latitude = position.getCoordinates().getLatitude(); double longitude = position.getCoordinates().getLongitude(); view.addMapMarker(position); view.setMapLocation(latitude, longitude, 12); } @Override public void onFailure(PositionError error) { switch (error.getCode()) { case PositionError.PERMISSION_DENIED: phoneGap.getNotification() .alert("You can turn Location Services on at Settings > Privacy > Location Services.", new AlertCallback() { @Override public void onOkButtonClicked() { // TODO Auto-generated method stub } }, "Location Services Off"); break; default: break; } } }); } @Override public void onFerrySelected(VesselWatchItem vessel) { if (Consts.ANALYTICS_ENABLED) { analytics.trackScreen("/Ferries/Vessel Watch/Vessel Details/" + vessel.getName()); } clientFactory.getPlaceController().goTo(new VesselDetailsPlace(vessel)); } @Override public void onMapIsIdle() { captureClickEvents(); getCameras(); } /** * JSNI method to capture click events and open urls in PhoneGap * InAppBrowser. * * Tapping external links on the Google map like the Google logo and 'Terms * of Use' will cause those links to open in the same browser window as the * app with no way for the user to return to the app. * * http://docs.phonegap.com/en/2.4.0/cordova_inappbrowser_inappbrowser.md.html */ public static native void captureClickEvents() /*-{ anchors = $doc.getElementsByTagName('a'); for ( var i = 0; i < anchors.length; i++) { anchors[i].addEventListener('click', function(e) { e.preventDefault(); $wnd.open(this.href, '_blank', 'location=yes,enableViewportScale=yes'); }); } }-*/; }