encoded_polyline 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.markdown +33 -0
- data/Rakefile +8 -0
- data/encoded_polyline.gemspec +22 -0
- data/lib/encoded_polyline.rb +2 -0
- data/lib/encoded_polyline/core.rb +65 -0
- data/lib/encoded_polyline/version.rb +3 -0
- data/script/benchmark.rb +41 -0
- data/spec/encoded_polyline/encoded_polyline_spec.rb +51 -0
- data/spec/spec_helper.rb +1 -0
- metadata +105 -0
data/README.markdown
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# EncodedPolyline
|
2
|
+
|
3
|
+
EncodedPolyline implements [Google's algorithm](http://code.google.com/apis/maps/documentation/utilities/polylinealgorithm.html) for compressing arrays of geographic points. The compressed string can then be passed to the Google Maps API.
|
4
|
+
The Maps API limits URL requests to 2048 characters. This library does not enforce this limit on the encoding length.
|
5
|
+
|
6
|
+
## Examples
|
7
|
+
|
8
|
+
An [example map](http://maps.googleapis.com/maps/api/staticmap?size=400x400&sensor=false&path=weight:3|color:blue|enc:~fzz\]_uvsOdX`a@jR~Boe@_XfPZoe@N{N}p@jTqIxCpYvi@?yApz@{r@}HkQ|l@xi@pa@vB{IiHg`@ao@jWxA_Bi_@gN_S|\[zN}s@vy@mQ|c@bd@TyHti@x\]ca@vZ~Di}@mb@`\hPxe@hRky@~Rk`@nq@_p@t_@mUdw@oHaWc@dNus@kVz\[gu@oi@ne@}Yxg@mu@iCo}@mm@gQhMgWtc@}Kxv@x_@gSlWq|@vT{v@q{@_m@jL_Td\]
|
9
|
+
) from a 50-point random walk, encoded using the library.
|
10
|
+
|
11
|
+
Using the library:
|
12
|
+
|
13
|
+
EncodedPolyline.encode(-179.9832104) # "`~oia@"
|
14
|
+
EncodedPolyline.decode("`~oia@") # -179.98321
|
15
|
+
EncodedPolyline.encode_points([[37.782,-122.406]]) # "ohreFnlbjV"
|
16
|
+
EncodedPolyline.decode_points("ohreFnlbjV") # [[37.782,-122.406]]
|
17
|
+
|
18
|
+
## Performance
|
19
|
+
|
20
|
+
Some of the steps outlined in the algorithm spec are composed together, for speediness. Included is a benchmark that creates a 100-point random walk and encodes/decodes it 10,000 times (1 million points total).
|
21
|
+
|
22
|
+
Raw length(JSON): 3682 characters
|
23
|
+
Encoded length: 495 characters
|
24
|
+
Compression ratio: 0.134437805540467
|
25
|
+
|
26
|
+
user system total real
|
27
|
+
encoding 9.090000 0.000000 9.090000 ( 9.092519)
|
28
|
+
decoding 16.470000 0.010000 16.480000 ( 16.473547)
|
29
|
+
|
30
|
+
## Known Issues
|
31
|
+
|
32
|
+
Haven't tested with Ruby 1.9; probably doesn't work.
|
33
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
$:.push File.expand_path("../lib", __FILE__)
|
4
|
+
require 'encoded_polyline/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "encoded_polyline"
|
8
|
+
s.version = EncodedPolyline::VERSION
|
9
|
+
s.authors = ["Brandon Liu"]
|
10
|
+
s.email = "bdon@bdon.org"
|
11
|
+
s.date = "2011-09-02"
|
12
|
+
s.description = "Encode an array of coordinates for the Google Static Maps API."
|
13
|
+
s.summary = "Encode an array of coordinates"
|
14
|
+
s.homepage = "http://github.com/bdon/encoded_polyline"
|
15
|
+
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- spec/*`.split("\n")
|
19
|
+
|
20
|
+
s.add_development_dependency("rake")
|
21
|
+
s.add_development_dependency("rspec", "~> 1.3.1")
|
22
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class EncodedPolyline
|
2
|
+
def self.encode(number)
|
3
|
+
number = (number * 1e5).round
|
4
|
+
n = number << 1
|
5
|
+
n = ~n if number < 0
|
6
|
+
str = ""
|
7
|
+
while n >= 0x20 do
|
8
|
+
str << ((0x20 | (n & 0x1f)) + 63).chr
|
9
|
+
n = n >> 5
|
10
|
+
end
|
11
|
+
return str << (n + 63).chr
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.decode_arr(arr)
|
15
|
+
num = 0
|
16
|
+
arr[0..-2].each_with_index do |elem, i|
|
17
|
+
num = (num | (((elem[0] - 63) ^ 0x20) << (i * 5)))
|
18
|
+
end
|
19
|
+
num = (num | ((arr.last[0] - 63) << ((arr.size-1) * 5)))
|
20
|
+
num = ~num if ((num & 0x1) == 1)
|
21
|
+
num = num >> 1
|
22
|
+
num = num.to_f / 1e5
|
23
|
+
return num
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.decode(str)
|
27
|
+
self.decode_arr(str.split(//))
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.encode_points(coordinates)
|
31
|
+
output = ""
|
32
|
+
output << encode(coordinates.first[0])
|
33
|
+
output << encode(coordinates.first[1])
|
34
|
+
|
35
|
+
(1..coordinates.size-1).each do |i|
|
36
|
+
previous = coordinates[i-1]
|
37
|
+
current = coordinates[i]
|
38
|
+
output << encode(current[0] - previous[0])
|
39
|
+
output << encode(current[1] - previous[1])
|
40
|
+
end
|
41
|
+
return output
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.decode_points(str)
|
45
|
+
decoded_points = []
|
46
|
+
point_chars = []
|
47
|
+
|
48
|
+
str.each_char do |char|
|
49
|
+
point_chars << char
|
50
|
+
if ((char[0] - 63) & 0x20).zero?
|
51
|
+
decoded_points << decode_arr(point_chars)
|
52
|
+
point_chars = []
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
coordinates = [[decoded_points.shift, decoded_points.shift]]
|
57
|
+
while decoded_points.any?
|
58
|
+
coordinates << [
|
59
|
+
coordinates.last[0] + decoded_points.shift,
|
60
|
+
coordinates.last[1] + decoded_points.shift
|
61
|
+
]
|
62
|
+
end
|
63
|
+
return coordinates
|
64
|
+
end
|
65
|
+
end
|
data/script/benchmark.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
require "lib/encoded_polyline/core"
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
def rsign
|
6
|
+
(rand(2) == 1) ? -1 : 1
|
7
|
+
end
|
8
|
+
|
9
|
+
random_walk = [[rand * 180 * rsign, rand * 180 * rsign]]
|
10
|
+
99.times do
|
11
|
+
lat = random_walk.last[0] + rand * 0.01 * rsign
|
12
|
+
long =random_walk.last[1] + rand * 0.01 * rsign
|
13
|
+
random_walk << [lat, long]
|
14
|
+
end
|
15
|
+
|
16
|
+
random_walk_text = EncodedPolyline.encode_points(random_walk)
|
17
|
+
|
18
|
+
points_length = random_walk.inspect.gsub(' ','').to_s.length
|
19
|
+
compressed_length = random_walk_text.length
|
20
|
+
|
21
|
+
puts "Raw length(JSON):\t#{points_length} characters"
|
22
|
+
puts "Encoded length:\t\t#{compressed_length} characters"
|
23
|
+
|
24
|
+
puts "Compression ratio:\t#{compressed_length.to_f / points_length}"
|
25
|
+
|
26
|
+
puts "Static map:\t\thttp://maps.googleapis.com/maps/api/staticmap?size=400x400&sensor=false&path=weight:3%7Ccolor:blue%7Cenc:#{random_walk_text}"
|
27
|
+
puts "\n"
|
28
|
+
|
29
|
+
Benchmark.bm(20) do |x|
|
30
|
+
x.report("encoding") do
|
31
|
+
10000.times do |i|
|
32
|
+
EncodedPolyline.encode_points(random_walk)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
x.report("decoding") do
|
37
|
+
10000.times do |i|
|
38
|
+
EncodedPolyline.decode_points(random_walk_text)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EncodedPolyline, ".encode" do
|
4
|
+
it "encodes a single point" do
|
5
|
+
EncodedPolyline.encode(-179.9832104).should == "`~oia@"
|
6
|
+
end
|
7
|
+
|
8
|
+
it "encodes a very slightly negative point" do
|
9
|
+
# Due to precision errors, verify that a negative value that rounds to 0 works.
|
10
|
+
EncodedPolyline.encode(-3e-6).should == "?"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe EncodedPolyline, ".decode" do
|
15
|
+
it "decodes a single point" do
|
16
|
+
EncodedPolyline.decode('`~oia@').should be_close(-179.9832104, 0.0001)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def should_be_similar_points(arr1, arr2)
|
21
|
+
arr1.each_with_index do |(x,y),i|
|
22
|
+
x.should be_close(arr2[i][0], 0.0001)
|
23
|
+
y.should be_close(arr2[i][1], 0.0001)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe EncodedPolyline, ".decode_points" do
|
28
|
+
let(:ring_points) { [[37.79297, -122.39676], [37.78987, -122.40075], [37.7894, -122.401], [37.78573, -122.40512], [37.78553, -122.40598], [37.78417, -122.40744], [37.78153, -122.41122], [37.77895, -122.41474], [37.77501, -122.41894], [37.77447, -122.41851], [37.77332, -122.41688], [37.77081, -122.41345], [37.77013, -122.40959], [37.77291, -122.40332], [37.77379, -122.40186], [37.77515, -122.40075], [37.77691, -122.39766], [37.77997, -122.39525], [37.78417, -122.39242], [37.78729, -122.38959], [37.78906, -122.38959], [37.79068, -122.39131], [37.79258, -122.39431], [37.79258, -122.39431], [37.79297, -122.39676], [37.78987, -122.40075], [37.7894, -122.401], [37.78573, -122.40512], [37.78553, -122.40598], [37.78417, -122.40744], [37.78153, -122.41122], [37.77895, -122.41474], [37.77501, -122.41894], [37.77447, -122.41851], [37.77332, -122.41688], [37.77081, -122.41345], [37.77013, -122.40959], [37.77291, -122.40332], [37.77379, -122.40186], [37.77515, -122.40075], [37.77691, -122.39766], [37.77997, -122.39525], [37.78417, -122.39242], [37.78729, -122.38959], [37.78906, -122.38959], [37.79068, -122.39131], [37.79258, -122.39431], [37.79258, -122.39431]] }
|
29
|
+
|
30
|
+
let(:ring_text) { "amteFvr`jVjR|W|Ap@|UvXf@jDnGbHnOrVbO~TrWfYjBuAdFeItNmTfCcWkPef@oDcHoG}E_JiRcRaNgYuPoRuPaJ?cIvI{JvQ??mAhNjR|W|Ap@|UvXf@jDnGbHnOrVbO~TrWfYjBuAdFeItNmTfCcWkPef@oDcHoG}E_JiRcRaNgYuPoRuPaJ?cIvI{JvQ??" }
|
31
|
+
|
32
|
+
it "encodes a pair of points" do
|
33
|
+
EncodedPolyline.encode_points([[37.782,-122.406]]).should == "ohreFnlbjV"
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should decode" do
|
37
|
+
should_be_similar_points(EncodedPolyline.decode_points(ring_text), ring_points)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "encodes points with little variation" do
|
41
|
+
points = [[37.79297, -122.39676],[37.79297, -122.39676],[37.79297, -122.39676]]
|
42
|
+
should_be_similar_points(EncodedPolyline.decode_points(EncodedPolyline.encode_points(points)), points)
|
43
|
+
end
|
44
|
+
|
45
|
+
it "truncates to 5 digits of accuracy" do
|
46
|
+
points = [[37.79297111, -122.39676222],[37.79297333, -122.39676444],[37.79297555, -122.39676666]]
|
47
|
+
points_2 = [[37.79297, -122.39676],[37.79297, -122.39676],[37.79297, -122.39676]]
|
48
|
+
should_be_similar_points(EncodedPolyline.decode_points(EncodedPolyline.encode_points(points)), points_2)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'encoded_polyline'
|
metadata
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: encoded_polyline
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Brandon Liu
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-09-02 00:00:00 -07:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rake
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :development
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: rspec
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 25
|
44
|
+
segments:
|
45
|
+
- 1
|
46
|
+
- 3
|
47
|
+
- 1
|
48
|
+
version: 1.3.1
|
49
|
+
type: :development
|
50
|
+
version_requirements: *id002
|
51
|
+
description: Encode an array of coordinates for the Google Static Maps API.
|
52
|
+
email: bdon@bdon.org
|
53
|
+
executables: []
|
54
|
+
|
55
|
+
extensions: []
|
56
|
+
|
57
|
+
extra_rdoc_files: []
|
58
|
+
|
59
|
+
files:
|
60
|
+
- README.markdown
|
61
|
+
- Rakefile
|
62
|
+
- encoded_polyline.gemspec
|
63
|
+
- lib/encoded_polyline.rb
|
64
|
+
- lib/encoded_polyline/core.rb
|
65
|
+
- lib/encoded_polyline/version.rb
|
66
|
+
- script/benchmark.rb
|
67
|
+
- spec/encoded_polyline/encoded_polyline_spec.rb
|
68
|
+
- spec/spec_helper.rb
|
69
|
+
has_rdoc: true
|
70
|
+
homepage: http://github.com/bdon/encoded_polyline
|
71
|
+
licenses: []
|
72
|
+
|
73
|
+
post_install_message:
|
74
|
+
rdoc_options: []
|
75
|
+
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
hash: 3
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 3
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
version: "0"
|
96
|
+
requirements: []
|
97
|
+
|
98
|
+
rubyforge_project:
|
99
|
+
rubygems_version: 1.6.2
|
100
|
+
signing_key:
|
101
|
+
specification_version: 3
|
102
|
+
summary: Encode an array of coordinates
|
103
|
+
test_files:
|
104
|
+
- spec/encoded_polyline/encoded_polyline_spec.rb
|
105
|
+
- spec/spec_helper.rb
|