fast-polylines 0.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![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 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
|