/*
This file is part of RouteConverter.
RouteConverter 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 2 of the License, or
(at your option) any later version.
RouteConverter 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 RouteConverter; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
Copyright (C) 2007 Christian Pesch. All Rights Reserved.
*/
package slash.navigation.mapview.browser;
import slash.navigation.base.RouteCharacteristics;
import slash.navigation.common.BoundingBox;
import slash.navigation.common.NavigationPosition;
import slash.navigation.mapview.MapView;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import static java.lang.Math.max;
import static java.lang.String.format;
import static java.lang.System.currentTimeMillis;
import static slash.navigation.base.RouteCalculations.getSignificantPositions;
import static slash.navigation.base.RouteCharacteristics.Route;
import static slash.navigation.base.RouteCharacteristics.Waypoints;
/**
* Helps to reduce the amount of positions for rending routes, tracks, waypoint lists.
*
* @author Christian Pesch
*/
class PositionReducer {
private static final Preferences preferences = Preferences.userNodeForPackage(PositionReducer.class);
private static final Logger log = Logger.getLogger(MapView.class.getName());
private static final double[] THRESHOLD_PER_ZOOM = {
120000,
70000,
40000,
20000,
10000, // level 4
2700,
2100,
1500,
800, // level 8
500,
225,
125,
80,
45,
20,
10,
4,
1 // level 17
};
private static final int MAXIMUM_ZOOM_FOR_SIGNIFICANCE_CALCULATION = THRESHOLD_PER_ZOOM.length;
private final Callback callback;
private final Map<Integer, List<NavigationPosition>> reducedPositions = new HashMap<>(THRESHOLD_PER_ZOOM.length);
private BoundingBox visible;
PositionReducer(Callback callback) {
this.callback = callback;
}
public List<NavigationPosition> reducePositions(List<NavigationPosition> positions, RouteCharacteristics characteristics, boolean showWaypointDescription) {
List<NavigationPosition> result = filterPositionsWithoutCoordinates(positions);
// if it's more than one segment, reduce the positions
if (result.size() > getMaximumSegmentLength(characteristics)) {
int zoom = callback.getZoom();
result = reducedPositions.get(zoom);
if (result == null) {
result = reducePositions(positions, zoom, characteristics, showWaypointDescription);
reducedPositions.put(zoom, result);
}
}
return result;
}
public List<NavigationPosition> reduceSelectedPositions(List<NavigationPosition> positions, int[] indices) {
List<NavigationPosition> result = filterPositionsWithoutCoordinates(positions);
// reduce selected result if they're not selected
result = filterSelectedPositions(result, indices);
// reduce the number of selected result by a visibility heuristic
int maximumSelectionCount = preferences.getInt("maximumSelectionCount", 5 * 10);
if (result.size() > maximumSelectionCount) {
double visibleSelectedPositionAreaFactor = preferences.getDouble("visibleSelectionAreaFactor", 1.25);
result = filterVisiblePositions(result, visibleSelectedPositionAreaFactor, true);
}
// reduce the number of visible result by a JS-stability heuristic
if (result.size() > maximumSelectionCount)
result = filterEveryNthPosition(result, maximumSelectionCount);
return result;
}
public boolean hasFilteredVisibleArea() {
return visible != null;
}
public boolean isWithinVisibleArea(BoundingBox boundingBox) {
return !hasFilteredVisibleArea() || visible.contains(boundingBox);
}
public void clear() {
reducedPositions.clear();
visible = null;
}
interface Callback {
int getZoom();
NavigationPosition getNorthEastBounds();
NavigationPosition getSouthWestBounds();
}
int getMaximumSegmentLength(RouteCharacteristics characteristics) {
switch (characteristics) {
case Route:
return preferences.getInt("maximumRouteSegmentLength", 8);
case Track:
return preferences.getInt("maximumTrackSegmentLength", 35);
case Waypoints:
return preferences.getInt("maximumWaypointSegmentLength", 10);
default:
throw new IllegalArgumentException("RouteCharacteristics " + characteristics + " is not supported");
}
}
private int getMaximumPositionCount(RouteCharacteristics characteristics, boolean showWaypointDescription) {
switch (characteristics) {
case Route:
return preferences.getInt("maximumRoutePositionCount", 30 * getMaximumSegmentLength(characteristics));
case Track:
return preferences.getInt("maximumTrackPositionCount", 50 * getMaximumSegmentLength(characteristics));
case Waypoints:
return preferences.getInt("maximumWaypointPositionCount", (showWaypointDescription ? 5 : 50) * getMaximumSegmentLength(characteristics));
default:
throw new IllegalArgumentException("RouteCharacteristics " + characteristics + " is not supported");
}
}
List<NavigationPosition> filterVisiblePositions(List<NavigationPosition> positions, int zoom) {
double visiblePositionAreaFactor = preferences.getDouble("visiblePositionAreaFactor", 3.0);
double factor = max(visiblePositionAreaFactor * (zoom - MAXIMUM_ZOOM_FOR_SIGNIFICANCE_CALCULATION), 1) * visiblePositionAreaFactor;
return filterVisiblePositions(positions, factor, false);
}
private List<NavigationPosition> reducePositions(List<NavigationPosition> positions, int zoom, RouteCharacteristics characteristics, boolean showWaypointDescription) {
int maximumPositionCount = getMaximumPositionCount(characteristics, showWaypointDescription);
// reduce the number of result to those that are visible for tracks and waypoint lists
if (positions.size() > maximumPositionCount && !characteristics.equals(Route)) {
positions = filterVisiblePositions(positions, zoom);
visible = new BoundingBox(positions);
} else {
visible = null;
}
// reduce the number of result by selecting every Nth to limit significance computation time
int maximumSignificantPositionCount = preferences.getInt("maximumSignificantPositionCount", 50000);
if (positions.size() > maximumSignificantPositionCount)
positions = filterEveryNthPosition(positions, maximumSignificantPositionCount);
// determine significant result for routes and tracks for this zoom level if there are too many positions
if (!characteristics.equals(Waypoints))
positions = filterSignificantPositions(positions, zoom);
// reduce the number of result to ensure browser stability
if (positions.size() > maximumPositionCount)
positions = filterEveryNthPosition(positions, maximumPositionCount);
return positions;
}
private List<NavigationPosition> filterPositionsWithoutCoordinates(List<NavigationPosition> positions) {
long start = currentTimeMillis();
List<NavigationPosition> result = new ArrayList<>();
for (NavigationPosition position : positions) {
if (position.hasCoordinates())
result.add(position);
}
long end = currentTimeMillis();
if (positions.size() != result.size())
log.info(format("Filtered positions without coordinates to reduce %d positions to %d in %d milliseconds",
positions.size(), result.size(), (end - start)));
return result;
}
private List<NavigationPosition> filterSignificantPositions(List<NavigationPosition> positions, int zoom) {
long start = currentTimeMillis();
List<NavigationPosition> result = new ArrayList<>();
if (zoom < MAXIMUM_ZOOM_FOR_SIGNIFICANCE_CALCULATION) {
double threshold = THRESHOLD_PER_ZOOM[zoom];
int[] significantPositions = getSignificantPositions(positions, threshold);
for (int significantPosition : significantPositions) {
result.add(positions.get(significantPosition));
}
log.info(format("Zoom %d smaller than %d: for threshold %f use %d significant positions",
zoom, MAXIMUM_ZOOM_FOR_SIGNIFICANCE_CALCULATION, threshold, significantPositions.length));
} else {
// on all zoom about MAXIMUM_ZOOM_FOR_SIGNIFICANCE_CALCULATION
// use all positions since the calculation is too expensive
result.addAll(positions);
log.info("Zoom " + zoom + " large: use all " + positions.size() + " positions");
}
long end = currentTimeMillis();
if (positions.size() != result.size())
log.info(format("Filtered significant positions to reduce %d positions to %d in %d milliseconds",
positions.size(), result.size(), (end - start)));
return result;
}
List<NavigationPosition> filterVisiblePositions(List<NavigationPosition> positions,
double threshold, boolean includeFirstAndLastPosition) {
long start = currentTimeMillis();
NavigationPosition northEast = callback.getNorthEastBounds();
NavigationPosition southWest = callback.getSouthWestBounds();
if (northEast == null || southWest == null)
return positions;
double width = Math.abs(northEast.getLongitude() - southWest.getLongitude()) * threshold;
double height = Math.abs(southWest.getLatitude() - northEast.getLatitude()) * threshold;
northEast.setLongitude(northEast.getLongitude() + width);
northEast.setLatitude(northEast.getLatitude() + height);
southWest.setLongitude(southWest.getLongitude() - width);
southWest.setLatitude(southWest.getLatitude() - height);
BoundingBox boundingBox = new BoundingBox(northEast, southWest);
List<NavigationPosition> result = new ArrayList<>();
if (includeFirstAndLastPosition)
result.add(positions.get(0));
int firstIndex = includeFirstAndLastPosition ? 1 : 0;
int lastIndex = includeFirstAndLastPosition ? positions.size() - 1 : positions.size();
NavigationPosition previousPosition = positions.get(firstIndex);
boolean previousPositionVisible = boundingBox.contains(previousPosition);
for (int i = firstIndex; i < lastIndex; i += 1) {
NavigationPosition position = positions.get(i);
if(!position.hasCoordinates())
continue;
boolean visible = boundingBox.contains(position);
if (visible) {
// if the previous position was not visible but the current position is visible:
// add the previous position to render transition from non-visible to visible area
if (!previousPositionVisible)
result.add(previousPosition);
result.add(position);
} else {
// if the previous position was visible but the current position is not visible:
// add the current position to render transition from visible to non-visible area
if (previousPositionVisible)
result.add(position);
}
previousPositionVisible = visible;
previousPosition = position;
}
if (includeFirstAndLastPosition)
result.add(positions.get(positions.size() - 1));
long end = currentTimeMillis();
if (positions.size() != result.size())
log.info(format("Filtered visible positions with a threshold of %f to reduce %d positions to %d in %d milliseconds",
threshold, positions.size(), result.size(), (end - start)));
return result;
}
List<NavigationPosition> filterEveryNthPosition(List<NavigationPosition> positions, int maximumPositionCount) {
long start = currentTimeMillis();
List<NavigationPosition> result = new ArrayList<>();
result.add(positions.get(0));
double increment = (positions.size() - 1) / (double) (maximumPositionCount - 1);
for (double i = (increment + 1.0); i < positions.size() - 1; i += increment)
result.add(positions.get((int) i));
result.add(positions.get(positions.size() - 1));
long end = currentTimeMillis();
if (positions.size() != result.size())
log.info(format("Filtered every %fth position to reduce %d positions to %d in %d milliseconds",
increment, positions.size(), result.size(), (end - start)));
return result;
}
private List<NavigationPosition> filterSelectedPositions(List<NavigationPosition> positions, int[] selectedIndices) {
long start = currentTimeMillis();
List<NavigationPosition> result = new ArrayList<>();
for (int selectedIndex : selectedIndices) {
if (selectedIndex >= positions.size())
continue;
result.add(positions.get(selectedIndex));
}
long end = currentTimeMillis();
if (positions.size() != result.size())
log.info(format("Filtered selected positions to reduce %d positions to %d in %d milliseconds",
selectedIndices.length, result.size(), (end - start)));
return result;
}
}