Forum Archive

Display in share sheet the map where a photo has been taken

cvp

A little script to show how to extract exifs from a photo, get its GPS tags and display an Apple map in the sheet view, with a pin where the photo has been taken.
Select a photo in a mail (long press on the photo) or in the photos app (or anywhere else) and run the script in the Pythonista share sheet.
The script only runs in Python 2 because appex.get_image crashes in Python 3 (bug for @omz).
In appex mode, dezooming too much crashes, perhaps due to a memory problem.
The MKMapView part was initially copied from OMZ MapView demo
then "cleaned" to keep the easiest code as possible

#!python2
# coding: utf-8
#
# Needs Python 2 because appex.get_image crashes in Python 3
#
# MKMapView part initially copied from OMZ MapView demo
#        https://gist.github.com/omz/451a6685fddcf8ccdfc5
# then "cleaned" to keep the easiest code as possible
#
# In appex mode, dezooming too much crashes, perhaps memory problem?
import console
import clipboard
from objc_util import *
import ctypes
import ui
from PIL import Image
from PIL.ExifTags import TAGS,GPSTAGS
import appex
#import photos          # used to test in non-appex mode

class CLLocationCoordinate2D (Structure):
    _fields_ = [('latitude', c_double), ('longitude', c_double)]
class MKCoordinateSpan (Structure):
    _fields_ = [('d_lat', c_double), ('d_lon', c_double)]
class MKCoordinateRegion (Structure):
    _fields_ = [('center', CLLocationCoordinate2D), ('span', MKCoordinateSpan)]

def get_gps(image):
    gps_latitude      = None
    gps_latitude_ref  = None
    gps_longitude     = None
    gps_longitude_ref = None
    exif_info = image._getexif()                            # Extract exifs from photo
    if exif_info:
        for tag, value in exif_info.items():        # Loop on exifs of photo    
            decoded = TAGS.get(tag, tag)                    # Exif code
            if decoded == "GPSInfo":                            # Exif is GPSInfo
                for t in value:                                         # Loop on sub-exifs
                    sub_decoded = GPSTAGS.get(t, t)     # Sub-exif code
                    if   sub_decoded == 'GPSLatitude':
                        gps_latitude = value[t]
                    elif sub_decoded == 'GPSLatitudeRef':
                        gps_latitude_ref = value[t]     
                    elif sub_decoded == 'GPSLongitude':
                        gps_longitude = value[t]
                    elif sub_decoded == 'GPSLongitudeRef':
                        gps_longitude_ref = value[t]                    

                if gps_latitude and gps_latitude_ref and gps_longitude and gps_longitude_ref:
                    lat = convert_to_degrees(gps_latitude)
                    lat_lon_txt = str(lat)+gps_latitude_ref
                    if gps_latitude_ref != "N":                     
                        lat = 0 - lat
                    lon = convert_to_degrees(gps_longitude)
                    lat_lon_txt = lat_lon_txt+' - '+str(lon)+gps_longitude_ref
                    if gps_longitude_ref != "E":
                        lon = 0 - lon
                    return lat, lon, lat_lon_txt
    return False,False,None

def convert_to_degrees(value):
    # convert the GPS coordinates d m s to degrees in float
    d0 = value[0][0]
    d1 = value[0][1]
    d = float(d0) / float(d1)

    m0 = value[1][0]
    m1 = value[1][1]
    m = float(m0) / float(m1)

    s0 = value[2][0]
    s1 = value[2][1]
    s = float(s0) / float(s1)

    return d + (m / 60.0) + (s / 3600.0)

class MapView(ui.View):

    @on_main_thread
    def __init__(self, *args, **kwargs):
        ui.View.__init__(self, *args, **kwargs)
        MKMapView = ObjCClass('MKMapView')
        frame = CGRect(CGPoint(0, 0), CGSize(self.width, self.height))
        self.mk_map_view = MKMapView.alloc().initWithFrame_(frame)
        flex_width, flex_height = (1<<1), (1<<4)
        self.mk_map_view.setAutoresizingMask_(flex_width|flex_height)
        self_objc = ObjCInstance(self)
        self_objc.addSubview_(self.mk_map_view)
        self.mk_map_view.release()

    @on_main_thread
    def add_pin(self, lat, lon, title):
        '''Add a pin annotation to the map'''
        MKPointAnnotation = ObjCClass('MKPointAnnotation')
        coord = CLLocationCoordinate2D(lat, lon)
        annotation = MKPointAnnotation.alloc().init().autorelease()
        annotation.setTitle_(title)
        annotation.setCoordinate_(coord, restype=None, argtypes=[CLLocationCoordinate2D])
        self.mk_map_view.addAnnotation_(annotation)

    @on_main_thread
    def set_region(self, lat, lon, d_lat, d_lon, animated=False):
        '''Set latitude/longitude of the view's center and the zoom level (specified implicitly as a latitude/longitude delta)'''
        region = MKCoordinateRegion(CLLocationCoordinate2D(lat, lon), MKCoordinateSpan(d_lat, d_lon))
        self.mk_map_view.setRegion_animated_(region, animated, restype=None, argtypes=[MKCoordinateRegion, c_bool])

    def will_close(self):
        appex.finish()

def main():

    #----- Main process -----
    console.clear()

    if not appex.is_running_extension():
        #img = photos.pick_image()
        console.hud_alert('Must run in appex mod','error')
        return  
    else:
        img = appex.get_image(image_type='pil')

    if img == None:
        console.alert('No image passed','','Ok',hide_cancel_button=True)
        return  

    lat,lon, lat_lon_txt = get_gps(img)     

    if not lat or not lon:
        console.alert('No GPS tags in image','','Ok',hide_cancel_button=True)
        return  

    # Hide script
    back = MapView(frame=(0, 0, 540, 620))
    back.background_color='white'
    back.name = 'GPS = '+lat_lon_txt
    back.present('sheet', hide_title_bar=False)

    back.set_region(lat, lon, 0.05, 0.05, animated=True)
    back.add_pin(lat, lon,str((lat, lon)))      

# Protect against import    
if __name__ == '__main__':
    main()
Phuket2

@cvp , thanks. It works really well. I only tried it In Photos. Amazing how many of my pics don't have GPS's info. But I have never really worried about settings for this. I am doing this on my ipad, so the pics could have been stripped of the exilf info in any number of places before arriving on my ipad 😱
But the ones I found from around the world were almost exact with the pin.

cvp

Perhaps, you have remarked that with a few modifications, the script could also work as "normal" mode (not appex):
- uncomment the import photos
- uncomment the img = photos.pick_image()
- comment the hud_alert and the return
And in this mode, you don't have any problem of dezooming