├── .github └── workflows │ └── main.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── example_test.go ├── go.mod ├── go.sum ├── polyline.go └── polyline_test.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go-version: 15 | - stable 16 | - oldstable 17 | steps: 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 19 | - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a 20 | with: 21 | cache: true 22 | go-version: ${{ matrix.go-version }} 23 | - name: Build 24 | run: go build ./... 25 | - name: Test 26 | run: go test -covermode=atomic -coverprofile=profile.cov -race ./... 27 | - uses: shogo82148/actions-goveralls@7b1bd2871942af030d707d6574e5f684f9891fb2 28 | with: 29 | path-to-profile: profile.cov 30 | lint: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 34 | - uses: actions/setup-go@3041bf56c941b39c61721a86cd11f3bb1338122a 35 | with: 36 | cache: true 37 | go-version: stable 38 | - uses: golangci/golangci-lint-action@971e284b6050e8a5849b72094c50ab08da042db8 39 | with: 40 | version: v1.62.2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | /corpus 3 | /coverage.out 4 | /crashes 5 | /polyline-fuzz.zip 6 | /suppressions 7 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | go: '1.22' 3 | 4 | linters: 5 | enable: 6 | - asasalint 7 | - asciicheck 8 | - bidichk 9 | - bodyclose 10 | - canonicalheader 11 | - containedctx 12 | - contextcheck 13 | - copyloopvar 14 | - decorder 15 | - dogsled 16 | - dupl 17 | - dupword 18 | - durationcheck 19 | - err113 20 | - errcheck 21 | - errchkjson 22 | - errname 23 | - errorlint 24 | - exhaustive 25 | - fatcontext 26 | - forbidigo 27 | - forcetypeassert 28 | - gci 29 | - ginkgolinter 30 | - gocheckcompilerdirectives 31 | - gochecknoinits 32 | - gochecksumtype 33 | - gocognit 34 | - goconst 35 | - gocritic 36 | - gocyclo 37 | - godot 38 | - godox 39 | - gofmt 40 | - gofumpt 41 | - goheader 42 | - goimports 43 | - gomoddirectives 44 | - gomodguard 45 | - goprintffuncname 46 | - gosimple 47 | - gosmopolitan 48 | - govet 49 | - grouper 50 | - iface 51 | - importas 52 | - inamedparam 53 | - ineffassign 54 | - interfacebloat 55 | - intrange 56 | - ireturn 57 | - lll 58 | - loggercheck 59 | - maintidx 60 | - makezero 61 | - mirror 62 | - misspell 63 | - musttag 64 | - nakedret 65 | - nestif 66 | - nilerr 67 | - nilnil 68 | - noctx 69 | - nolintlint 70 | - nonamedreturns 71 | - nosprintfhostport 72 | - perfsprint 73 | - prealloc 74 | - predeclared 75 | - promlinter 76 | - protogetter 77 | - reassign 78 | - recvcheck 79 | - revive 80 | - rowserrcheck 81 | - sloglint 82 | - spancheck 83 | - sqlclosecheck 84 | - staticcheck 85 | - stylecheck 86 | - tagalign 87 | - tagliatelle 88 | - tenv 89 | - testifylint 90 | - testpackage 91 | - thelper 92 | - typecheck 93 | - unconvert 94 | - unparam 95 | - unused 96 | - usestdlibvars 97 | - wastedassign 98 | - whitespace 99 | - zerologlint 100 | disable: 101 | - cyclop 102 | - depguard 103 | - exhaustruct 104 | - funlen 105 | - gochecknoglobals 106 | - gosec 107 | - mnd 108 | - nlreturn 109 | - paralleltest 110 | - testableexamples 111 | - tparallel 112 | - varnamelen 113 | - wrapcheck 114 | - wsl 115 | 116 | linters-settings: 117 | gci: 118 | sections: 119 | - standard 120 | - default 121 | - prefix(github.com/twpayne/go-polyline) 122 | gofumpt: 123 | extra-rules: true 124 | module-path: github.com/twpayne/go-polyline 125 | goimports: 126 | local-prefixes: github.com/twpayne/go-polyline 127 | misspell: 128 | locale: US 129 | 130 | issues: 131 | exclude-rules: 132 | - linters: 133 | - err113 134 | text: "do not define dynamic errors, use wrapped static errors instead" 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Tom Payne 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-polyline 2 | 3 | [![Build Status](https://github.com/twpayne/go-polyline/workflows/Test/badge.svg)](https://github.com/twpayne/go-polyline/actions?query=workflow%3ATest) 4 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/twpayne/go-polyline)](https://pkg.go.dev/github.com/twpayne/go-polyline) 5 | [![Coverage Status](https://coveralls.io/repos/github/twpayne/go-polyline/badge.svg)](https://coveralls.io/github/twpayne/go-polyline) 6 | 7 | Package `polyline` implements a Google Maps Encoding Polyline encoder and decoder. 8 | 9 | ## Encoding example 10 | 11 | ```go 12 | func ExampleEncodeCoords() { 13 | coords := [][]float64{ 14 | {38.5, -120.2}, 15 | {40.7, -120.95}, 16 | {43.252, -126.453}, 17 | } 18 | fmt.Println(string(polyline.EncodeCoords(coords))) 19 | // Output: _p~iF~ps|U_ulLnnqC_mqNvxq`@ 20 | } 21 | ``` 22 | 23 | ## Decoding example 24 | 25 | ```go 26 | func ExampleDecodeCoords() { 27 | buf := []byte("_p~iF~ps|U_ulLnnqC_mqNvxq`@") 28 | coords, _, _ := polyline.DecodeCoords(buf) 29 | fmt.Println(coords) 30 | // Output: [[38.5 -120.2] [40.7 -120.95] [43.252 -126.453]] 31 | } 32 | ``` 33 | 34 | ## License 35 | 36 | BSD-2-Clause 37 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package polyline_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/twpayne/go-polyline" 7 | ) 8 | 9 | func ExampleEncodeCoords() { 10 | coords := [][]float64{ 11 | {38.5, -120.2}, 12 | {40.7, -120.95}, 13 | {43.252, -126.453}, 14 | } 15 | fmt.Println(string(polyline.EncodeCoords(coords))) 16 | // Output: _p~iF~ps|U_ulLnnqC_mqNvxq`@ 17 | } 18 | 19 | func ExampleDecodeCoords() { 20 | buf := []byte("_p~iF~ps|U_ulLnnqC_mqNvxq`@") 21 | coords, _, _ := polyline.DecodeCoords(buf) 22 | fmt.Println(coords) 23 | // Output: [[38.5 -120.2] [40.7 -120.95] [43.252 -126.453]] 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/twpayne/go-polyline 2 | 3 | go 1.22 4 | 5 | require github.com/alecthomas/assert/v2 v2.2.1 6 | 7 | require ( 8 | github.com/alecthomas/repr v0.2.0 // indirect 9 | github.com/hexops/gotextdiff v1.0.3 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= 2 | github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= 3 | github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk= 4 | github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 5 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 6 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 7 | -------------------------------------------------------------------------------- /polyline.go: -------------------------------------------------------------------------------- 1 | // Package polyline implements a Google Maps Encoding Polyline encoder and 2 | // decoder. See 3 | // https://developers.google.com/maps/documentation/utilities/polylinealgorithm. 4 | // 5 | // The default codec encodes and decodes two-dimensional coordinates scaled by 6 | // 1e5. For other dimensionalities and scales create a custom Codec. 7 | // 8 | // The package operates on byte slices. Encoding functions take an existing byte 9 | // slice as input (which can be nil) and return a new byte slice with the 10 | // encoded value appended to it, similarly to how Go's append function works. To 11 | // increase performance, you can pre-allocate byte slices, for example by 12 | // passing make([]byte, 0, 128) as the input byte slice. Similarly, decoding 13 | // functions take a byte slice as input and return the remaining unconsumed 14 | // bytes as output. 15 | package polyline 16 | 17 | import ( 18 | "errors" 19 | "math" 20 | "strconv" 21 | ) 22 | 23 | // Errors. 24 | var ( 25 | ErrDimensionalMismatch = errors.New("dimensional mismatch") 26 | ErrEmpty = errors.New("empty") 27 | ErrInvalidByte = errors.New("invalid byte") 28 | ErrOverflow = errors.New("overflow") 29 | ErrUnterminatedSequence = errors.New("unterminated sequence") 30 | ) 31 | 32 | func round(x float64) int { 33 | if x < 0 { 34 | return int(-math.Floor(-x + 0.5)) 35 | } 36 | return int(math.Floor(x + 0.5)) 37 | } 38 | 39 | // A Codec represents an encoder. 40 | type Codec struct { 41 | Dim int // Dimensionality, normally 2 42 | Scale float64 // Scale, normally 1e5 43 | } 44 | 45 | var defaultCodec = Codec{Dim: 2, Scale: 1e5} 46 | 47 | // DecodeUint decodes a single unsigned integer from buf. It returns the decoded 48 | // uint, the remaining unconsumed bytes of buf, and any error. 49 | func DecodeUint(buf []byte) (uint, []byte, error) { 50 | if len(buf) == 0 { 51 | return 0, nil, ErrEmpty 52 | } 53 | n := strconv.IntSize / 5 54 | if n > len(buf) { 55 | n = len(buf) 56 | } 57 | var u, shift uint 58 | for i := range n { 59 | switch b := buf[i]; { 60 | case 95 <= b && b < 127: 61 | u += (uint(b) - 95) << shift 62 | shift += 5 63 | case 63 <= b && b < 95: 64 | u += (uint(b) - 63) << shift 65 | return u, buf[i+1:], nil 66 | default: 67 | return 0, nil, ErrInvalidByte 68 | } 69 | } 70 | if len(buf) <= strconv.IntSize/5 { 71 | return 0, nil, ErrUnterminatedSequence 72 | } 73 | maxDigit := byte(1<<(strconv.IntSize-5*(strconv.IntSize/5)) - 1) 74 | switch b := buf[n]; { 75 | case 63 <= b && b <= 63+maxDigit: 76 | u += (uint(b) - 63) << shift 77 | return u, buf[n+1:], nil 78 | case b < 127: 79 | return 0, nil, ErrOverflow 80 | default: 81 | return 0, nil, ErrInvalidByte 82 | } 83 | } 84 | 85 | // DecodeInt decodes a single signed integer from buf. It returns the decoded 86 | // int, the remaining unconsumed bytes of buf, and any error. 87 | func DecodeInt(buf []byte) (int, []byte, error) { 88 | switch u, buf, err := DecodeUint(buf); { 89 | case err != nil: 90 | return 0, nil, err 91 | case u&1 == 0: 92 | return int(u >> 1), buf, nil 93 | case u == math.MaxUint64: 94 | return math.MinInt64, buf, nil 95 | default: 96 | return -int((u + 1) >> 1), buf, nil 97 | } 98 | } 99 | 100 | // EncodeUint appends the encoding of a single unsigned integer u to buf and 101 | // returns the new buf. 102 | func EncodeUint(buf []byte, u uint) []byte { 103 | for u >= 32 { 104 | buf = append(buf, byte((u&31)+95)) 105 | u >>= 5 106 | } 107 | buf = append(buf, byte(u+63)) 108 | return buf 109 | } 110 | 111 | // EncodeInt appends the encoding of a single signed integer i to buf and 112 | // returns the new buf. 113 | func EncodeInt(buf []byte, i int) []byte { 114 | var u uint 115 | if i < 0 { 116 | u = uint(^(i << 1)) 117 | } else { 118 | u = uint(i << 1) 119 | } 120 | return EncodeUint(buf, u) 121 | } 122 | 123 | // DecodeCoord decodes a single coordinate from buf. It returns the coordinate, 124 | // the remaining unconsumed bytes of buf, and any error. 125 | func (c Codec) DecodeCoord(buf []byte) ([]float64, []byte, error) { 126 | coord := make([]float64, c.Dim) 127 | for i := range coord { 128 | var err error 129 | var j int 130 | j, buf, err = DecodeInt(buf) 131 | if err != nil { 132 | return nil, nil, err 133 | } 134 | coord[i] = float64(j) / c.Scale 135 | } 136 | return coord, buf, nil 137 | } 138 | 139 | // DecodeCoords decodes an array of coordinates from buf. It returns the 140 | // coordinates, the remaining unconsumed bytes of buf, and any error. 141 | func (c Codec) DecodeCoords(buf []byte) ([][]float64, []byte, error) { 142 | if len(buf) == 0 { 143 | return nil, buf, nil 144 | } 145 | var coord []float64 146 | var err error 147 | coord, buf, err = c.DecodeCoord(buf) 148 | if err != nil { 149 | return nil, nil, err 150 | } 151 | coords := [][]float64{coord} 152 | for i := 1; len(buf) > 0; i++ { 153 | coord, buf, err = c.DecodeCoord(buf) 154 | if err != nil { 155 | return nil, nil, err 156 | } 157 | for j := range coord { 158 | coord[j] += coords[i-1][j] 159 | } 160 | coords = append(coords, coord) 161 | } 162 | return coords, nil, nil 163 | } 164 | 165 | // DecodeFlatCoords decodes coordinates from buf, appending them to a 166 | // one-dimensional array. It returns the coordinates, the remaining unconsumed 167 | // bytes in buf, and any error. 168 | func (c Codec) DecodeFlatCoords(flatCoords []float64, buf []byte) ([]float64, []byte, error) { 169 | if len(flatCoords)%c.Dim != 0 { 170 | return nil, nil, ErrDimensionalMismatch 171 | } 172 | last := make([]int, c.Dim) 173 | for len(buf) > 0 { 174 | for j := range c.Dim { 175 | var err error 176 | var k int 177 | k, buf, err = DecodeInt(buf) 178 | if err != nil { 179 | return nil, nil, err 180 | } 181 | last[j] += k 182 | flatCoords = append(flatCoords, float64(last[j])/c.Scale) 183 | } 184 | } 185 | return flatCoords, nil, nil 186 | } 187 | 188 | // EncodeCoord encodes a single coordinate to buf and returns the new buf. 189 | func (c Codec) EncodeCoord(buf []byte, coord []float64) []byte { 190 | for _, x := range coord { 191 | buf = EncodeInt(buf, round(c.Scale*x)) 192 | } 193 | return buf 194 | } 195 | 196 | // EncodeCoords appends the encoding of an array of coordinates coords to buf 197 | // and returns the new buf. 198 | func (c Codec) EncodeCoords(buf []byte, coords [][]float64) []byte { 199 | last := make([]int, c.Dim) 200 | for _, coord := range coords { 201 | for i, x := range coord { 202 | ex := round(c.Scale * x) 203 | buf = EncodeInt(buf, ex-last[i]) 204 | last[i] = ex 205 | } 206 | } 207 | return buf 208 | } 209 | 210 | // EncodeFlatCoords encodes a one-dimensional array of coordinates to buf. It 211 | // returns the new buf and any error. 212 | func (c Codec) EncodeFlatCoords(buf []byte, flatCoords []float64) ([]byte, error) { 213 | if len(flatCoords)%c.Dim != 0 { 214 | return nil, ErrDimensionalMismatch 215 | } 216 | last := make([]int, c.Dim) 217 | for i, x := range flatCoords { 218 | ex := round(c.Scale * x) 219 | j := i % c.Dim 220 | buf = EncodeInt(buf, ex-last[j]) 221 | last[j] = ex 222 | } 223 | return buf, nil 224 | } 225 | 226 | // DecodeCoord decodes a single coordinate from buf using the default codec. It 227 | // returns the coordinate, the remaining bytes in buf, and any error. 228 | func DecodeCoord(buf []byte) ([]float64, []byte, error) { 229 | return defaultCodec.DecodeCoord(buf) 230 | } 231 | 232 | // DecodeCoords decodes an array of coordinates from buf using the default 233 | // codec. It returns the coordinates, the remaining bytes in buf, and any error. 234 | func DecodeCoords(buf []byte) ([][]float64, []byte, error) { 235 | return defaultCodec.DecodeCoords(buf) 236 | } 237 | 238 | // EncodeCoord returns the encoding of an array of coordinates using the default 239 | // codec. 240 | func EncodeCoord(coord []float64) []byte { 241 | return defaultCodec.EncodeCoord(nil, coord) 242 | } 243 | 244 | // EncodeCoords returns the encoding of an array of coordinates using the 245 | // default codec. 246 | func EncodeCoords(coords [][]float64) []byte { 247 | return defaultCodec.EncodeCoords(nil, coords) 248 | } 249 | -------------------------------------------------------------------------------- /polyline_test.go: -------------------------------------------------------------------------------- 1 | package polyline_test 2 | 3 | import ( 4 | "math" 5 | "math/rand" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | "testing/quick" 10 | 11 | "github.com/alecthomas/assert/v2" 12 | 13 | "github.com/twpayne/go-polyline" 14 | ) 15 | 16 | func TestUint(t *testing.T) { 17 | for i, tc := range []struct { 18 | u uint 19 | s string 20 | nonCanonical bool 21 | }{ 22 | {u: 0, s: "?"}, 23 | {u: 2, s: "a?", nonCanonical: true}, 24 | {u: 2, s: "A"}, 25 | {u: 31, s: "^"}, 26 | {u: 32, s: "_@"}, 27 | {u: 174, s: "mD"}, 28 | {u: 18446744073709551614, s: "}~~~~~~~~~~~N"}, 29 | {u: 18446744073709551615, s: "~~~~~~~~~~~~N"}, 30 | } { 31 | t.Run(strconv.Itoa(i), func(t *testing.T) { 32 | got, b, err := polyline.DecodeUint([]byte(tc.s)) 33 | assert.NoError(t, err) 34 | assert.Equal(t, tc.u, got) 35 | assert.Equal(t, 0, len(b)) 36 | if !tc.nonCanonical { 37 | assert.Equal(t, []byte(tc.s), polyline.EncodeUint(nil, tc.u)) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestDecodeErrors(t *testing.T) { 44 | for i, tc := range []struct { 45 | s string 46 | err error 47 | coordsErr error 48 | }{ 49 | {s: "", err: polyline.ErrEmpty}, 50 | {s: ">", err: polyline.ErrInvalidByte, coordsErr: polyline.ErrInvalidByte}, 51 | {s: "\x80", err: polyline.ErrInvalidByte, coordsErr: polyline.ErrInvalidByte}, 52 | {s: "_", err: polyline.ErrUnterminatedSequence, coordsErr: polyline.ErrUnterminatedSequence}, 53 | {s: "~~~~~~~~~~~~", err: polyline.ErrUnterminatedSequence, coordsErr: polyline.ErrUnterminatedSequence}, 54 | {s: "~~~~~~~~~~~~O", err: polyline.ErrOverflow, coordsErr: polyline.ErrOverflow}, 55 | {s: "~~~~~~~~~~~~_", err: polyline.ErrOverflow, coordsErr: polyline.ErrOverflow}, 56 | {s: "~~~~~~~~~~~~\x80", err: polyline.ErrInvalidByte, coordsErr: polyline.ErrInvalidByte}, 57 | } { 58 | t.Run(strconv.Itoa(i), func(t *testing.T) { 59 | _, _, err := polyline.DecodeUint([]byte(tc.s)) 60 | assert.Equal(t, tc.err, err) 61 | _, _, err = polyline.DecodeInt([]byte(tc.s)) 62 | assert.Equal(t, tc.err, err) 63 | _, _, err = polyline.DecodeCoord([]byte(tc.s)) 64 | assert.Equal(t, tc.err, err) 65 | _, _, err = polyline.DecodeCoords([]byte(tc.s)) 66 | assert.Equal(t, tc.coordsErr, err) 67 | c := polyline.Codec{Dim: 1, Scale: 1e5} 68 | _, _, err = c.DecodeFlatCoords([]float64{0}, []byte(tc.s)) 69 | assert.Equal(t, tc.coordsErr, err) 70 | }) 71 | } 72 | } 73 | 74 | func TestMultidimensionalDecodeErrors(t *testing.T) { 75 | for i, tc := range []struct { 76 | s string 77 | err error 78 | }{ 79 | {s: "_p~iF~ps|U_p~iF>", err: polyline.ErrInvalidByte}, 80 | {s: "_p~iF~ps|U_p~iF\x80", err: polyline.ErrInvalidByte}, 81 | {s: "_p~iF~ps|U_p~iF~ps|", err: polyline.ErrUnterminatedSequence}, 82 | } { 83 | t.Run(strconv.Itoa(i), func(t *testing.T) { 84 | _, _, err := polyline.DecodeCoords([]byte(tc.s)) 85 | assert.Equal(t, tc.err, err) 86 | c := polyline.Codec{Dim: 2, Scale: 1e5} 87 | _, _, err = c.DecodeFlatCoords([]float64{0, 0}, []byte(tc.s)) 88 | assert.Equal(t, tc.err, err) 89 | }) 90 | } 91 | } 92 | 93 | func TestInt(t *testing.T) { 94 | for i, tc := range []struct { 95 | i int 96 | s string 97 | }{ 98 | {i: 3850000, s: "_p~iF"}, 99 | {i: -12020000, s: "~ps|U"}, 100 | {i: -17998321, s: "`~oia@"}, 101 | {i: 220000, s: "_ulL"}, 102 | {i: -75000, s: "nnqC"}, 103 | {i: 255200, s: "_mqN"}, 104 | {i: -550300, s: "vxq`@"}, 105 | {i: math.MaxInt64, s: "}~~~~~~~~~~~N"}, 106 | {i: math.MaxInt64 - 1, s: "{~~~~~~~~~~~N"}, 107 | {i: math.MinInt64 + 1, s: "|~~~~~~~~~~~N"}, 108 | {i: math.MinInt64, s: "~~~~~~~~~~~~N"}, 109 | } { 110 | t.Run(strconv.Itoa(i), func(t *testing.T) { 111 | got, b, err := polyline.DecodeInt([]byte(tc.s)) 112 | assert.NoError(t, err) 113 | assert.Equal(t, 0, len(b)) 114 | assert.Equal(t, tc.i, got) 115 | assert.Equal(t, []byte(tc.s), polyline.EncodeInt(nil, tc.i)) 116 | }) 117 | } 118 | } 119 | 120 | func TestCoord(t *testing.T) { 121 | for i, tc := range []struct { 122 | s string 123 | c []float64 124 | nonCanonical bool 125 | }{ 126 | { 127 | s: "_p~iF~ps|U", 128 | c: []float64{38.5, -120.2}, 129 | }, 130 | { 131 | s: "a?Z", 132 | c: []float64{1e-05, -0.00014}, 133 | nonCanonical: true, 134 | }, 135 | } { 136 | t.Run(strconv.Itoa(i), func(t *testing.T) { 137 | got, b, err := polyline.DecodeCoord([]byte(tc.s)) 138 | assert.NoError(t, err) 139 | assert.Equal(t, 0, len(b)) 140 | assert.Equal(t, tc.c, got) 141 | if !tc.nonCanonical { 142 | assert.Equal(t, []byte(tc.s), polyline.EncodeCoord(tc.c)) 143 | } 144 | }) 145 | } 146 | } 147 | 148 | func TestCoords(t *testing.T) { 149 | for i, tc := range []struct { 150 | cs [][]float64 151 | s string 152 | }{ 153 | { 154 | cs: [][]float64{{38.5, -120.2}, {40.7, -120.95}, {43.252, -126.453}}, 155 | s: "_p~iF~ps|U_ulLnnqC_mqNvxq`@", 156 | }, 157 | } { 158 | t.Run(strconv.Itoa(i), func(t *testing.T) { 159 | got, b, err := polyline.DecodeCoords([]byte(tc.s)) 160 | assert.NoError(t, err) 161 | assert.Equal(t, 0, len(b)) 162 | assert.Equal(t, tc.cs, got) 163 | assert.Equal(t, []byte(tc.s), polyline.EncodeCoords(tc.cs)) 164 | }) 165 | } 166 | } 167 | 168 | func TestFlatCoords(t *testing.T) { 169 | for i, tc := range []struct { 170 | fcs []float64 171 | s string 172 | }{ 173 | { 174 | fcs: []float64{38.5, -120.2, 40.7, -120.95, 43.252, -126.453}, 175 | s: "_p~iF~ps|U_ulLnnqC_mqNvxq`@", 176 | }, 177 | } { 178 | t.Run(strconv.Itoa(i), func(t *testing.T) { 179 | codec := polyline.Codec{Dim: 2, Scale: 1e5} 180 | gotFCS, b, err := codec.DecodeFlatCoords(nil, []byte(tc.s)) 181 | assert.NoError(t, err) 182 | assert.Equal(t, 0, len(b)) 183 | assert.Equal(t, tc.fcs, gotFCS) 184 | gotBytes, err := codec.EncodeFlatCoords(nil, tc.fcs) 185 | assert.NoError(t, err) 186 | assert.Equal(t, []byte(tc.s), gotBytes) 187 | }) 188 | } 189 | } 190 | 191 | func TestFlatCoordsEmpty(t *testing.T) { 192 | codec := polyline.Codec{Dim: 2, Scale: 1e5} 193 | gotFCS, b, err := codec.DecodeFlatCoords(nil, nil) 194 | assert.NoError(t, err) 195 | assert.Equal(t, 0, len(b)) 196 | assert.Equal(t, 0, len(gotFCS)) 197 | gotBytes, err := codec.EncodeFlatCoords(nil, nil) 198 | assert.NoError(t, err) 199 | assert.Equal(t, 0, len(gotBytes)) 200 | } 201 | 202 | func TestDecodeFlatCoordsErrors(t *testing.T) { 203 | for i, tc := range []struct { 204 | fcs []float64 205 | s string 206 | err error 207 | }{ 208 | { 209 | fcs: []float64{0}, 210 | s: "", 211 | err: polyline.ErrDimensionalMismatch, 212 | }, 213 | { 214 | s: "_p~iF", 215 | err: polyline.ErrEmpty, 216 | }, 217 | { 218 | s: "_p~iF~ps|U_p~iF", 219 | err: polyline.ErrEmpty, 220 | }, 221 | } { 222 | t.Run(strconv.Itoa(i), func(t *testing.T) { 223 | codec := polyline.Codec{Dim: 2, Scale: 1e5} 224 | _, _, err := codec.DecodeFlatCoords(tc.fcs, []byte(tc.s)) 225 | assert.Equal(t, tc.err, err) 226 | }) 227 | } 228 | } 229 | 230 | func TestEncodeFlatCoordErrors(t *testing.T) { 231 | for i, tc := range []struct { 232 | fcs []float64 233 | err error 234 | }{ 235 | { 236 | fcs: []float64{0}, 237 | err: polyline.ErrDimensionalMismatch, 238 | }, 239 | } { 240 | t.Run(strconv.Itoa(i), func(t *testing.T) { 241 | codec := polyline.Codec{Dim: 2, Scale: 1e5} 242 | _, err := codec.EncodeFlatCoords(nil, tc.fcs) 243 | assert.Equal(t, tc.err, err) 244 | }) 245 | } 246 | } 247 | 248 | func TestCodec(t *testing.T) { 249 | for i, tc := range []struct { 250 | c polyline.Codec 251 | cs [][]float64 252 | s string 253 | }{ 254 | { 255 | c: polyline.Codec{Dim: 2, Scale: 1e5}, 256 | cs: [][]float64{{38.5, -120.2}, {40.7, -120.95}, {43.252, -126.453}}, 257 | s: "_p~iF~ps|U_ulLnnqC_mqNvxq`@", 258 | }, 259 | { 260 | c: polyline.Codec{Dim: 2, Scale: 1e6}, 261 | cs: [][]float64{{38.5, -120.2}, {40.7, -120.95}, {43.252, -126.453}}, 262 | s: "_izlhA~rlgdF_{geC~ywl@_kwzCn`{nI", 263 | }, 264 | } { 265 | t.Run(strconv.Itoa(i), func(t *testing.T) { 266 | got, b, err := tc.c.DecodeCoords([]byte(tc.s)) 267 | assert.NoError(t, err) 268 | assert.Equal(t, tc.cs, got) 269 | assert.Equal(t, 0, len(b)) 270 | assert.Equal(t, []byte(tc.s), tc.c.EncodeCoords(nil, tc.cs)) 271 | }) 272 | } 273 | } 274 | 275 | func float64ArrayWithin(a, b []float64, prec float64) bool { 276 | if len(a) != len(b) { 277 | return false 278 | } 279 | for i, xa := range a { 280 | if math.Abs(xa-b[i]) > prec { 281 | return false 282 | } 283 | } 284 | return true 285 | } 286 | 287 | type QuickCoords [][]float64 288 | 289 | func (qc QuickCoords) Generate(r *rand.Rand, size int) reflect.Value { 290 | result := make([][]float64, size) 291 | for i := range result { 292 | result[i] = []float64{180*r.Float64() - 90, 360*r.Float64() - 180} 293 | } 294 | return reflect.ValueOf(result) 295 | } 296 | 297 | func TestCoordsQuick(t *testing.T) { 298 | f := func(qc QuickCoords) bool { 299 | buf := polyline.EncodeCoords([][]float64(qc)) 300 | cs, buf, err := polyline.DecodeCoords(buf) 301 | if len(buf) != 0 || err != nil { 302 | return false 303 | } 304 | if len(cs) != len(qc) { 305 | return false 306 | } 307 | for i, c := range cs { 308 | if !float64ArrayWithin(c, qc[i], 5e-6) { 309 | return false 310 | } 311 | } 312 | return true 313 | } 314 | assert.NoError(t, quick.Check(f, nil)) 315 | } 316 | 317 | type QuickFlatCoords []float64 318 | 319 | func (qfc QuickFlatCoords) Generate(r *rand.Rand, size int) reflect.Value { 320 | result := make([]float64, 2*size) 321 | for i := range result { 322 | if i%2 == 0 { 323 | result[i] = 180*r.Float64() - 90 324 | } else { 325 | result[i] = 360*r.Float64() - 180 326 | } 327 | } 328 | return reflect.ValueOf(result) 329 | } 330 | 331 | func TestFlatCoordsQuick(t *testing.T) { 332 | f := func(fqc QuickFlatCoords) bool { 333 | codec := polyline.Codec{Dim: 2, Scale: 1e5} 334 | buf, err := codec.EncodeFlatCoords(nil, []float64(fqc)) 335 | if err != nil { 336 | return false 337 | } 338 | fcs, _, err := codec.DecodeFlatCoords(nil, buf) 339 | if err != nil { 340 | return false 341 | } 342 | return float64ArrayWithin([]float64(fqc), fcs, 5e-6) 343 | } 344 | assert.NoError(t, quick.Check(f, nil)) 345 | } 346 | 347 | func FuzzDecodeCoords(f *testing.F) { 348 | f.Add([]byte("_p~iF~ps|U")) 349 | f.Fuzz(func(_ *testing.T, data []byte) { 350 | _, _, _ = polyline.DecodeCoords(data) 351 | }) 352 | } 353 | --------------------------------------------------------------------------------