File size: 5,208 Bytes
9e88b3e
55d18b1
 
 
 
8cbe888
55d18b1
 
 
9e88b3e
8cbe888
 
f87663a
 
 
 
 
 
 
 
 
 
 
55d18b1
f87663a
55d18b1
 
 
 
f87663a
55d18b1
 
 
 
 
 
 
 
 
 
 
 
 
f87663a
55d18b1
 
 
 
 
 
 
 
 
 
 
7dee860
 
 
55d18b1
 
f87663a
55d18b1
9e88b3e
55d18b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8cbe888
 
55d18b1
 
 
8cbe888
 
f87663a
8cbe888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9e88b3e
 
 
8cbe888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da246ad
 
7dee860
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
from typing import Tuple, Union
import random
import string
import hashlib
import re
from fractions import Fraction
from PIL import Image
from PIL import ExifTags

import streamlit as st
from streamlit.runtime.uploaded_file_manager import UploadedFile

def generate_random_md5(length:int=16) -> str:
    """
    Generate a random MD5 hash.

    Args:
        length (int): The length of the random string to generate. Default is 16.

    Returns:
        str: The MD5 hash of the generated random string.
    """

    # Generate a random string
    random_string = ''.join(random.choices(string.ascii_letters + string.digits, length=16))
    # Encode the string and compute its MD5 hash
    md5_hash = hashlib.md5(random_string.encode()).hexdigest()
    return md5_hash


def is_valid_number(number:str) -> bool:
    """
    Check if the given string is a valid number (int or float, sign ok)

    Args:
        number (str): The string to be checked.

    Returns:
        bool: True if the string is a valid number, False otherwise.
    """
    pattern = r'^[-+]?[0-9]*\.?[0-9]+$'
    return re.match(pattern, number) is not None


# Function to validate email address
def is_valid_email(email:str) -> bool:
    """
    Validates if the provided email address is in a correct format.

    Args:
        email (str): The email address to validate.

    Returns:
        bool: True if the email address is valid, False otherwise.
    """
    #pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    # do not allow starting with a +
    pattern = r'^[a-zA-Z0-9_]+[a-zA-Z0-9._%+-]*@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
    return re.match(pattern, email) is not None


# Function to extract date and time from image metadata
def get_image_datetime(image_file:UploadedFile) -> Union[str, None]: 
    """
    Extracts the original date and time from the EXIF metadata of an uploaded image file.

    Args:
        image_file (UploadedFile): The uploaded image file from which to extract the date and time.

    Returns:
        str: The original date and time as a string if available, otherwise None.

    Raises:
        Warning: If the date and time could not be extracted from the image metadata.
    """
    try:
        image = Image.open(image_file)
        exif_data = image._getexif()
        if exif_data is not None:
            if ExifTags.Base.DateTimeOriginal in exif_data:
                return exif_data.get(ExifTags.Base.DateTimeOriginal)
    except Exception as e: # FIXME: what types of exception?
         st.warning(f"Could not extract date from image metadata. (file: {image_file.name})")
         # TODO: add to logger
    return None


def decimal_coords(coords:tuple, ref:str) -> Fraction:
    """
    Converts coordinates from degrees, minutes, and seconds to decimal degrees.

    Args:
        coords (tuple): A tuple containing three elements representing degrees, minutes, and seconds.
        ref (str): A string representing the reference direction ('N', 'S', 'E', 'W').

    Returns:
        Fraction: The coordinates in decimal degrees. Negative if the reference is 'S' or 'W'.

    Example:
        decimal_coords((40, 26, 46), 'N') -> 40.44611111111111
        decimal_coords((40, 26, 46), 'W') -> -40.44611111111111
    """
    # https://stackoverflow.com/a/73267185
    if ref not in ['N', 'S', 'E', 'W']:
        raise ValueError("Invalid reference direction. Must be 'N', 'S', 'E', or 'W'.")
    if len(coords) != 3:
        raise ValueError("Coordinates must be a tuple of three elements (degrees, minutes, seconds).")
    
    decimal_degrees = coords[0] + coords[1] / 60 + coords[2] / 3600
    if ref == "S" or ref =='W':
        decimal_degrees = -decimal_degrees
    return decimal_degrees


#def get_image_latlon(image_file: UploadedFile) : # if it is still not working
#def get_image_latlon(image_file: UploadedFile) -> Tuple[float, float] | None: # Python >=3.10
def get_image_latlon(image_file: UploadedFile) -> Union[Tuple[float, float], None]: # 3.6 <= Python < 3.10
    """
    Extracts the latitude and longitude from the EXIF metadata of an uploaded image file.

    Args:
        image_file (UploadedFile): The uploaded image file from which to extract the latitude and longitude.

    Returns:
        tuple[float, float]: The latitude and longitude as a tuple if available, otherwise None.

    Raises:
        Warning: If the latitude and longitude could not be extracted from the image metadata.
    """
    try:
        image = Image.open(image_file)
        exif_data = image._getexif()
        if exif_data is not None:
            if ExifTags.Base.GPSInfo in exif_data:
                gps_ifd = exif_data.get(ExifTags.Base.GPSInfo) 
                
                lat = float(decimal_coords(gps_ifd[ExifTags.GPS.GPSLatitude], gps_ifd[ExifTags.GPS.GPSLatitudeRef]))
                lon = float(decimal_coords(gps_ifd[ExifTags.GPS.GPSLongitude], gps_ifd[ExifTags.GPS.GPSLongitudeRef]))
            
                return lat, lon
            
    except Exception as e: # FIXME: what types of exception?
         st.warning(f"Could not extract latitude and longitude from image metadata. (file: {str(image_file)}")
    
    return None, None