cvp
Sep 15, 2016 - 11:18
The script
asks to pick 2 or more photos
get their "taken date"
selects either the picked photos (if more than 2) or all photos (if 2) with a "taken date" between the 2 photos dates
gets their localizations
displays a map containg all photos
displays pin's of their localizations
sorts all photos on their ascending "taken date"
displays a polygonal line showing the route of the photos
This last part has been possible with the help of two guru's, @JonB and @dgelessus
see
# todo
# - settîngs pin's visible or not
# - settings route color/width
# coding: utf-8
#
# MKMapView part initially copied from OMZ MapView demo
# https://gist.github.com/omz/451a6685fddcf8ccdfc5
# then "cleaned" to keep the easiest code as possible
#
# For use of objc_util calls and crashes trace, more than help received from
# @dgelssus and @JonB in Pythonista forum
# https://forum.omz-software.com/topic/3507/need-help-for-calling-an-objective_c-function
#
# Display MKPolyline in Mapkit from Robert Kerr
# http://blog.robkerr.com/adding-a-mkpolyline-overlay-using-swift-to-an-ios-mapkit-map/
#
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
import webbrowser # if the launcher app is installed
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)]
MKPolyline = ObjCClass('MKPolyline')
MKPolylineRenderer = ObjCClass('MKPolylineRenderer')
def mapView_rendererForOverlay_(self,cmd,mk_mapview,mk_overlay):
try:
overlay = ObjCInstance(mk_overlay)
mapView = ObjCInstance(mk_mapview)
if overlay.isKindOfClass_(MKPolyline):
pr = MKPolylineRenderer.alloc().initWithPolyline(overlay);
pr.strokeColor = UIColor.redColor().colorWithAlphaComponent(0.5);
pr.lineWidth = 2;
return pr.ptr
pass
return None
except Exception as e:
print('exception: ',e)
# Build method of MKMapView Delegate
methods = [mapView_rendererForOverlay_]
protocols = ['MKMapViewDelegate']
try:
MyMapViewDelegate = ObjCClass('MyMapViewDelegate')
except:
MyMapViewDelegate = create_objc_class('MyMapViewDelegate', NSObject, methods=methods, protocols=protocols)
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()
# Set Delegate of mk_map_view
self.map_delegate = MyMapViewDelegate.alloc().init().autorelease()
self.mk_map_view.setDelegate_(self.map_delegate)
@on_main_thread
def add_pin(self, lat, lon, title):
global all_points
'''Add a pin annotation to the map'''
MKPointAnnotation = ObjCClass('MKPointAnnotation')
coord = CLLocationCoordinate2D(lat, lon)
all_points.append(coord) # store all pin's for MKPolyline
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])
@on_main_thread
def addPolyLineToMap(self):
global all_points
global all_points_array
all_points_array = (CLLocationCoordinate2D * len(all_points))(*all_points)
polyline = ObjCInstance(MKPolyline.polylineWithCoordinates_count_(
all_points_array,
len(all_points),
restype=c_void_p,
argtypes=[POINTER(CLLocationCoordinate2D), c_ulong],
))
self.mk_map_view.addOverlay_(polyline)
def will_close(self):
# Back to home screen
return # temporary during tests
webbrowser.open('launcher://crash')
def main():
global all_points
#----- Main process -----
console.clear()
# Hide script
back = MapView(frame=(0, 0, 540, 620))
back.background_color='white'
back.name = 'Display route of selected localized photos'
back.present('full_screen', hide_title_bar=False)
# Get a list of all photos
c = photos.get_assets(media_type='image')
# Pick at least two photos from all photos
ps = photos.pick_asset(assets=c, title='Pick begin/end or all photos of the route', multi=True)
if ps == None or len(ps) < 2:
# Pick has been canceled
console.hud_alert('At least two photos are needed','error')
back.close()
return
# Loop on all photos
route_points = []
if len(ps) > 2: # more than 2 picked photos
scan_ph = ps # use picked photos only
else: # 2 photos picked
scan_ph = c # scan all photos
min_date = min(ps[0].creation_date,ps[1].creation_date).date()
max_date = max(ps[0].creation_date,ps[1].creation_date).date()
for p in scan_ph:
p_date = p.creation_date.date()
if (len(ps) > 2) or (len(ps) == 2 and p_date >= min_date and p_date <= max_date):
# Photo belongs to the route period
if p.location:
# Photo has GPS tags
lat = p.location['latitude']
lon = p.location['longitude']
# store latitude, longitude and taken date
route_points.append((lat,lon,p_date))
if len(route_points) < 2:
console.hud_alert('At least two localized photos neded','error')
back.close()
return
# Sort points by ascending taken date
route_points = sorted(route_points,key = lambda x: x[2])
# Compute min and max of latitude and longitude
min_lat = min(route_points,key = lambda x:x[0])[0]
max_lat = max(route_points,key = lambda x:x[0])[0]
min_lon = min(route_points,key = lambda x:x[1])[1]
max_lon = max(route_points,key = lambda x:x[1])[1]
# Display map, center and zoom so all points are visible
back.set_region((min_lat+max_lat)/2,(min_lon+max_lon)/2, 1.2*(max_lat-min_lat), 1.2*(max_lon-min_lon), animated=True)
# Display pin's
all_points = []
for point in route_points:
back.add_pin(point[0],point[1],str(point[2]))
# Display polygon line of sorted locations
back.addPolyLineToMap()
# Protect against import
if __name__ == '__main__':
main()