fast-polylines 0.1.1 → 2.2.0
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.
- checksums.yaml +5 -5
- data/.rspec +1 -0
- data/CHANGELOG.md +48 -0
- data/README.md +113 -17
- data/ext/fast_polylines/extconf.rb +14 -0
- data/ext/fast_polylines/fast_polylines.c +173 -0
- data/lib/fast-polylines.rb +3 -5
- data/lib/fast_polylines.rb +19 -0
- data/lib/fast_polylines/version.rb +5 -0
- data/spec/fast-polylines_spec.rb +10 -0
- data/spec/fast_polylines_spec.rb +119 -0
- data/spec/spec_helper.rb +3 -5
- metadata +30 -25
- 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 -39
- data/spec/fast-polylines/encoder_spec.rb +0 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0ab14d087b6992d7abec3047ad38aea4e7474f4bfff06b921ea91699bd788172
|
4
|
+
data.tar.gz: 4225fc53ff0801e6b145058461d9cf6d5f65be902787a7496e85c83883b52ba5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 82f43939d565a422fdf0696539f1c668d851aa88ff36d52a9f3ad2d909ebfe095c1ccd0b7266243943c834f70dc5c954c85715a3c435768df2c6ccce871443b4
|
7
|
+
data.tar.gz: 97ab65e7683ecbb53bf5673532e5ff99399b7f0b97b140221127b302e2d54020c1e3b1b6c174a624c49d276801b0f76089e3378cf5949b7a8fdad64494d89db0
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
|
2
|
+
# Changelog
|
3
|
+
|
4
|
+
All notable changes to this project will be documented in this file.
|
5
|
+
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
6
|
+
This project adheres to [Semantic Versioning](http://semver.org/).
|
7
|
+
|
8
|
+
## [unreleased] —
|
9
|
+
|
10
|
+
### Added
|
11
|
+
|
12
|
+
### Changed
|
13
|
+
|
14
|
+
### Fixed
|
15
|
+
|
16
|
+
## [2.2.0] — 2020-11-24
|
17
|
+
|
18
|
+
### Added
|
19
|
+
|
20
|
+
- support for windows (#19 #23) by [@mohits]
|
21
|
+
|
22
|
+
### Changed
|
23
|
+
|
24
|
+
- Speedup calculation by avoiding calling pow(10,x) (#22) by [@mohits]
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
|
28
|
+
- move from travis to github actions
|
29
|
+
|
30
|
+
[@mohits]: https://github.com/mohits
|
31
|
+
|
32
|
+
## [2.1.0] — 2020-04-30
|
33
|
+
|
34
|
+
### Added
|
35
|
+
|
36
|
+
- A Require ptah that match the gem name (#18)
|
37
|
+
|
38
|
+
## [2.0.1] — 2020-04-02
|
39
|
+
|
40
|
+
### Fixed
|
41
|
+
|
42
|
+
- Broken behavior when approaching the chunk size limit (#16)
|
43
|
+
|
44
|
+
|
45
|
+
[unreleased]: https://github.com/klaxit/fast-polylines/compare/v2.2.0...HEAD
|
46
|
+
[2.2.0]: https://github.com/klaxit/fast-polylines/compare/v2.1.0...v2.2.0
|
47
|
+
[2.1.0]: https://github.com/klaxit/fast-polylines/compare/v2.0.1...v2.1.0
|
48
|
+
[2.0.1]: https://github.com/klaxit/fast-polylines/compare/v2.0.0...v2.0.1
|
data/README.md
CHANGED
@@ -1,50 +1,146 @@
|
|
1
1
|
# Fast Polylines
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/fast-polylines)
|
4
|
-
[](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 of FastPolylines includes breaking changes, see [Migrate from V1](#migrate-from-V1)
|
10
9
|
|
11
|
-
But an **at least 5x faster** implementation.
|
12
10
|
|
13
|
-
|
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:
|
14
14
|
|
15
15
|
```
|
16
|
-
|
16
|
+
——————————————————————————————— ENCODING ————————————————————————————————
|
17
|
+
|
18
|
+
Warming up --------------------------------------
|
19
|
+
Polylines 310.000 i/100ms
|
20
|
+
FastPolylinesV1 2.607k i/100ms
|
21
|
+
FastPolylinesV2 59.833k i/100ms
|
22
|
+
Calculating -------------------------------------
|
23
|
+
Polylines 2.957k (± 5.9%) i/s - 14.880k in 5.049867s
|
24
|
+
FastPolylinesV1 25.644k (± 5.8%) i/s - 127.743k in 4.999954s
|
25
|
+
FastPolylinesV2 682.981k (± 7.7%) i/s - 3.410M in 5.025952s
|
26
|
+
|
27
|
+
Comparison:
|
28
|
+
FastPolylinesV2: 682980.7 i/s
|
29
|
+
FastPolylinesV1: 25643.7 i/s - 26.63x slower
|
30
|
+
Polylines: 2957.1 i/s - 230.97x slower
|
31
|
+
|
32
|
+
|
33
|
+
——————————————————————————————— DECODING ————————————————————————————————
|
34
|
+
|
35
|
+
Warming up --------------------------------------
|
36
|
+
Polylines 127.000 i/100ms
|
37
|
+
FastPolylinesV1 1.225k i/100ms
|
38
|
+
FastPolylinesV2 40.667k i/100ms
|
39
|
+
Calculating -------------------------------------
|
40
|
+
Polylines 1.289k (± 6.1%) i/s - 6.477k in 5.046552s
|
41
|
+
FastPolylinesV1 15.445k (± 4.4%) i/s - 77.175k in 5.006896s
|
42
|
+
FastPolylinesV2 468.413k (± 7.8%) i/s - 2.359M in 5.068936s
|
43
|
+
|
44
|
+
Comparison:
|
45
|
+
FastPolylinesV2: 468412.8 i/s
|
46
|
+
FastPolylinesV1: 15445.4 i/s - 30.33x slower
|
47
|
+
Polylines: 1288.8 i/s - 363.46x slower
|
17
48
|
```
|
18
49
|
|
19
|
-
|
50
|
+
## Install
|
51
|
+
|
52
|
+
```bash
|
53
|
+
gem install fast-polylines
|
20
54
|
```
|
21
|
-
|
55
|
+
|
56
|
+
or in your `Gemfile`:
|
57
|
+
```ruby
|
58
|
+
gem "fast-polylines", "~> 2.0.0"
|
22
59
|
```
|
23
60
|
|
24
61
|
## Usage
|
25
62
|
|
26
|
-
```
|
27
|
-
require
|
63
|
+
```ruby
|
64
|
+
require "fast_polylines"
|
28
65
|
|
29
|
-
FastPolylines
|
66
|
+
FastPolylines.encode([[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]])
|
30
67
|
# "_p~iF~ps|U_ulLnnqC_mqNvxq`@"
|
31
68
|
|
32
|
-
FastPolylines
|
69
|
+
FastPolylines.decode("_p~iF~ps|U_ulLnnqC_mqNvxq`@")
|
33
70
|
# [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]]
|
34
71
|
```
|
35
72
|
|
36
73
|
## Advanced usage
|
37
74
|
|
38
|
-
|
75
|
+
**Use a different precision**
|
39
76
|
|
40
|
-
|
41
|
-
|
77
|
+
Default precision is `5` decimals, to use a precision of `6`:
|
78
|
+
```ruby
|
79
|
+
FastPolylines.encode([[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]], 6)
|
42
80
|
# "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI"
|
43
81
|
|
44
|
-
FastPolylines
|
82
|
+
FastPolylines.decode("_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI", 6)
|
45
83
|
# [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]]
|
46
84
|
```
|
85
|
+
The precision max is `13`.
|
86
|
+
|
87
|
+
## Migrate from V1
|
88
|
+
|
89
|
+
**TL;DR:**
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
# before
|
93
|
+
require "fast-polylines"
|
94
|
+
FastPolylines::Encoder.encode([[1.2, 1.2], [2.4, 2.4]], 1e6)
|
95
|
+
# after
|
96
|
+
require "fast_polylines"
|
97
|
+
FastPolylines.encode([[1.2, 1.2], [2.4, 2.4]], 6)
|
98
|
+
```
|
99
|
+
|
100
|
+
**Detailled:**
|
101
|
+
|
102
|
+
The new version of `FastPolylines` doesn't support precision more than `1e13`,
|
103
|
+
you should not consider using it anyway since [it is way too precise][xkcd].
|
104
|
+
|
105
|
+
`Encoder` and `Decoder` modules are deprecated in favor of the single parent
|
106
|
+
module. Even though you can still use those, a deprecation warning will be
|
107
|
+
printed.
|
108
|
+
|
109
|
+
The precision is now an integer representing the number of decimals. It is
|
110
|
+
slightly smaller, and mostly this will avoid having any float value as
|
111
|
+
precision.
|
112
|
+
|
113
|
+
The file name to require is now snake_cased, you'll have to require
|
114
|
+
`fast_polylines`. The gem name stays the same however.
|
115
|
+
|
116
|
+
## Run the Benchmark
|
117
|
+
|
118
|
+
You can run the benchmark with `make benchmark`.
|
119
|
+
|
120
|
+
## Contributing
|
121
|
+
|
122
|
+
```bash
|
123
|
+
git clone git@github.com:klaxit/fast-polylines
|
124
|
+
cd fast-polylines
|
125
|
+
bundle install
|
126
|
+
# Implement a feature, resolve a bug...
|
127
|
+
make rubocop
|
128
|
+
make test
|
129
|
+
git commit "My new feature!"
|
130
|
+
# Make a PR
|
131
|
+
```
|
132
|
+
|
133
|
+
There is a `make console` command as well to open a ruby console with the
|
134
|
+
current version loaded.
|
135
|
+
|
136
|
+
[And here's a good starting point for Ruby C extensions knowledge.][ruby-c]
|
47
137
|
|
48
138
|
## License
|
49
139
|
|
50
140
|
Please see LICENSE
|
141
|
+
|
142
|
+
|
143
|
+
[algorithm]: https://code.google.com/apis/maps/documentation/utilities/polylinealgorithm.html
|
144
|
+
[polylines]: https://github.com/joshuaclayton/polylines
|
145
|
+
[xkcd]: https://xkcd.com/2170/
|
146
|
+
[ruby-c]: https://github.com/ruby/ruby/blob/master/doc/extension.rdoc
|
@@ -0,0 +1,173 @@
|
|
1
|
+
#include <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
|
+
typedef unsigned int uint;
|
28
|
+
|
29
|
+
// Speed up the code a bit by not calling pow(10.0, precision) since it is
|
30
|
+
// always 10^precision. Convert that into a lookup instead of doing the
|
31
|
+
// calculation.
|
32
|
+
// The precision parameter is already checked in +_get_precision()+ to ensure
|
33
|
+
// that it is within the range.
|
34
|
+
static inline double _fast_pow10(uint precision) {
|
35
|
+
const double lookup[MAX_PRECISION + 1] = {
|
36
|
+
1.0, 10.0, 1.0e+2, 1.0e+3, 1.0e+4, 1.0e+5, 1.0e+6, 1.0e+7,
|
37
|
+
1.0e+8, 1.0e+9, 1.0e+10, 1.0e+11, 1.0e+12, 1.0e+13
|
38
|
+
};
|
39
|
+
return lookup[precision];
|
40
|
+
}
|
41
|
+
|
42
|
+
static inline uint _get_precision(VALUE value) {
|
43
|
+
int precision = NIL_P(value) ? DEFAULT_PRECISION : NUM2INT(value);
|
44
|
+
if (precision > MAX_PRECISION) rb_raise(rb_eArgError, "precision too high (https://xkcd.com/2170/)");
|
45
|
+
if (precision < 0) rb_raise(rb_eArgError, "negative precision doesn't make sense");
|
46
|
+
return (uint)precision;
|
47
|
+
}
|
48
|
+
|
49
|
+
static inline VALUE
|
50
|
+
rb_FastPolylines__decode(int argc, VALUE *argv, VALUE self) {
|
51
|
+
rb_check_arity(argc, 1, 2);
|
52
|
+
Check_Type(argv[0], T_STRING);
|
53
|
+
char* polyline = StringValueCStr(argv[0]);
|
54
|
+
uint precision = _get_precision(argc > 1 ? argv[1] : Qnil);
|
55
|
+
double precision_value = _fast_pow10(precision);
|
56
|
+
|
57
|
+
VALUE ary = rb_ary_new();
|
58
|
+
// Helps keeping track of whether we are computing lat (0) or lng (1).
|
59
|
+
uint8_t index = 0;
|
60
|
+
size_t shift = 0;
|
61
|
+
int64_t delta = 0;
|
62
|
+
VALUE sub_ary[2];
|
63
|
+
double latlng[2] = {0.0, 0.0};
|
64
|
+
// Loops until end of string nul character is encountered.
|
65
|
+
while (*polyline) {
|
66
|
+
int64_t chunk = *polyline++;
|
67
|
+
|
68
|
+
if (chunk < 63 || chunk > 126) {
|
69
|
+
rb_raise(rb_eArgError, "invalid character '%c'", (char)chunk);
|
70
|
+
}
|
71
|
+
|
72
|
+
chunk -= 63;
|
73
|
+
delta = delta | ((chunk & ~0x20) << shift);
|
74
|
+
shift += 5;
|
75
|
+
|
76
|
+
if (!(chunk & 0x20)) {
|
77
|
+
delta = (delta & 1) ? ~(delta >> 1) : (delta >> 1);
|
78
|
+
latlng[index] += delta;
|
79
|
+
sub_ary[index] = rb_float_new((double) latlng[index] / precision_value);
|
80
|
+
// When both coordinates are parsed, we can push those to the result ary.
|
81
|
+
if (index) rb_ary_push(ary, rb_ary_new_from_values(2, sub_ary));
|
82
|
+
// Reinitilize since we are done for current coordinate.
|
83
|
+
index = 1 - index; delta = 0; shift = 0;
|
84
|
+
}
|
85
|
+
}
|
86
|
+
return ary;
|
87
|
+
}
|
88
|
+
|
89
|
+
static inline uint8_t
|
90
|
+
_polyline_encode_number(char *chunks, int64_t number) {
|
91
|
+
dbg("_polyline_encode_number(\"%s\", %lli)\n", chunks, number);
|
92
|
+
number = number < 0 ? ~(number << 1) : (number << 1);
|
93
|
+
uint8_t i = 0;
|
94
|
+
while (number >= 0x20) {
|
95
|
+
uint8_t chunk = number & 0x1f;
|
96
|
+
chunks[i++] = (0x20 | chunk) + 63;
|
97
|
+
number = number >> 5;
|
98
|
+
}
|
99
|
+
chunks[i++] = number + 63;
|
100
|
+
dbg("%u encoded chunks\n", i);
|
101
|
+
dbg("chunks: %s\n", chunks);
|
102
|
+
dbg("/_polyline_encode_number\n");
|
103
|
+
return i;
|
104
|
+
}
|
105
|
+
|
106
|
+
static inline VALUE
|
107
|
+
rb_FastPolylines__encode(int argc, VALUE *argv, VALUE self) {
|
108
|
+
rb_check_arity(argc, 1, 2);
|
109
|
+
Check_Type(argv[0], T_ARRAY);
|
110
|
+
size_t len = RARRAY_LEN(argv[0]);
|
111
|
+
uint64_t i;
|
112
|
+
VALUE current_pair;
|
113
|
+
int64_t prev_pair[2] = {0, 0};
|
114
|
+
uint precision = _get_precision(argc > 1 ? argv[1] : Qnil);
|
115
|
+
dbg("rb_FastPolylines__encode(..., %u)\n", precision);
|
116
|
+
rdbg(argv[0]);
|
117
|
+
double precision_value = _fast_pow10(precision);
|
118
|
+
|
119
|
+
// This is the maximum possible size the polyline may have. This
|
120
|
+
// being **without** null character. To copy it later as a Ruby
|
121
|
+
// string we'll have to use `rb_str_new` with the length.
|
122
|
+
dbg("allocated size: %u * 2 * %lu = %lu\n",
|
123
|
+
MAX_ENCODED_CHUNKS(precision),
|
124
|
+
len,
|
125
|
+
MAX_ENCODED_CHUNKS(precision) * 2 * len);
|
126
|
+
char *chunks = malloc(MAX_ENCODED_CHUNKS(precision) * 2 * len * sizeof(char));
|
127
|
+
size_t chunks_index = 0;
|
128
|
+
for (i = 0; i < len; i++) {
|
129
|
+
current_pair = RARRAY_AREF(argv[0], i);
|
130
|
+
uint8_t j;
|
131
|
+
Check_Type(current_pair, T_ARRAY);
|
132
|
+
if (RARRAY_LEN(current_pair) != 2) {
|
133
|
+
free(chunks);
|
134
|
+
rb_raise(rb_eArgError, "wrong number of coordinates");
|
135
|
+
}
|
136
|
+
for (j = 0; j < 2; j++) {
|
137
|
+
VALUE current_value = RARRAY_AREF(current_pair, j);
|
138
|
+
switch (TYPE(current_value)) {
|
139
|
+
case T_BIGNUM:
|
140
|
+
case T_FLOAT:
|
141
|
+
case T_FIXNUM:
|
142
|
+
case T_RATIONAL:
|
143
|
+
break;
|
144
|
+
default:
|
145
|
+
free(chunks);
|
146
|
+
rb_raise(rb_eTypeError, "no implicit conversion to Float from %s", rb_obj_classname(current_value));
|
147
|
+
};
|
148
|
+
|
149
|
+
double parsed_value = NUM2DBL(current_value);
|
150
|
+
if (-180.0 > parsed_value || parsed_value > 180.0) {
|
151
|
+
free(chunks);
|
152
|
+
rb_raise(rb_eArgError, "coordinates must be between -180.0 and 180.0");
|
153
|
+
}
|
154
|
+
int64_t rounded_value = round(parsed_value * precision_value);
|
155
|
+
int64_t delta = rounded_value - prev_pair[j];
|
156
|
+
prev_pair[j] = rounded_value;
|
157
|
+
// We pass a pointer to the current chunk that need to be filled. Doing so
|
158
|
+
// avoid having to copy the string every single iteration.
|
159
|
+
chunks_index += _polyline_encode_number(chunks + chunks_index * sizeof(char), delta);
|
160
|
+
dbg("%s\n", chunks);
|
161
|
+
}
|
162
|
+
}
|
163
|
+
dbg("final chunks_index: %zu\n", chunks_index);
|
164
|
+
VALUE polyline = rb_str_new(chunks, chunks_index);
|
165
|
+
free(chunks);
|
166
|
+
return polyline;
|
167
|
+
}
|
168
|
+
|
169
|
+
void Init_fast_polylines() {
|
170
|
+
VALUE mFastPolylines = rb_define_module("FastPolylines");
|
171
|
+
rb_define_module_function(mFastPolylines, "decode", rb_FastPolylines__decode, -1);
|
172
|
+
rb_define_module_function(mFastPolylines, "encode", rb_FastPolylines__encode, -1);
|
173
|
+
}
|
data/lib/fast-polylines.rb
CHANGED
@@ -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,10 @@
|
|
1
|
+
require "fast-polylines"
|
2
|
+
|
3
|
+
describe FastPolylines do
|
4
|
+
let(:points) { [[38.5, -120.2], [40.7, -120.95], [43.252, -126.453]] }
|
5
|
+
let(:polyline) { "_p~iF~ps|U_ulLnnqC_mqNvxq`@" }
|
6
|
+
it "should work with kebab-case requirement" do
|
7
|
+
expect(FastPolylines.encode(points)).to eq polyline
|
8
|
+
expect(FastPolylines.decode(polyline)).to eq points
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require "fast_polylines"
|
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
|
+
let(:polyline) { "_p~iF~ps|U_ulLnnqC_mqNvxq`@" }
|
7
|
+
context "with default precision" do
|
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
|
+
let(:polyline) { "_p~iF~ps|U_ulLnnqC_mqNvxq`@" }
|
52
|
+
it "should raise for invalid input" do
|
53
|
+
expect { described_class.encode(points[0]) }.to raise_error(
|
54
|
+
TypeError,
|
55
|
+
"wrong argument type Float (expected Array)"
|
56
|
+
)
|
57
|
+
expect { described_class.encode([points]) }.to raise_error(
|
58
|
+
ArgumentError,
|
59
|
+
"wrong number of coordinates"
|
60
|
+
)
|
61
|
+
expect { described_class.encode([points[0..1]]) }.to raise_error(
|
62
|
+
TypeError,
|
63
|
+
"no implicit conversion to Float from Array"
|
64
|
+
)
|
65
|
+
end
|
66
|
+
# The method `_polyline_encode_number("", 16)` will check for a chunk
|
67
|
+
# of the size 32, which is the chunk size limit. This was errored due to
|
68
|
+
# a bad sign.
|
69
|
+
# Reported in issue #15, closed in PR #16
|
70
|
+
context "with points close to the chunk size limit" do
|
71
|
+
let(:points) { [[0, 0.00016]] }
|
72
|
+
let(:polyline) { "?_@" }
|
73
|
+
it "should encode points correctly" do
|
74
|
+
expect(described_class.encode(points)).to eq polyline
|
75
|
+
end
|
76
|
+
end
|
77
|
+
context "with default precision" do
|
78
|
+
it "should encode points correctly" do
|
79
|
+
expect(described_class.encode(points)).to eq polyline
|
80
|
+
end
|
81
|
+
end
|
82
|
+
context "with a 6 decimals precision" do
|
83
|
+
let(:polyline) { "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI" }
|
84
|
+
it "should encode points correctly" do
|
85
|
+
expect(described_class.encode(points, 6)).to eq polyline
|
86
|
+
end
|
87
|
+
end
|
88
|
+
context "with points that were close together" do
|
89
|
+
let(:points) { [[41.35222, -86.04563], [41.35222, -86.04544]] }
|
90
|
+
it "should encode points correctly" do
|
91
|
+
expect(described_class.encode(points)).to eq "krk{FdxdlO?e@"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
context "with possible rounding ambiguity" do
|
95
|
+
let(:points) { [[39.13594499,-94.4243478], [39.13558757,-94.4243471]] }
|
96
|
+
it "should encode points as Google API do" do
|
97
|
+
expect(described_class.encode(points)).to eq "svzmFdgi_QdA?"
|
98
|
+
end
|
99
|
+
end
|
100
|
+
context "with points out of bounds" do
|
101
|
+
it "should raise" do
|
102
|
+
expect { described_class.encode([[180.1, 0.0]]) }.to raise_error ArgumentError
|
103
|
+
expect { described_class.encode([[0, 180.1]]) }.to raise_error ArgumentError
|
104
|
+
expect { described_class.encode([[-180.1, 0.0]]) }.to raise_error ArgumentError
|
105
|
+
expect { described_class.encode([[0, -180.1]]) }.to raise_error ArgumentError
|
106
|
+
end
|
107
|
+
end
|
108
|
+
context "with a 15 decimals precision" do
|
109
|
+
it "should raise" do
|
110
|
+
expect { described_class.encode([[1.2, 1.2]], 15) }.to raise_error(ArgumentError, /too high/)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
context "with a negative precision" do
|
114
|
+
it "should raise" do
|
115
|
+
expect { described_class.encode([[1.2, 1.2]], -rand(1..10)) }.to raise_error(ArgumentError, /negative/)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
require "rubygems"
|
2
2
|
require "bundler/setup"
|
3
3
|
|
4
|
-
|
5
|
-
|
6
|
-
require "rspec-benchmark"
|
7
|
-
require "polylines"
|
4
|
+
$LOAD_PATH.unshift File.join(__dir__, "..", "lib")
|
5
|
+
$LOAD_PATH.unshift File.join(__dir__, "..", "ext")
|
8
6
|
|
9
7
|
RSpec.configure do |config|
|
10
|
-
|
8
|
+
# Additional config goes here
|
11
9
|
end
|
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.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cyrille Courtière
|
8
|
-
|
8
|
+
- Ulysse Buonomo
|
9
|
+
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2020-11-24 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
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
|
@@ -39,39 +40,43 @@ dependencies:
|
|
39
40
|
- !ruby/object:Gem::Version
|
40
41
|
version: '0.3'
|
41
42
|
- !ruby/object:Gem::Dependency
|
42
|
-
name: rspec
|
43
|
+
name: rspec
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
44
45
|
requirements:
|
45
46
|
- - "~>"
|
46
47
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
48
|
+
version: '3.5'
|
48
49
|
type: :development
|
49
50
|
prerelease: false
|
50
51
|
version_requirements: !ruby/object:Gem::Requirement
|
51
52
|
requirements:
|
52
53
|
- - "~>"
|
53
54
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
55
|
-
description:
|
55
|
+
version: '3.5'
|
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:
|
64
|
+
- ".rspec"
|
65
|
+
- CHANGELOG.md
|
62
66
|
- README.md
|
67
|
+
- ext/fast_polylines/extconf.rb
|
68
|
+
- ext/fast_polylines/fast_polylines.c
|
63
69
|
- lib/fast-polylines.rb
|
64
|
-
- lib/
|
65
|
-
- lib/
|
66
|
-
-
|
67
|
-
- spec/
|
68
|
-
- spec/fast-polylines/encoder_spec.rb
|
70
|
+
- lib/fast_polylines.rb
|
71
|
+
- lib/fast_polylines/version.rb
|
72
|
+
- spec/fast-polylines_spec.rb
|
73
|
+
- spec/fast_polylines_spec.rb
|
69
74
|
- spec/spec_helper.rb
|
70
|
-
homepage:
|
75
|
+
homepage: https://github.com/klaxit/fast-polylines
|
71
76
|
licenses:
|
72
77
|
- MIT
|
73
78
|
metadata: {}
|
74
|
-
post_install_message:
|
79
|
+
post_install_message:
|
75
80
|
rdoc_options: []
|
76
81
|
require_paths:
|
77
82
|
- lib
|
@@ -79,19 +84,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
84
|
requirements:
|
80
85
|
- - ">="
|
81
86
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
87
|
+
version: 2.4.6
|
83
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
84
89
|
requirements:
|
85
90
|
- - ">="
|
86
91
|
- !ruby/object:Gem::Version
|
87
92
|
version: '0'
|
88
93
|
requirements: []
|
89
|
-
|
90
|
-
|
91
|
-
signing_key:
|
94
|
+
rubygems_version: 3.1.2
|
95
|
+
signing_key:
|
92
96
|
specification_version: 4
|
93
97
|
summary: Fast & easy Google polylines
|
94
98
|
test_files:
|
95
|
-
- spec/fast-polylines/decoder_spec.rb
|
96
|
-
- spec/fast-polylines/encoder_spec.rb
|
97
99
|
- spec/spec_helper.rb
|
100
|
+
- spec/fast-polylines_spec.rb
|
101
|
+
- spec/fast_polylines_spec.rb
|
102
|
+
- ".rspec"
|
@@ -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,39 +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
|
-
it "should perform at least 5x faster than the Polylines gem" do
|
12
|
-
expect {
|
13
|
-
described_class.decode(polyline)
|
14
|
-
}.to perform_faster_than {
|
15
|
-
Polylines::Decoder.decode_polyline(polyline)
|
16
|
-
}.at_least(5).times
|
17
|
-
end
|
18
|
-
end
|
19
|
-
context "with 1e6 precision" do
|
20
|
-
let(:polyline) { "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI" }
|
21
|
-
it "should decode a polyline correctly" do
|
22
|
-
expect(described_class.decode(polyline, 1e6)).to eq points
|
23
|
-
end
|
24
|
-
end
|
25
|
-
context "with points that were close together" do
|
26
|
-
let(:points) { [[41.35222, -86.04563], [41.35222, -86.04544]] }
|
27
|
-
it "should decode a polyline correctly" do
|
28
|
-
expect(described_class.decode("krk{FdxdlO?e@")).to eq points
|
29
|
-
end
|
30
|
-
end
|
31
|
-
context "with points that cause float overflow" do
|
32
|
-
let(:polyline) { "ahdiHsmeMHPDN|A`FVt@" }
|
33
|
-
let(:points) { [[48.85137, 2.32682], [48.85132, 2.32673], [48.85129, 2.32665], [48.85082, 2.32552], [48.8507, 2.32525]] }
|
34
|
-
it "should respect precision" do
|
35
|
-
expect(described_class.decode(polyline)).to eq points
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
@@ -1,38 +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
|
-
it "should perform at least 5x faster than the Polylines gem" do
|
12
|
-
expect {
|
13
|
-
described_class.encode(points)
|
14
|
-
}.to perform_faster_than {
|
15
|
-
Polylines::Encoder.encode_points(points)
|
16
|
-
}.at_least(5).times
|
17
|
-
end
|
18
|
-
end
|
19
|
-
context "with 1e6 precision" do
|
20
|
-
let(:polyline) { "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI" }
|
21
|
-
it "should encode points correctly" do
|
22
|
-
expect(described_class.encode(points, 1e6)).to eq polyline
|
23
|
-
end
|
24
|
-
end
|
25
|
-
context "with points that were close together" do
|
26
|
-
let(:points) { [[41.35222, -86.04563], [41.35222, -86.04544]] }
|
27
|
-
it "should encode points correctly" do
|
28
|
-
expect(described_class.encode(points)).to eq "krk{FdxdlO?e@"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
context "with possible rounding ambiguity" do
|
32
|
-
let(:points) { [[39.13594499,-94.4243478], [39.13558757,-94.4243471]] }
|
33
|
-
it "should encode points as Google API do" do
|
34
|
-
expect(described_class.encode(points)).to eq "svzmFdgi_QdA?"
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|