CompassHeadingLib is a small, dependency-free, pure python library for working with compass headings in terms of comparison and transformation between decimal degree and natural language space. While it originally only suported English, it supports multiple languages.
CompassHeadingLib follows the worse is better design philosophy; it's better to have a slower less featureful implementation then no implementation at all. Optimizations will be executed when we have to.
CompassHeadingLib is licensed under the MIT License and offered as is without warranty of any kind, express or implied.
pip install -u compassheadinglib
pip install pip@git+https://github.com/Peter-E-Lenz/compassheadinglib/
# Import with default English language support
from compassheadinglib import Compass
# Or import with specific language support
from compassheadinglib.fr import Compass # French
from compassheadinglib.de import Compass # German
from compassheadinglib.es import Compass # Spanish
compass = Compass()
# Find the closest heading for a given bearing (in decimal degrees)
heading = compass.findHeading(45.0)
print(heading.name) # "Northeast"
print(heading.abbr) # "NE"
print(heading.azimuth) # 45.0
print(heading.order) # 2
# You can also call the compass object directly
heading = compass(45.0) # Same as compass.findHeading(45.0)
bearing = 80.0
# Order 1: Cardinal directions only (N, E, S, W)
heading_order1 = compass.findHeading(bearing, order=1)
print(heading_order1.name) # "East"
# Order 2: Half-winds (NE, SE, SW, NW)
heading_order2 = compass.findHeading(bearing, order=2)
print(heading_order2.name) # "East"
# Order 3: Quarter-winds (default)
heading_order3 = compass.findHeading(bearing, order=3)
print(heading_order3.name) # "East-Northeast"
# Order 4: Eighth-winds (most precise)
heading_order4 = compass.findHeading(bearing, order=4)
print(heading_order4.name) # "East by North"
# Start with a North heading
north = compass.findHeading(0.0)
print(north.name) # "North"
# Rotate 45 degrees clockwise
northeast = north.rotate(45)
print(northeast.name) # "Northeast"
# Rotate 90 degrees counter-clockwise
west = north.rotate(-90)
print(west.name) # "West"
# Rotation automatically wraps around
north_by_west = north.rotate(-30)
print(north_by_west.name) # "North by West"
print(north_by_west.azimuth) # 330.0 (wraps from -30 to 330)
# Large rotations also wrap
east_northeast = north.rotate(405) # 405° = 45° after wrapping
print(east_northeast.name) # "Northeast"
# Start with an East heading
east = compass.findHeading(90.0)
# Turn to starboard (right/clockwise)
southeast = east.starboard(45)
print(southeast.name) # "Southeast"
# Turn to port (left/counter-clockwise)
northeast = east.port(45)
print(northeast.name) # "Northeast"
# Wraparound examples
north = compass.findHeading(0.0)
northwest = north.port(45) # 0° - 45° = 315° (wraps around)
print(northwest.name) # "Northwest"
west = compass.findHeading(270.0)
northeast = west.starboard(135) # 270° + 135° = 405° → 45° (wraps around)
print(northeast.name) # "Northeast"
# Same functionality with familiar left/right terms
east = compass.findHeading(90.0)
# Turn right (clockwise)
southeast = east.right(45)
print(southeast.name) # "Southeast"
# Turn left (counter-clockwise)
northeast = east.left(45)
print(northeast.name) # "Northeast"
compass = Compass()
# Create some headings
north = compass(0) # 0°
east = compass(90) # 90°
south = compass(180) # 180°
west = compass(270) # 270°
# Add headings (adds their azimuth values)
result1 = north + east # 0° + 90° = 90°
print(result1.name) # "East"
# Add degrees to headings
northeast = north + 45 # 0° + 45° = 45°
print(northeast.name) # "Northeast"
# Reverse addition works too
northeast2 = 45 + north # Same as above
print(northeast2.name) # "Northeast"
# Subtract headings
result2 = east - north # 90° - 0° = 90°
print(result2.name) # "East"
# Subtract degrees from headings
northwest = north - 45 # 0° - 45° = 315° (wraps around)
print(northwest.name) # "Northwest"
# Reverse subtraction
result3 = 360 - east # 360° - 90° = 270°
print(result3.name) # "West"
# Wraparound examples
west_by_north = west + 45 # 270° + 45° = 315°
print(west_by_north.name) # "Northwest"
south_by_east = south - 45 # 180° - 45° = 135°
print(south_by_east.name) # "Southeast"
# Start with English compass
from compassheadinglib import Compass
compass = Compass()
# Get a heading in English
north_en = compass.findHeading(0.0)
print(north_en.name) # "North"
# Translate to other languages
north_fr = north_en.translate('FR')
print(north_fr.name) # "Nord" (French)
north_de = north_en.translate('DE')
print(north_de.name) # "Nord" (German)
north_es = north_en.translate('ES')
print(north_es.name) # "Norte" (Spanish)
# Create compass objects for different languages
from compassheadinglib.fr import Compass as CompassFR
from compassheadinglib.de import Compass as CompassDE
compass_fr = CompassFR()
compass_de = CompassDE()
# Get headings directly in different languages
heading_fr = compass_fr.findHeading(45.0)
print(heading_fr.name) # "Nord-Est" (French)
heading_de = compass_de.findHeading(45.0)
print(heading_de.name) # "Nordost" (German)
compass = Compass()
# Current heading
current_bearing = 75.5
current_heading = compass.findHeading(current_bearing)
print(f"Current heading: {current_heading.name} ({current_heading.azimuth}°)")
# Need to turn 30 degrees to starboard
new_heading = current_heading.starboard(30)
print(f"New heading: {new_heading.name} ({new_heading.azimuth}°)")
# Alternative using addition
new_heading_alt = current_heading + 30
print(f"Same result with addition: {new_heading_alt.name}")
# Calculate the course correction needed
correction = new_heading.azimuth - current_heading.azimuth
print(f"Turn {correction}° to starboard")
compass = Compass()
# Get multiple headings
bearings = [0, 45, 90, 135, 180, 225, 270, 315]
headings = [compass.findHeading(b) for b in bearings]
# Convert to dictionary format for data processing
heading_data = [h.asDict() for h in headings]
print(heading_data)
# [{'name': 'North', 'abbr': 'N', 'azimuth': 0.0, 'order': 1}, ...]
# Compare headings
north = compass.findHeading(0)
east = compass.findHeading(90)
print(north < east) # True (0° < 90°)
print(float(north)) # 0.0
print(str(north)) # "North"
compass = Compass()
# Define a base course
base_course = compass(0) # North
print(f"Base course: {base_course.name}")
# Apply course corrections using arithmetic
corrections = [15, -30, 45, -60]
current_course = base_course
print("Course corrections:")
for i, correction in enumerate(corrections):
current_course = current_course + correction
print(f"Leg {i+1}: {current_course.name} ({current_course.azimuth}°)")
# Calculate reciprocal bearings (opposite direction)
reciprocal = current_course + 180
print(f"Reciprocal bearing: {reciprocal.name} ({reciprocal.azimuth}°)")
# Calculate relative bearings
target_bearing = compass(120) # East-Southeast
relative_bearing = target_bearing - current_course
print(f"Relative bearing to target: {relative_bearing.azimuth}°")
To use a specific language import the library as import compassheadinglib., i.e. to have Korean language support use from compassheadinglib.kr import Compass
or to use Portugese use from compassheadinglib.kr import Compass
. Because the library was originally available with only English language support from compassheadinglib import Compass
is equivalent to from compassheadinglib.en import Compass
.
Language | Two letter code |
---|---|
ar | Arabic |
cn | Chinese |
de | German |
en | English |
es | Spanish |
fr | French |
hi | Hindi |
jp | Japanese |
kr | Korean |
pt | Portugese |
Unfortunately the developer of this library in an English monoglot. Translations were achieved via machine. The developer apologizes for any mistakes in those translations - corrections are graciously accepted.
CompassHeadingLib has only dependencies from the python's standard library. It was originally written to run on Python 2.7 but is now only tested on Python 3.7+.
Type | Returns |
---|---|
Object(based on Dict) | Heading |
These functions take a heading between two points as a float (i.e. in decimal degrees) and returns the best matching heading with order
degree of specificity. Order
is a 1-indexed description of how specific the natural language The higher the order the more specific the heading. At an order
of 1 the decimal degree heading of 80.0 will return a heading object of 'East' while at an order
of 4 it would return 'East by North' heading object.
Internally, calling the Compass object directly will silently call it's findHeading
method.
Heading objects are returned by Compass objects and are not intended to be created by end users.
Type | Returns |
---|---|
Object | N/A |
Heading objects are containers for information about headings that are designed to be comparable to each other (and other python objects) using built-in methods. There are four pieces of information for each heading, each a method of the object: name
, abbr
, azimuth
, and order
. The various built-in comparisons look to different methods (and thus different pieces of the information) as appropriate. For the most part you can safely ignore all this background stuff.
Type | Returns |
---|---|
string | string |
The full name of this heading, along the lines of 'North' or 'South by East'. Note: despite what the festival has told you there is no such heading as 'South by Southwest'.
Type | Returns |
---|---|
string | string |
The abbreviated name of this heading, along the lines of 'N' or 'SbE'
Type | Returns |
---|---|
float | float |
The decimal degree value of this heading. For example; 'West' is 270.0 while 'North-Northeast' is 22.5
Type | Returns |
---|---|
integer | integer |
Order defines how specific the heading is. The cardinal directions ('North', 'East', 'South' & 'West') are of order 1 while 'South by East' is order 4. The Compass Headings Reference chart at the end of this document will be more illustrative of this difference. Put another way: order 1 headings are 90° apart, order 2 headings are 45° apart, order 3 headings are 22.5° apart, and order 4 headings are 11.25° apart. By default this library uses order 3 where ever that value can be specified. Each order includes the headings of that order and all headings of any lower valued orders. Hence order 2 includes all headings labeled order 2 and order 1.
When treated as a string the Heading object returns the value for the name
method
When treated as a numeric(regardless of int or float) it will return the values for the azimuth
method.
Heading objects support addition and subtraction operations that work with their azimuth values:
- Adding headings:
heading1 + heading2
adds their azimuth values - Adding degrees:
heading + 45
or45 + heading
adds degrees to the heading - Subtracting headings:
heading1 - heading2
subtracts their azimuth values - Subtracting degrees:
heading - 30
subtracts degrees from the heading - Reverse subtraction:
360 - heading
subtracts the heading's azimuth from a number
All arithmetic operations automatically wrap around to keep results within 0-360°.
Examples:
north = compass(0) # 0°
east = compass(90) # 90°
northeast = north + east # 0° + 90° = 90° (East)
southeast = east + 45 # 90° + 45° = 135° (Southeast)
northwest = north - 45 # 0° - 45° = 315° (Northwest, wraps around)
Type | Returns |
---|---|
Heading | Heading object |
Returns a new Heading object with the name and abbreviation translated to the specified language. The language parameter should be the two-letter language code (e.g., 'EN', 'FR', 'DE'). The azimuth, order, and other properties remain unchanged. This method allows you to get the same compass heading in different languages while maintaining all the mathematical properties.
Example: north_heading.translate('FR')
would return a Heading object with French names for the same compass direction.
Type | Returns |
---|---|
Heading | Heading object |
Rotates the current heading by the specified number of degrees and returns a new Heading object for the resulting direction. Positive degrees rotate clockwise, negative degrees rotate counter-clockwise. The resulting azimuth automatically wraps around to stay within 0-360°. This method calls the parent Compass object to find the appropriate heading for the new azimuth.
Example: north_heading.rotate(45)
would return a Northeast heading. north_heading.rotate(-30)
would return a heading at 330° (North-Northwest).
Type | Returns |
---|---|
Heading | Heading object |
Rotates the current heading to starboard (right/clockwise) by the specified number of degrees. The degrees parameter must be non-negative (>= 0). The resulting azimuth automatically wraps around to stay within 0-360°. Returns a new Heading object for the resulting direction. right()
is an alias for starboard()
for those more familiar with land-based terminology.
Example: north_heading.starboard(90)
would return an East heading. west_heading.starboard(135)
would return a Northeast heading (270° + 135° = 405°, wraps to 45°).
Type | Returns |
---|---|
Heading | Heading object |
Rotates the current heading to port (left/counter-clockwise) by the specified number of degrees. The degrees parameter must be non-negative (>= 0). The resulting azimuth automatically wraps around to stay within 0-360°. Returns a new Heading object for the resulting direction. left()
is an alias for port()
for those more familiar with land-based terminology.
Example: east_heading.port(45)
would return a Northeast heading. north_heading.port(30)
would return a heading at 330° (0° - 30°, wraps to 330°).
Type | Returns |
---|---|
dict | dictionary |
Returns a dictionary representation of the Heading object containing the core properties (name, abbr, azimuth, order) but excluding the langs and parent references. This is useful for serialization or when you need a simple data structure representation of the heading.
Heading | Abbreviation | Azimuth | Order |
---|---|---|---|
North | N | 0 | 1 |
North by East | NbE | 11.25 | 4 |
North-Northeast | NNE | 22.5 | 3 |
Northeast by North | NEbN | 33.75 | 4 |
Northeast | NE | 45 | 2 |
Northeast by East | NEbE | 56.25 | 4 |
East-Northeast | ENE | 67.5 | 3 |
East by North | EbN | 78.75 | 4 |
East | E | 90 | 1 |
East by South | EbS | 101.25 | 4 |
East-Southeast | ESE | 112.5 | 3 |
Southeast by East | SEbE | 123.75 | 4 |
Southeast | SE | 135 | 2 |
Southeast by South | SEbS | 146.25 | 4 |
South-Southeast | SSE | 157.5 | 3 |
South by East | SbE | 168.75 | 4 |
South | S | 180 | 1 |
South by West | SbW | 191.25 | 4 |
South-Southwest | SSW | 202.5 | 3 |
Southwest by South | SWbS | 213.75 | 4 |
Southwest | SW | 225 | 2 |
Southwest by West | SWbW | 236.25 | 4 |
West-Southwest | WSW | 247.5 | 3 |
West by South | WbS | 258.75 | 4 |
West | W | 270 | 1 |
West by North | WbN | 281.25 | 4 |
West-Northwest | WNW | 292.5 | 3 |
Northwest by West | NWbW | 303.75 | 4 |
Northwest | NW | 315 | 2 |
Northwest by North | NWbN | 326.25 | 4 |
North-Northwest | NNW | 337.5 | 3 |
North by West | NbW | 348.75 | 4 |