import freemarker.template.utility.StringUtil;
import lambo.gis.Point; //this point class is very basic, just a lat/long deciaml values.
/**
*
* @author Scott Lambeth
*
* Creating an encoder for the google maps GPolyLine class, from their API here
* is the algorithm for performing the encoding:
*
* The steps for encoding such a signed value are specified below.
*
* 1. Take the initial signed value:
* -179.9832104
* 2. Take the decimal value and multiply it by 1e5, rounding the result:
* -17998321
* 3. Convert the decimal value to binary. Note that a negative value must be calculated using its two's complement by inverting the binary value and adding one to the result:
* 00000001 00010010 10100001 11110001
* 11111110 11101101 01011110 00001110
* 11111110 11101101 01011110 00001111
* 4. Left-shift the binary value one bit:
* 11111101 11011010 10111100 00011110
* 5. If the original decimal value is negative, invert this encoding:
* 00000010 00100101 01000011 11100001
* 6. Break the binary value out into 5-bit chunks (starting from the right hand side):
* 00001 00010 01010 10000 11111 00001
* 7. Place the 5-bit chunks into reverse order:
* 00001 11111 10000 01010 00010 00001
* 8. OR each value with 0x20 if another bit chunk follows:
* 100001 111111 110000 101010 100010 000001
* 9. Convert each value to decimal:
* 33 63 48 42 34 1
* 10. Add 63 to each value:
* 96 126 111 105 97 64
* 11. Convert each value to its ASCII equivalent:
* `~oia@
*/
public class LineEncoder {
public static String encodeTrack(Point[] points) {
String sEncVal = "";
long lLastLat = 0;
long lLastLng = 0;
for(Point point : points) {
//the encoded line expects to only represent change from point to point
long lLat = Math.round(point.getLat() * 1e5);
long lLng = Math.round(point.getLng() * 1e5);
long lLatChange = lLat - lLastLat;
long lLngChange = lLng - lLastLng;
//System.out.println(lLastLat + ", " + lLastLng);
//only calculate change
sEncVal += (encodeLong(lLatChange) + encodeLong(lLngChange));
/*
String sTest = (PolylineEncoder.encodeNumber((int)lLatChange) + PolylineEncoder.encodeNumber((int)lLngChange));
if(sTest.equals(sEncVal)) {
System.err.printf("%s does not equal %s", sEncVal, sTest);
}
*/
lLastLat = lLat;
lLastLng = lLng;
}
return sEncVal;
}
public static String encodeLong(long Value) {
//Steps 2-4 of the encoding algorithm can be accomplished with this:
Value = Value << 1;
//Step 5 can be accomplished with the XOR operator
if(Value < 0) {
Value = ~Value;
}
//Convert the shifted integer to its binary representation
String binary = Long.toBinaryString(Value);
//6. Break the binary value out into 5-bit chunks (starting from the right hand side):
//We'll accmoplish this by simply padding the start of the string with leading 0s to get an even multiplier of 5
int iRemainder = binary.length() % 5;
if(iRemainder > 0) {
int iMinLen = binary.length() + (5 - (binary.length() % 5));
binary = StringUtil.leftPad(binary, iMinLen, '0');
}
//So, we've evenly padded it to multiples of 5, lets clear any leading 0 bytes that are not needed.
int iFirst1 = binary.indexOf("1");
//step back from the first 0 bit to a position which will get even 5 bit bytes to the end of the string
int iStartNDX = iFirst1 - (iFirst1 % 5);
binary = binary.substring(iStartNDX, binary.length());
//System.out.println(binary);
//Steps 7, 8, and 9, reverse the 5 bit chunks, convert to number, add 63, then convert to char.
char[] cEncodedChars = new char[(int)binary.length()/5];
boolean b5BitByteRem = false;
int iCharNDX=0;
for(int ndx = binary.length() - 5; ndx >=0; ndx -= 5) {
String s5BitByte = binary.substring(ndx, ndx + 5);
long lVal = Long.parseLong(s5BitByte, 2);
//8. OR each value with 0x20 if another bit chunk follows (with we don't OR the last one)
if(iCharNDX < cEncodedChars.length-1) {
lVal = (lVal|0x20);
}
cEncodedChars[iCharNDX] = (char)(lVal+63);
//System.out.println(s5BitByte + " = " + Long.toBinaryString(lVal) + " = " + lVal + " + 63 = " + (lVal+63) + " = " + cEncodedChars[iCharNDX]);
iCharNDX++;
}
String sEncodedStr = String.valueOf(cEncodedChars);
return sEncodedStr;
}
public static void main(String[] args) {
LineEncoder encoder = new LineEncoder();
Point[] points = new Point[3];
points[0] = new Point(38.5, -120.2);
points[1] = new Point(40.7, -120.95);
points[2] = new Point(43.252, -126.453);
System.out.println(encoder.encodeTrack(points));
//System.out.println(encoder.encodeLong(-12020000));
}
}
Thursday, February 6, 2014
Google Maps Line Encoder
So, I made a few cheesy posts to this cheesy blog when I started it, and then haven't done anything since. I figured I'd post something useful and see what happens. The below is a class I created some time ago in java for encoding google line objects for the google maps api. It compresses them to a more efficient version that can be directly added to a google map using the JavaScript API. The algorithm is specific to google, and I believe it was published somewhere by google and is pasted in the code comments below. Originally I was pulling the line objects as Lat/Long arrays from a GPS unit, and I would then convert them the the encoded size to save on bandwidth when pulling them to the page to display on the map. I haven't messed with this for a couple years, but maybe it will be of value to someone.
Subscribe to:
Comments (Atom)