Skip to content

Commit 6e92f27

Browse files
committed
bump & docs updates
1 parent e8ab464 commit 6e92f27

File tree

5 files changed

+258
-14
lines changed

5 files changed

+258
-14
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ members = [
99
]
1010

1111
[workspace.package]
12-
version = "0.2.1"
12+
version = "0.2.2"
1313
edition = "2021"
1414
license = "MIT OR Apache-2.0"
1515
authors = ["gaker"]

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ This repository contains four crates:
2222

2323
```toml
2424
[dependencies]
25-
rapidgeo-distance = "0.1"
26-
rapidgeo-polyline = "0.1"
27-
rapidgeo-simplify = "0.1"
28-
rapidgeo-similarity = "0.1"
25+
rapidgeo-distance = "0.2"
26+
rapidgeo-polyline = "0.2"
27+
rapidgeo-simplify = "0.2"
28+
rapidgeo-similarity = "0.2"
2929
```
3030

3131
### Python Package

rapidgeo-distance/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rapidgeo-distance"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
edition.workspace = true
55
license.workspace = true
66
authors.workspace = true

rapidgeo-py/docs/examples.rst

Lines changed: 133 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -438,23 +438,23 @@ Compare Original vs Simplified Tracks
438438
.. code-block:: python
439439
440440
from rapidgeo.similarity.hausdorff import hausdorff
441-
441+
442442
def evaluate_simplification(original_track, tolerance_m):
443443
"""Evaluate the effect of different simplification tolerances."""
444-
444+
445445
simplified = douglas_peucker(original_track, tolerance_m=tolerance_m)
446-
446+
447447
# Calculate metrics
448448
original_length = path_length_haversine(original_track)
449449
simplified_length = path_length_haversine(simplified)
450-
450+
451451
# Measure maximum deviation
452452
max_deviation = hausdorff(original_track, simplified)
453-
453+
454454
# Calculate reductions
455455
point_reduction = (1 - len(simplified) / len(original_track)) * 100
456456
length_error = abs(simplified_length - original_length) / original_length * 100
457-
457+
458458
return {
459459
"tolerance_m": tolerance_m,
460460
"original_points": len(original_track),
@@ -465,13 +465,138 @@ Compare Original vs Simplified Tracks
465465
"original_length_km": original_length / 1000,
466466
"simplified_length_km": simplified_length / 1000,
467467
}
468-
468+
469469
# Test different tolerance levels
470470
sample_track = [LngLat(-122.4 + i*0.001, 37.7 + i*0.001) for i in range(50)]
471-
471+
472472
print("Simplification analysis:")
473473
for tolerance in [1, 5, 10, 25, 50, 100]:
474474
results = evaluate_simplification(sample_track, tolerance)
475475
print(f"Tolerance {tolerance}m: {results['point_reduction_pct']:.1f}% fewer points, "
476476
f"{results['length_error_pct']:.2f}% length error, "
477477
f"max deviation {results['max_deviation_m']:.1f}m")
478+
479+
Bearing and Direction Analysis
480+
-------------------------------
481+
482+
Calculate Bearing Between Two Points
483+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
484+
485+
.. code-block:: python
486+
487+
from rapidgeo import LngLat
488+
from rapidgeo.distance.geo import bearing
489+
490+
# San Francisco to New York
491+
sf = LngLat(-122.4194, 37.7749)
492+
nyc = LngLat(-74.0060, 40.7128)
493+
494+
bearing_deg = bearing(sf, nyc)
495+
print(f"Bearing from SF to NYC: {bearing_deg:.1f}°")
496+
# Output: Bearing from SF to NYC: 75.4° (ENE)
497+
498+
# Convert to cardinal direction
499+
def bearing_to_cardinal(deg):
500+
directions = ['N', 'NE', 'E', 'SE', 'S', 'SW', 'W', 'NW']
501+
index = round(deg / 45) % 8
502+
return directions[index]
503+
504+
print(f"Direction: {bearing_to_cardinal(bearing_deg)}")
505+
# Output: Direction: E
506+
507+
Fill Missing GPS Heading Data with Pandas
508+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
509+
510+
.. code-block:: python
511+
512+
import pandas as pd
513+
import numpy as np
514+
from rapidgeo import LngLat
515+
from rapidgeo.distance.batch import pairwise_bearings
516+
517+
# data with missing heading values
518+
df = pd.DataFrame({
519+
'timestamp': ['2024-01-01 10:00:00', '2024-01-01 10:00:05', '2024-01-01 10:00:10', '2024-01-01 10:00:15'],
520+
'latitude': [37.7749, 37.7755, 37.7762, 37.7768],
521+
'longitude': [-122.4194, -122.4188, -122.4180, -122.4172],
522+
'speed_mph': [25.0, 28.0, 30.0, 27.0],
523+
'heading': [None, 65.0, None, None] # Some heading data missing
524+
})
525+
526+
print("Original data:")
527+
print(df)
528+
529+
# Calculate bearings between consecutive points
530+
points = [LngLat(lng, lat) for lat, lng in
531+
zip(df['latitude'], df['longitude'])]
532+
533+
calculated_bearings = pairwise_bearings(points)
534+
535+
# Fill missing headings with calculated bearings
536+
# Note: bearings list has len(points) - 1 elements, so we forward-fill
537+
bearings_with_first = [None] + calculated_bearings # First point has no previous bearing
538+
539+
df['calculated_heading'] = bearings_with_first
540+
df['heading_filled'] = df['heading'].fillna(df['calculated_heading'])
541+
542+
print("\nData with filled headings:")
543+
print(df[['timestamp', 'heading', 'calculated_heading', 'heading_filled']])
544+
545+
# Output:
546+
# timestamp heading calculated_heading heading_filled
547+
# 0 2024-01-01 10:00:00 NaN None NaN
548+
# 1 2024-01-01 10:00:05 65.0 65.23 65.0
549+
# 2 2024-01-01 10:00:10 NaN 66.15 66.15
550+
# 3 2024-01-01 10:00:15 NaN 65.89 65.89
551+
552+
Analyze Route Directions with NumPy
553+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
554+
555+
.. code-block:: python
556+
557+
import numpy as np
558+
from rapidgeo import LngLat
559+
from rapidgeo.distance.batch import pairwise_bearings, pairwise_haversine
560+
561+
# GPS track data as NumPy arrays
562+
lats = np.array([37.7749, 37.7755, 37.7762, 37.7768, 37.7775, 37.7780])
563+
lngs = np.array([-122.4194, -122.4188, -122.4180, -122.4172, -122.4165, -122.4160])
564+
565+
# Convert to LngLat objects
566+
points = [LngLat(lng, lat) for lat, lng in zip(lats, lngs)]
567+
568+
# Calculate bearings and distances
569+
bearings = np.array(pairwise_bearings(points))
570+
distances = np.array(pairwise_haversine(points))
571+
572+
print("Segment analysis:")
573+
for i, (bearing, distance) in enumerate(zip(bearings, distances)):
574+
print(f"Segment {i}: {distance:.1f}m at {bearing:.1f}°")
575+
576+
# Detect turns (significant bearing changes)
577+
bearing_changes = np.abs(np.diff(bearings))
578+
# Handle wraparound (e.g., 359° to 1° is 2°, not 358°)
579+
bearing_changes = np.minimum(bearing_changes, 360 - bearing_changes)
580+
581+
turn_threshold = 15.0 # degrees
582+
turns = np.where(bearing_changes > turn_threshold)[0]
583+
584+
print(f"\nDetected {len(turns)} turns:")
585+
for turn_idx in turns:
586+
print(f" Turn at segment {turn_idx+1}: {bearing_changes[turn_idx]:.1f}° change")
587+
588+
# Calculate total distance
589+
total_distance = np.sum(distances)
590+
print(f"\nTotal distance: {total_distance:.1f}m")
591+
592+
# Output:
593+
# Segment analysis:
594+
# Segment 0: 75.3m at 65.2°
595+
# Segment 1: 76.1m at 66.1°
596+
# Segment 2: 75.8m at 65.9°
597+
# Segment 3: 76.2m at 66.3°
598+
# Segment 4: 75.5m at 65.7°
599+
#
600+
# Detected 0 turns:
601+
#
602+
# Total distance: 378.9m

rapidgeo-py/src/distance.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,44 @@ pub mod euclid_mod {
411411
pub mod batch_mod {
412412
use super::*;
413413

414+
/// Calculate haversine distances between consecutive points in a path.
415+
///
416+
/// Computes the great-circle distance between each pair of consecutive points
417+
/// using the Haversine formula. Returns a list of distances with length ``len(points) - 1``.
418+
///
419+
/// Parameters
420+
/// ----------
421+
/// points : list[LngLat]
422+
/// List of coordinates representing a path
423+
///
424+
/// Returns
425+
/// -------
426+
/// list[float]
427+
/// Distances in meters between consecutive points. Length is ``len(points) - 1``.
428+
///
429+
/// Examples
430+
/// --------
431+
/// >>> from rapidgeo import LngLat
432+
/// >>> from rapidgeo.distance.batch import pairwise_haversine
433+
/// >>> path = [
434+
/// ... LngLat(-122.4194, 37.7749), # San Francisco
435+
/// ... LngLat(-87.6298, 41.8781), # Chicago
436+
/// ... LngLat(-74.0060, 40.7128), # New York
437+
/// ... ]
438+
/// >>> distances = pairwise_haversine(path)
439+
/// >>> [f"{d/1000:.0f} km" for d in distances]
440+
/// ['2984 km', '1145 km']
441+
///
442+
/// Notes
443+
/// -----
444+
/// - Uses spherical Earth approximation (accurate to ±0.5% for distances <1000km)
445+
/// - Releases GIL during computation
446+
/// - For high precision, use Vincenty-based functions
447+
///
448+
/// See Also
449+
/// --------
450+
/// path_length_haversine : Sum of all consecutive distances
451+
/// pairwise_bearings : Bearings between consecutive points
414452
#[pyfunction]
415453
pub fn pairwise_haversine(py: Python, points: &Bound<'_, PyList>) -> PyResult<Vec<f64>> {
416454
let core_pts: Vec<CoreLngLat> = points
@@ -429,6 +467,45 @@ pub mod batch_mod {
429467
}))
430468
}
431469

470+
/// Calculate the total haversine distance along a path.
471+
///
472+
/// Computes the sum of great-circle distances between all consecutive points
473+
/// using the Haversine formula.
474+
///
475+
/// Parameters
476+
/// ----------
477+
/// points : list[LngLat]
478+
/// List of coordinates representing a path (minimum 2 points)
479+
///
480+
/// Returns
481+
/// -------
482+
/// float
483+
/// Total path length in meters
484+
///
485+
/// Examples
486+
/// --------
487+
/// >>> from rapidgeo import LngLat
488+
/// >>> from rapidgeo.distance.batch import path_length_haversine
489+
/// >>> route = [
490+
/// ... LngLat(-122.4194, 37.7749), # San Francisco
491+
/// ... LngLat(-87.6298, 41.8781), # Chicago
492+
/// ... LngLat(-74.0060, 40.7128), # New York
493+
/// ... ]
494+
/// >>> total_km = path_length_haversine(route) / 1000
495+
/// >>> print(f"Total route: {total_km:.0f} km")
496+
/// Total route: 4129 km
497+
///
498+
/// Notes
499+
/// -----
500+
/// - Uses spherical Earth approximation (accurate to ±0.5% for distances <1000km)
501+
/// - Releases Python GIL during computation
502+
/// - Returns 0.0 for paths with fewer than 2 points
503+
/// - For millimeter precision, use ``path_length_vincenty()``
504+
///
505+
/// See Also
506+
/// --------
507+
/// pairwise_haversine : Get individual segment distances
508+
/// pairwise_bearings : Get bearings between consecutive points
432509
#[pyfunction]
433510
pub fn path_length_haversine(py: Python, points: &Bound<'_, PyList>) -> PyResult<f64> {
434511
let core_pts: Vec<CoreLngLat> = points
@@ -447,6 +524,48 @@ pub mod batch_mod {
447524
}))
448525
}
449526

527+
/// Calculate initial bearings between consecutive points in a path.
528+
///
529+
/// Computes the compass bearing (azimuth) from each point to the next point
530+
/// along the great circle path. Returns a list of bearings with length ``len(points) - 1``.
531+
///
532+
/// Bearings are measured in degrees (0-360°) clockwise from North:
533+
/// 0° = North, 90° = East, 180° = South, 270° = West
534+
///
535+
/// Parameters
536+
/// ----------
537+
/// points : list[LngLat]
538+
/// List of coordinates representing a path
539+
///
540+
/// Returns
541+
/// -------
542+
/// list[float]
543+
/// Initial bearings in degrees (0-360°). Length is ``len(points) - 1``.
544+
///
545+
/// Examples
546+
/// --------
547+
/// >>> from rapidgeo import LngLat
548+
/// >>> from rapidgeo.distance.batch import pairwise_bearings
549+
/// >>> path = [
550+
/// ... LngLat(0.0, 0.0), # Origin
551+
/// ... LngLat(1.0, 0.0), # East
552+
/// ... LngLat(1.0, 1.0), # North
553+
/// ... ]
554+
/// >>> bearings = pairwise_bearings(path)
555+
/// >>> [f"{b:.1f}°" for b in bearings]
556+
/// ['90.0°', '0.0°']
557+
///
558+
/// Notes
559+
/// -----
560+
/// - Returns initial bearing at each point (bearing changes along great circles)
561+
/// - Releases Python GIL during computation
562+
/// - Returns empty list for paths with fewer than 2 points
563+
/// - Handles antimeridian crossing correctly
564+
///
565+
/// See Also
566+
/// --------
567+
/// pairwise_haversine : Distances between consecutive points
568+
/// bearing : Single bearing calculation
450569
#[pyfunction]
451570
pub fn pairwise_bearings(py: Python, points: &Bound<'_, PyList>) -> PyResult<Vec<f64>> {
452571
let core_pts: Vec<CoreLngLat> = points

0 commit comments

Comments
 (0)