/**
* Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.onebusaway.webapp.gwt.where_library.view.stops;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import org.onebusaway.geospatial.model.CoordinateBounds;
import org.onebusaway.geospatial.model.EncodedPolylineBean;
import org.onebusaway.transit_data.model.RouteBean;
import org.onebusaway.transit_data.model.StopBean;
import org.onebusaway.transit_data.model.StopsForRouteBean;
import org.onebusaway.webapp.gwt.common.MapOverlayManager;
import org.onebusaway.webapp.gwt.common.control.Place;
import org.onebusaway.webapp.gwt.common.resources.map.StopIconFactory;
import org.onebusaway.webapp.gwt.common.resources.map.StopIconFactory.ESize;
import org.onebusaway.webapp.gwt.where_library.GeocoderAccuracyToBounds;
import org.onebusaway.webapp.gwt.where_library.impl.StopsForRegionServiceImpl;
import org.onebusaway.webapp.gwt.where_library.view.events.StopClickedEvent;
import org.onebusaway.webapp.gwt.where_library.view.events.StopClickedHandler;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.maps.client.MapWidget;
import com.google.gwt.maps.client.event.MapMoveEndHandler;
import com.google.gwt.maps.client.event.MarkerClickHandler;
import com.google.gwt.maps.client.geom.LatLng;
import com.google.gwt.maps.client.geom.LatLngBounds;
import com.google.gwt.maps.client.overlay.EncodedPolyline;
import com.google.gwt.maps.client.overlay.Icon;
import com.google.gwt.maps.client.overlay.Marker;
import com.google.gwt.maps.client.overlay.MarkerOptions;
import com.google.gwt.maps.client.overlay.Overlay;
import com.google.gwt.maps.client.overlay.Polyline;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.DeferredCommand;
import com.google.gwt.user.client.rpc.AsyncCallback;
public class TransitMapManager {
private StopsForRegionServiceImpl _stopsForRegionService = new StopsForRegionServiceImpl();
private Map<String, StopAndOverlays> _visibleStopsById = new HashMap<String, StopAndOverlays>();
private StopsHandler _stopsHandler = new StopsHandler();
private Set<String> _stopIdsToAlwaysShow = new HashSet<String>();
private Set<String> _selectedStopIds = new HashSet<String>();
private List<Overlay> _otherOverlays = new ArrayList<Overlay>();
private boolean _showStopsInCurrentView = true;
private MapWidget _map;
private MapOverlayManager _manager;
private HandlerManager _handlerManager;
private RouteBean _selectedRoute = null;
public TransitMapManager(MapWidget map) {
_map = map;
_manager = new MapOverlayManager(_map);
_map.addMapMoveEndHandler(new MapMoveEndHandlerImpl());
}
public HandlerRegistration addStopClickedHandler(StopClickedHandler handler) {
return ensureHandlers().addHandler(StopClickedEvent.TYPE, handler);
}
public MapWidget getMap() {
return _map;
}
public RouteBean getSelectedRoute() {
return _selectedRoute;
}
/****
* Visualization Modes
****/
public void showStopsInCurrentView() {
reset(true);
}
public void showStopsAtLocation(LatLng point, int zoomLevel) {
_map.setCenter(point, zoomLevel);
reset(true);
}
public void showStops(List<StopBean> stops) {
reset(false, stops);
}
public void showStopsForRoute(RouteBean route,
StopsForRouteBean stopsForRoute, boolean centerViewOnRoute) {
reset(false, stopsForRoute.getStops());
_selectedRoute = route;
for (EncodedPolylineBean path : stopsForRoute.getPolylines()) {
EncodedPolyline epl = EncodedPolyline.newInstance(path.getPoints(), 32,
path.getLevels(3), 4);
epl.setColor("#4F64BA");
epl.setWeight(3);
epl.setOpacity(1.0);
Polyline polyline = Polyline.fromEncoded(epl);
_manager.addOverlay(polyline);
_otherOverlays.add(polyline);
}
if (centerViewOnRoute) {
System.out.println("center on route");
// Center the map on the bounds of the route
LatLngBounds bounds = LatLngBounds.newInstance();
for (StopBean stop : stopsForRoute.getStops())
bounds.extend(LatLng.newInstance(stop.getLat(), stop.getLon()));
if (!bounds.isEmpty()) {
int zoomLevel = _map.getBoundsZoomLevel(bounds);
_map.setCenter(bounds.getCenter(), zoomLevel);
}
}
}
public void showStop(StopBean stop, boolean showStopsInCurrentView) {
LatLng point = LatLng.newInstance(stop.getLat(), stop.getLon());
_map.setCenter(point, 17);
Set<String> selectedStopIds = new HashSet<String>();
selectedStopIds.add(stop.getId());
reset(showStopsInCurrentView, Arrays.asList(stop), selectedStopIds);
}
public void showPlace(Place place, boolean showStopsInCurrentView,
PlaceClickHandler clickHandler) {
System.out.println("accuracy=" + place.getAccuracy());
int zoomLevel = GeocoderAccuracyToBounds.getZoomLevelForAccuracy(place.getAccuracy());
_map.setCenter(place.getLocation(), zoomLevel);
reset(true);
addPlaceToMap(place, clickHandler);
}
public void showPlaces(List<Place> places, boolean showStopsInCurrentView,
PlaceClickHandler clickHandler) {
LatLngBounds bounds = LatLngBounds.newInstance();
for (Place place : places)
bounds.extend(place.getLocation());
if (!bounds.isEmpty())
_map.setCenter(bounds.getCenter(), _map.getBoundsZoomLevel(bounds));
reset(showStopsInCurrentView);
for (Place place : places)
addPlaceToMap(place, clickHandler);
}
/****
* Private Methods
****/
private HandlerManager ensureHandlers() {
return _handlerManager == null ? _handlerManager = new HandlerManager(this)
: _handlerManager;
}
private void reset(boolean showStopsInCurrentView) {
reset(showStopsInCurrentView, new ArrayList<StopBean>());
}
private void reset(boolean showStopsInCurrentView,
List<StopBean> stopsToAlwaysShow) {
reset(showStopsInCurrentView, stopsToAlwaysShow, new HashSet<String>());
}
private void reset(boolean showStopsInCurrentView,
List<StopBean> stopsToAlwaysShow, Set<String> selectedStopIds) {
_showStopsInCurrentView = showStopsInCurrentView;
_stopIdsToAlwaysShow.clear();
for (StopBean stop : stopsToAlwaysShow)
_stopIdsToAlwaysShow.add(stop.getId());
for (Overlay overlay : _otherOverlays)
_manager.removeOverlay(overlay);
_otherOverlays.clear();
for (StopAndOverlays stopAndOverlay : _visibleStopsById.values()) {
List<Overlay> overlays = stopAndOverlay.getOverlays();
for (Overlay overlay : overlays)
_manager.removeOverlay(overlay);
}
_visibleStopsById.clear();
_selectedStopIds.clear();
_selectedStopIds.addAll(selectedStopIds);
_stopsHandler.onSuccess(stopsToAlwaysShow);
_selectedRoute = null;
refresh();
}
private void refresh() {
if (_showStopsInCurrentView) {
int zoom = _map.getZoomLevel();
if (zoom < 17) {
return;
}
final CoordinateBounds bounds = computeBounds();
for (Iterator<Map.Entry<String, StopAndOverlays>> it = _visibleStopsById.entrySet().iterator(); it.hasNext();) {
Entry<String, StopAndOverlays> entry = it.next();
StopAndOverlays stopAndOverlays = entry.getValue();
StopBean stop = stopAndOverlays.getStop();
if (!bounds.contains(stop.getLat(), stop.getLon())) {
it.remove();
for (Overlay overlay : stopAndOverlays.getOverlays())
_manager.removeOverlay(overlay);
}
}
// Update the map
DeferredCommand.addCommand(new Command() {
@Override
public void execute() {
_stopsForRegionService.getStopsForRegion(bounds, _stopsHandler);
}
});
} else {
for (StopAndOverlays stopAndOverlays : _visibleStopsById.values()) {
StopBean stop = stopAndOverlays.getStop();
if (_stopIdsToAlwaysShow.contains(stop.getId()))
continue;
for (Overlay overlay : stopAndOverlays.getOverlays())
_manager.removeOverlay(overlay);
}
_visibleStopsById.keySet().retainAll(_stopIdsToAlwaysShow);
}
}
private CoordinateBounds computeBounds() {
LatLngBounds r = _map.getBounds();
LatLng ne = r.getNorthEast();
LatLng sw = r.getSouthWest();
double latMin = sw.getLatitude();
double lonMin = sw.getLongitude();
double latMax = ne.getLatitude();
double lonMax = ne.getLongitude();
return new CoordinateBounds(latMin, lonMin, latMax, lonMax);
}
private MarkerClickHandler getClickHandlerForStop(final StopBean stop) {
MarkerClickHandler clickHandler = new MarkerClickHandler() {
public void onClick(MarkerClickEvent event) {
ensureHandlers().fireEvent(new StopClickedEvent(stop));
}
};
return clickHandler;
}
private Marker getStopMarker(final StopBean stop, final LatLng p, ESize size) {
MarkerOptions opts = MarkerOptions.newInstance();
boolean isSelected = false;
Icon icon = StopIconFactory.getStopIcon(stop, size, isSelected);
opts.setIcon(icon);
return new Marker(p, opts);
}
private void addOverlayAtZoom(Overlay overlay, int from, int to) {
_manager.addOverlay(overlay, from, to);
}
private void addPlaceToMap(final Place place,
final PlaceClickHandler clickHandler) {
LatLng point = place.getLocation();
Marker marker = new Marker(point);
if (clickHandler != null) {
marker.addMarkerClickHandler(new MarkerClickHandler() {
@Override
public void onClick(MarkerClickEvent event) {
clickHandler.onPlaceClicked(place);
}
});
}
_manager.addOverlay(marker);
_otherOverlays.add(marker);
}
/****
* Internal Classes
****/
private class MapMoveEndHandlerImpl implements MapMoveEndHandler {
@Override
public void onMoveEnd(MapMoveEndEvent event) {
refresh();
}
}
private class StopsHandler implements AsyncCallback<List<StopBean>> {
public void onSuccess(List<StopBean> stops) {
for (StopBean stop : stops) {
if (_visibleStopsById.containsKey(stop.getId()))
continue;
StopAndOverlays stopAndOverlays = new StopAndOverlays(stop);
_visibleStopsById.put(stop.getId(), stopAndOverlays);
LatLng p = LatLng.newInstance(stop.getLat(), stop.getLon());
MarkerClickHandler clickHandler = getClickHandlerForStop(stop);
// Show some close up stops by default
Marker largerMarker = getStopMarker(stop, p, ESize.LARGE);
largerMarker.addMarkerClickHandler(clickHandler);
addOverlayAtZoom(largerMarker, 17, 20);
stopAndOverlays.addOverlays(largerMarker);
// If we're not showing stops in our current view, it must mean we're in
// route view, so we show the stops more zoomed out
if (!_showStopsInCurrentView) {
Marker mediumMarker = getStopMarker(stop, p, ESize.MEDIUM);
mediumMarker.addMarkerClickHandler(clickHandler);
addOverlayAtZoom(mediumMarker, 16, 17);
Marker smallMarker = getStopMarker(stop, p, ESize.SMALL);
smallMarker.addMarkerClickHandler(clickHandler);
addOverlayAtZoom(smallMarker, 13, 16);
Marker tinyMarker = getStopMarker(stop, p, ESize.TINY);
tinyMarker.addMarkerClickHandler(clickHandler);
addOverlayAtZoom(tinyMarker, 9, 13);
stopAndOverlays.addOverlays(mediumMarker, smallMarker, tinyMarker);
}
if (_selectedStopIds.contains(stop.getId())) {
Marker big = StopIconFactory.getStopSelectionCircle(p, true);
Marker small = StopIconFactory.getStopSelectionCircle(p, false);
addOverlayAtZoom(small, 16, 17);
addOverlayAtZoom(big, 17, 20);
stopAndOverlays.addOverlays(big, small);
}
}
}
@Override
public void onFailure(Throwable arg0) {
arg0.printStackTrace();
}
}
private class StopAndOverlays {
private StopBean _stop;
private List<Overlay> _overlays = new ArrayList<Overlay>();
public StopAndOverlays(StopBean stop) {
_stop = stop;
}
public StopBean getStop() {
return _stop;
}
public List<Overlay> getOverlays() {
return _overlays;
}
public void addOverlays(Overlay... overlays) {
for (Overlay overlay : overlays)
_overlays.add(overlay);
}
}
}