Skip to content

Commit

Permalink
Remove external dependencies and provide own implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Zaczero committed Oct 30, 2024
1 parent 5601872 commit 159d96a
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 48 deletions.
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
[package]
edition = "2021"
name = "polyline_rs"
version = "1.0.0"
version = "1.1.0"

[lib]
crate-type = ["cdylib"]
name = "polyline_rs"

[dependencies]
geo-types = "0.7.13"
polyline = "0.11.0"
pyo3 = { version = "0.22.5", features = ["abi3-py39", "extension-module"] }

[profile.release]
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
[![Liberapay Patrons](https://shields.monicz.dev/liberapay/patrons/Zaczero?logo=liberapay&label=Patrons)](https://liberapay.com/Zaczero/)
[![GitHub Sponsors](https://shields.monicz.dev/github/sponsors/Zaczero?logo=github&label=Sponsors&color=%23db61a2)](https://github.com/sponsors/Zaczero)

Fast Google Encoded Polyline encoding & decoding in Rust. A Python PyO3 wrapper for [polyline](https://crates.io/crates/polyline) with out-of-the-box support for both (lat, lon) and (lon, lat) coordinates.
Fast Google Encoded Polyline encoding & decoding in Rust. A Python PyO3 library with out-of-the-box support for both (lat, lon) and (lon, lat) coordinates.

[Encoded Polyline Algorithm Format](https://developers.google.com/maps/documentation/utilities/polylinealgorithm)

## Installation

Expand Down
140 changes: 104 additions & 36 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,122 @@
use geo_types::{Coord, LineString};
use pyo3::{exceptions::PyValueError, prelude::*};
use pyo3::prelude::*;

const CHAR_OFFSET: u8 = 63;

#[inline(always)]
fn _encode_value(line: &mut String, mut value: i32) {
let is_negative = value < 0;
value <<= 1;
if is_negative {
value = !value;
}
loop {
let mut chunk = (value & 0b11111) as u8;
value >>= 5;
let is_last_chunk = value == 0;
if !is_last_chunk {
chunk |= 0x20;
}
line.push(char::from(chunk + CHAR_OFFSET));
if is_last_chunk {
break;
}
}
}

#[inline(always)]
fn _encode(coordinates: Vec<Vec<f64>>, precision: i32, latlon: bool) -> String {
let lat_idx = if latlon { 0 } else { 1 };
let lon_idx = if latlon { 1 } else { 0 };
let factor = 10_f64.powi(precision);
let mut line = String::with_capacity(6 * 2 * coordinates.len());
let mut last_lat = 0_i32;
let mut last_lon = 0_i32;

for coord in coordinates {
let lat = (coord[lat_idx] * factor) as i32;
_encode_value(&mut line, lat - last_lat);
last_lat = lat;
let lon = (coord[lon_idx] * factor) as i32;
_encode_value(&mut line, lon - last_lon);
last_lon = lon;
}
line
}

#[inline(always)]
fn _decode_value(mut value: i32) -> i32 {
let is_negative = (value & 0x1) == 1;
if is_negative {
value = !value;
}
value >> 1
}

#[inline(always)]
fn _decode(line: &str, precision: i32, latlon: bool) -> Vec<(f64, f64)> {
let factor = 10_f64.powi(precision);
let mut coords = Vec::with_capacity(line.len() / 4);
let mut last_lat = 0_i32;
let mut last_lon = 0_i32;
let mut first_set = false;
let mut first_value = 0_i32;
let mut second_value = 0_i32;
let mut shift = 0;

for c in line.chars() {
let chunk = (c as u8) - CHAR_OFFSET;
let is_last_chunk = (chunk & 0x20) == 0;
let chunk = (chunk & 0b11111) as i32;
if first_set {
second_value |= chunk << shift;
} else {
first_value |= chunk << shift;
}
shift += 5;
if is_last_chunk {
if first_set {
let lat = _decode_value(first_value) + last_lat;
let lon = _decode_value(second_value) + last_lon;
last_lat = lat;
last_lon = lon;
let lat = lat as f64 / factor;
let lon = lon as f64 / factor;
coords.push(if latlon { (lat, lon) } else { (lon, lat) });
first_set = false;
first_value = 0;
second_value = 0;
shift = 0;
} else {
first_set = true;
shift = 0;
}
}
}
coords
}

#[pyfunction]
#[pyo3(signature = (coordinates, precision = 5))]
fn encode_lonlat(coordinates: Vec<Vec<f64>>, precision: u32) -> PyResult<String> {
let line = LineString(
coordinates
.into_iter()
.map(|c| Coord { x: c[0], y: c[1] })
.collect(),
);
match polyline::encode_coordinates(line, precision) {
Ok(polyline) => Ok(polyline),
Err(err) => Err(PyValueError::new_err(err.to_string())),
}
fn encode_lonlat(coordinates: Vec<Vec<f64>>, precision: i32) -> String {
_encode(coordinates, precision, false)
}

#[pyfunction]
#[pyo3(signature = (coordinates, precision = 5))]
fn encode_latlon(coordinates: Vec<Vec<f64>>, precision: u32) -> PyResult<String> {
let line = LineString(
coordinates
.into_iter()
.map(|c| Coord { x: c[1], y: c[0] })
.collect(),
);
match polyline::encode_coordinates(line, precision) {
Ok(polyline) => Ok(polyline),
Err(err) => Err(PyValueError::new_err(err.to_string())),
}
fn encode_latlon(coordinates: Vec<Vec<f64>>, precision: i32) -> String {
_encode(coordinates, precision, true)
}

#[pyfunction]
#[pyo3(signature = (polyline, precision = 5))]
fn decode_lonlat(polyline: &str, precision: u32) -> PyResult<Vec<(f64, f64)>> {
let line = match polyline::decode_polyline(polyline, precision) {
Ok(line) => line,
Err(err) => return Err(PyValueError::new_err(err.to_string())),
};
Ok(line.0.into_iter().map(|c| (c.x, c.y)).collect())
fn decode_lonlat(polyline: &str, precision: i32) -> Vec<(f64, f64)> {
_decode(polyline, precision, false)
}

#[pyfunction]
#[pyo3(signature = (polyline, precision = 5))]
fn decode_latlon(polyline: &str, precision: u32) -> PyResult<Vec<(f64, f64)>> {
let line = match polyline::decode_polyline(polyline, precision) {
Ok(line) => line,
Err(err) => return Err(PyValueError::new_err(err.to_string())),
};
Ok(line.0.into_iter().map(|c| (c.y, c.x)).collect())
fn decode_latlon(polyline: &str, precision: i32) -> Vec<(f64, f64)> {
_decode(polyline, precision, true)
}

#[pymodule]
Expand Down
15 changes: 8 additions & 7 deletions tests/test___init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ def test_encode_decode(coords, precision, expected):
coords_lonlat = tuple((x, y) for y, x in coords)
assert encode_latlon(coords, precision) == expected
assert encode_lonlat(coords_lonlat, precision) == expected
pytest.approx(decode_latlon(expected, precision), coords, abs=0.1**precision)
pytest.approx(decode_lonlat(expected, precision), coords_lonlat, abs=0.1**precision)


def test_encode_out_of_bounds():
with pytest.raises(ValueError, match='latitude out of bounds: -120.2 at position 0'):
encode_latlon([(-120.2, 38.5), (-120.95, 40.7), (-126.453, 43.252)])
assert all(
decoded == pytest.approx(coord, abs=0.1**precision)
for decoded, coord in zip(decode_latlon(expected, precision), coords)
)
assert all(
decoded == pytest.approx(coord, abs=0.1**precision)
for decoded, coord in zip(decode_lonlat(expected, precision), coords_lonlat)
)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 159d96a

Please sign in to comment.