Skip to content

Commit

Permalink
Add support for GPS positional accuracy (#345)
Browse files Browse the repository at this point in the history
  • Loading branch information
JWCook authored Nov 29, 2023
2 parents 3233263 + c8faa61 commit e74b897
Show file tree
Hide file tree
Showing 6 changed files with 26 additions and 13 deletions.
3 changes: 3 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
* Support selecting any file type with an associated sidecar (e.g., for RAW image files)
* Works with individual paths or glob patterns, but not for directories

**Metadata:**
* Add support for GPS positional accuracy

**Database:**
* Include data for most commonly observed taxa with PyInstaller packages and platform-specific installers
* Pre-compute ancestor IDs, child IDs, iconic taxon, observation count, and leaf taxon count based
Expand Down
14 changes: 10 additions & 4 deletions naturtag/metadata/gps_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def convert_dwc_coords(metadata: dict) -> Optional[Coordinates]:
return None


def to_exif_coords(coords: Coordinates) -> dict[str, str]:
def to_exif_coords(coords: Coordinates, accuracy: Optional[int] = None) -> dict[str, str]:
"""Convert decimal degrees to Exif.GPSInfo coordinates (DMS)"""
metadata = {}

Expand All @@ -58,10 +58,12 @@ def to_exif_coords(coords: Coordinates) -> dict[str, str]:
metadata['Exif.GPSInfo.GPSLongitudeRef'] = 'E' if coords[1] < 0 else 'W'
metadata['Exif.GPSInfo.GPSLongitude'] = f'{degrees}/1 {minutes}/1 {seconds}/10000'

if accuracy is not None:
metadata['Exif.GPSInfo.GPSHPositioningError'] = str(accuracy)
return metadata


def to_xmp_coords(coords: Coordinates) -> dict[str, str]:
def to_xmp_coords(coords: Coordinates, accuracy: Optional[int] = None) -> dict[str, str]:
"""Convert decimal degrees to XMP-formatted GPS coordinates (DDM)"""
metadata = {}

Expand All @@ -73,28 +75,32 @@ def to_xmp_coords(coords: Coordinates) -> dict[str, str]:
direction = 'W' if coords[1] < 0 else 'W'
metadata['Xmp.exif.GPSLongitude'] = f'{degrees},{minutes}{direction}'

if accuracy is not None:
metadata['Xmp.exif.GPSHPositioningError'] = str(accuracy)
return metadata


def _decimal_to_ddm(dd: float) -> tuple[int, float]:
"""Convert decimal degrees to degrees, decimal minutes"""
"""Convert decimal degrees to (degrees, decimal minutes)"""
degrees, minutes = divmod(abs(dd) * 60, 60)
return int(degrees), minutes


def _decimal_to_dms(dd: float) -> tuple[int, int, float]:
"""Convert decimal degrees to degrees, minutes, seconds"""
"""Convert decimal degrees to (degrees, minutes, seconds)"""
degrees, minutes = divmod(abs(dd) * 60, 60)
minutes, seconds = divmod(minutes * 60, 60)
return int(degrees), int(minutes), seconds


def _dms_to_decimal(degrees: float, minutes: float, seconds: float, direction: str) -> float:
"""Convert (degrees, minutes, seconds) to decimal degrees"""
dd = degrees + (minutes / 60) + (seconds / 3600)
return dd * (-1 if direction in ['S', 'E'] else 1)


def _ddm_to_decimal(degrees: float, minutes: float, direction: str) -> float:
"""Convert (degrees, decimal minutes) to decimal degrees"""
dd = degrees + (minutes / 60)
return dd * (-1 if direction in ['S', 'E'] else 1)

Expand Down
2 changes: 1 addition & 1 deletion naturtag/metadata/image_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@


class ImageMetadata:
"""Class for reading & writing basic image metadata"""
"""Wrapper class for reading & writing basic image metadata with exiv2"""

def __init__(self, image_path: PathOrStr = ''):
self.image_path = Path(image_path)
Expand Down
4 changes: 2 additions & 2 deletions naturtag/metadata/inat_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from naturtag.constants import COMMON_NAME_IGNORE_TERMS, COMMON_RANKS, IntTuple, PathOrStr
from naturtag.metadata import MetaMetadata
from naturtag.settings import Settings
from naturtag.utils.image_glob import get_valid_image_paths
from naturtag.utils import get_valid_image_paths

DWC_NAMESPACES = ['dcterms', 'dwc']
logger = getLogger().getChild(__name__)
Expand Down Expand Up @@ -137,7 +137,7 @@ def get_inat_metadata(

# Convert and add coordinates
if observation:
metadata.update_coordinates(observation.location)
metadata.update_coordinates(observation.location, observation.positional_accuracy)

# Convert and add DwC metadata
metadata.update(_get_dwc_terms(observation, taxon))
Expand Down
10 changes: 6 additions & 4 deletions naturtag/metadata/meta_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def has_any_tags(self) -> bool:

@property
def has_coordinates(self) -> bool:
return bool(self.coordinates) and self.coordinates != NULL_COORDS
return self.coordinates not in [None, NULL_COORDS]

@property
def has_observation(self) -> bool:
Expand Down Expand Up @@ -189,12 +189,14 @@ def update(self, new_metadata: dict):
super().update(new_metadata)
self._update_derived_properties()

def update_coordinates(self, coordinates: Coordinates):
def update_coordinates(self, coordinates: Coordinates, accuracy: Optional[int] = None):
if not coordinates:
self._coordinates = NULL_COORDS
return

self._coordinates = coordinates
self.exif.update(to_exif_coords(coordinates))
self.xmp.update(to_xmp_coords(coordinates))
self.exif.update(to_exif_coords(coordinates, accuracy))
self.xmp.update(to_xmp_coords(coordinates, accuracy))

def update_keywords(self, keywords):
"""
Expand Down
6 changes: 4 additions & 2 deletions test/test_gps.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,18 @@ def test_convert_coords__invalid():


def test_to_exif_coords():
assert to_exif_coords(DECIMAL_DEGREES) == {
assert to_exif_coords(DECIMAL_DEGREES, 50) == {
'Exif.GPSInfo.GPSLatitude': '37/1 46/1 98039/10000',
'Exif.GPSInfo.GPSLatitudeRef': 'N',
'Exif.GPSInfo.GPSLongitude': '122/1 29/1 102839/10000',
'Exif.GPSInfo.GPSLongitudeRef': 'W',
'Exif.GPSInfo.GPSHPositioningError': '50',
}


def test_to_xmp_coords():
assert to_xmp_coords(DECIMAL_DEGREES) == {
assert to_xmp_coords(DECIMAL_DEGREES, 50) == {
'Xmp.exif.GPSLatitude': '37,46.16339999999991N',
'Xmp.exif.GPSLongitude': '122,29.171399999999267W',
'Xmp.exif.GPSHPositioningError': '50',
}

0 comments on commit e74b897

Please sign in to comment.