File Coverage

blib/lib/Algorithm/GooglePolylineEncoding.pm
Criterion Covered Total %
statement 76 79 96.2
branch 11 12 91.6
condition n/a
subroutine 8 8 100.0
pod 4 4 100.0
total 99 103 96.1


line stmt bran cond sub pod time code
1             # -*- perl -*-
2              
3             #
4             # Author: Slaven Rezic
5             #
6             # Copyright (C) 2009,2010,2012,2017,2018 Slaven Rezic. All rights reserved.
7             # This package is free software; you can redistribute it and/or
8             # modify it under the same terms as Perl itself.
9             #
10             # Mail: slaven@rezic.de
11             # WWW: http://www.rezic.de/eserte/
12             #
13              
14             package Algorithm::GooglePolylineEncoding;
15              
16 1     1   1423 use 5.006; # sprintf("%b")
  1         3  
17              
18 1     1   5 use strict;
  1         1  
  1         20  
19 1     1   4 use vars qw($VERSION);
  1         1  
  1         661  
20             $VERSION = '0.06';
21              
22             sub encode_number {
23             # 1. Take the initial signed value:
24             # -179.9832104
25 48     48 1 59 my $number = shift;
26             # 2. Take the decimal value and multiply it by 1e5, rounding the result:
27             # -17998321
28 48 100       106 $number = int($number * 1e5 + ($number < 0 ? -0.5 : 0.5));
29             # Don't do this before rounding. Negativeness may change if for example
30             # using very small negative numbers.
31 48         59 my $is_negative = $number < 0;
32             # 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:
33             # 00000001 00010010 10100001 11110001
34             # 11111110 11101101 01011110 00001110
35             # 11111110 11101101 01011110 00001111
36             # nothing to do here, we don't calculate with binary strings...
37             # 4. Left-shift the binary value one bit:
38             # 11111101 11011010 10111100 00011110
39 48         57 $number <<= 1;
40 48         57 $number &= 0xffffffff; # to assure 32 bit
41             # 5. If the original decimal value is negative, invert this encoding:
42             # 00000010 00100101 01000011 11100001
43 48 100       80 if ($is_negative) {
44 22         39 $number = (~$number);
45 22         27 $number &= 0xffffffff;
46             }
47             # 6. Break the binary value out into 5-bit chunks (starting from the right hand side):
48             # 00001 00010 01010 10000 11111 00001
49 48         92 my $bin = sprintf '%b', $number;
50 48 100       114 $bin = '0'x(5-length($bin)%5) . $bin if length($bin)%5 != 0; # pad
51 48         66 my @chunks;
52 48         81 my $revbin = reverse $bin;
53 48         376 push @chunks, scalar reverse($1) while $revbin =~ m{(.....)}g;
54             # 7. Place the 5-bit chunks into reverse order:
55             # 00001 11111 10000 01010 00010 00001
56             # It's already reversed
57             # 8. OR each value with 0x20 if another bit chunk follows:
58             # 100001 111111 110000 101010 100010 000001
59 48         104 @chunks = ((map { oct("0b$_") | 0x20 } @chunks[0 .. $#chunks-1]), oct("0b".$chunks[-1])); # and also decode to decimal on the fly
  179         306  
60             # 9. Convert each value to decimal:
61             # 33 63 48 42 34 1
62             # Done above
63             # 10. Add 63 to each value:
64             # 96 126 111 105 97 64
65 48         74 @chunks = map { $_+63 } @chunks;
  227         278  
66             # 11. Convert each value to its ASCII equivalent:
67             # `~oia@
68 48         62 @chunks = map { chr } @chunks;
  227         326  
69 48         135 join '', @chunks;
70             }
71              
72             sub encode_polyline {
73 2     2 1 359 my(@path) = @_;
74 2         4 my @res;
75 2         4 my($curr_lat_e5,$curr_lon_e5) = (0,0);
76 2         4 for my $lat_lon (@path) {
77 23         34 my($lat_e5,$lon_e5) = map { sprintf("%.0f", $_*1e5) } ($lat_lon->{lat}, $lat_lon->{lon});
  46         102  
78 23         46 my $deltay = ($lat_e5 - $curr_lat_e5) / 1e5;
79 23         31 my $deltax = ($lon_e5 - $curr_lon_e5) / 1e5;
80 23         37 push @res, encode_number($deltay), encode_number($deltax);
81 23         59 ($curr_lat_e5,$curr_lon_e5) = ($lat_e5,$lon_e5);
82             }
83 2         12 join '', @res;
84             }
85              
86             sub encode_level {
87             # 1. Take the initial unsigned value:
88             # 174
89 2     2 1 3 my $number = shift;
90             # 2. Convert the decimal value to a binary value:
91             # 10101110
92 2         3 my $bin;
93 2 50       5 if ($number > ~0) {
94             # sprintf '%b' works only for integers
95 0         0 require Math::BigInt;
96 0         0 $bin = Math::BigInt->new($number)->as_bin;
97 0         0 $bin =~ s{^0b}{};
98             } else {
99 2         7 $bin = sprintf '%b', $number;
100             }
101             # 3. Break the binary value out into 5-bit chunks (starting from the right hand side):
102             # 101 01110
103 2 100       8 $bin = '0'x(5-length($bin)%5) . $bin if length($bin)%5 != 0; # pad
104 2         4 my @chunks;
105 2         4 my $revbin = reverse $bin;
106 2         23 push @chunks, scalar reverse($1) while $revbin =~ m{(.....)}g;
107             # 4. Place the 5-bit chunks into reverse order:
108             # 01110 101
109             # It's already reversed
110             # 5. OR each value with 0x20 if another bit chunk follows:
111             # 101110 00101
112 2         5 @chunks = ((map { oct("0b$_") | 0x20 } @chunks[0 .. $#chunks-1]), oct("0b".$chunks[-1])); # and also decode to decimal on the fly
  8         17  
113             # 6. Convert each value to decimal:
114             # 46 5
115             # Done above
116             # 7. Add 63 to each value:
117             # 109 68
118 2         4 @chunks = map { $_+63 } @chunks;
  10         13  
119             # 8. Convert each value to its ASCII equivalent:
120             # mD
121 2         3 @chunks = map { chr } @chunks;
  10         16  
122 2         10 join '', @chunks;
123             }
124              
125             # Translated this php script
126             #
127             # to perl
128             sub decode_polyline {
129 2     2 1 10 my($encoded) = @_;
130              
131 2         4 my $length = length $encoded;
132 2         3 my $index = 0;
133 2         2 my @points;
134 2         4 my $lat = 0;
135 2         2 my $lng = 0;
136              
137 2         5 while ($index < $length) {
138             # The encoded polyline consists of a latitude value followed
139             # by a longitude value. They should always come in pairs. Read
140             # the latitude value first.
141 23         32 for my $val (\$lat, \$lng) {
142 46         53 my $shift = 0;
143 46         48 my $result = 0;
144             # Temporary variable to hold each ASCII byte.
145 46         51 my $b;
146 46         52 do {
147             # The `ord(substr($encoded, $index++))` statement returns
148             # the ASCII code for the character at $index. Subtract 63
149             # to get the original value. (63 was added to ensure
150             # proper ASCII characters are displayed in the encoded
151             # polyline string, which is `human` readable)
152 225         266 $b = ord(substr($encoded, $index++, 1)) - 63;
153              
154             # AND the bits of the byte with 0x1f to get the original
155             # 5-bit `chunk. Then left shift the bits by the required
156             # amount, which increases by 5 bits each time. OR the
157             # value into $results, which sums up the individual 5-bit
158             # chunks into the original value. Since the 5-bit chunks
159             # were reversed in order during encoding, reading them in
160             # this way ensures proper summation.
161 225         262 $result |= ($b & 0x1f) << $shift;
162 225         356 $shift += 5;
163             }
164             # Continue while the read byte is >= 0x20 since the last
165             # `chunk` was not OR'd with 0x20 during the conversion
166             # process. (Signals the end)
167             while ($b >= 0x20);
168              
169 1     1   411 use integer; # see last paragraph of "Integer Arithmetic" in perlop.pod
  1         12  
  1         4  
170              
171             # Check if negative, and convert. (All negative values have the last bit
172             # set)
173 46 100       62 my $dtmp = (($result & 1) ? ~($result >> 1) : ($result >> 1));
174              
175             # Compute actual latitude (resp. longitude) since value is
176             # offset from previous value.
177 46         66 $$val += $dtmp;
178             }
179              
180             # The actual latitude and longitude values were multiplied by
181             # 1e5 before encoding so that they could be converted to a 32-bit
182             # integer representation. (With a decimal accuracy of 5 places)
183             # Convert back to original values.
184 23         61 push @points, {lat => $lat * 1e-5, lon => $lng * 1e-5};
185             }
186              
187 2         9 @points;
188             }
189              
190             1;
191              
192             __END__