@@ -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 (" \n Data 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 " \n Detected { 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 " \n Total 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
0 commit comments