/**
* 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.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.onebusaway.geospatial.model.CoordinateBounds;
import org.onebusaway.geospatial.services.SphericalGeometryLibrary;
import org.onebusaway.transit_data.model.SearchQueryBean;
import org.onebusaway.transit_data.model.StopBean;
import org.onebusaway.transit_data.model.StopsBean;
import org.onebusaway.transit_data.model.SearchQueryBean.EQueryType;
import org.onebusaway.webapp.gwt.where_library.rpc.WebappServiceAsync;
import org.onebusaway.webapp.gwt.where_library.services.StopsForRegionService;
import com.google.gwt.user.client.rpc.AsyncCallback;
public class StopsForRegionServiceImpl implements StopsForRegionService {
private static final int MAX_STOP_COUNT_PER_REGION = 200;
private LinkedList<RegionOp> _pendingOps = new LinkedList<RegionOp>();
private AsyncCallback<List<StopBean>> _callback;
private Map<CoordinateBounds, RegionCache> _cache = new HashMap<CoordinateBounds, RegionCache>();
private Set<CoordinateBounds> _sentToClient = new HashSet<CoordinateBounds>();
private double _latStep = Double.NaN;
private double _lonStep = Double.NaN;
private WebappServiceAsync _webappService = WebappServiceAsync.SERVICE;
public void setLatStep(double latStep) {
_latStep = latStep;
}
public void setLonStep(double lonStep) {
_lonStep = lonStep;
}
public void setWebappService(WebappServiceAsync webappService) {
_webappService = webappService;
}
public WebappServiceAsync getWebappService() {
return _webappService;
}
public void getStopsForRegion(final CoordinateBounds bounds,
final AsyncCallback<List<StopBean>> callback) {
_pendingOps.clear();
_sentToClient.clear();
_callback = callback;
checkSteps(bounds);
/**
* We attempt to load the visible region directly at first
*/
exploreVisibleRegion(bounds);
/**
* We follow up by pre-fetching the buffered region
*/
exploreBufferedRegion(bounds);
/**
* Events queued up, we fire off any pending events
*/
checkPending();
}
/*****
* Private Methods
****/
private void checkSteps(CoordinateBounds bounds) {
if (!(Double.isNaN(_latStep) || Double.isNaN(_lonStep)))
return;
CoordinateBounds steps = SphericalGeometryLibrary.bounds(
bounds.getMinLat(), bounds.getMinLon(), 200);
_latStep = snap(steps.getMaxLat() - steps.getMinLat(), 1e3);
_lonStep = snap(steps.getMaxLon() - steps.getMinLon(), 1e3);
}
private void exploreVisibleRegion(CoordinateBounds bounds) {
double minLat = floor(bounds.getMinLat(), _latStep);
double maxLat = floor(bounds.getMaxLat(), _latStep);
double minLon = floor(bounds.getMinLon(), _lonStep);
double maxLon = floor(bounds.getMaxLon(), _lonStep);
CoordinateBounds dirtyRegion = new CoordinateBounds();
List<CoordinateBounds> dirtyRegions = new ArrayList<CoordinateBounds>();
for (double lat = minLat; lat <= maxLat; lat += _latStep) {
for (double lon = minLon; lon <= maxLon; lon += _lonStep) {
CoordinateBounds region = snapBounds(lat, lon, lat + _latStep, lon
+ _lonStep);
RegionCache cache = _cache.get(region);
if (cache == null) {
dirtyRegion.addBounds(region);
dirtyRegions.add(region);
} else {
handleRegion(region, cache);
}
}
}
if (!dirtyRegions.isEmpty()) {
MultiRegionOp op = new MultiRegionOp(dirtyRegion, dirtyRegions);
addOp(op);
}
}
private void exploreBufferedRegion(CoordinateBounds bounds) {
double minLat = floor(bounds.getMinLat() - _latStep, _latStep);
double maxLat = floor(bounds.getMaxLat() + _latStep, _latStep);
double minLon = floor(bounds.getMinLon() - _lonStep, _lonStep);
double maxLon = floor(bounds.getMaxLon() + _lonStep, _lonStep);
for (double lat = minLat; lat <= maxLat; lat += _latStep) {
for (double lon = minLon; lon <= maxLon; lon += _lonStep) {
CoordinateBounds region = snapBounds(lat, lon, lat + _latStep, lon
+ _lonStep);
checkRegion(region);
}
}
}
private void checkRegion(CoordinateBounds region) {
RegionCache cache = _cache.get(region);
if (cache == null) {
addOp(new RegionOp(region));
} else {
handleRegion(region, cache);
}
}
private void addOp(RegionOp op) {
_pendingOps.addLast(op);
}
private void handleRegion(CoordinateBounds region, RegionCache cache) {
// Only send stops to client if we haven't already
if (_sentToClient.contains(region))
return;
// There were too many stops in the region, so jump to sub-regions
if (cache.hasOverflow()) {
for (CoordinateBounds subRegion : splitRegion(region))
checkRegion(subRegion);
return;
}
List<StopBean> stopsInView = new ArrayList<StopBean>();
for (StopBean stop : cache.getStops()) {
// if (_bounds.contains(stop.getLat(), stop.getLon()))
stopsInView.add(stop);
}
_sentToClient.add(region);
// And only if we have stops to show
if (!stopsInView.isEmpty())
_callback.onSuccess(stopsInView);
}
private void checkPending() {
if (_pendingOps.isEmpty())
return;
RegionOp regionOp = _pendingOps.removeFirst();
CoordinateBounds region = regionOp.getRequestRegion();
RegionCache cache = _cache.get(region);
if (cache != null) {
handleRegion(region, cache);
return;
}
SearchQueryBean query = new SearchQueryBean();
query.setBounds(region);
query.setMaxCount(MAX_STOP_COUNT_PER_REGION);
query.setType(EQueryType.BOUNDS);
_webappService.getStops(query, new StopHandler(regionOp));
}
/****
* Private Static Methods
****/
private static List<CoordinateBounds> splitRegion(CoordinateBounds region) {
List<CoordinateBounds> splits = new ArrayList<CoordinateBounds>();
double minLat = region.getMinLat();
double maxLat = region.getMaxLat();
double minLon = region.getMinLon();
double maxLon = region.getMaxLon();
double centerLat = (minLat + maxLat) / 2;
double centerLon = (minLon + maxLon) / 2;
splits.add(snapBounds(minLat, minLon, centerLat, centerLon));
splits.add(snapBounds(minLat, centerLon, centerLat, maxLon));
splits.add(snapBounds(centerLat, minLon, maxLat, centerLon));
splits.add(snapBounds(centerLat, centerLon, maxLat, maxLon));
return splits;
}
private static double floor(double value, double step) {
return Math.floor(value / step) * step;
}
private static CoordinateBounds snapBounds(double latMin, double lonMin,
double latMax, double lonMax) {
return new CoordinateBounds(snap(latMin), snap(lonMin), snap(latMax),
snap(lonMax));
}
private static double snap(double latOrLon) {
return snap(latOrLon, 1e5);
}
private static double snap(double latOrLon, double factor) {
return Math.round(latOrLon * factor) / factor;
}
private class StopHandler implements AsyncCallback<StopsBean> {
private RegionOp _regionOp;
public StopHandler(RegionOp regionOp) {
_regionOp = regionOp;
}
public void onSuccess(StopsBean result) {
if (result.isLimitExceeded()) {
_cache.put(_regionOp.getRequestRegion(), new RegionCache(true));
for (CoordinateBounds region : _regionOp.getSplitRegions())
checkRegion(region);
} else {
Map<CoordinateBounds, List<StopBean>> stopsByRegion = getStopsByActualRegion(result);
for (Map.Entry<CoordinateBounds, List<StopBean>> entry : stopsByRegion.entrySet()) {
CoordinateBounds bounds = entry.getKey();
List<StopBean> stops = entry.getValue();
RegionCache cache = new RegionCache(stops, false);
_cache.put(bounds, cache);
handleRegion(bounds, cache);
}
}
checkPending();
}
public void onFailure(Throwable caught) {
checkPending();
}
private Map<CoordinateBounds, List<StopBean>> getStopsByActualRegion(
StopsBean result) {
Map<CoordinateBounds, List<StopBean>> stopsByBounds = new HashMap<CoordinateBounds, List<StopBean>>();
List<CoordinateBounds> actualRegions = _regionOp.getActualRegions();
// Make sure each region has a stop list, even if it ultimately has no
// stops (want to cache that fact too)
for (CoordinateBounds region : actualRegions)
stopsByBounds.put(region, new ArrayList<StopBean>());
for (StopBean stop : result.getStops()) {
for (CoordinateBounds bounds : actualRegions) {
if (bounds.contains(stop.getLat(), stop.getLon())) {
List<StopBean> stops = stopsByBounds.get(bounds);
stops.add(stop);
continue;
}
}
}
return stopsByBounds;
}
}
private static class RegionOp {
protected final CoordinateBounds _region;
public RegionOp(CoordinateBounds region) {
_region = region;
}
public CoordinateBounds getRequestRegion() {
return _region;
}
public List<CoordinateBounds> getActualRegions() {
return Arrays.asList(_region);
}
public List<CoordinateBounds> getSplitRegions() {
return splitRegion(_region);
}
@Override
public String toString() {
return "RegionOp(region=" + _region + ")";
}
}
private static class MultiRegionOp extends RegionOp {
private List<CoordinateBounds> _actualRegions;
public MultiRegionOp(CoordinateBounds region,
List<CoordinateBounds> actualRegions) {
super(region);
_actualRegions = actualRegions;
}
@Override
public List<CoordinateBounds> getActualRegions() {
return _actualRegions;
}
@Override
public List<CoordinateBounds> getSplitRegions() {
return _actualRegions;
}
@Override
public String toString() {
return "MultiRegionOp(region=" + _region + " + actualRegions="
+ _actualRegions + ")";
}
}
private static class RegionCache {
private List<StopBean> _stops;
private boolean _overflow = false;
public RegionCache(List<StopBean> stops, boolean limitExceeded) {
_stops = stops;
_overflow = limitExceeded;
}
@SuppressWarnings("unchecked")
public RegionCache(boolean limitExceeded) {
this(Collections.EMPTY_LIST, limitExceeded);
}
public List<StopBean> getStops() {
return _stops;
}
public boolean hasOverflow() {
return _overflow;
}
}
}