├── .gitignore ├── examples ├── fig1.png ├── fig2.png └── example.jl ├── .github └── workflows │ └── TagBot.yml ├── Project.toml ├── .travis.yml ├── src ├── decode.jl ├── Polyline.jl ├── googleMaps.jl ├── loadGPX.jl └── encode.jl ├── LICENSE.md ├── README.md └── test └── runtests.jl /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | Manifest.toml 3 | -------------------------------------------------------------------------------- /examples/fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikunia/Polyline.jl/master/examples/fig1.png -------------------------------------------------------------------------------- /examples/fig2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wikunia/Polyline.jl/master/examples/fig2.png -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | schedule: 4 | - cron: 0 * * * * 5 | jobs: 6 | TagBot: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: JuliaRegistries/TagBot@v1 10 | with: 11 | token: ${{ secrets.GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "Polyline" 2 | uuid = "737ade88-6689-11e9-2266-a1567ed38d42" 3 | authors = ["Nikola Stoyanov "] 4 | version = "1.0.0" 5 | 6 | [deps] 7 | HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" 8 | LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179" 9 | 10 | [compat] 11 | HTTP = "0.8" 12 | LightXML = "0.8" 13 | julia = "1.3" 14 | 15 | [extras] 16 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 17 | 18 | [targets] 19 | test = ["Test"] 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Documentation: http://docs.travis-ci.com/user/languages/julia/ 2 | language: julia 3 | os: 4 | - linux 5 | - osx 6 | julia: 7 | - 1.3 8 | - nightly 9 | notifications: 10 | email: true 11 | 12 | matrix: 13 | allow_failures: 14 | - julia: nightly 15 | 16 | after_success: 17 | # push coverage results to Coveralls 18 | - julia -e 'using Pkg; 19 | Pkg.add("Coverage"); 20 | using Coverage; 21 | Coveralls.submit(Coveralls.process_folder())' 22 | -------------------------------------------------------------------------------- /src/decode.jl: -------------------------------------------------------------------------------- 1 | function transformPolyline(value, index) 2 | end 3 | 4 | decodePolyline(str::AbstractString; kwargs...) = decodePolyline(Vector{UInt8}(str); kwargs...) 5 | function decodePolyline(polyline::Vector{UInt8}; precision=5) 6 | nums = Int32[0] 7 | count = 0 8 | for chunk = polyline .- 0x3f 9 | 10 | # accumulate bit chunks starting at LSB 11 | nums[end] |= eltype(nums)(chunk & 0x1f) << (precision*count) 12 | 13 | # the bit string continues if this bit is set, otherwise move to next 14 | if chunk & 0x20 == 0x20 15 | count += 1 16 | else 17 | nums[end] = (isodd(nums[end]) ? ~nums[end] : nums[end]) >> 1; 18 | count = 0 19 | push!(nums, 0) 20 | end 21 | end 22 | 23 | # rescale and accumulate the flat list of numbers 24 | return cumsum(reshape(nums[1:end-1]./10^precision, 2, :); dims=2)' 25 | end 26 | -------------------------------------------------------------------------------- /src/Polyline.jl: -------------------------------------------------------------------------------- 1 | #= Polyline encoder and decoder 2 | 3 | This module performes an encoding and decoding for 4 | GPS coordinates into a polyline using the algorithm 5 | detailed in: 6 | https://developers.google.com/maps/documentation/utilities/polylinealgorithm 7 | 8 | Example: 9 | julia> enc = encodePolyline([[38.5 -120.2]; [40.7 -120.95]; [43.252 -126.453]]) 10 | "_p~iF~ps|U_ulLnnqC_mqNvxq`@" 11 | 12 | julia> dec = decodePolyline("_p~iF~ps|U_ulLnnqC_mqNvxq`@") 13 | [[38.5 -120.2]; [40.7 -120.95]; [43.252 -126.453]] 14 | 15 | =# 16 | 17 | __precompile__() 18 | 19 | module Polyline 20 | 21 | using HTTP 22 | 23 | # coordinate is a type to represent a GPS data point 24 | struct coordinate{T} 25 | Lat::T 26 | Lng::T 27 | end 28 | 29 | include("encode.jl") 30 | include("decode.jl") 31 | include("loadGPX.jl") 32 | include("googleMaps.jl") 33 | 34 | export decodePolyline, encodePolyline, getMapImage, mapsURL 35 | 36 | end # module 37 | -------------------------------------------------------------------------------- /src/googleMaps.jl: -------------------------------------------------------------------------------- 1 | function mapsURL(path::String; type="terrain", token::String=ENV["GOOGLE_MAPS_API"], 2 | size=800, scale=2, MarkersStart=(0.0, 0.0), MarkersEnd=(0.0, 0.0)) 3 | mapURL = "https://maps.googleapis.com/maps/api/staticmap?maptype=" 4 | mapType = type 5 | mapPath = "&path=enc:" * path * "&" 6 | mapKey = "key=$token" * "&" 7 | mapSize = "size=$size" * "x$size" * "&" 8 | mapScale = "scale=$scale" 9 | 10 | mapMarkersStart = "" 11 | if MarkersStart != (0.0, 0.0) 12 | mapMarkersStart = "&markers=color:yellow|label:S|$(MarkersStart[1]),$(MarkersStart[2])" 13 | end 14 | 15 | mapMarkersEnd = "" 16 | if MarkersEnd != (0.0, 0.0) 17 | mapMarkersEnd = "&markers=color:green|label:F|$(MarkersEnd[1]),$(MarkersEnd[2])" 18 | end 19 | 20 | return mapURL * mapType * mapPath * mapKey * mapSize * mapScale * 21 | mapMarkersStart * mapMarkersEnd 22 | end 23 | 24 | function getMapImage(URL::String; pathFig="/tmp/polyline.png") 25 | download(URL, pathFig) 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019: 2 | 3 | Nikola Stoyanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /src/loadGPX.jl: -------------------------------------------------------------------------------- 1 | using LightXML 2 | 3 | export parseGPX, readGPX 4 | 5 | # Converts a file to XML Document. 6 | function readGPX(filename::String) 7 | return parse_file(filename) 8 | end 9 | 10 | #= Converts a XMLDocument to array of longitude and latitude. 11 | 12 | Args: 13 | gpxDoc(XMLDocument): XML document in GPX format. 14 | Returns 15 | Array{Float64, 2}: GPS coordinates with longitude and latitude. 16 | =# 17 | function parseGPX(gpxDoc::XMLDocument) 18 | 19 | gpxPath = Array{Float64, 2} 20 | gpxLng = zeros(0) 21 | gpxLon = zeros(0) 22 | 23 | xroot = root(gpxDoc) 24 | trk = get_elements_by_tagname(xroot, "trk")[1] 25 | trkseg = get_elements_by_tagname(trk, "trkseg")[1] 26 | trkpt = get_elements_by_tagname(trkseg, "trkpt") 27 | 28 | for c in child_nodes(trkseg) 29 | if is_elementnode(c) 30 | ad = attributes_dict(XMLElement(c)) 31 | 32 | append!(gpxLng, parse(Float64, (ad["lat"]))) 33 | append!(gpxLon, parse(Float64, (ad["lon"]))) 34 | end 35 | end 36 | 37 | return cat(dims=2, gpxLng, gpxLon) 38 | end 39 | -------------------------------------------------------------------------------- /examples/example.jl: -------------------------------------------------------------------------------- 1 | using Polyline 2 | 3 | # Load gpx file and parse. 4 | gpxFile = readGPX("route.gpx") 5 | gpxRoute = parseGPX(gpxFile) 6 | 7 | # Encode polyline 8 | polyline = encodePolyline(gpxRoute) 9 | 10 | # Decode polyline 11 | route = decodePolyline(polyline) 12 | 13 | # Plot the route in Goole maps. You would need to obtain a token from the static maps API 14 | # which you can find here: 15 | # https://developers.google.com/maps/documentation/maps-static/get-api-key 16 | # Then either pass it as an argument or set it as the environment variable GOOGLE_MAPS_API 17 | #url = mapsURL(polyline) 18 | 19 | # Or set the API token as an argument. 20 | url = mapsURL(polyline; token="...") 21 | 22 | # With the custom defaults you can plot the route. 23 | getMapImage(url; pathFig="/tmp/fig1.png") 24 | 25 | # Full customization. 26 | url = mapsURL(polyline; token="...", 27 | type = "roadmap", # https://developers.google.com/maps/documentation/maps-static/dev-guide#MapTypes 28 | size = 1000, 29 | scale = 2, # https://developers.google.com/maps/documentation/maps-static/dev-guide#scale_values 30 | MarkersStart = (gpxRoute[1, 1], gpxRoute[1, 2]), 31 | MarkersEnd = (gpxRoute[end, 1], gpxRoute[end, 2])) 32 | 33 | getMapImage(url; pathFig="/tmp/fig2.png") 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Polyline.jl 2 | 3 | [![Build Status](https://travis-ci.com/NikStoyanov/Polyline.jl.svg?branch=master)](https://travis-ci.com/NikStoyanov/Polyline.jl) 4 | [![Coverage Status](https://coveralls.io/repos/github/NikStoyanov/Polyline.jl/badge.svg?branch=master)](https://coveralls.io/github/NikStoyanov/Polyline.jl?branch=master) 5 | 6 | A Julia implementation of the algorithm described in [Google's Encoded Polyline Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) 7 | to encode/decode polylines and plot them using Google Maps. 8 | 9 | ## Polyline.jl with Google Maps 10 | 11 | Parse a GPX file. 12 | ```julia 13 | using Polyline 14 | 15 | # Load gpx file and parse. 16 | gpxFile = readGPX("./examples/route.gpx") 17 | gpxRoute = parseGPX(gpxFile) 18 | ``` 19 | 20 | Encode/decode polyline. 21 | ```julia 22 | # Encode polyline 23 | polyline = encodePolyline(gpxRoute) 24 | 25 | # Decode polyline 26 | route = decodePolyline(polyline) 27 | ``` 28 | 29 | Plot using Google Maps. You need to obtain a token from the static maps API which you can find here: 30 | https://developers.google.com/maps/documentation/maps-static/get-api-key 31 | ```julia 32 | # Then either set the token as the environment variable GOOGLE_MAPS_API 33 | # url = mapsURL(polyline) 34 | 35 | # Or pass the API token as an argument. 36 | url = mapsURL(polyline; token="...") 37 | 38 | # With the defaults you can plot the route. 39 | getMapImage(url; pathFig="/tmp/fig1.png") 40 | ``` 41 | ![Default](./examples/fig1.png) 42 | 43 | ```julia 44 | # Full customization. 45 | url = mapsURL(polyline; token="...", 46 | type = "roadmap", # https://developers.google.com/maps/documentation/maps-static/dev-guide#MapTypes 47 | size = 1000, 48 | scale = 2, # https://developers.google.com/maps/documentation/maps-static/dev-guide#scale_values 49 | MarkersStart = (gpxRoute[1, 1], gpxRoute[1, 2]), 50 | MarkersEnd = (gpxRoute[end, 1], gpxRoute[end, 2])) 51 | 52 | getMapImage(url; pathFig="/tmp/fig2.png") 53 | ``` 54 | ![Modified](./examples/fig2.png) 55 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Polyline 2 | using Test 3 | using LightXML 4 | 5 | pointCurrent = Polyline.coordinate{Float64}(38.5, -120.2) 6 | precision = 10. ^5 7 | 8 | @testset "Round coordinates" begin 9 | pointRound::Polyline.coordinate = Polyline.coordinate{Float64}(pointCurrent.Lat * precision, pointCurrent.Lng * precision) 10 | roundValue = Polyline.roundCoordinate(pointRound) 11 | @test roundValue.Lat == 3850000 && roundValue.Lng == -12020000 12 | end 13 | 14 | @testset "Write polyline" begin 15 | @test isequal(Polyline.diffCoordinate( 16 | Polyline.coordinate{Int64}(3850000, -12020000), 17 | Polyline.coordinate{Int64}(0, 0)), 18 | Polyline.coordinate{Int64}(3850000, -12020000)) 19 | 20 | @test isequal(Polyline.leftShiftCoordinate( 21 | Polyline.coordinate{Int64}(3850000, -12020000)), 22 | Polyline.coordinate{Int64}(7700000, -24040000)) 23 | 24 | @test isequal(Polyline.encodeToChar(7700000), 25 | collect("_p~iF")) 26 | 27 | @test isequal(Polyline.encodeToChar(-24040000), 28 | collect("~ps|U")) 29 | 30 | conv = Polyline.convertToChar(Polyline.coordinate{Int64}(7700000, -24040000)) 31 | @test isequal(conv.Lat, collect("_p~iF")) 32 | @test isequal(conv.Lng, collect("~ps|U")) 33 | 34 | gps_coord = [[38.5 -120.2]; [40.7 -120.95]; [43.252 -126.453]] 35 | @test isequal(encodePolyline(gps_coord), 36 | "_p~iF~ps|U_ulLnnqC_mqNvxq`@") 37 | end 38 | 39 | @testset "Decode polyline" begin 40 | @test decodePolyline("_p~iF~ps|U_ulLnnqC_mqNvxq`@") ≈ [[38.5 -120.2]; [40.7 -120.95]; [43.252 -126.453]] atol=1e-7 41 | end 42 | 43 | @testset "Read GPX" begin 44 | gpxFile = """ 45 | 46 | 47 | 48 | 49 | 50 | 51 | Bridgewater canal - St Georges island 52 | 9 53 | 54 | 55 | 29.2 56 | 57 | 58 | 59 | 76 60 | 0 61 | 62 | 63 | 64 | 65 | 29.2 66 | 67 | 68 | 69 | 76 70 | 0 71 | 72 | 73 | 74 | 75 | 76 | """ 77 | 78 | xDoc = parse_string(gpxFile) 79 | gpxRoute = [53.4718 -2.26402; 53.4718 -2.26402] 80 | 81 | @test gpxRoute ≈ parseGPX(xDoc) atol=0.0001 82 | end 83 | 84 | @testset "Google maps image" begin 85 | @test mapsURL(";_p~iF~ps|U_ulLnnqC_mqNvxq`@"; token="aa", type="terrain", 86 | size=1000, scale=1, MarkersStart=(1.0, 1.0), MarkersEnd=(2.0,2.0)) == 87 | "https://maps.googleapis.com/maps/api/staticmap?maptype=terrain&path=enc:;_p~iF~ps|U_ulLnnqC_mqNvxq`@&key=aa&size=1000x1000&scale=1&markers=color:yellow|label:S|1.0,1.0&markers=color:green|label:F|2.0,2.0" 88 | end 89 | -------------------------------------------------------------------------------- /src/encode.jl: -------------------------------------------------------------------------------- 1 | function encodePolyline(coord::Array{Float64}, precision::Int64=5) 2 | #= Encodes an array of GPS coordinates to a polyline. 3 | 4 | Args: 5 | coord(Array{Float64, 1}(undef, 1)): GPS coordinates. 6 | precision(Int16): Exponent for rounding the GPS coordinates. 7 | Returns: 8 | String: Polyline encoded GPS coordinates. 9 | =# 10 | 11 | # Compute the rounding precision 12 | factor::Float64 = 10. ^precision 13 | coord = coord .* factor 14 | 15 | output = Array{Char, 1}(undef, 1) 16 | 17 | for c in range(1, stop=size(coord)[1]) 18 | if c == 1 19 | writePolyline!(output, coordinate{Float64}(coord[c, 1], coord[c, 2]), 20 | coordinate{Float64}(0., 0.)) 21 | else 22 | writePolyline!(output, coordinate{Float64}(coord[c, 1], coord[c, 2]), 23 | coordinate{Float64}(coord[c-1, 1], coord[c-1, 2])) 24 | end 25 | end 26 | 27 | return join(output[2:end]) 28 | end 29 | 30 | function writePolyline!(output::Array{Char, 1}, currValue::coordinate{Float64}, 31 | prevValue::coordinate{Float64}) 32 | #= Convert the given coordinate points in a polyline and mutate the output. 33 | 34 | Args: 35 | output(Array{Char, 1}): Holds the resultant polyline. 36 | currValue(coordinate{Float64}): Current GPS data point. 37 | prevValue(coordinate{Float64}): Previous GPS data point. 38 | 39 | Returns: 40 | output(Array{Char, 1}): Mutate output by adding the current addition to the 41 | polyline. 42 | =# 43 | 44 | # Transform GPS coordinates to Integers and round 45 | roundCurrValue::coordinate{Int64} = roundCoordinate(currValue) 46 | roundPrevValue::coordinate{Int64} = roundCoordinate(prevValue) 47 | 48 | # Get the difference from the previous GPS coordinated 49 | diffCurrValue::coordinate{Int64} = diffCoordinate(roundCurrValue, roundPrevValue) 50 | 51 | # Left shift the data points 52 | leftShift::coordinate{Int64} = leftShiftCoordinate(diffCurrValue) 53 | 54 | # Transform into ASCII 55 | charCoordinate::coordinate{Array{Char, 1}} = convertToChar(leftShift) 56 | 57 | # Add the characters to the polyline 58 | append!(output, collect(charCoordinate.Lat)) 59 | append!(output, collect(charCoordinate.Lng)) 60 | end 61 | 62 | function encodeToChar(c::Int64)::Array{Char, 1} 63 | #= Perform the encoding of the character from a binary number to ASCII. 64 | 65 | Args: 66 | c(Int64): GPS coordinate. 67 | 68 | Returns: 69 | String: ASCII characters of the polyline. 70 | =# 71 | 72 | LatChars = Array{Char, 1}(undef, 1) 73 | 74 | # Invert a negative coordinate using two's complement 75 | if c < 0 76 | c = ~c 77 | end 78 | 79 | # Add a continuation bit at the LHS for non-last chunks using OR 0x20 80 | # (0x20 = 100000) 81 | while c >= 0x20 82 | # Get the last 5 bits (0x1f) 83 | # Add 63 (in order to get "better" looking polyline characters in ASCII) 84 | CharMod = (0x20 | (c & 0x1f)) + 63 85 | append!(LatChars, Char(CharMod)) 86 | 87 | # Shift 5 bits 88 | c = c >> 5 89 | end 90 | 91 | # Modify the last chunk 92 | append!(LatChars, Char(c + 63)) 93 | 94 | # The return string holds a beginning character at the start 95 | # skip it and return the rest 96 | return LatChars[2:end] 97 | end 98 | 99 | function roundCoordinate(currValue::coordinate{Float64})::coordinate{Int64} 100 | #= Convert the coordinate to an integer and round. 101 | 102 | Args: 103 | currValue (coordinate{Float64}): GPS data point as a real number. 104 | Returns: 105 | roundedValue (coordinate{Int64}): GPS data point as rounded integer. 106 | =# 107 | 108 | roundedValue::coordinate = coordinate{Int64}(copysign(floor(abs(currValue.Lat)), currValue.Lat), 109 | copysign(floor(abs(currValue.Lng)), currValue.Lng)) 110 | return roundedValue 111 | end 112 | 113 | function diffCoordinate(currValue::coordinate{Int64}, 114 | prevValue::coordinate{Int64})::coordinate{Int64} 115 | #= Polyline encoding only considers the difference between GPS data points 116 | in order to reduce memory. diffCoordinate obtains the difference between 117 | consecutive coordinate points. 118 | 119 | Args: 120 | currValue (coordinate{Int64}): The current GPS data point. 121 | prevValue (coordinate{Int64}): The previous GPS data point. The count 122 | starts from 0. 123 | Returns: 124 | coordinate{Int64}: The difference between the GPS data points. 125 | =# 126 | 127 | return coordinate{Int64}(currValue.Lat - prevValue.Lat, 128 | currValue.Lng - prevValue.Lng) 129 | end 130 | 131 | function leftShiftCoordinate(currValue::coordinate{Int64})::coordinate{Int64} 132 | #= Left bitwise shift to leave space for a sign bit as the right most bit. 133 | 134 | Args: 135 | currValue(coordinate{Int64}): The difference between the last two 136 | consecutive GPS data points. 137 | Returns: 138 | coordinate{Int64}: Left bitwise shifted values. 139 | =# 140 | 141 | return coordinate{Int64}(currValue.Lat << 1, 142 | currValue.Lng << 1) 143 | end 144 | 145 | function convertToChar(currValue::coordinate{Int64})::coordinate{Array{Char, 1}} 146 | #= Convert the coordinates into ascii symbols. 147 | 148 | Args: 149 | currValue(coordinate{Int64}): Integer GPS coordinates. 150 | 151 | Returns: 152 | coordinate{String}: GPS coordinates as ASCII characters. 153 | =# 154 | 155 | Lat::Int64 = currValue.Lat 156 | Lng::Int64 = currValue.Lng 157 | 158 | return coordinate{Array{Char, 1}}(encodeToChar(Lat), encodeToChar(Lng)) 159 | end 160 | --------------------------------------------------------------------------------