fast-polylines 1.0.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +87 -31
- data/ext/fast_polylines/.gitignore +3 -0
- data/ext/fast_polylines/extconf.rb +4 -0
- data/ext/fast_polylines/fast_polylines.c +151 -0
- data/lib/fast_polylines.rb +19 -0
- data/lib/fast_polylines/version.rb +5 -0
- data/spec/fast_polylines_spec.rb +94 -0
- data/spec/spec_helper.rb +1 -1
- metadata +18 -17
- data/lib/fast-polylines.rb +0 -5
- data/lib/fast-polylines/decoder.rb +0 -41
- data/lib/fast-polylines/encoder.rb +0 -47
- data/lib/fast-polylines/version.rb +0 -3
- data/spec/fast-polylines/decoder_spec.rb +0 -32
- data/spec/fast-polylines/encoder_spec.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a3a21f83abca7c1b9ef3bb824145ceea3189fbdb9d428a2faff1daa94e2a50fb
|
4
|
+
data.tar.gz: 0e6578cb705d0121daadb0a019bedd2d0d1b2589b2905134f933f4ae0859d3e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 16c293fc12361086fc39002b10fd81faaeb68632e4c6e670de1c2ec242b8277a7c453da2e5f1a9176104ccdafab99447807871f8afdb09188d97f07ed6a307d0
|
7
|
+
data.tar.gz: e48e91654bc2746c4c36d3d6ce744914941ec4966f1aa6b56bbe61cacdcce841dc936b997dd477794ad8a0546ebb7c9317afc007dc1b9f74dd630d9de5d2f417
|
data/README.md
CHANGED
@@ -1,83 +1,139 @@
|
|
1
1
|
# Fast Polylines
|
2
2
|
|
3
3
|
[![Gem Version](https://badge.fury.io/rb/fast-polylines.svg)](https://badge.fury.io/rb/fast-polylines)
|
4
|
-
[![
|
4
|
+
[![Build Status](https://travis-ci.org/klaxit/fast-polylines.svg?branch=master)](https://travis-ci.org/klaxit/fast-polylines)
|
5
5
|
|
6
|
-
Implementation of the Google polyline algorithm
|
7
|
-
http://code.google.com/apis/maps/documentation/utilities/polylinealgorithm.html
|
6
|
+
Implementation of the [Google polyline algorithm][algorithm].
|
8
7
|
|
9
|
-
|
8
|
+
**BREAKING CHANGES:** The version 2.0.0 of FastPolylines includes breaking changes, see [Migrate from 1.0.0](#migrate-from-1.0.0)
|
10
9
|
|
11
|
-
|
10
|
+
|
11
|
+
About **300x faster encoding and decoding** than [Joshua Clayton's gem][polylines].
|
12
|
+
|
13
|
+
`make benchmark` on a MacBook pro 13 - 2,3 GHz Intel Core i5:
|
12
14
|
|
13
15
|
```
|
14
|
-
|
16
|
+
——————————————————————————————— ENCODING ————————————————————————————————
|
15
17
|
|
16
18
|
Warming up --------------------------------------
|
17
|
-
|
18
|
-
|
19
|
+
Polylines 277.000 i/100ms
|
20
|
+
FastPolylinesV1 2.050k i/100ms
|
21
|
+
FastPolylinesV2 73.822k i/100ms
|
19
22
|
Calculating -------------------------------------
|
20
|
-
|
21
|
-
|
23
|
+
Polylines 3.254k (± 1.8%) i/s - 16.343k in 5.023767s
|
24
|
+
FastPolylinesV1 25.715k (± 3.7%) i/s - 129.150k in 5.029675s
|
25
|
+
FastPolylinesV2 933.751k (± 4.3%) i/s - 4.725M in 5.072446s
|
22
26
|
|
23
27
|
Comparison:
|
24
|
-
|
25
|
-
|
28
|
+
FastPolylinesV2: 933750.7 i/s
|
29
|
+
FastPolylinesV1: 25715.1 i/s - 36.31x slower
|
30
|
+
Polylines: 3254.3 i/s - 286.93x slower
|
26
31
|
|
27
32
|
|
28
|
-
|
33
|
+
——————————————————————————————— DECODING ————————————————————————————————
|
29
34
|
|
30
35
|
Warming up --------------------------------------
|
31
|
-
|
32
|
-
|
36
|
+
Polylines 140.000 i/100ms
|
37
|
+
FastPolylinesV1 1.602k i/100ms
|
38
|
+
FastPolylinesV2 36.432k i/100ms
|
33
39
|
Calculating -------------------------------------
|
34
|
-
|
35
|
-
|
40
|
+
Polylines 1.401k (± 2.2%) i/s - 7.000k in 5.000321s
|
41
|
+
FastPolylinesV1 16.465k (± 3.7%) i/s - 83.304k in 5.067786s
|
42
|
+
FastPolylinesV2 396.100k (± 5.2%) i/s - 2.004M in 5.074500s
|
36
43
|
|
37
44
|
Comparison:
|
38
|
-
|
39
|
-
|
45
|
+
FastPolylinesV2: 396100.0 i/s
|
46
|
+
FastPolylinesV1: 16464.6 i/s - 24.06x slower
|
47
|
+
Polylines: 1400.6 i/s - 282.81x slower
|
40
48
|
```
|
41
49
|
|
42
50
|
## Install
|
43
51
|
|
44
|
-
```
|
52
|
+
```bash
|
45
53
|
gem install fast-polylines
|
46
54
|
```
|
47
55
|
|
48
|
-
or in
|
49
|
-
```
|
50
|
-
gem "fast-polylines"
|
56
|
+
or in your `Gemfile`:
|
57
|
+
```ruby
|
58
|
+
gem "fast-polylines", "~> 2.0.0"
|
51
59
|
```
|
52
60
|
|
53
61
|
## Usage
|
54
62
|
|
55
|
-
```
|
56
|
-
require
|
63
|
+
```ruby
|
64
|
+
require "fast-polylines"
|
57
65
|
|
58
|
-
FastPolylines
|
66
|
+
FastPolylines.encode([[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]])
|
59
67
|
# "_p~iF~ps|U_ulLnnqC_mqNvxq`@"
|
60
68
|
|
61
|
-
FastPolylines
|
69
|
+
FastPolylines.decode("_p~iF~ps|U_ulLnnqC_mqNvxq`@")
|
62
70
|
# [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]]
|
63
71
|
```
|
64
72
|
|
65
73
|
## Advanced usage
|
66
74
|
|
67
|
-
|
75
|
+
**Use a different precision**
|
68
76
|
|
69
|
-
|
70
|
-
|
77
|
+
Default precision is `5` decimals, to use a precision of `6` decimals:
|
78
|
+
```ruby
|
79
|
+
FastPolylines.encode([[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]], 6)
|
71
80
|
# "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI"
|
72
81
|
|
73
|
-
FastPolylines
|
82
|
+
FastPolylines.decode("_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI", 6)
|
74
83
|
# [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]]
|
75
84
|
```
|
85
|
+
The precision max is `13`.
|
86
|
+
|
87
|
+
## Migrate from 1.0.0
|
88
|
+
|
89
|
+
**TL;DR:**
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# before
|
93
|
+
FastPolylines::Encoder.encode([[1.2, 1.2], [2.4, 2.4]], 1e6)
|
94
|
+
# after
|
95
|
+
FastPolylines.encode([[1.2, 1.2], [2.4, 2.4]], 6)
|
96
|
+
```
|
97
|
+
|
98
|
+
The new version of `FastPolylines` doesn't support precision more than `1e13`,
|
99
|
+
you should not consider using it anyway since [it is way too precise][xkcd].
|
100
|
+
|
101
|
+
`Encoder` and `Decoder` modules are deprecated in favor of the single parent
|
102
|
+
module. Even though you can still use those, a deprecation warning will be
|
103
|
+
printed.
|
104
|
+
|
105
|
+
The precision is now an integer representing the number of decimals. It is
|
106
|
+
slightly smaller, and mostly this will avoid having any float value as
|
107
|
+
precision.
|
76
108
|
|
77
109
|
## Run the Benchmark
|
78
110
|
|
79
111
|
You can run the benchmark with `make benchmark`.
|
80
112
|
|
113
|
+
## Contributing
|
114
|
+
|
115
|
+
```bash
|
116
|
+
git clone git@github.com:klaxit/fast-polylines
|
117
|
+
cd fast-polylines
|
118
|
+
bundle install
|
119
|
+
# Implement a feature, resolve a bug...
|
120
|
+
make rubocop
|
121
|
+
make test
|
122
|
+
git commit "My new feature!"
|
123
|
+
# Make a PR
|
124
|
+
```
|
125
|
+
|
126
|
+
There is a `make console` command as well to open a ruby console with the
|
127
|
+
current version loaded.
|
128
|
+
|
129
|
+
[And here's a good starting point for Ruby C extensions knowledge.][ruby-c]
|
130
|
+
|
81
131
|
## License
|
82
132
|
|
83
133
|
Please see LICENSE
|
134
|
+
|
135
|
+
|
136
|
+
[algorithm]: https://code.google.com/apis/maps/documentation/utilities/polylinealgorithm.html
|
137
|
+
[polylines]: https://github.com/joshuaclayton/polylines
|
138
|
+
[xkcd]: https://xkcd.com/2170/
|
139
|
+
[ruby-c]: https://github.com/ruby/ruby/blob/master/doc/extension.rdoc
|
@@ -0,0 +1,151 @@
|
|
1
|
+
#include <ruby/ruby.h>
|
2
|
+
|
3
|
+
// An encoded number can have at most _precision_ characters. However,
|
4
|
+
// it seems like we have a fuzzy behavior on low precisions. Hence a guard
|
5
|
+
// for those lower precisions.
|
6
|
+
#define MAX_ENCODED_CHUNKS(precision) (precision < 5 ? 5 : precision)
|
7
|
+
|
8
|
+
#define DEFAULT_PRECISION 5
|
9
|
+
|
10
|
+
// Already smaller than a sand grain. https://xkcd.com/2170/
|
11
|
+
// In fact, at 15 precision, we can be sure there will be precision loss
|
12
|
+
// in the C/Ruby value conversion. And 14 precision may be an edge case
|
13
|
+
// since we work with signed values.
|
14
|
+
#define MAX_PRECISION 13
|
15
|
+
|
16
|
+
// Uncomment this line to show debug logs.
|
17
|
+
// #define DEBUG 1
|
18
|
+
|
19
|
+
#ifdef DEBUG
|
20
|
+
#define dbg(...) printf(__VA_ARGS__)
|
21
|
+
#define rdbg(value) rb_funcall(Qnil, rb_intern("p"), 1, value)
|
22
|
+
#else
|
23
|
+
#define dbg(...)
|
24
|
+
#define rdbg(...)
|
25
|
+
#endif
|
26
|
+
|
27
|
+
static inline uint _get_precision(VALUE value) {
|
28
|
+
int precision = NIL_P(value) ? DEFAULT_PRECISION : NUM2INT(value);
|
29
|
+
if (precision > MAX_PRECISION) rb_raise(rb_eArgError, "precision too high (https://xkcd.com/2170/)");
|
30
|
+
if (precision < 0) rb_raise(rb_eArgError, "negative precision doesn't make sense");
|
31
|
+
return (uint)precision;
|
32
|
+
}
|
33
|
+
|
34
|
+
static inline VALUE
|
35
|
+
rb_FastPolylines__decode(int argc, VALUE *argv, VALUE self) {
|
36
|
+
rb_check_arity(argc, 1, 2);
|
37
|
+
Check_Type(argv[0], T_STRING);
|
38
|
+
char* polyline = StringValueCStr(argv[0]);
|
39
|
+
uint precision = _get_precision(argc > 1 ? argv[1] : Qnil);
|
40
|
+
double precision_value = pow(10.0, precision);
|
41
|
+
VALUE ary = rb_ary_new();
|
42
|
+
// Helps keeping track of whether we are computing lat (0) or lng (1).
|
43
|
+
uint8_t index = 0;
|
44
|
+
size_t shift = 0;
|
45
|
+
int64_t delta = 0;
|
46
|
+
VALUE sub_ary[2];
|
47
|
+
double latlng[2] = {0.0, 0.0};
|
48
|
+
// Loops until end of string nul character is encountered.
|
49
|
+
while (*polyline) {
|
50
|
+
int64_t chunk = *polyline++;
|
51
|
+
|
52
|
+
if (chunk < 63 || chunk > 126) {
|
53
|
+
rb_raise(rb_eArgError, "invalid character '%c'", (char)chunk);
|
54
|
+
}
|
55
|
+
|
56
|
+
chunk -= 63;
|
57
|
+
delta = delta | ((chunk & ~0x20) << shift);
|
58
|
+
shift += 5;
|
59
|
+
|
60
|
+
if (!(chunk & 0x20)) {
|
61
|
+
delta = (delta & 1) ? ~(delta >> 1) : (delta >> 1);
|
62
|
+
latlng[index] += delta;
|
63
|
+
sub_ary[index] = rb_float_new((double) latlng[index] / precision_value);
|
64
|
+
// When both coordinates are parsed, we can push those to the result ary.
|
65
|
+
if (index) rb_ary_push(ary, rb_ary_new_from_values(2, sub_ary));
|
66
|
+
// Reinitilize since we are done for current coordinate.
|
67
|
+
index = 1 - index; delta = 0; shift = 0;
|
68
|
+
}
|
69
|
+
}
|
70
|
+
return ary;
|
71
|
+
}
|
72
|
+
|
73
|
+
static inline uint8_t
|
74
|
+
_polyline_encode_number(char *chunks, int64_t number) {
|
75
|
+
number = number < 0 ? ~(number << 1) : (number << 1);
|
76
|
+
uint8_t i = 0;
|
77
|
+
while (number > 0x20) {
|
78
|
+
uint8_t chunk = number & 0x1f;
|
79
|
+
chunks[i++] = (0x20 | chunk) + 63;
|
80
|
+
number = number >> 5;
|
81
|
+
}
|
82
|
+
dbg("%u encoded chunks\n", i);
|
83
|
+
chunks[i++] = number + 63;
|
84
|
+
return i;
|
85
|
+
}
|
86
|
+
|
87
|
+
static inline VALUE
|
88
|
+
rb_FastPolylines__encode(int argc, VALUE *argv, VALUE self) {
|
89
|
+
rb_check_arity(argc, 1, 2);
|
90
|
+
Check_Type(argv[0], T_ARRAY);
|
91
|
+
size_t len = RARRAY_LEN(argv[0]);
|
92
|
+
uint64_t i;
|
93
|
+
VALUE current_pair;
|
94
|
+
int64_t prev_pair[2] = {0, 0};
|
95
|
+
uint precision = _get_precision(argc > 1 ? argv[1] : Qnil);
|
96
|
+
dbg("rb_FastPolylines__encode(..., %u)\n", precision);
|
97
|
+
rdbg(argv[0]);
|
98
|
+
double precision_value = pow(10.0, precision);
|
99
|
+
// This is the maximum possible size the polyline may have. This
|
100
|
+
// being **without** null character. To copy it later as a Ruby
|
101
|
+
// string we'll have to use `rb_str_new` with the length.
|
102
|
+
dbg("allocated size: %u * 2 * %lu = %lu\n",
|
103
|
+
MAX_ENCODED_CHUNKS(precision),
|
104
|
+
len,
|
105
|
+
MAX_ENCODED_CHUNKS(precision) * 2 * len);
|
106
|
+
char *chunks = malloc(MAX_ENCODED_CHUNKS(precision) * 2 * len * sizeof(char));
|
107
|
+
size_t chunks_index = 0;
|
108
|
+
for (i = 0; i < len; i++) {
|
109
|
+
current_pair = RARRAY_AREF(argv[0], i);
|
110
|
+
uint8_t j;
|
111
|
+
if (RARRAY_LEN(current_pair) != 2) {
|
112
|
+
free(chunks);
|
113
|
+
rb_raise(rb_eArgError, "wrong number of coordinates");
|
114
|
+
}
|
115
|
+
for (j = 0; j < 2; j++) {
|
116
|
+
VALUE current_value = RARRAY_AREF(current_pair, j);
|
117
|
+
switch (TYPE(current_value)) {
|
118
|
+
case T_BIGNUM:
|
119
|
+
case T_FLOAT:
|
120
|
+
case T_FIXNUM:
|
121
|
+
case T_RATIONAL:
|
122
|
+
break;
|
123
|
+
default:
|
124
|
+
free(chunks);
|
125
|
+
rb_raise(rb_eTypeError, "no implicit conversion to Float from %s", rb_obj_classname(current_value));
|
126
|
+
};
|
127
|
+
|
128
|
+
double parsed_value = NUM2DBL(current_value);
|
129
|
+
if (-180.0 > parsed_value || parsed_value > 180.0) {
|
130
|
+
free(chunks);
|
131
|
+
rb_raise(rb_eArgError, "coordinates must be between -180.0 and 180.0");
|
132
|
+
}
|
133
|
+
int64_t rounded_value = round(parsed_value * precision_value);
|
134
|
+
int64_t delta = rounded_value - prev_pair[j];
|
135
|
+
prev_pair[j] = rounded_value;
|
136
|
+
// We pass a pointer to the current chunk that need to be filled. Doing so
|
137
|
+
// avoid having to copy the string every single iteration.
|
138
|
+
chunks_index += _polyline_encode_number(chunks + chunks_index * sizeof(char), delta);
|
139
|
+
}
|
140
|
+
}
|
141
|
+
dbg("final chunks_index: %zu\n", chunks_index);
|
142
|
+
VALUE polyline = rb_str_new(chunks, chunks_index);
|
143
|
+
free(chunks);
|
144
|
+
return polyline;
|
145
|
+
}
|
146
|
+
|
147
|
+
void Init_fast_polylines() {
|
148
|
+
VALUE mFastPolylines = rb_define_module("FastPolylines");
|
149
|
+
rb_define_module_function(mFastPolylines, "decode", rb_FastPolylines__decode, -1);
|
150
|
+
rb_define_module_function(mFastPolylines, "encode", rb_FastPolylines__encode, -1);
|
151
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fast_polylines/fast_polylines"
|
4
|
+
|
5
|
+
module FastPolylines::Encoder
|
6
|
+
module_function def encode(points, precision = 1e5)
|
7
|
+
warn "Deprecated use of `FastPolylines::Encoder.encode`, " \
|
8
|
+
"use `FastPolylines.encode`."
|
9
|
+
FastPolylines.encode(points, Math.log10(precision))
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
module FastPolylines::Decoder
|
14
|
+
module_function def decode(polyline, precision = 1e5)
|
15
|
+
warn "Deprecated use of `FastPolylines::Decoder.decode`, " \
|
16
|
+
"use `FastPolylines.decode`."
|
17
|
+
FastPolylines.decode(polyline, Math.log10(precision))
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe FastPolylines do
|
4
|
+
describe ".decode" do
|
5
|
+
let(:points) { [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]] }
|
6
|
+
context "with default precision" do
|
7
|
+
let(:polyline) { "_p~iF~ps|U_ulLnnqC_mqNvxq`@" }
|
8
|
+
it "should decode a polyline correctly" do
|
9
|
+
expect(described_class.decode(polyline)).to eq points
|
10
|
+
end
|
11
|
+
end
|
12
|
+
context "with a 6 decimals precision" do
|
13
|
+
let(:polyline) { "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI" }
|
14
|
+
it "should decode a polyline correctly" do
|
15
|
+
expect(described_class.decode(polyline, 6)).to eq points
|
16
|
+
end
|
17
|
+
end
|
18
|
+
context "with a 15 decimals precision" do
|
19
|
+
it "should raise" do
|
20
|
+
expect { described_class.decode("_kiF_kiF", 15) }.to raise_error(ArgumentError, /too high/)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
context "with a negative precision" do
|
24
|
+
it "should raise" do
|
25
|
+
expect { described_class.decode("_kiF_kiF", -rand(1..10)) }.to raise_error(ArgumentError, /negative/)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
context "with wrong characters" do
|
29
|
+
it "should raise" do
|
30
|
+
chr = "~".next
|
31
|
+
expect { described_class.decode(chr) }.to raise_error(ArgumentError, /'#{chr}'/)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
context "with points that were close together" do
|
35
|
+
let(:points) { [[41.35222, -86.04563], [41.35222, -86.04544]] }
|
36
|
+
it "should decode a polyline correctly" do
|
37
|
+
expect(described_class.decode("krk{FdxdlO?e@")).to eq points
|
38
|
+
end
|
39
|
+
end
|
40
|
+
context "with points that cause float overflow" do
|
41
|
+
let(:polyline) { "ahdiHsmeMHPDN|A`FVt@" }
|
42
|
+
let(:points) { [[48.85137, 2.32682], [48.85132, 2.32673], [48.85129, 2.32665], [48.85082, 2.32552], [48.8507, 2.32525]] }
|
43
|
+
it "should respect precision" do
|
44
|
+
expect(described_class.decode(polyline)).to eq points
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe ".encode" do
|
50
|
+
let(:points) { [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]] }
|
51
|
+
context "with default precision" do
|
52
|
+
let(:polyline) { "_p~iF~ps|U_ulLnnqC_mqNvxq`@" }
|
53
|
+
it "should encode points correctly" do
|
54
|
+
expect(described_class.encode(points)).to eq polyline
|
55
|
+
end
|
56
|
+
end
|
57
|
+
context "with a 6 decimals precision" do
|
58
|
+
let(:polyline) { "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI" }
|
59
|
+
it "should encode points correctly" do
|
60
|
+
expect(described_class.encode(points, 6)).to eq polyline
|
61
|
+
end
|
62
|
+
end
|
63
|
+
context "with points that were close together" do
|
64
|
+
let(:points) { [[41.35222, -86.04563], [41.35222, -86.04544]] }
|
65
|
+
it "should encode points correctly" do
|
66
|
+
expect(described_class.encode(points)).to eq "krk{FdxdlO?e@"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
context "with possible rounding ambiguity" do
|
70
|
+
let(:points) { [[39.13594499,-94.4243478], [39.13558757,-94.4243471]] }
|
71
|
+
it "should encode points as Google API do" do
|
72
|
+
expect(described_class.encode(points)).to eq "svzmFdgi_QdA?"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
context "with points out of bounds" do
|
76
|
+
it "should raise" do
|
77
|
+
expect { described_class.encode([[180.1, 0.0]]) }.to raise_error ArgumentError
|
78
|
+
expect { described_class.encode([[0, 180.1]]) }.to raise_error ArgumentError
|
79
|
+
expect { described_class.encode([[-180.1, 0.0]]) }.to raise_error ArgumentError
|
80
|
+
expect { described_class.encode([[0, -180.1]]) }.to raise_error ArgumentError
|
81
|
+
end
|
82
|
+
end
|
83
|
+
context "with a 15 decimals precision" do
|
84
|
+
it "should raise" do
|
85
|
+
expect { described_class.encode([[1.2, 1.2]], 15) }.to raise_error(ArgumentError, /too high/)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
context "with a negative precision" do
|
89
|
+
it "should raise" do
|
90
|
+
expect { described_class.encode([[1.2, 1.2]], -rand(1..10)) }.to raise_error(ArgumentError, /negative/)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,30 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: fast-polylines
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyrille Courtière
|
8
|
+
- Ulysse Buonomo
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2020-03-26 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: benchmark-ips
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
16
17
|
requirements:
|
17
|
-
- - "
|
18
|
+
- - "~>"
|
18
19
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
+
version: '2.7'
|
20
21
|
type: :development
|
21
22
|
prerelease: false
|
22
23
|
version_requirements: !ruby/object:Gem::Requirement
|
23
24
|
requirements:
|
24
|
-
- - "
|
25
|
+
- - "~>"
|
25
26
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
27
|
+
version: '2.7'
|
27
28
|
- !ruby/object:Gem::Dependency
|
28
29
|
name: polylines
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -54,20 +55,21 @@ dependencies:
|
|
54
55
|
version: '3.5'
|
55
56
|
description:
|
56
57
|
email:
|
57
|
-
-
|
58
|
+
- dev@klaxit.com
|
58
59
|
executables: []
|
59
|
-
extensions:
|
60
|
+
extensions:
|
61
|
+
- ext/fast_polylines/extconf.rb
|
60
62
|
extra_rdoc_files: []
|
61
63
|
files:
|
62
64
|
- README.md
|
63
|
-
-
|
64
|
-
-
|
65
|
-
-
|
66
|
-
- lib/
|
67
|
-
-
|
68
|
-
- spec/
|
65
|
+
- ext/fast_polylines/.gitignore
|
66
|
+
- ext/fast_polylines/extconf.rb
|
67
|
+
- ext/fast_polylines/fast_polylines.c
|
68
|
+
- lib/fast_polylines.rb
|
69
|
+
- lib/fast_polylines/version.rb
|
70
|
+
- spec/fast_polylines_spec.rb
|
69
71
|
- spec/spec_helper.rb
|
70
|
-
homepage:
|
72
|
+
homepage: https://github.com/klaxit/fast-polylines
|
71
73
|
licenses:
|
72
74
|
- MIT
|
73
75
|
metadata: {}
|
@@ -91,6 +93,5 @@ signing_key:
|
|
91
93
|
specification_version: 4
|
92
94
|
summary: Fast & easy Google polylines
|
93
95
|
test_files:
|
94
|
-
- spec/
|
95
|
-
- spec/fast-polylines/encoder_spec.rb
|
96
|
+
- spec/fast_polylines_spec.rb
|
96
97
|
- spec/spec_helper.rb
|
data/lib/fast-polylines.rb
DELETED
@@ -1,41 +0,0 @@
|
|
1
|
-
module FastPolylines
|
2
|
-
##
|
3
|
-
# Provide an interface to decode a Google polyline
|
4
|
-
# to an array of lat / lng points
|
5
|
-
class Decoder
|
6
|
-
##
|
7
|
-
# Decode a polyline to a list of points
|
8
|
-
#
|
9
|
-
# @param [String] polyline to be decoded
|
10
|
-
# @param [Float] precision of the polyline (default 1e5)
|
11
|
-
# @return [Array<Array>] A list of lat / lng pairs
|
12
|
-
def self.decode(polyline, precision = 1e5)
|
13
|
-
coords = []
|
14
|
-
acc = ""
|
15
|
-
polyline.each_byte do |b|
|
16
|
-
acc << b
|
17
|
-
next unless b < 0x5f
|
18
|
-
coords << acc
|
19
|
-
acc = ""
|
20
|
-
end
|
21
|
-
lat = lng = 0
|
22
|
-
coords.each_slice(2).map do |coords_pair|
|
23
|
-
lat += decode_number(coords_pair[0])
|
24
|
-
lng += decode_number(coords_pair[1])
|
25
|
-
[lat / precision, lng / precision]
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.decode_number(string)
|
30
|
-
result = 1
|
31
|
-
shift = 0
|
32
|
-
string.each_byte do |b|
|
33
|
-
b = b - 63 - 1
|
34
|
-
result += b << shift
|
35
|
-
shift += 5
|
36
|
-
end
|
37
|
-
(result & 1).nonzero? ? (~result >> 1) : (result >> 1)
|
38
|
-
end
|
39
|
-
private_class_method :decode_number
|
40
|
-
end
|
41
|
-
end
|
@@ -1,47 +0,0 @@
|
|
1
|
-
module FastPolylines
|
2
|
-
##
|
3
|
-
# Provides an interface to encode an array of lat / lng points
|
4
|
-
# to a Google polyline
|
5
|
-
#
|
6
|
-
class Encoder
|
7
|
-
##
|
8
|
-
# Encode a list of points into a polyline string
|
9
|
-
#
|
10
|
-
# @param [Array<Array>] points as lat/lng pairs
|
11
|
-
# @param [Float] precision of the encoding (default 1e5)
|
12
|
-
# @return [String] the encoded polyline
|
13
|
-
def self.encode(points, precision = 1e5)
|
14
|
-
result = ""
|
15
|
-
last_lat = last_lng = 0
|
16
|
-
points.each do |point|
|
17
|
-
lat = (point[0] * precision).round
|
18
|
-
lng = (point[1] * precision).round
|
19
|
-
d_lat = lat - last_lat
|
20
|
-
d_lng = lng - last_lng
|
21
|
-
chunks_lat = encode_number(d_lat)
|
22
|
-
chunks_lng = encode_number(d_lng)
|
23
|
-
result << chunks_lat << chunks_lng
|
24
|
-
last_lat = lat
|
25
|
-
last_lng = lng
|
26
|
-
end
|
27
|
-
result
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.encode_number(num)
|
31
|
-
sgn_num = num << 1
|
32
|
-
sgn_num = ~sgn_num if num < 0
|
33
|
-
encode_unsigned_number(sgn_num)
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.encode_unsigned_number(num)
|
37
|
-
encoded = ""
|
38
|
-
while num >= 0x20
|
39
|
-
encoded << (0x20 | (num & 0x1f)) + 63
|
40
|
-
num = num >> 5
|
41
|
-
end
|
42
|
-
encoded << num + 63
|
43
|
-
end
|
44
|
-
private_class_method :encode_number
|
45
|
-
private_class_method :encode_unsigned_number
|
46
|
-
end
|
47
|
-
end
|
@@ -1,32 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe FastPolylines::Decoder do
|
4
|
-
describe ".decode" do
|
5
|
-
let(:points) { [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]] }
|
6
|
-
context "with default precision" do
|
7
|
-
let(:polyline) { "_p~iF~ps|U_ulLnnqC_mqNvxq`@" }
|
8
|
-
it "should decode a polyline correctly" do
|
9
|
-
expect(described_class.decode(polyline)).to eq points
|
10
|
-
end
|
11
|
-
end
|
12
|
-
context "with 1e6 precision" do
|
13
|
-
let(:polyline) { "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI" }
|
14
|
-
it "should decode a polyline correctly" do
|
15
|
-
expect(described_class.decode(polyline, 1e6)).to eq points
|
16
|
-
end
|
17
|
-
end
|
18
|
-
context "with points that were close together" do
|
19
|
-
let(:points) { [[41.35222, -86.04563], [41.35222, -86.04544]] }
|
20
|
-
it "should decode a polyline correctly" do
|
21
|
-
expect(described_class.decode("krk{FdxdlO?e@")).to eq points
|
22
|
-
end
|
23
|
-
end
|
24
|
-
context "with points that cause float overflow" do
|
25
|
-
let(:polyline) { "ahdiHsmeMHPDN|A`FVt@" }
|
26
|
-
let(:points) { [[48.85137, 2.32682], [48.85132, 2.32673], [48.85129, 2.32665], [48.85082, 2.32552], [48.8507, 2.32525]] }
|
27
|
-
it "should respect precision" do
|
28
|
-
expect(described_class.decode(polyline)).to eq points
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
|
-
describe FastPolylines::Encoder do
|
4
|
-
describe ".encode" do
|
5
|
-
let(:points) { [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]] }
|
6
|
-
context "with default precision" do
|
7
|
-
let(:polyline) { "_p~iF~ps|U_ulLnnqC_mqNvxq`@" }
|
8
|
-
it "should encode points correctly" do
|
9
|
-
expect(described_class.encode(points)).to eq polyline
|
10
|
-
end
|
11
|
-
end
|
12
|
-
context "with 1e6 precision" do
|
13
|
-
let(:polyline) { "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI" }
|
14
|
-
it "should encode points correctly" do
|
15
|
-
expect(described_class.encode(points, 1e6)).to eq polyline
|
16
|
-
end
|
17
|
-
end
|
18
|
-
context "with points that were close together" do
|
19
|
-
let(:points) { [[41.35222, -86.04563], [41.35222, -86.04544]] }
|
20
|
-
it "should encode points correctly" do
|
21
|
-
expect(described_class.encode(points)).to eq "krk{FdxdlO?e@"
|
22
|
-
end
|
23
|
-
end
|
24
|
-
context "with possible rounding ambiguity" do
|
25
|
-
let(:points) { [[39.13594499,-94.4243478], [39.13558757,-94.4243471]] }
|
26
|
-
it "should encode points as Google API do" do
|
27
|
-
expect(described_class.encode(points)).to eq "svzmFdgi_QdA?"
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|