/* * Copyright (c) 2014 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.trafficincidents; 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.i18n.client.DateTimeFormat; 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.place.shared.Place; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.AcceptsOneWidget; import com.googlecode.mgwt.mvp.client.MGWTAbstractActivity; import com.googlecode.mgwt.ui.client.widget.panel.pull.PullArrowStandardHandler; import com.googlecode.mgwt.ui.client.widget.panel.pull.PullArrowStandardHandler.PullActionHandler; import gov.wa.wsdot.mobile.client.ClientFactory; import gov.wa.wsdot.mobile.client.activities.alert.AlertPlace; import gov.wa.wsdot.mobile.client.event.ActionEvent; import gov.wa.wsdot.mobile.client.event.ActionNames; import gov.wa.wsdot.mobile.client.service.WSDOTContract.CachesColumns; import gov.wa.wsdot.mobile.client.service.WSDOTContract.HighwayAlertsColumns; 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.CacheItem; import gov.wa.wsdot.mobile.shared.HighwayAlertItem; import gov.wa.wsdot.mobile.shared.HighwayAlerts; import gov.wa.wsdot.mobile.shared.LatLonItem; import java.util.*; import java.util.Map.Entry; public class TrafficAlertsActivity extends MGWTAbstractActivity implements TrafficAlertsView.Presenter { private final ClientFactory clientFactory; private TrafficAlertsView view; private EventBus eventBus; private WSDOTDataService dbService; private static ArrayList<HighwayAlertItem> highwayAlertItems = new ArrayList<HighwayAlertItem>(); private static ArrayList<HighwayAlertItem> alertsToAdd = new ArrayList<HighwayAlertItem>(); private static final String HIGHWAY_ALERTS_URL = Consts.HOST_URL + "/traveler/api/highwayalerts"; private static DateTimeFormat dateFormat = DateTimeFormat.getFormat("MMMM d, yyyy h:mm a"); private static LatLngBounds bounds; private ArrayList<HighwayAlertItem> amberAlerts = new ArrayList<HighwayAlertItem>(); private ArrayList<HighwayAlertItem> blockingIncidents = new ArrayList<HighwayAlertItem>(); private ArrayList<HighwayAlertItem> constructionClosures = new ArrayList<HighwayAlertItem>(); private ArrayList<HighwayAlertItem> trafficClosures = new ArrayList<HighwayAlertItem>(); private ArrayList<HighwayAlertItem> specialEvents = new ArrayList<HighwayAlertItem>(); public TrafficAlertsActivity(ClientFactory clientFactory) { this.clientFactory = clientFactory; } @Override public void start(AcceptsOneWidget panel, final EventBus eventBus) { view = clientFactory.getTrafficAlertsView(); this.eventBus = eventBus; dbService = clientFactory.getDbService(); view.setPresenter(this); Place place = clientFactory.getPlaceController().getWhere(); if (place instanceof TrafficAlertsPlace) { TrafficAlertsPlace alertPlace = (TrafficAlertsPlace) place; bounds = alertPlace.getBounds(); } view.getPullHeader().setHTML("pull down"); PullArrowStandardHandler headerHandler = new PullArrowStandardHandler( view.getPullHeader(), view.getPullPanel()); headerHandler.setErrorText("Error"); headerHandler.setLoadingText("Loading"); headerHandler.setNormalText("pull down"); headerHandler.setPulledText("release to load"); headerHandler.setPullActionHandler(new PullActionHandler() { @Override public void onPullAction(final AsyncCallback<Void> callback) { new Timer() { @Override public void run() { getHighwayAlerts(); view.refresh(); callback.onSuccess(null); } }.schedule(1); } }); view.setHeaderPullHandler(headerHandler); view.showProgressIndicator(); getHighwayAlerts(); view.hideProgressIndicator(); panel.setWidget(view); } private void getHighwayAlerts() { /** * 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.HIGHWAY_ALERTS, 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) > (5 * 60000)); // Refresh every 5 minutes. } if (shouldUpdate) { try { JsonpRequestBuilder jsonp = new JsonpRequestBuilder(); // Set timeout for 30 seconds (30000 milliseconds) jsonp.setTimeout(30000); jsonp.requestObject(HIGHWAY_ALERTS_URL, new AsyncCallback<HighwayAlerts>() { @Override public void onFailure(Throwable caught) { } @Override public void onSuccess(HighwayAlerts result) { alertsToAdd.clear(); if (result.getAlerts() != null) { HighwayAlertItem item; int size = result.getAlerts().getItems().length(); for (int i = 0; i < size; i++) { item = new HighwayAlertItem(); item.setAlertId(result.getAlerts().getItems().get(i).getAlertID()); item.setHeadlineDescription(result.getAlerts().getItems().get(i).getHeadlineDescription()); item.setEventCategory(result.getAlerts().getItems().get(i).getEventCategory()); item.setStartLatitude(result.getAlerts().getItems().get(i).getStartRoadwayLocation().getLatitude()); item.setStartLongitude(result.getAlerts().getItems().get(i).getStartRoadwayLocation().getLongitude()); item.setLastUpdatedTime(dateFormat.format(new Date( Long.parseLong(result .getAlerts() .getItems() .get(i) .getLastUpdatedTime() .substring(6, 19))))); alertsToAdd.add(item); } // Purge existing highway alerts covered by incoming data dbService.deleteHighwayAlerts(new VoidCallback() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess() { // Bulk insert all the new highway alerts dbService.insertHighwayAlerts(alertsToAdd, 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 List<CacheItem> cacheItems = new ArrayList<CacheItem>(); cacheItems.add(new CacheItem(Tables.HIGHWAY_ALERTS, System.currentTimeMillis())); dbService.updateCachesTable(cacheItems, new VoidCallback() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess() { addAlerts(); } }); } }); } }); } } }); } catch (Exception e) { } } else { addAlerts(); } } }); } private void addAlerts() { highwayAlertItems.clear(); dbService.getHighwayAlerts(new ListCallback<GenericRow>() { @Override public void onFailure(DataServiceException error) { } @Override public void onSuccess(List<GenericRow> result) { LatLng nePoint = bounds.getNorthEast(); LatLng swPoint = bounds.getSouthWest(); 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())); for (GenericRow alert: result) { if (inPolygon( viewableMapArea, alert.getDouble(HighwayAlertsColumns.HIGHWAY_ALERT_LATITUDE), alert.getDouble(HighwayAlertsColumns.HIGHWAY_ALERT_LONGITUDE)) || alert.getString(HighwayAlertsColumns.HIGHWAY_ALERT_CATEGORY) == "amber alert") { HighwayAlertItem item = new HighwayAlertItem(); item.setAlertId(alert.getInt(HighwayAlertsColumns.HIGHWAY_ALERT_ID)); item.setHeadlineDescription(alert.getString(HighwayAlertsColumns.HIGHWAY_ALERT_HEADLINE)); item.setEventCategory(alert.getString(HighwayAlertsColumns.HIGHWAY_ALERT_CATEGORY)); item.setStartLatitude(alert.getDouble(HighwayAlertsColumns.HIGHWAY_ALERT_LATITUDE)); item.setStartLongitude(alert.getDouble(HighwayAlertsColumns.HIGHWAY_ALERT_LONGITUDE)); item.setLastUpdatedTime(alert.getString(HighwayAlertsColumns.HIGHWAY_ALERT_LAST_UPDATED)); highwayAlertItems.add(item); } } if (!result.isEmpty()) { categorizeAlerts(); view.refresh(); } } }); } private void categorizeAlerts() { Stack<HighwayAlertItem> amberalert = new Stack<HighwayAlertItem>(); Stack<HighwayAlertItem> blocking = new Stack<HighwayAlertItem>(); Stack<HighwayAlertItem> construction = new Stack<HighwayAlertItem>(); Stack<HighwayAlertItem> closures = new Stack<HighwayAlertItem>(); Stack<HighwayAlertItem> special = new Stack<HighwayAlertItem>(); amberAlerts.clear(); blockingIncidents.clear(); constructionClosures.clear(); trafficClosures.clear(); specialEvents.clear(); for (HighwayAlertItem item : highwayAlertItems) { int category = getCategoryID(item.getEventCategory()); // Check if there is an active amber alert if (category == Consts.AMBER) { amberalert.push(item); } else if (category == Consts.BLOCKING) { blocking.push(item); } else if (category == Consts.CONSTRUCTION) { construction.push(item); } else if (category == Consts.CLOSURES) { closures.push(item); } else if (category == Consts.SPECIAL) { special.push(item); } } if (amberalert != null && amberalert.size() != 0) { while (!amberalert.empty()) { amberAlerts.add(amberalert.pop()); } view.showAmberAlerts(); view.renderAmberAlerts(amberAlerts); } else { view.hideAmberAlerts(); } if (blocking.empty()) { blockingIncidents.add(new HighwayAlertItem(0, "None reported")); } else { while (!blocking.empty()) { blockingIncidents.add(blocking.pop()); } } view.renderBlocking(blockingIncidents); if (construction.empty()) { constructionClosures.add(new HighwayAlertItem(0, "None reported")); } else { while (!construction.empty()) { constructionClosures.add(construction.pop()); } } view.renderConstruction(constructionClosures); if (closures.empty()) { trafficClosures.add(new HighwayAlertItem(0, "None reported")); } else { while (!closures.empty()) { trafficClosures.add(closures.pop()); } } view.renderClosure(trafficClosures); if (special.empty()) { specialEvents.add(new HighwayAlertItem(0, "None reported")); } else { while (!special.empty()) { specialEvents.add(special.pop()); } } view.renderSpecial(specialEvents); } @Override public void onStop() { view.setPresenter(null); } @Override public void onDoneButtonPressed() { ActionEvent.fire(eventBus, ActionNames.BACK); } @Override public void onItemSelected(int alertType, int index) { int alertId = 0; switch (alertType){ case Consts.BLOCKING: alertId = blockingIncidents.get(index).getAlertId(); break; case Consts.CONSTRUCTION: alertId = constructionClosures.get(index).getAlertId(); break; case Consts.CLOSURES: alertId = trafficClosures.get(index).getAlertId(); break; case Consts.SPECIAL: alertId = specialEvents.get(index).getAlertId(); break; default: //TODO Shouldn't get here, if we do treat as if nothing was clicked } if (alertId != 0) clientFactory.getPlaceController().goTo( new AlertPlace(Integer.toString(alertId))); } /** * 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; } /** * * Maps a category name to one of four possible types. These four IDs * represent the four lists displayed by this activity. * * @param category The name of the category of alert * @return category ID */ private int getCategoryID(String category) { // Types of categories String[] event_closure = {"closed", "closure"}; String[] event_construction = {"construction", "maintenance", "lane closure"}; String[] event_amber = {"amber"}; String[] event_special = {"special event"}; HashMap<String, String[]> eventCategories = new HashMap<>(); eventCategories.put("closure", event_closure); eventCategories.put("construction", event_construction); eventCategories.put("amber", event_amber); eventCategories.put("special", event_special); Set<Entry<String, String[]>> set = eventCategories.entrySet(); Iterator<Entry<String, String[]>> i = set.iterator(); if (category.equals("")) return Consts.BLOCKING; while(i.hasNext()) { Entry<String, String[]> me = i.next(); for (String phrase: 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 Consts.CLOSURES; } else if (keyWord.equalsIgnoreCase("construction")) { return Consts.CONSTRUCTION; } else if (keyWord.equalsIgnoreCase("special")) { return Consts.SPECIAL; } else if (keyWord.equalsIgnoreCase("amber")){ return Consts.AMBER; } } } } return Consts.BLOCKING; } }