302 lines
11 KiB
Python
302 lines
11 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
''' Classes used by main program. '''
|
|
|
|
from datetime import datetime, timedelta
|
|
import os
|
|
import shutil
|
|
from fractions import Fraction
|
|
import pyexiv2
|
|
|
|
class Radiation:
|
|
'''
|
|
Reiceives values vom CSV file and creates a list of the relevant data
|
|
|
|
Arguments:
|
|
timestamp: Date/time string from CSV as string
|
|
radiation: Radiation from CSV in CP/M as float
|
|
local_timezone: timezone for timezone-unware CSV / Photo, if GPX is timezone aware
|
|
si_factor: CP/M to (µS/h) conversion factor - specific to GMC-tube
|
|
|
|
Returns:
|
|
timestamp: timestamp of CSV value als datetime object
|
|
radiation: radiation in µS/h as str (for Exif comment, UTF-8)
|
|
'''
|
|
|
|
def __init__(self, timestamp, radiation, local_timezone, si_factor):
|
|
self.timestamp = self._time_conversion(timestamp, local_timezone)
|
|
self.radiation = self._radiation_conversion(radiation, si_factor)
|
|
|
|
def __repr__(self):
|
|
return '%s %f µS/h' % (str(self.timestamp), self.radiation)
|
|
|
|
def _time_conversion(self, timestamp, local_timezone):
|
|
csv_naive_time = datetime.fromisoformat(timestamp)
|
|
# Set timezone
|
|
csv_aware_time = csv_naive_time.astimezone(local_timezone)
|
|
return csv_aware_time
|
|
|
|
def _radiation_conversion(self, radiation, si_factor):
|
|
# Convert CP/M to µS/h using si_factor
|
|
radiation = float(radiation) * si_factor
|
|
return radiation
|
|
|
|
class Photo:
|
|
'''
|
|
Reads Exif metadata.
|
|
|
|
Arguments:
|
|
photo: source photo ()
|
|
local_timezone: timezone for timezone-unware CSV / Photo, if GPX is timezone aware
|
|
dest_dir: destination directory where the photo is going to be copied to.
|
|
dry_run: whether to acutally write (True / False)
|
|
|
|
Returns:
|
|
get_date: timestamp of photo als datetime object
|
|
get_photo_filename: full path to photo file to work on
|
|
get_photo_basename: only filename (e. g. for print output)
|
|
'''
|
|
|
|
def __init__(self, photo, local_timezone, dest_dir, dry_run):
|
|
self.get_date = self._get_creation_date(photo, local_timezone)
|
|
self.get_photo_filename = self._copy_photo(photo, dest_dir, dry_run)[1]
|
|
self.get_photo_basename = self._copy_photo(photo, dest_dir, dry_run)[0]
|
|
|
|
def __repr__(self):
|
|
return 'Photo: %s Creation Date: %s' % (str(self.get_photo_basename), str(self.get_date))
|
|
|
|
def _copy_photo(self, photo, dest_dir, dry_run):
|
|
# Determine where to work on photo and copy it there if needed.
|
|
|
|
# Get image file name out of path
|
|
photo_basename = os.path.basename(photo)
|
|
# be os aware and use the correct directory delimiter for dest_photo
|
|
dest_photo = os.path.join(dest_dir, photo_basename)
|
|
|
|
# Copy photo to dest_dir and return its (new) filename
|
|
# if not in dry_run mode or if dest_dir is different from src_dir.
|
|
if dry_run is True:
|
|
return photo_basename, photo
|
|
if dest_dir != '.':
|
|
shutil.copy(photo, dest_photo)
|
|
return photo_basename, dest_photo
|
|
return photo_basename, photo
|
|
|
|
def _get_creation_date(self, photo, local_timezone):
|
|
# Load Exif data from photo
|
|
metadata = pyexiv2.ImageMetadata(photo)
|
|
metadata.read()
|
|
date = metadata['Exif.Photo.DateTimeOriginal']
|
|
# date.value creates datetime object in pic_naive_time
|
|
pic_naive_time = date.value
|
|
# Set timezone
|
|
pic_aware_time = pic_naive_time.astimezone(local_timezone)
|
|
return pic_aware_time
|
|
|
|
class Match:
|
|
'''
|
|
Receives lists of time / radiation and GPS data and compares it to timestamp.
|
|
Then returns relevant values matching to time - or None
|
|
|
|
Arguments:
|
|
photo_time: timestamp of photo
|
|
radiation_list: list of timestamp / radiation values
|
|
position_list: list of timestamp / position / elevation values
|
|
|
|
Returns:
|
|
minimal timedelta: as timedelta object
|
|
best matching values
|
|
'''
|
|
|
|
def __init__(self, photo_time, radiation_list, position_list):
|
|
self.radiation_value = self._find_radiation_match(photo_time, radiation_list)[1]
|
|
self.radiation_delta = self._find_radiation_match(photo_time, radiation_list)[0]
|
|
self.position_delta = self._find_position_match(photo_time, position_list)[0]
|
|
self.position_latitude = self._find_position_match(photo_time, position_list)[1][1]
|
|
self.position_longitude = self._find_position_match(photo_time, position_list)[1][2]
|
|
self.position_altitude = self._find_position_match(photo_time, position_list)[1][3]
|
|
|
|
def __repr__(self):
|
|
if self.radiation_value:
|
|
radiation = round(self.radiation_value, 2)
|
|
else:
|
|
radiation = None
|
|
|
|
if self.position_altitude:
|
|
altitude = round(self.position_altitude)
|
|
else:
|
|
altitude = None
|
|
return 'Radiation: %s µS/h (Δt %s) \nPosition: Lat: %s, Long: %s, Alt: %sm (Δt %s)' % \
|
|
(str(radiation), str(self.radiation_delta), str(self.position_latitude), \
|
|
str(self.position_longitude), altitude, str(self.position_delta))
|
|
|
|
def _find_radiation_match(self, photo_time, list):
|
|
valuelist = []
|
|
for row in list:
|
|
# Define timedelta and define timedelta datetime object.
|
|
delta = timedelta(seconds=60)
|
|
if row.timestamp:
|
|
time_delta = abs(row.timestamp - photo_time)
|
|
# datetime objects should match with 1 minute precision.
|
|
if time_delta < delta:
|
|
element = (time_delta, row)
|
|
valuelist.append(element)
|
|
# Return the list item with the lowest timedelta in column 0.
|
|
# Column 2 contains the source objects untouched.
|
|
if valuelist:
|
|
result = min(valuelist, key=lambda x: x[0])
|
|
return result[0], result[1].radiation
|
|
# Return a tuple of 2x None, if there was no match.
|
|
return None, None
|
|
|
|
def _find_position_match(self, photo_time, list):
|
|
valuelist = []
|
|
for row in list:
|
|
# Define timedelta and define timedelta datetime object.
|
|
delta = timedelta(seconds=300)
|
|
if row[0]:
|
|
time_delta = abs(row[0] - photo_time)
|
|
# datetime objects should match with 5 minute precision.
|
|
if time_delta < delta:
|
|
element = (time_delta, row)
|
|
valuelist.append(element)
|
|
# Return the list item with the lowest timedelta in column 0.
|
|
# Column 2 contains the source objects untouched.
|
|
if valuelist:
|
|
#print(min(valuelist, key=lambda x: x[0]))
|
|
return min(valuelist, key=lambda x: x[0])
|
|
# Return Nones in the same cascaded manner as if it matched.
|
|
return [None, [None, None, None, None]]
|
|
|
|
class Exif:
|
|
'''
|
|
Converts, compiles and writes Exif-Tags from given arguemnts.
|
|
|
|
Arguments:
|
|
photo: file name of photo to modify
|
|
radiation: radiation levels float
|
|
latitude: latitude as float
|
|
longitude: longitude as float
|
|
elevation: elevation as float
|
|
dry_run: whether to acutally write (True / False)
|
|
|
|
Returns:
|
|
Latitude / Longitude: in degrees
|
|
Exif-Comment: that has been written (incl. radiation)
|
|
'''
|
|
|
|
def __init__(self, photo, dry_run, radiation, latitude, longitude, elevation):
|
|
self.write_exif = self._write_exif(photo, dry_run, radiation, latitude,
|
|
longitude, elevation)
|
|
|
|
def __repr__(self):
|
|
return 'Position: %s, %s: %s ' % self.write_exif
|
|
|
|
def _to_degree(self, value, loc):
|
|
if value < 0:
|
|
loc_value = loc[0]
|
|
elif value > 0:
|
|
loc_value = loc[1]
|
|
else:
|
|
loc_value = ""
|
|
abs_value = abs(value)
|
|
deg = int(abs_value)
|
|
t1 = (abs_value - deg) * 60
|
|
minute = int(t1)
|
|
second = round((t1 - minute) * 60, 5)
|
|
return (deg, minute, second, loc_value)
|
|
|
|
def _write_exif(self, photo, dry_run, radiation, latitude, longitude, elevation):
|
|
|
|
metadata = pyexiv2.ImageMetadata(photo)
|
|
metadata.read()
|
|
|
|
if latitude and longitude:
|
|
latitude_degree = self._to_degree(latitude, ["S", "N"])
|
|
longitude_degree = self._to_degree(longitude, ["W", "E"])
|
|
|
|
# convert decimal coordinates into fractions required for pyexiv2
|
|
exiv2_latitude = (Fraction(latitude_degree[0] * 60 + latitude_degree[1], 60),
|
|
Fraction(int(round(latitude_degree[2] * 100, 0)), 6000),
|
|
Fraction(0, 1))
|
|
exiv2_longitude = (Fraction(longitude_degree[0] * 60 + longitude_degree[1], 60),
|
|
Fraction(int(round(longitude_degree[2] * 100, 0)), 6000),
|
|
Fraction(0, 1))
|
|
|
|
# Exif tags to write
|
|
metadata['Exif.GPSInfo.GPSLatitude'] = exiv2_latitude
|
|
metadata['Exif.GPSInfo.GPSLatitudeRef'] = latitude_degree[3]
|
|
metadata['Exif.GPSInfo.GPSLongitude'] = exiv2_longitude
|
|
metadata['Exif.GPSInfo.GPSLongitudeRef'] = longitude_degree[3]
|
|
metadata['Exif.Image.GPSTag'] = 654
|
|
metadata['Exif.GPSInfo.GPSMapDatum'] = "WGS-84"
|
|
metadata['Exif.GPSInfo.GPSVersionID'] = '2 0 0 0'
|
|
|
|
if not elevation:
|
|
metadata['Exif.GPSInfo.GPSAltitude'] = Fraction(elevation)
|
|
metadata['Exif.GPSInfo.GPSAltitudeRef'] = '0'
|
|
else:
|
|
latitude_degree = None
|
|
longitude_degree = None
|
|
|
|
if radiation:
|
|
# Set new UserComment
|
|
new_comment = 'Radiation ☢ : %s µS/h' % str(round(radiation, 2))
|
|
|
|
metadata['Exif.Photo.UserComment'] = new_comment
|
|
metadata['Exif.Image.ImageDescription'] = new_comment
|
|
metadata['Iptc.Application2.Caption'] = [new_comment]
|
|
metadata['Xmp.dc.description'] = new_comment
|
|
else:
|
|
new_comment = None
|
|
|
|
# Write Exif tags to file, if not in dry-run mode
|
|
if dry_run is not True:
|
|
metadata.write()
|
|
|
|
return latitude_degree, longitude_degree, new_comment
|
|
|
|
class Output:
|
|
'''
|
|
Receives values to be printed, formats them and returns a string for printing.
|
|
|
|
Arguments:
|
|
radiation: radiation as float
|
|
latitude: latitude as float
|
|
longitude: longitude as float
|
|
elevation: elevation as float
|
|
|
|
Returns:
|
|
A String that can be printed in output
|
|
'''
|
|
|
|
def __init__(self, radiation, latitude, longitude, altitude):
|
|
self.get_string = self._get_string(radiation, latitude, longitude, altitude)
|
|
|
|
def __repr__(self):
|
|
return self.get_string
|
|
|
|
def _get_string(self, radiation, latitude, longitude, altitude):
|
|
# Convert values to styled strings
|
|
if radiation:
|
|
rad = '☢: %sµS/h ' % str(round(radiation, 2))
|
|
else:
|
|
rad = '☢: N/A '
|
|
|
|
if latitude and longitude:
|
|
latlon = 'Lat.: %s Long.: %s ' % (str(latitude), str(longitude))
|
|
else:
|
|
latlon = 'Lat.: N/A, Long.: N/A '
|
|
|
|
if altitude:
|
|
alt = 'Alt.: %sm' % str(round(altitude, 1))
|
|
else:
|
|
alt = 'Alt.: N/A'
|
|
|
|
data = rad + latlon + alt
|
|
|
|
# Return data string
|
|
return data
|
|
|