/**
* Reimplementation of Mark McClures Javascript PolylineEncoder
* All the mathematical logic is more or less copied by McClure
*
* @author Mark Rambow
* @e-mail markrambow[at]gmail[dot]com
* @version 0.1
*
*/
package com.ubike.processor;
import com.ubike.model.TrackPoint;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
/**
*
*/
public class PolylineEncoder {
private int numLevels = 18;
private int zoomFactor = 2;
private double verySmall = 0.00001;
private boolean forceEndpoints = true;
private double[] zoomLevelBreaks;
private HashMap<String, Double> bounds;
/**
*
* @param numLevels
* @param zoomFactor
* @param verySmall
* @param forceEndpoints
*/
public PolylineEncoder(int numLevels, int zoomFactor, double verySmall, boolean forceEndpoints) {
this.numLevels = numLevels;
this.zoomFactor = zoomFactor;
this.verySmall = verySmall;
this.forceEndpoints = forceEndpoints;
this.zoomLevelBreaks = new double[numLevels];
for (int i = 0; i < numLevels; i++) {
this.zoomLevelBreaks[i] = verySmall * Math.pow(this.zoomFactor, numLevels - i - 1);
}
}
/**
*
*/
public PolylineEncoder() {
this.zoomLevelBreaks = new double[numLevels];
for (int i = 0; i < numLevels; i++) {
this.zoomLevelBreaks[i] = verySmall * Math.pow(this.zoomFactor, numLevels - i - 1);
}
}
/**
* Douglas-Peucker algorithm, adapted for encoding
*
* @return HashMap [EncodedPoints;EncodedLevels]
*/
public HashMap<String, String> dpEncode(List<TrackPoint> trackPoints) {
int i, maxLoc = 0;
Stack<int[]> stack = new Stack<int[]>();
double[] dists = new double[trackPoints.size()];
double maxDist, absMaxDist = 0.0, temp = 0.0;
int[] current;
if (trackPoints.size() > 2) {
int[] stackVal = new int[]{0, (trackPoints.size() - 1)};
stack.push(stackVal);
while (stack.size() > 0) {
current = stack.pop();
maxDist = 0;
for (i = current[0] + 1; i < current[1]; i++) {
temp = this.distance(trackPoints.get(i), trackPoints.get(current[0]), trackPoints.get(current[1]));
if (temp > maxDist) {
maxDist = temp;
maxLoc = i;
if (maxDist > absMaxDist) {
absMaxDist = maxDist;
}
}
}
if (maxDist > this.verySmall) {
dists[maxLoc] = maxDist;
int[] stackValCurMax = {current[0], maxLoc};
stack.push(stackValCurMax);
int[] stackValMaxCur = {maxLoc, current[1]};
stack.push(stackValMaxCur);
}
}
}
HashMap<String, String> hm = createEncodings(trackPoints, dists);
String encodedPoints = hm.get("encodedPoints").replace("\\", "\\\\");
String encodedLevels = encodeLevels(trackPoints, dists, absMaxDist);
hm.put("encodedPoints", encodedPoints);
hm.put("encodedLevels", encodedLevels);
return hm;
}
/**
* distance(p0, p1, p2) computes the distance between the point p0 and the
* segment [p1,p2]. This could probably be replaced with something that is a
* bit more numerically stable.
*
* @param p0
* @param p1
* @param p2
* @return
*/
public double distance(TrackPoint p0, TrackPoint p1, TrackPoint p2) {
double u, out = 0.0;
if (p1.getLatitude() == p2.getLatitude() && p1.getLongitude() == p2.getLongitude()) {
out = Math.sqrt(Math.pow(p2.getLatitude() - p0.getLatitude(), 2) + Math.pow(p2.getLongitude() - p0.getLongitude(), 2));
} else {
u = ((p0.getLatitude() - p1.getLatitude()) * (p2.getLatitude() - p1.getLatitude()) + (p0.getLongitude() - p1.getLongitude()) * (p2.getLongitude() - p1.getLongitude())) / (Math.pow(p2.getLatitude() - p1.getLatitude(), 2) + Math.pow(p2.getLongitude() - p1.getLongitude(), 2));
if (u <= 0) {
out = Math.sqrt(Math.pow(p0.getLatitude() - p1.getLatitude(),
2) + Math.pow(p0.getLongitude() - p1.getLongitude(), 2));
}
if (u >= 1) {
out = Math.sqrt(Math.pow(p0.getLatitude() - p2.getLatitude(),
2) + Math.pow(p0.getLongitude() - p2.getLongitude(), 2));
}
if (0 < u && u < 1) {
out = Math.sqrt(Math.pow(p0.getLatitude() - p1.getLatitude() - u * (p2.getLatitude() - p1.getLatitude()), 2) + Math.pow(p0.getLongitude() - p1.getLongitude() - u * (p2.getLongitude() - p1.getLongitude()), 2));
}
}
return out;
}
/**
*
* @param coordinate
* @return
*/
private static int floor1e5(double coordinate) {
return (int) Math.floor(coordinate * 1e5);
}
/**
*
* @param num
* @return
*/
private static String encodeSignedNumber(int num) {
int sgn_num = num << 1;
if (num < 0) {
sgn_num = ~(sgn_num);
}
return (encodeNumber(sgn_num));
}
/**
*
* @param num
* @return
*/
private static String encodeNumber(int num) {
StringBuffer encodeString = new StringBuffer();
while (num >= 0x20) {
int nextValue = (0x20 | (num & 0x1f)) + 63;
encodeString.append((char) (nextValue));
num >>= 5;
}
num += 63;
encodeString.append((char) (num));
return encodeString.toString();
}
/**
* Now we can use the previous function to march down the list of points and
* encode the levels. Like createEncodings, we ignore points whose distance
* (in dists) is undefined.
*/
private String encodeLevels(List<TrackPoint> points, double[] dists, double absMaxDist) {
int i;
StringBuffer encoded_levels = new StringBuffer();
if (this.forceEndpoints) {
encoded_levels.append(encodeNumber(this.numLevels - 1));
} else {
encoded_levels.append(encodeNumber(this.numLevels - computeLevel(absMaxDist) - 1));
}
for (i = 1; i < points.size() - 1; i++) {
if (dists[i] != 0) {
encoded_levels.append(encodeNumber(this.numLevels - computeLevel(dists[i]) - 1));
}
}
if (this.forceEndpoints) {
encoded_levels.append(encodeNumber(this.numLevels - 1));
} else {
encoded_levels.append(encodeNumber(this.numLevels - computeLevel(absMaxDist) - 1));
}
return encoded_levels.toString();
}
/**
* This computes the appropriate zoom level of a point in terms of it's
* distance from the relevant segment in the DP algorithm. Could be done in
* terms of a logarithm, but this approach makes it a bit easier to ensure
* that the level is not too large.
*/
private int computeLevel(double absMaxDist) {
int lev = 0;
if (absMaxDist > this.verySmall) {
lev = 0;
while (absMaxDist < this.zoomLevelBreaks[lev]) {
lev++;
}
return lev;
}
return lev;
}
/**
*
* @param points
* @param dists
* @return
*/
private HashMap<String, String> createEncodings(List<TrackPoint> points, double[] dists) {
StringBuffer encodedPoints = new StringBuffer();
double maxlat = points.get(0).getLatitude();
double minlat = points.get(0).getLatitude();
double maxlon = points.get(0).getLongitude();
double minlon = points.get(0).getLongitude();
int plat = 0, plng = 0, count = 0;
double avgSpeed = 0.0, maxSpeed = 0.0;
for (int i = 0; i < points.size(); i++) {
maxlat = maxlat < points.get(i).getLatitude() ? points.get(i).getLatitude() : maxlat;
minlat = minlat > points.get(i).getLatitude() ? points.get(i).getLatitude() : minlat;
maxlon = maxlon < points.get(i).getLongitude() ? points.get(i).getLongitude() : maxlon;
minlon = minlon > points.get(i).getLongitude() ? points.get(i).getLongitude() : minlon;
if (points.get(i).getSpeedKnot() > 0) {
avgSpeed += points.get(i).getSpeedKnot();
count++;
}
maxSpeed = points.get(i).getSpeedKnot() > maxSpeed ? points.get(i).getSpeedKnot() : maxSpeed;
if (dists[i] != 0 || i == 0 || i == points.size() - 1) {
TrackPoint point = points.get(i);
int late5 = floor1e5(point.getLatitude());
int lnge5 = floor1e5(point.getLongitude());
int dlat = late5 - plat;
int dlng = lnge5 - plng;
plat = late5;
plng = lnge5;
encodedPoints.append(encodeSignedNumber(dlat));
encodedPoints.append(encodeSignedNumber(dlng));
}
}
avgSpeed = avgSpeed / (count == 0 ? 1 : count);
HashMap<String, String> my_hm = new HashMap<String, String>();
my_hm.put("encodedPoints", encodedPoints.toString());
my_hm.put("maxSpeed", "" + maxSpeed);
my_hm.put("avgSpeed", "" + avgSpeed);
my_hm.put("maxlat", "" + maxlat);
my_hm.put("minlat", "" + minlat);
my_hm.put("maxlon", "" + maxlon);
my_hm.put("minlon", "" + minlon);
my_hm.put("avgLat", "" + ((maxlat + minlat) / 2));
my_hm.put("avgLon", "" + ((maxlon + minlon) / 2));
return my_hm;
}
/**
*
* @param bounds
*/
public void setBounds(HashMap<String, Double> bounds) {
this.bounds = bounds;
}
/**
*
* @param tracks
* @param level
* @param step
* @return
*/
public static HashMap<String, String> createEncodings(List<TrackPoint> tracks, int level) {
StringBuffer encodedPoints = new StringBuffer();
StringBuffer encodedLevels = new StringBuffer();
double maxlat = tracks.get(0).getLatitude();
double minlat = tracks.get(0).getLatitude();
double maxlon = tracks.get(0).getLongitude();
double minlon = tracks.get(0).getLongitude();
double avgSpeed = 0.0, maxSpeed = 0.0;
int plat = 0, plng = 0, count = 0;
for (TrackPoint trackpoint : tracks) {
int late5 = floor1e5(trackpoint.getLatitude());
int lnge5 = floor1e5(trackpoint.getLongitude());
int dlat = late5 - plat;
int dlng = lnge5 - plng;
plat = late5;
plng = lnge5;
encodedPoints.append(encodeSignedNumber(dlat)).append(encodeSignedNumber(dlng));
encodedLevels.append(encodeNumber(level));
maxlat = maxlat < trackpoint.getLatitude() ? trackpoint.getLatitude() : maxlat;
minlat = minlat > trackpoint.getLatitude() ? trackpoint.getLatitude() : minlat;
maxlon = maxlon < trackpoint.getLongitude() ? trackpoint.getLongitude() : maxlon;
minlon = minlon > trackpoint.getLongitude() ? trackpoint.getLongitude() : minlon;
if (trackpoint.getSpeedKnot() > 0) {
avgSpeed += trackpoint.getSpeedKnot();
count++;
}
maxSpeed = trackpoint.getSpeedKnot() > maxSpeed ? trackpoint.getSpeedKnot() : maxSpeed;
}
avgSpeed = avgSpeed / (count == 0 ? 1 : count);
HashMap<String, String> my_hm = new HashMap<String, String>();
my_hm.put("encodedPoints", encodedPoints.toString());
my_hm.put("encodedLevels", encodedLevels.toString());
my_hm.put("maxSpeed", "" + maxSpeed);
my_hm.put("avgSpeed", "" + avgSpeed);
my_hm.put("maxlat", "" + maxlat);
my_hm.put("minlat", "" + minlat);
my_hm.put("maxlon", "" + maxlon);
my_hm.put("minlon", "" + minlon);
my_hm.put("avgLat", "" + ((maxlat + minlat) / 2));
my_hm.put("avgLon", "" + ((maxlon + minlon) / 2));
return my_hm;
}
/**
* @return the map bounds
*/
public HashMap<String, Double> getBounds() {
return bounds;
}
}