coveralls: 
[![Coverage Status](https://coveralls.io/builds/20765923/badge)](https://coveralls.io/builds/20765923)

Coverage increased (+0.1%) to 85.619% when pulling **d62adcc802a09b7bd55424ff571a565117c7356b on gvenewour:course-between** into **412d190dda52a91e1647cabce82965b5349689fd on tkrajina:dev**.

tkrajina: OK, just jumping quickly to say that this looks like in interesting feature and I definitely plan to check the code and merge it (once I find some time, which may not be in the next couple of weeks).
diff --git a/gpxpy/geo.py b/gpxpy/geo.py
index bb597c1..0b37443 100644
--- a/gpxpy/geo.py
+++ b/gpxpy/geo.py
@@ -50,6 +50,48 @@ def haversine_distance(latitude_1: float, longitude_1: float, latitude_2: float,
     return d
 
 
+def get_course(latitude_1: float, longitude_1: float, latitude_2: float, longitude_2: float,
+               loxodromic: bool=True) -> float:
+    """
+    The initial course from one point to another,
+    expressed in decimal degrees clockwise from true North
+    (not magnetic)
+    (0.0 <= value < 360.0)
+
+    Use the default loxodromic model in most cases
+    (except when visualizing the long routes of maritime transport and aeroplanes)
+
+    Implemented from http://www.movable-type.co.uk/scripts/latlong.html
+    (sections 'Bearing' and 'Rhumb lines')
+    """
+
+    d_lon = mod_math.radians(longitude_2 - longitude_1)
+    lat1 = mod_math.radians(latitude_1)
+    lat2 = mod_math.radians(latitude_2)
+
+    if not loxodromic:
+        y = mod_math.sin(d_lon) * mod_math.cos(lat2)
+        x = mod_math.cos(lat1) * mod_math.sin(lat2) - \
+            mod_math.sin(lat1) * mod_math.cos(lat2) * mod_math.cos(d_lon)
+    else:
+        radian_circle = 2*mod_math.pi
+
+        if abs(d_lon) > mod_math.pi:
+            if d_lon > 0:
+                d_lon = - (radian_circle - d_lon)
+            else:
+                d_lon = radian_circle + d_lon
+
+        y = d_lon
+
+        delta = mod_math.pi/4
+        x = mod_math.log(mod_math.tan(delta + 0.5*lat2)
+                         / mod_math.tan(delta + 0.5*lat1))
+
+    course = mod_math.degrees(mod_math.atan2(y, x))
+    return (course + 360.) % 360.
+
+
 def length(locations: List["Location"]=[], _3d: bool=False) -> float:
     if not locations:
         return 0
diff --git a/gpxpy/gpx.py b/gpxpy/gpx.py
index d82cdb0..c09750c 100644
--- a/gpxpy/gpx.py
+++ b/gpxpy/gpx.py
@@ -21,7 +21,6 @@ import math as mod_math
 import collections as mod_collections
 import copy as mod_copy
 import datetime as mod_datetime
-import datetime as mod_datetime
 
 from . import utils as mod_utils
 from . import geo as mod_geo
@@ -628,10 +627,57 @@ class GPXTrackPoint(mod_geo.Location):
 
         return length / float(seconds)
 
+    def course_between(self, track_point: "GPXTrackPoint", loxodromic: bool=True) -> Optional[float]:
+        """
+        Compute the instantaneous course from one point to another.
+
+        Both loxodromic (Rhumb line) and orthodromic (Great circle) navigation
+        models are available.
+
+        The default navigation model is loxodromic.
+
+        There is no difference between these models in course computation
+        when points are relatively close to each other (less than ≈150 km)
+
+        In most cases the default model is OK.
+
+        However, the orthodromic navigation model can be important for the
+        long-distance (> ≈1000 km) logs acquired from maritime transport
+        or aeroplanes
+
+        Generally, the choice between these two models depends on a vehicle type,
+        distance and navigation equipment used.
+
+        More information on these two navigation models:
+        https://www.movable-type.co.uk/scripts/latlong.html
+
+        NOTE: This is a computed course, not the GPXTrackPoint course that comes in
+              the GPX file.
+
+        Parameters
+        ----------
+        track_point : GPXTrackPoint
+        loxodromic : True
+                     Set to False to use the orthodromic navigation model
+
+        Returns
+        ----------
+        course : float
+                Course returned in decimal degrees, true (not magnetic)
+                (0.0 <= value < 360.0)
+        """
+
+        if not track_point:
+            return None
+
+        course = mod_geo.get_course(self.latitude, self.longitude,
+                                    track_point.latitude, track_point.longitude,
+                                    loxodromic)
+        return course
+
     def __str__(self) -> str:
         return f'[trkpt:{self.latitude},{self.longitude}@{self.elevation}@{self.time}]'
 
-
 class GPXTrackSegment:
     gpx_10_fields = [
             mod_gpxfield.GPXComplexField('points', tag='trkpt', classs=GPXTrackPoint, is_list=True),

Add course_between-Method for Track Points
As described in the readme, the speed-attribute is not supported in GPX 1.1 anymore, I guess that's the same with the course-attribute. As for speed, there is a method `speed_between()` to calculate the speed between two points, is it possible to implement a similar method for the course between two points?
diff --git a/test.py b/test.py
index 0b3e734..a7d4b84 100644
--- a/test.py
+++ b/test.py
@@ -879,6 +879,88 @@ class GPXTests(mod_unittest.TestCase):
             self.assertTrue(test_gpx.tracks[0].segments[0].points[0].vertical_dilution == 301.1)
             self.assertTrue(test_gpx.tracks[0].segments[0].points[0].position_dilution == 302.1)
 
+    def test_course_between(self) -> None:
+        gpx = mod_gpx.GPX()
+        track = mod_gpx.GPXTrack()
+
+        segment = mod_gpx.GPXTrackSegment()
+        points = segment.points
+
+        # The points are extremely distant.
+        # Therefore, the computed orthodromic and loxodromic courses
+        # should diverge significantly.
+
+        points.append(mod_gpx.GPXTrackPoint(latitude=-73, longitude=-150))
+        points.append(mod_gpx.GPXTrackPoint(latitude=43.5798, longitude=35.71265))
+        points.append(mod_gpx.GPXTrackPoint(latitude=85, longitude=0.12345))
+        track.segments.append(segment)
+        gpx.tracks.append(track)
+
+        self.assertEqual(points[0].course_between(points[0]), 0)
+        # self.assertIsNone(points[2].course_between(None))
+
+        course_01 = points[0].course_between(points[1])
+        course_12 = points[1].course_between(points[2])
+        course_02 = points[0].course_between(points[2])
+
+        self.assertAlmostEqual(course_01, 312.089, 3)  # type: ignore
+        self.assertAlmostEqual(course_12, 344.790, 3)  # type: ignore
+        self.assertAlmostEqual(course_02, 27.5055, 3)  # type: ignore
+
+        # The default computational model should be loxodromic:
+
+        self.assertAlmostEqual(points[0].course_between(points[1], loxodromic=True), course_01, 6)  # type: ignore
+        self.assertAlmostEqual(points[1].course_between(points[2], loxodromic=True), course_12, 6)  # type: ignore
+        self.assertAlmostEqual(points[0].course_between(points[2], loxodromic=True), course_02, 6)  # type: ignore
+
+        # Verifying the orthodromic results
+
+        course_orthodromic_01 = points[0].course_between(points[1], loxodromic=False)
+        course_orthodromic_12 = points[1].course_between(points[2], loxodromic=False)
+        course_orthodromic_02 = points[0].course_between(points[2], loxodromic=False)
+
+        self.assertAlmostEqual(course_orthodromic_01, 188.409, 3)  # type: ignore
+        self.assertAlmostEqual(course_orthodromic_12, 355.6886, 3)  # type: ignore
+        self.assertAlmostEqual(course_orthodromic_02, 11.2136, 3)  # type: ignore
+
+        # Short distance tests:
+
+        gpx_short = self.parse('track_with_speed.gpx')
+        points_short = gpx_short.tracks[0].segments[0].points
+
+        course_short_01 = points_short[0].course_between(points_short[1])
+        course_short_12 = points_short[1].course_between(points_short[2])
+        course_short_02 = points_short[0].course_between(points_short[2])
+
+        # When the points are not too distant (less than about 100-150km),
+        # the orthodromic and loxodromic bearings should be almost identical:
+
+        self.assertAlmostEqual(points_short[0].course_between(points_short[1], loxodromic=False), course_short_01, 3)  # type: ignore
+        self.assertAlmostEqual(points_short[1].course_between(points_short[2], loxodromic=False), course_short_12, 3)  # type: ignore
+        self.assertAlmostEqual(points_short[0].course_between(points_short[2], loxodromic=False), course_short_02, 3)  # type: ignore
+
+    def test_get_course(self) -> None:
+        pts = [[-73, -150], [43.5798, 35.71265], [85, 0.12345]]
+
+        # same long distance checks as in test_get_course_between
+        self.assertAlmostEqual(mod_geo.get_course(pts[0][0], pts[0][1], pts[1][0], pts[1][1]), 312.089, 3)  # type: ignore
+        self.assertAlmostEqual(mod_geo.get_course(pts[1][0], pts[1][1], pts[2][0], pts[2][1]), 344.790, 3)  # type: ignore
+        self.assertAlmostEqual(mod_geo.get_course(pts[0][0], pts[0][1], pts[2][0], pts[2][1]), 27.5055, 3)  # type: ignore
+
+        self.assertAlmostEqual(mod_geo.get_course(pts[0][0], pts[0][1], pts[1][0], pts[1][1],  # type: ignore
+                                                  loxodromic=True), 312.089, 3)
+        self.assertAlmostEqual(mod_geo.get_course(pts[1][0], pts[1][1], pts[2][0], pts[2][1],  # type: ignore
+                                                  loxodromic=True), 344.790, 3)
+        self.assertAlmostEqual(mod_geo.get_course(pts[0][0], pts[0][1], pts[2][0], pts[2][1],  # type: ignore
+                                                  loxodromic=True), 27.5055, 3)
+
+        self.assertAlmostEqual(mod_geo.get_course(pts[0][0], pts[0][1], pts[1][0], pts[1][1],  # type: ignore
+                                                  loxodromic=False), 188.409, 3)
+        self.assertAlmostEqual(mod_geo.get_course(pts[1][0], pts[1][1], pts[2][0], pts[2][1],  # type: ignore
+                                                  loxodromic=False), 355.6886, 3)
+        self.assertAlmostEqual(mod_geo.get_course(pts[0][0], pts[0][1], pts[2][0], pts[2][1],  # type: ignore
+                                                  loxodromic=False), 11.2136, 3)
+
     def test_name_comment_and_symbol(self) -> None:
         gpx = mod_gpx.GPX()
         track = mod_gpx.GPXTrack()

pip install -e .[dev]
parse_log_pytest
requirements.txt
lxml coverage pycodestyle pyflakes pytest
apt-get update apt-get install -y gcc
3.7
requirements/base.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi @ file:///croot/certifi_1671487769961/work/certifi
coverage==7.2.7
exceptiongroup==1.2.2
-e git+https://github.com/tkrajina/gpxpy.git@646c25029e6a5fa328b07ea649195fd19b6531f6#egg=gpxpy
importlib-metadata==6.7.0
iniconfig==2.0.0
lxml==5.3.1
packaging==24.0
pluggy==1.2.0
pycodestyle==2.10.0
pyflakes==3.0.1
pytest==7.4.4
tomli==2.0.1
typing_extensions==4.7.1
zipp==3.15.0

name: gpxpy
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=22.3.1=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.2.7
      - exceptiongroup==1.2.2
      - importlib-metadata==6.7.0
      - iniconfig==2.0.0
      - lxml==5.3.1
      - packaging==24.0
      - pluggy==1.2.0
      - pycodestyle==2.10.0
      - pyflakes==3.0.1
      - pytest==7.4.4
      - tomli==2.0.1
      - typing-extensions==4.7.1
      - zipp==3.15.0
prefix: /opt/conda/envs/gpxpy

test.py::GPXTests::test_course_between test.py::GPXTests::test_get_course
test.py::GPXTests::test_10_to_11_conversion test.py::GPXTests::test_add_elevation test.py::GPXTests::test_add_missing_data_no_intervals test.py::GPXTests::test_add_missing_data_one_interval test.py::GPXTests::test_add_missing_data_one_interval_and_empty_points_on_start_and_end test.py::GPXTests::test_add_missing_elevations test.py::GPXTests::test_add_missing_elevations_without_ele test.py::GPXTests::test_add_missing_speeds test.py::GPXTests::test_add_missing_times test.py::GPXTests::test_add_missing_times_2 test.py::GPXTests::test_adjust_time_all test.py::GPXTests::test_adjust_time_tracks_only test.py::GPXTests::test_angle test.py::GPXTests::test_angle_0 test.py::GPXTests::test_angle_2 test.py::GPXTests::test_angle_3 test.py::GPXTests::test_angle_4 test.py::GPXTests::test_angle_loc test.py::GPXTests::test_bounds test.py::GPXTests::test_clone_and_smooth test.py::GPXTests::test_creator_field test.py::GPXTests::test_custom_schema_locations test.py::GPXTests::test_default_schema_locations test.py::GPXTests::test_delta_add_and_move test.py::GPXTests::test_dilutions test.py::GPXTests::test_distance test.py::GPXTests::test_distance_between_points_near_0_longitude test.py::GPXTests::test_distance_from_line test.py::GPXTests::test_extension_without_namespaces test.py::GPXTests::test_force_version test.py::GPXTests::test_get_bounds_and_refresh_bounds test.py::GPXTests::test_get_duration test.py::GPXTests::test_get_location_at test.py::GPXTests::test_gpx_10_fields test.py::GPXTests::test_gpx_11_fields test.py::GPXTests::test_gpx_fill_time_data_raises_when_force_is_false test.py::GPXTests::test_gpx_fill_time_data_raises_when_not_enough_parameters test.py::GPXTests::test_gpx_fill_time_data_raises_when_start_time_after_end_time test.py::GPXTests::test_gpx_fill_time_data_with_end_time_and_time_delta test.py::GPXTests::test_gpx_fill_time_data_with_start_time_and_end_time test.py::GPXTests::test_gpx_fill_time_data_with_start_time_and_end_time_and_time_delta test.py::GPXTests::test_gpx_fill_time_data_with_start_time_and_time_delta test.py::GPXTests::test_has_elevation_false test.py::GPXTests::test_has_elevation_true test.py::GPXTests::test_has_times test.py::GPXTests::test_has_times_false test.py::GPXTests::test_haversine_and_nonhaversine test.py::GPXTests::test_haversine_distance test.py::GPXTests::test_horizontal_and_vertical_smooth_remove_extremes test.py::GPXTests::test_horizontal_smooth_remove_extremes test.py::GPXTests::test_ignore_maximums_for_max_speed test.py::GPXTests::test_join_gpx_xml_files test.py::GPXTests::test_location_delta test.py::GPXTests::test_location_equator_delta_distance_111120 test.py::GPXTests::test_location_equator_delta_distance_50 test.py::GPXTests::test_location_nonequator_delta_distance_111120 test.py::GPXTests::test_location_nonequator_delta_distance_50 test.py::GPXTests::test_long_timestamps test.py::GPXTests::test_microsecond test.py::GPXTests::test_min_max test.py::GPXTests::test_moving_stopped_times test.py::GPXTests::test_name_comment_and_symbol test.py::GPXTests::test_named_tuples_values_elevation_extremes test.py::GPXTests::test_named_tuples_values_moving_data test.py::GPXTests::test_named_tuples_values_nearest_location_data test.py::GPXTests::test_named_tuples_values_point_data test.py::GPXTests::test_named_tuples_values_time_bounds test.py::GPXTests::test_named_tuples_values_uphill_downhill test.py::GPXTests::test_nan_elevation test.py::GPXTests::test_nearest_location_1 test.py::GPXTests::test_no_10_extensions test.py::GPXTests::test_no_creator_field test.py::GPXTests::test_no_track test.py::GPXTests::test_parse_custom_schema_locations test.py::GPXTests::test_parse_gpx_with_node_with_comments test.py::GPXTests::test_parse_time test.py::GPXTests::test_positions_on_track test.py::GPXTests::test_positions_on_track_2 test.py::GPXTests::test_read_extensions test.py::GPXTests::test_reduce_by_min_distance test.py::GPXTests::test_reduce_gpx_file test.py::GPXTests::test_remove_elevation test.py::GPXTests::test_remove_point_from_segment test.py::GPXTests::test_remove_time_all test.py::GPXTests::test_remove_time_tracks_only test.py::GPXTests::test_simple_parse_function test.py::GPXTests::test_simple_parse_function_invalid_xml test.py::GPXTests::test_simplify test.py::GPXTests::test_simplify_circular_gpx test.py::GPXTests::test_single_quotes_xmlns test.py::GPXTests::test_small_floats test.py::GPXTests::test_smooth_without_removing_extreemes_preserves_point_count test.py::GPXTests::test_smooth_without_removing_extreemes_preserves_point_count_2 test.py::GPXTests::test_smooth_without_removing_extreemes_preserves_point_count_3 test.py::GPXTests::test_spaces_in_elevation test.py::GPXTests::test_speed test.py::GPXTests::test_split test.py::GPXTests::test_split_and_join test.py::GPXTests::test_split_on_impossible_index test.py::GPXTests::test_split_tracks_without_gpxpy test.py::GPXTests::test_time_bounds test.py::GPXTests::test_time_difference test.py::GPXTests::test_timestamp_with_single_digits test.py::GPXTests::test_timezone_from_timestamp test.py::GPXTests::test_to_xml_creator test.py::GPXTests::test_track_points_data test.py::GPXTests::test_track_with_elevation_zero test.py::GPXTests::test_track_with_empty_segment test.py::GPXTests::test_track_with_some_points_are_without_elevations test.py::GPXTests::test_track_without_elevation test.py::GPXTests::test_unicode test.py::GPXTests::test_unicode_2 test.py::GPXTests::test_unicode_bom test.py::GPXTests::test_unicode_bom_noencoding test.py::GPXTests::test_unicode_name test.py::GPXTests::test_vertical_smooth_remove_extremes test.py::GPXTests::test_walk_gpx_points test.py::GPXTests::test_walk_gpx_points2 test.py::GPXTests::test_walk_route_points test.py::GPXTests::test_walk_segment_points test.py::GPXTests::test_waypoint_time test.py::GPXTests::test_waypoints_equality_after_reparse test.py::GPXTests::test_with_ns_namespace test.py::GPXTests::test_write_read_extensions test.py::GPXTests::test_xml_chars_encode_decode test.py::GPXTests::test_zero_latlng
gpxpy/gpx.py gpxpy/geo.py
gpxpy/gpx.py gpxpy/geo.py