├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── benches └── distance_benchmark.rs ├── renovate.json ├── rust-toolchain.toml ├── src └── lib.rs └── tests └── integration_test.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Turbo87 2 | custom: https://paypal.me/tobiasbieniek 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | check: 12 | name: check 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3.0.2 16 | - uses: Swatinem/rust-cache@v2.2.1 17 | - run: cargo check --workspace --all-targets 18 | env: 19 | RUSTFLAGS: "-D warnings" 20 | 21 | tests: 22 | name: test 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3.0.2 26 | - uses: Swatinem/rust-cache@v2.2.1 27 | - run: cargo test --workspace 28 | 29 | fmt: 30 | name: rustfmt 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v3.0.2 34 | - run: rustup component add rustfmt 35 | - uses: Swatinem/rust-cache@v2.2.1 36 | - run: cargo fmt --all -- --check 37 | 38 | clippy: 39 | name: clippy 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3.0.2 43 | - run: rustup component add clippy 44 | - uses: Swatinem/rust-cache@v2.2.1 45 | - run: cargo clippy --workspace -- --deny warnings 46 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | name: release 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | 22 | - name: cargo login 23 | uses: actions-rs/cargo@v1 24 | with: 25 | command: login 26 | args: ${{ secrets.CRATES_IO_TOKEN }} 27 | 28 | - name: cargo publish 29 | uses: actions-rs/cargo@v1 30 | with: 31 | command: publish 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | .criterion 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flat_projection" 3 | version = "0.4.0" 4 | authors = ["Tobias Bieniek "] 5 | description = "Fast geodesic distance approximations via flat surface projection." 6 | license = "MIT" 7 | repository = "https://github.com/Turbo87/flat-projection-rs.git" 8 | homepage = "https://github.com/Turbo87/flat-projection-rs.git" 9 | documentation = "https://docs.rs/flat_projection/" 10 | readme = "README.md" 11 | 12 | [badges] 13 | travis-ci = { repository = "Turbo87/flat-projection-rs", branch = "master" } 14 | 15 | [dependencies] 16 | num-traits = "0.2.19" 17 | 18 | [dev-dependencies] 19 | assert_approx_eq = "1.1.0" 20 | criterion = "0.3.6" 21 | 22 | [[bench]] 23 | name = "distance_benchmark" 24 | harness = false 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Tobias Bieniek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | flat-projection 3 | ============================================================================== 4 | 5 | [![Build Status](https://travis-ci.org/Turbo87/flat-projection-rs.svg?branch=master)](https://travis-ci.org/Turbo87/flat-projection-rs) 6 | 7 | Fast geodesic distance approximations via flat surface projection 8 | 9 | The `FlatProjection` struct can by used to project geographical 10 | coordinates from [WGS84] into a cartesian coordinate system. 11 | In the projected form approximated distance and bearing calculations 12 | can be performed much faster than on a sphere. The precision of these 13 | calculations is very precise for distances up to about 500 km. 14 | 15 | [WGS84]: https://en.wikipedia.org/wiki/World_Geodetic_System 16 | 17 | 18 | Usage 19 | ------------------------------------------------------------------------------ 20 | 21 | ```rust 22 | extern crate flat_projection; 23 | 24 | use flat_projection::FlatProjection; 25 | 26 | fn main() { 27 | let (lon1, lat1) = (6.186389, 50.823194); 28 | let (lon2, lat2) = (6.953333, 51.301389); 29 | 30 | let proj = FlatProjection::new(51.05); 31 | 32 | let p1 = proj.project(lon1, lat1); 33 | let p2 = proj.project(lon2, lat2); 34 | 35 | let distance = p1.distance(&p2); 36 | // -> 75.648 km 37 | } 38 | ``` 39 | 40 | 41 | Benchmark 42 | ------------------------------------------------------------------------------ 43 | 44 | ``` 45 | $ cargo bench 46 | 47 | distance/flat time: [322.21 ps 323.82 ps 326.41 ps] 48 | distance/haversine time: [12.604 ns 12.831 ns 13.162 ns] 49 | distance/vincenty time: [346.79 ns 348.00 ns 349.61 ns] 50 | ``` 51 | 52 | According to these results the flat surface approximation is about 40x faster 53 | than the [Haversine] formula. 54 | 55 | [Haversine]: https://en.wikipedia.org/wiki/Haversine_formula 56 | 57 | 58 | Related 59 | ------------------------------------------------------------------------------ 60 | 61 | - [cheap-ruler] – Similar calculations in JavaScripts 62 | - [cheap-ruler-cpp] – C++ port of cheap-ruler 63 | 64 | [cheap-ruler]: https://github.com/mapbox/cheap-ruler 65 | [cheap-ruler-cpp]: https://github.com/mapbox/cheap-ruler-cpp 66 | 67 | 68 | License 69 | ------------------------------------------------------------------------------- 70 | 71 | This project is released under the [MIT license](LICENSE). 72 | -------------------------------------------------------------------------------- /benches/distance_benchmark.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate criterion; 3 | extern crate flat_projection; 4 | 5 | use criterion::Criterion; 6 | use flat_projection::FlatProjection; 7 | use std::f64; 8 | 9 | const AACHEN: (f64, f64) = (6.186389, 50.823194); 10 | const MEIERSBERG: (f64, f64) = (6.953333, 51.301389); 11 | 12 | fn flat_distance(p1: (f64, f64), p2: (f64, f64)) -> f64 { 13 | let proj = FlatProjection::new((p1.0 + p2.0) / 2., (p1.1 + p2.1) / 2.); 14 | 15 | let flat1 = proj.project(p1.0, p1.1); 16 | let flat2 = proj.project(p2.0, p2.1); 17 | 18 | flat1.distance(&flat2) 19 | } 20 | 21 | fn haversine_distance(p1: (f64, f64), p2: (f64, f64)) -> f64 { 22 | let dlat = (p2.1 - p1.1).to_radians(); 23 | let dlon = (p2.0 - p1.0).to_radians(); 24 | let lat1 = p1.1.to_radians(); 25 | let lat2 = p2.1.to_radians(); 26 | 27 | let a = (dlat / 2.).sin().powi(2) + (dlon / 2.).sin().powi(2) * lat1.cos() * lat2.cos(); 28 | 29 | 2. * a.sqrt().atan2((1. - a).sqrt()) * 6371008.8 30 | } 31 | 32 | fn vincenty_distance(p1: (f64, f64), p2: (f64, f64)) -> f64 { 33 | let a = 6378137.; 34 | let b = 6356752.314245; 35 | let f = 1. / 298.257223563; // WGS-84 ellipsoid params 36 | 37 | let l = (p2.0 - p1.0).to_radians(); 38 | let u1 = ((1. - f) * p1.1.to_radians().tan()).atan(); 39 | let u2 = ((1. - f) * p2.1.to_radians().tan()).atan(); 40 | let sin_u1 = u1.sin(); 41 | let cos_u1 = u1.cos(); 42 | let sin_u2 = u2.sin(); 43 | let cos_u2 = u2.cos(); 44 | 45 | let mut sin_sigma: f64; 46 | let mut cos_sigma: f64; 47 | let mut sigma: f64; 48 | let mut cos_sq_alpha: f64; 49 | let mut cos2_sigma_m: f64; 50 | 51 | let mut lambda = l; 52 | let mut lambda_p: f64; 53 | let mut iter_limit = 100; 54 | while { 55 | let sin_lambda = lambda.sin(); 56 | let cos_lambda = lambda.cos(); 57 | sin_sigma = ((cos_u2 * sin_lambda) * (cos_u2 * sin_lambda) 58 | + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda) 59 | * (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda)) 60 | .sqrt(); 61 | 62 | if sin_sigma == 0. { 63 | // co-incident points 64 | return 0.; 65 | } 66 | 67 | cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda; 68 | sigma = sin_sigma.atan2(cos_sigma); 69 | let sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma; 70 | cos_sq_alpha = 1. - sin_alpha * sin_alpha; 71 | cos2_sigma_m = cos_sigma - 2. * sin_u1 * sin_u2 / cos_sq_alpha; 72 | if cos2_sigma_m.is_nan() { 73 | cos2_sigma_m = 0.; 74 | } // equatorial line: cos_sq_alpha=0 (§6) 75 | let c = f / 16. * cos_sq_alpha * (4. + f * (4. - 3. * cos_sq_alpha)); 76 | lambda_p = lambda; 77 | lambda = l 78 | + (1. - c) 79 | * f 80 | * sin_alpha 81 | * (sigma 82 | + c * sin_sigma 83 | * (cos2_sigma_m 84 | + c * cos_sigma * (-1. + 2. * cos2_sigma_m * cos2_sigma_m))); 85 | 86 | iter_limit -= 1; 87 | 88 | (lambda - lambda_p).abs() > 1e-12 && iter_limit > 0 89 | } {} 90 | 91 | if iter_limit == 0 { 92 | return f64::NAN; 93 | } // formula failed to converge 94 | 95 | let u_sq = cos_sq_alpha * (a * a - b * b) / (b * b); 96 | let a = 1. + u_sq / 16384. * (4096. + u_sq * (-768. + u_sq * (320. - 175. * u_sq))); 97 | let b = u_sq / 1024. * (256. + u_sq * (-128. + u_sq * (74. - 47. * u_sq))); 98 | let delta_sigma = b 99 | * sin_sigma 100 | * (cos2_sigma_m 101 | + b / 4. 102 | * (cos_sigma * (-1. + 2. * cos2_sigma_m * cos2_sigma_m) 103 | - b / 6. 104 | * cos2_sigma_m 105 | * (-3. + 4. * sin_sigma * sin_sigma) 106 | * (-3. + 4. * cos2_sigma_m * cos2_sigma_m))); 107 | let s = b * a * (sigma - delta_sigma); 108 | 109 | return s; 110 | } 111 | 112 | fn criterion_benchmark(c: &mut Criterion) { 113 | let input = (AACHEN, MEIERSBERG); 114 | 115 | let mut group = c.benchmark_group("distance"); 116 | 117 | group.bench_with_input("flat", &input, |b, &(from, to)| { 118 | b.iter(|| flat_distance(from, to)) 119 | }); 120 | group.bench_with_input("haversine", &input, |b, &(from, to)| { 121 | b.iter(|| haversine_distance(from, to)) 122 | }); 123 | group.bench_with_input("vincenty", &input, |b, &(from, to)| { 124 | b.iter(|| vincenty_distance(from, to)) 125 | }); 126 | 127 | group.finish(); 128 | } 129 | 130 | criterion_group!(benches, criterion_benchmark); 131 | criterion_main!(benches); 132 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":automergePatch", 5 | ":dependencyDashboard", 6 | ":maintainLockFilesWeekly", 7 | ":prConcurrentLimitNone", 8 | ":prHourlyLimitNone", 9 | ":semanticCommitsDisabled" 10 | ], 11 | "packageRules": [ 12 | { 13 | "matchCurrentVersion": ">= 1.0.0", 14 | "updateTypes": ["minor"], 15 | "automerge": true 16 | }, 17 | { 18 | "depTypeList": ["devDependencies"], 19 | "automerge": true 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # This is the "Minimum Supported Rust Version" for this project. 3 | # Increasing it should be considered a breaking change and requires a new 4 | # major version release. 5 | channel = "1.65.0" 6 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Fast geodesic distance approximations via flat surface projection 2 | //! 3 | //! The [`FlatProjection`] struct can by used to project geographical 4 | //! coordinates from [WGS84] into a cartesian coordinate system. 5 | //! In the projected form approximated distance and bearing calculations 6 | //! can be performed much faster than on a sphere. The precision of these 7 | //! calculations is very precise for distances up to about 500 km. 8 | //! 9 | //! [`FlatProjection`]: struct.FlatProjection.html 10 | //! [WGS84]: https://en.wikipedia.org/wiki/World_Geodetic_System 11 | //! 12 | //! ## Example 13 | //! 14 | //! ``` 15 | //! # #[macro_use] 16 | //! # extern crate assert_approx_eq; 17 | //! # extern crate num_traits; 18 | //! # extern crate flat_projection; 19 | //! # 20 | //! # use num_traits::float::Float; 21 | //! # use flat_projection::FlatProjection; 22 | //! # 23 | //! # fn main() { 24 | //! let (lon1, lat1) = (6.186389, 50.823194); 25 | //! let (lon2, lat2) = (6.953333, 51.301389); 26 | //! 27 | //! let proj = FlatProjection::new(6.5, 51.05); 28 | //! 29 | //! let p1 = proj.project(lon1, lat1); 30 | //! let p2 = proj.project(lon2, lat2); 31 | //! 32 | //! let distance = p1.distance(&p2); 33 | //! // -> 75.648 km 34 | //! # 35 | //! # assert_approx_eq!(distance, 75.635_595, 0.02); 36 | //! # } 37 | //! ``` 38 | //! 39 | //! ## Related 40 | //! 41 | //! - [cheap-ruler] – Similar calculations in JavaScripts 42 | //! - [cheap-ruler-cpp] – C++ port of cheap-ruler 43 | //! 44 | //! [cheap-ruler]: https://github.com/mapbox/cheap-ruler 45 | //! [cheap-ruler-cpp]: https://github.com/mapbox/cheap-ruler-cpp 46 | 47 | #![allow(clippy::derive_partial_eq_without_eq)] 48 | 49 | extern crate num_traits; 50 | 51 | use num_traits::Float; 52 | 53 | /// Projection from [WGS84] to a cartesian coordinate system for fast 54 | /// geodesic approximations. 55 | /// 56 | /// [WGS84]: https://en.wikipedia.org/wiki/World_Geodetic_System 57 | /// 58 | /// ``` 59 | /// # #[macro_use] 60 | /// # extern crate assert_approx_eq; 61 | /// # extern crate num_traits; 62 | /// # extern crate flat_projection; 63 | /// # 64 | /// # use num_traits::float::Float; 65 | /// # use flat_projection::FlatProjection; 66 | /// # 67 | /// # fn main() { 68 | /// let (lon1, lat1) = (6.186389, 50.823194); 69 | /// let (lon2, lat2) = (6.953333, 51.301389); 70 | /// 71 | /// let proj = FlatProjection::new(6.5, 51.05); 72 | /// 73 | /// let p1 = proj.project(lon1, lat1); 74 | /// let p2 = proj.project(lon2, lat2); 75 | /// 76 | /// let distance = p1.distance(&p2); 77 | /// // -> 75.648 km 78 | /// # 79 | /// # assert_approx_eq!(distance, 75.635_595, 0.02); 80 | /// # } 81 | /// ``` 82 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] 83 | pub struct FlatProjection { 84 | kx: T, 85 | ky: T, 86 | 87 | lat: T, 88 | lon: T, 89 | } 90 | 91 | impl FlatProjection { 92 | /// Creates a new `FlatProjection` instance that will work best around 93 | /// the given latitude. 94 | /// 95 | /// ``` 96 | /// # use flat_projection::FlatProjection; 97 | /// # 98 | /// let proj = FlatProjection::new(7., 51.); 99 | /// ``` 100 | pub fn new(longitude: T, latitude: T) -> FlatProjection { 101 | // see https://github.com/mapbox/cheap-ruler/ 102 | 103 | let one = T::one(); 104 | let two = T::from(2).unwrap(); 105 | 106 | // Values that define WGS84 ellipsoid model of the Earth 107 | let re: T = T::from(6378.137).unwrap(); // equatorial radius 108 | let fe: T = one / T::from(298.257223563).unwrap(); // flattening 109 | let e2: T = fe * (two - fe); 110 | 111 | // Curvature formulas from https://en.wikipedia.org/wiki/Earth_radius#Meridional 112 | let cos_lat = latitude.to_radians().cos(); 113 | let w2 = one / (one - e2 * (one - cos_lat * cos_lat)); 114 | let w = w2.sqrt(); 115 | 116 | // multipliers for converting longitude and latitude degrees into distance 117 | let kx = (re * w * cos_lat).to_radians(); // based on normal radius of curvature 118 | let ky = (re * w * w2 * (one - e2)).to_radians(); // based on meridional radius of curvature 119 | 120 | FlatProjection { 121 | kx, 122 | ky, 123 | lat: latitude, 124 | lon: longitude, 125 | } 126 | } 127 | 128 | /// Converts a longitude and latitude (in degrees) to a [`FlatPoint`] 129 | /// instance that can be used for fast geodesic approximations. 130 | /// 131 | /// [`FlatPoint`]: struct.FlatPoint.html 132 | /// 133 | /// ``` 134 | /// # use flat_projection::FlatProjection; 135 | /// # 136 | /// let (lon, lat) = (6.186389, 50.823194); 137 | /// 138 | /// let proj = FlatProjection::new(6., 51.); 139 | /// 140 | /// let flat_point = proj.project(lon, lat); 141 | /// ``` 142 | pub fn project(&self, longitude: T, latitude: T) -> FlatPoint { 143 | let x = (longitude - self.lon) * self.kx; 144 | let y = (latitude - self.lat) * self.ky; 145 | 146 | FlatPoint { x, y } 147 | } 148 | 149 | /// Converts a [`FlatPoint`] back to a (lon, lat) tuple. 150 | /// 151 | /// [`FlatPoint`]: struct.FlatPoint.html 152 | /// 153 | /// ``` 154 | /// # use flat_projection::FlatProjection; 155 | /// # 156 | /// let (lon, lat) = (6.186389, 50.823194); 157 | /// 158 | /// let proj = FlatProjection::new(6., 51.); 159 | /// 160 | /// let flat_point = proj.project(lon, lat); 161 | /// 162 | /// let result = proj.unproject(&flat_point); 163 | /// 164 | /// assert_eq!(result.0, lon); 165 | /// 166 | /// assert_eq!(result.1, lat); 167 | /// ``` 168 | pub fn unproject(&self, p: &FlatPoint) -> (T, T) { 169 | (p.x / self.kx + self.lon, p.y / self.ky + self.lat) 170 | } 171 | } 172 | 173 | /// Representation of a geographical point on Earth as projected 174 | /// by a [`FlatProjection`] instance. 175 | /// 176 | /// [`FlatProjection`]: struct.FlatProjection.html 177 | /// 178 | /// ``` 179 | /// # #[macro_use] 180 | /// # extern crate assert_approx_eq; 181 | /// # 182 | /// # use flat_projection::FlatProjection; 183 | /// # 184 | /// # fn main() { 185 | /// let (lon, lat) = (6.186389, 50.823194); 186 | /// 187 | /// let proj = FlatProjection::new(6., 51.); 188 | /// 189 | /// let flat_point = proj.project(lon, lat); 190 | /// # 191 | /// # assert_approx_eq!(flat_point.x, 13.0845f64, 0.001); 192 | /// # assert_approx_eq!(flat_point.y, -19.6694f64, 0.001); 193 | /// # } 194 | /// ``` 195 | #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] 196 | pub struct FlatPoint { 197 | /// X-axis component of the flat-surface point in kilometers 198 | pub x: T, 199 | /// Y-axis component of the flat-surface point in kilometers 200 | pub y: T, 201 | } 202 | 203 | impl FlatPoint { 204 | /// Calculates the approximate distance in kilometers from 205 | /// this `FlatPoint` to another. 206 | /// 207 | /// ``` 208 | /// # #[macro_use] 209 | /// # extern crate assert_approx_eq; 210 | /// # extern crate num_traits; 211 | /// # extern crate flat_projection; 212 | /// # 213 | /// # use num_traits::float::Float; 214 | /// # use flat_projection::FlatProjection; 215 | /// # 216 | /// # fn main() { 217 | /// let (lon1, lat1) = (6.186389, 50.823194); 218 | /// let (lon2, lat2) = (6.953333, 51.301389); 219 | /// 220 | /// let proj = FlatProjection::new(6.5, 51.05); 221 | /// 222 | /// let p1 = proj.project(lon1, lat1); 223 | /// let p2 = proj.project(lon2, lat2); 224 | /// 225 | /// let distance = p1.distance(&p2); 226 | /// // -> 75.648 km 227 | /// # 228 | /// # assert_approx_eq!(distance, 75.635_595, 0.02); 229 | /// # } 230 | /// ``` 231 | pub fn distance(&self, other: &FlatPoint) -> T { 232 | self.distance_squared(other).sqrt() 233 | } 234 | 235 | /// Calculates the approximate squared distance from this `FlatPoint` to 236 | /// another. 237 | /// 238 | /// This method can be used for fast distance comparisons. 239 | pub fn distance_squared(&self, other: &FlatPoint) -> T { 240 | let (dx, dy) = self.delta(other); 241 | distance_squared(dx, dy) 242 | } 243 | 244 | /// Calculates the approximate average bearing in degrees 245 | /// between -180 and 180 from this `FlatPoint` to another. 246 | /// 247 | /// ``` 248 | /// # #[macro_use] 249 | /// # extern crate assert_approx_eq; 250 | /// # extern crate num_traits; 251 | /// # extern crate flat_projection; 252 | /// # 253 | /// # use num_traits::float::Float; 254 | /// # use flat_projection::FlatProjection; 255 | /// # 256 | /// # fn main() { 257 | /// let (lon1, lat1) = (6.186389, 50.823194); 258 | /// let (lon2, lat2) = (6.953333, 51.301389); 259 | /// 260 | /// let proj = FlatProjection::new(6.5, 51.05); 261 | /// 262 | /// let p1 = proj.project(lon1, lat1); 263 | /// let p2 = proj.project(lon2, lat2); 264 | /// 265 | /// let bearing = p1.bearing(&p2); 266 | /// // -> 45.3° 267 | /// # 268 | /// # assert_approx_eq!(bearing, 45.312, 0.001); 269 | /// # } 270 | /// ``` 271 | pub fn bearing(&self, other: &FlatPoint) -> T { 272 | let (dx, dy) = self.delta(other); 273 | bearing(dx, dy) 274 | } 275 | 276 | /// Calculates the approximate [`distance`] and average [`bearing`] 277 | /// from this `FlatPoint` to another. 278 | /// 279 | /// [`distance`]: #method.distance 280 | /// [`bearing`]: #method.bearing 281 | /// 282 | /// ``` 283 | /// # #[macro_use] 284 | /// # extern crate assert_approx_eq; 285 | /// # extern crate num_traits; 286 | /// # extern crate flat_projection; 287 | /// # 288 | /// # use num_traits::float::Float; 289 | /// # use flat_projection::FlatProjection; 290 | /// # 291 | /// # fn main() { 292 | /// let (lon1, lat1) = (6.186389, 50.823194); 293 | /// let (lon2, lat2) = (6.953333, 51.301389); 294 | /// 295 | /// let proj = FlatProjection::new(6.5, 51.05); 296 | /// 297 | /// let p1 = proj.project(lon1, lat1); 298 | /// let p2 = proj.project(lon2, lat2); 299 | /// 300 | /// let (distance, bearing) = p1.distance_bearing(&p2); 301 | /// // -> 75.648 km and 45.3° 302 | /// # 303 | /// # assert_approx_eq!(distance, 75.635_595, 0.02); 304 | /// # assert_approx_eq!(bearing, 45.312, 0.001); 305 | /// # } 306 | /// ``` 307 | pub fn distance_bearing(&self, other: &FlatPoint) -> (T, T) { 308 | let (dx, dy) = self.delta(other); 309 | (distance_squared(dx, dy).sqrt(), bearing(dx, dy)) 310 | } 311 | 312 | fn delta(&self, other: &FlatPoint) -> (T, T) { 313 | (self.x - other.x, self.y - other.y) 314 | } 315 | 316 | /// Returns a new `FlatPoint` given [`distance`] and [`bearing`] from this `FlatPoint`. 317 | /// 318 | /// [`distance`]: #method.distance (kilometers) 319 | /// [`bearing`]: #method.bearing (degrees) 320 | /// 321 | /// ``` 322 | /// # #[macro_use] 323 | /// # extern crate assert_approx_eq; 324 | /// # extern crate num_traits; 325 | /// # extern crate flat_projection; 326 | /// # 327 | /// # use num_traits::float::Float; 328 | /// # use flat_projection::FlatProjection; 329 | /// # 330 | /// # fn main() { 331 | /// let (lon, lat) = (30.5, 50.5); 332 | /// 333 | /// let proj = FlatProjection::new(31., 50.); 334 | /// 335 | /// let p1 = proj.project(lon, lat); 336 | /// let (distance, bearing) = (1., 45.0); 337 | /// let p2 = p1.destination(distance, bearing); 338 | /// # 339 | /// # let res_distance = p1.distance(&p2); 340 | /// # let (dest_lon, dest_lat) = proj.unproject(&p2); 341 | /// # 342 | /// # assert_approx_eq!(dest_lon, 30.5098622, 0.00001); 343 | /// # assert_approx_eq!(dest_lat, 50.5063572, 0.00001); 344 | /// # } 345 | /// ``` 346 | pub fn destination(&self, dist: T, bearing: T) -> FlatPoint { 347 | let a = bearing.to_radians(); 348 | self.offset(a.sin() * dist, a.cos() * dist) 349 | } 350 | 351 | /// Returns a new `FlatPoint` given easting and northing offsets 352 | /// (in kilometers) from this `FlatPoint`. 353 | /// 354 | /// ``` 355 | /// # #[macro_use] 356 | /// # extern crate assert_approx_eq; 357 | /// # extern crate num_traits; 358 | /// # extern crate flat_projection; 359 | /// # 360 | /// # use num_traits::float::Float; 361 | /// # use flat_projection::FlatProjection; 362 | /// # 363 | /// # fn main() { 364 | /// let (lon, lat) = (30.5, 50.5); 365 | /// 366 | /// let proj = FlatProjection::new(31., 50.); 367 | /// 368 | /// let p1 = proj.project(lon, lat); 369 | /// let p2 = p1.offset(10., 10.); 370 | /// # 371 | /// # let (dest_lon, dest_lat) = proj.unproject(&p2); 372 | /// # assert_approx_eq!(dest_lon, 30.6394736, 0.00001); 373 | /// # assert_approx_eq!(dest_lat, 50.5899044, 0.00001); 374 | /// # } 375 | /// ``` 376 | pub fn offset(&self, dx: T, dy: T) -> FlatPoint { 377 | FlatPoint { 378 | x: self.x + dx, 379 | y: self.y + dy, 380 | } 381 | } 382 | } 383 | 384 | fn distance_squared(dx: T, dy: T) -> T { 385 | dx.powi(2) + dy.powi(2) 386 | } 387 | 388 | fn bearing(dx: T, dy: T) -> T { 389 | (-dx).atan2(-dy).to_degrees() 390 | } 391 | 392 | #[cfg(test)] 393 | #[macro_use] 394 | extern crate assert_approx_eq; 395 | 396 | #[cfg(test)] 397 | mod tests { 398 | use num_traits::Float; 399 | use FlatProjection; 400 | 401 | #[test] 402 | fn flatpoint_destination_ne() { 403 | let (lon, lat) = (30.5, 50.5); 404 | let proj = FlatProjection::new(31., 50.); 405 | let p1 = proj.project(lon, lat); 406 | 407 | let (distance, bearing) = (1., 45.0); 408 | let p2 = p1.destination(distance, bearing); 409 | let res_distance = p1.distance(&p2); 410 | let (dest_lon, dest_lat) = proj.unproject(&p2); 411 | assert_approx_eq!(dest_lon, 30.5098622, 0.00001); 412 | assert_approx_eq!(dest_lat, 50.5063572, 0.00001); 413 | assert_approx_eq!(distance, res_distance, 0.00001); 414 | } 415 | 416 | #[test] 417 | fn flatpoint_destination_se() { 418 | let (lon, lat) = (30.5, 50.5); 419 | let proj = FlatProjection::new(31., 50.); 420 | let p1 = proj.project(lon, lat); 421 | 422 | let (distance, bearing) = (1., 135.0); 423 | 424 | let p2 = p1.destination(distance, bearing); 425 | let res_distance = p1.distance(&p2); 426 | let (dest_lon, dest_lat) = proj.unproject(&p2); 427 | assert_approx_eq!(dest_lon, 30.5098622, 0.00001); 428 | assert_approx_eq!(dest_lat, 50.4936427, 0.00001); 429 | assert_approx_eq!(distance, res_distance, 0.00001); 430 | } 431 | 432 | #[test] 433 | fn flatpoint_destination_sw() { 434 | let (lon, lat) = (30.5, 50.5); 435 | let proj = FlatProjection::new(31., 50.); 436 | let p1 = proj.project(lon, lat); 437 | 438 | let (distance, bearing) = (1., 225.0); 439 | let p2 = p1.destination(distance, bearing); 440 | let res_distance = p1.distance(&p2); 441 | let (dest_lon, dest_lat) = proj.unproject(&p2); 442 | assert_approx_eq!(dest_lon, 30.4901377, 0.00001); 443 | assert_approx_eq!(dest_lat, 50.4936427, 0.00001); 444 | assert_approx_eq!(distance, res_distance, 0.00001); 445 | } 446 | 447 | #[test] 448 | fn flatpoint_destination_nw() { 449 | let (lon, lat) = (30.5, 50.5); 450 | let proj = FlatProjection::new(31., 50.); 451 | let p1 = proj.project(lon, lat); 452 | 453 | let (distance, bearing) = (1., 315.0); 454 | let p2 = p1.destination(distance, bearing); 455 | let res_distance = p1.distance(&p2); 456 | let (dest_lon, dest_lat) = proj.unproject(&p2); 457 | assert_approx_eq!(dest_lon, 30.4901377, 0.00001); 458 | assert_approx_eq!(dest_lat, 50.5063572, 0.00001); 459 | assert_approx_eq!(distance, res_distance, 0.00001); 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /tests/integration_test.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate assert_approx_eq; 3 | 4 | extern crate flat_projection; 5 | 6 | use flat_projection::FlatProjection; 7 | 8 | #[test] 9 | fn it_works() { 10 | let aachen = (6.186389, 50.823194); 11 | let meiersberg = (6.953333, 51.301389); 12 | 13 | let average_longitude = (aachen.0 + meiersberg.0) / 2.; 14 | let average_latitude = (aachen.1 + meiersberg.1) / 2.; 15 | 16 | let proj = FlatProjection::new(average_longitude, average_latitude); 17 | 18 | let flat_aachen = proj.project(aachen.0, aachen.1); 19 | let flat_meiersberg = proj.project(meiersberg.0, meiersberg.1); 20 | 21 | let distance = flat_aachen.distance(&flat_meiersberg); 22 | let bearing = flat_aachen.bearing(&flat_meiersberg); 23 | 24 | const VINCENTY_DISTANCE: f64 = 75.635_595; 25 | assert_approx_eq!(distance, VINCENTY_DISTANCE, 0.003); 26 | 27 | const VINCENTY_INITIAL_BEARING: f64 = 45.005_741; 28 | const VINCENTY_FINAL_BEARING: f64 = 45.602_300; 29 | const VINCENTY_AVERAGE_BEARING: f64 = (VINCENTY_INITIAL_BEARING + VINCENTY_FINAL_BEARING) / 2.; 30 | assert_approx_eq!(bearing, VINCENTY_AVERAGE_BEARING, 0.003); 31 | } 32 | --------------------------------------------------------------------------------