Welcome!
This is the community forum for my apps Pythonista and Editorial.
For individual support questions, you can also send an email. If you have a very short question or just want to say hello — I'm @olemoritz on Twitter.
Show location of foto library for date range on a map
-
I created a pythonista script that prompts you for a date range and then extracts the GPS longitude and latitude from all assets in your foto library for fotos created in the given date range and draws a hybrid satellite image with a path following the image creation dates.
Thus you can create a path/map of your vacation / trip.
Find the gist here ShowWhereIWas gist
-
@bodobolero Now the script was extended to
- save the map as a png file in the current (scripts) folder
- save the list of geo-locations as a track (.gpx) file that can be imported into apps like komoot or outdoor active (or tracking devices like Garmin)
-
@bodobolero Perhaps could you be interested by this script that displays all your (localized) photos (no selection actually ) as a route on an Apple Map (zoom possible) with each photo (minimized) shown at its location. Works in UI, not displayed in console
Example
Tapping a photo displays its taken date and its order in the route
Zoom possible
-
@bodobolero Here is some more description/explanation https://www.bodobolero.com/2023/05/13/Pythonista-script-to-create-map-of-fotos-in-ios-foto-library/
-
@cvp This looks interesting, so I tried it.
It showed a map and a progress indicator for about 30 seconds then the app crashed.Note that I have about 10.000 images in my photo library.
I think your app may not scale to that amount of photos.
Also if you travel a lot you will basically display a world map to cover all locations which is not very helpful.Maybe we can combine our code - as a first step I suggest that you allow to specify the date range in your app to avoid the scalability issues and reduce the number of photos down to a smaller number
-
@bodobolero understood, too many photos, I guess.
Feel free to modify the script to use your dates range settings. Or do you want I do it? -
@cvp Don‘t have time now - if you have time you can give it a start and I can look into it the following days
-
@bodobolero ok, it is done, GitHub updated
-
@cvp Thanks a lot - but somehow it still crashes for me without giving a useful error message - maybe I will find some time to debug in the following days.
-
@bodobolero Ok, be sure that you don't select too much photos to test the script.
And could you also restart Pythonista (remove from tasks list and rerun).
And perhaps do you have in the root a file named
_objc_exception.txt
with the error reason. -
@cvp In your gist you had commented out the filtering of the images by date in line https://github.com/cvpe/Pythonista-scripts/blob/3d14d069fef70096e90776a98e6a5a78f7bdcd91/Gists/Photos Route on Map.py#L101
that is why it still was processing all pictures.
If I pick a date range with a time interval that has about 10 pictures it works, however if I select a date range of my latest trip with about 400 images it still crashes
-
@bodobolero ok, now we know it is a problem of memory. You could play with the thumb size for testing.
map_pin_size = 80
Perhaps, my code of storing thumbs could be modified to gain memory
# store reduced photo to win memory img = p.get_ui_image() wh = img.size # normal photo h = map_pin_size w = int(h * wh[0] / wh[1]) with ui.ImageContext(w,h) as ctx: img.draw(0,0,w,h) img = ctx.get_image() # store latitude, longitude and taken date route_points.append((lat,lon,p_date, img))
-
@bodobolero said
In your gist you had commented out the filtering of the images by date in line
Sorry, forgotten during my tests
-
@cvp Sorry, even with map_pin_size = 20 I could not get it to run for a reasonable number of photos (around 400) - it still crashed with out of memory.
At this point I think the interactive display of all images doesn‘t scale.
What the Apple photo library does in this case: it displays only one sample image for each location that has multiple images in a bounded area and on top it overlays a little number that shows how many images are taken at that location. Only when the user zooms into the area it shows more images.
Thus it manages the memory footprint and always stays within a max amount.However this is rebuilding the photo library function and beyond the scope of what I intended to do.
Thanks for your help anyway!
-
@cvp I have now solved the out of memory problem, the critical changed line (besides pin size) is
img = p.get_ui_image(size=(map_pin_size,map_pin_size), crop=True)
# 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 dialogs from objc_util import * import ctypes import ui from PIL import Image import photos 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') MKPointAnnotation = ObjCClass('MKPointAnnotation') MKAnnotationView = ObjCClass('MKAnnotationView') map_pin_size = 40 def mapView_viewForAnnotation_(self,cmd,mk_mapview,mk_annotation): global route_points try: # not specially called in the same sequence as pins created # should have one MK(Pin)AnnotationView by type (ex: pin color) annotation = ObjCInstance(mk_annotation) mapView = ObjCInstance(mk_mapview) if annotation.isKindOfClass_(MKPointAnnotation): tit = str(annotation.title()) subtit = str(annotation.subtitle()) id = subtit pinView = mapView.dequeueReusableAnnotationViewWithIdentifier_(id) if not pinView: # Modify pin image: use MKAnnotationView pinView = MKAnnotationView.alloc().initWithAnnotation_reuseIdentifier_(annotation,id) ui_image = route_points[int(subtit)][3] pinView.setImage_(ui_image.objc_instance) pinView.canShowCallout = True else: pinView.annotation = annotation return pinView.ptr return None except Exception as e: print('Error on line {}'.format(sys.exc_info()[-1].tb_lineno), type(e).__name__, e) 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.blueColor().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_, mapView_viewForAnnotation_] protocols = ['MKMapViewDelegate'] try: MyMapViewDelegate = ObjCClass('MyMapViewDelegate') except: MyMapViewDelegate = create_objc_class('MyMapViewDelegate', NSObject, methods=methods, protocols=protocols) # ask the user for start date and end date def pickDateInterval(): startDate = dialogs.date_dialog(title='When did your trip start?',done_button_title='Select trip start date') endDate = dialogs.date_dialog(title='When did you trip end?', done_button_title='Select trip end date') print(f'startDate:{startDate} endDate:{endDate}') return (startDate, endDate) # go to the foto library and extract all images with location data in a given date range def getAssetsWithLocationInDateInterval(startDate, endDate): all_assets = photos.get_assets(media_type='image', include_hidden=False) print("Number of all assets:") print(len(all_assets)) location_assets = [asset for asset in all_assets if asset.location != None] print("Number of assets with location:") print(len(location_assets)) timed_assets = [asset for asset in location_assets if ( asset.creation_date.date() >= startDate and asset.creation_date.date() <= endDate) ] #timed_assets = location_assets print("Number of assets with location in date interval:") print(len(timed_assets)) return timed_assets 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, subtitle): global all_points '''Add a pin annotation to the map''' coord = CLLocationCoordinate2D(lat, lon) all_points.append(coord) # store all pin's for MKPolyline annotation = MKPointAnnotation.alloc().init().autorelease() annotation.setTitle_(title) annotation.setSubtitle_(str(subtitle)) 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 main(): global all_points, route_points #----- Main process ----- console.clear() # Hide script back = MapView(frame=(0, 0, 540, 620)) back.background_color='white' back.name = 'Display route of localized photos' back.present('full_screen', hide_title_bar=False) # ask from and to date of range startDate, endDate = pickDateInterval() # get assets in dates range assets = getAssetsWithLocationInDateInterval(startDate, endDate) # Loop on all photos route_points = [] min_date = min(assets[0].creation_date,assets[1].creation_date).date() max_date = max(assets[0].creation_date,assets[1].creation_date).date() for p in assets: p_date = p.creation_date.date() if (len(assets) > 2) or (len(assets) == 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 reduced photo to win memory img = p.get_ui_image(size=(map_pin_size,map_pin_size), crop=True) if img !=None: wh = img.size # normal photo h = map_pin_size w = int(h * wh[0] / wh[1]) with ui.ImageContext(w,h) as ctx: img.draw(0,0,w,h) img = ctx.get_image() # store latitude, longitude and taken date route_points.append((lat,lon,p_date, img)) if len(route_points) < 2: console.hud_alert('At least two localized photos neded','error') back.close() return back.name = back.name + f' ({len(route_points)})' # 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 = [] idx = 0 for point in route_points: back.add_pin(point[0],point[1],str(point[2]), str(idx)) idx += 1 # Display polygon line of sorted locations back.addPolyLineToMap() # Protect against import if __name__ == '__main__': main()
-
@bodobolero I'll try it, I was just busy to change code so the images are o more memorized in memory but in a db local file. With max 400 photos and 80 pixels, it still works. Anyway, the photos in the map are still locally memorized.
-
@bodobolero with your modification and my new version with a local de, and a thumb size of 40, I can show 1400 photos on the same screen, before zooming
-
@cvp Great timework - I like it very much now!
Did you update the gist?
Here is my latest version https://gist.github.com/Bodobolero/f6dd134f3a349d69ce6f7eb68513d82d -
@bodobolero I'll update gist, I have tried same number of photos with old version and your "crop" modified, still crash
-