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.
Need help for calling an objective_c function
-
I try to call an objective_c function, but as I don't understand a lot, my script crashes and app is removed without any message.
I'm sure I uncorrectly use the setCoordinates_count function but I need help
Thanks# all_points is an array of CLLocationCoordinate2D all_points = [] for ...: coord = CLLocationCoordinate2D(lat, lon) all_points.append(coord) # store all pin's for MKPolyline #... MKPolyline = ObjCClass('MKPolyline') polyline = MKPolyline.alloc().init().autorelease() polyline._setCoordinates_count_(all_points,len(all_points),argtypes=[CLLocationCoordinate2D,c_double])
- (instancetype)polylineWithCoordinates:(const CLLocationCoordinate2D *)coords
count:(NSUInteger)count;
- (instancetype)polylineWithCoordinates:(const CLLocationCoordinate2D *)coords
-
I think you're using a private method (
_setCoordinates:count:
) by accident. If a method name starts with an underscore, it is private and should only be used by the class itself.The method listed in the Apple documentation is a class method, which means you need to call it on the class directly, without calling
alloc().init()
first, so you actually need to useMKPolyline.polylineWithCoordinates_count_
.I don't think the
autorelease()
call does anything useful in Pythonista.objc_util
already takes care of callingretain()
andrelease()
for you.The first argument has to be an array of
CLLocationCoordinate2D
structs, passed as a pointer. At the moment you've only set the first argtype to be a singleCLLocationCoordinate2D
struct. I think you need to change the first argtype toPOINTER(CLLocationCoordinate2D)
and the first argument to(CLLocationCoordinate2D * len(all_points))(*all_points)
to make it a ctypes array.The second argument is a
NSUInteger
, which is atypedef
name forunsigned long
, so your second argtypes needs to bec_ulong
, notc_double
.So in the end, I think your call has to look like this (untested):
polyline = MKPolyline.polylineWithCoordinates_count_( (CLLocationCoordinate2D * len(all_points))(*all_points), len(all_points), argtypes=[POINTER(CLLocationCoordinate2D), c_ulong] )
-
Thanks for your detailed answer.
I have to confess that all of that is too complicated for me.
But I try to continue my project with your explanation.
Unfortunately, I've the same problem , my script crashes without any trace.
I'll put my full code, if you want to test it, but it is not yet completely written.Μy script
- ask 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"
- would display a polygonal line showing the route of the photos
*** this part is not yet finished ***
-
# todo # - display route http://blog.robkerr.com/adding-a-mkpolyline-overlay-using-swift-to-an-ios-mapkit-map/ # # - 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 # 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)] def mapView_rendererForOverlay_(self,cmd,mk_mapview,mk_overlay): try: overlay = ObjCInstance(mk_overlay) mapView = ObjCInstance(mk_mapview) if overlay.isKindOfClass_(MKPolyline): #......still to be written 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 MKPolyline = ObjCClass('MKPolyline') polyline = MKPolyline.polylineWithCoordinates_count_( (CLLocationCoordinate2D * len(all_points))(*all_points), len(all_points), 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()
-
For debugging crashes when working with
objc_util
, you can install this fault handler script that I wrote: https://gist.github.com/dgelessus/fe8e267149862eb67127f4fff7e017beTo install it, you only need to copy it and paste it at the top of your pythonista_startup file, then restart Pythonista. Now the next time Pythonista crashes, the fault handler will create a log file (in the "faultlog" folder) which tells you why it crashed, in what line of Python code (this works only in Python 3), and if the crash is because of an Objective-C exception, its name and message is shown as well.
-
Installed, thanks, there is the fault log
Fatal Python error: Segmentation fault
Current thread 0x00000001a07c4000 (most recent call first):
File "/var/containers/Bundle/Application/606A7B2D-313E-48DB-BEB5-79336C85A91D/Pythonista3.app/Frameworks/PythonistaKit3.framework/pylib/site-packages/objc_util.py", line 771 in call
File "/private/var/mobile/Containers/Shared/AppGroup/BC53E549-355D-4E77-BC46-64C3D3E0BDAF/Pythonista3/Documents/Route sur Map.py", line 92 in addPolyLineToMap
File "/var/containers/Bundle/Application/606A7B2D-313E-48DB-BEB5-79336C85A91D/Pythonista3.app/Frameworks/PythonistaKit3.framework/pylib/site-packages/objc_util.py", line 1064 in OMMainThreadDispatcher_invoke_impThread 0x000000016e35f000 (most recent call first):
File "/var/containers/Bundle/Application/606A7B2D-313E-48DB-BEB5-79336C85A91D/Pythonista3.app/Frameworks/PythonistaKit3.framework/pylib/site-packages/objc_util.py", line 896 in call
File "/var/containers/Bundle/Application/606A7B2D-313E-48DB-BEB5-79336C85A91D/Pythonista3.app/Frameworks/PythonistaKit3.framework/pylib/site-packages/objc_util.py", line 1093 in new_func
File "/private/var/mobile/Containers/Shared/AppGroup/BC53E549-355D-4E77-BC46-64C3D3E0BDAF/Pythonista3/Documents/Route sur Map.py", line 161 in main
File "/private/var/mobile/Containers/Shared/AppGroup/BC53E549-355D-4E77-BC46-64C3D3E0BDAF/Pythonista3/Documents/Route sur Map.py", line 165 in <module> -
I suspect you need to pull
(CLLocationCoordinate2D * len(all_points))(*all_points),
out and create a separate variable, and possibly use retain_global on it.Since CLLocationCoordinate2D is a struct, and you pass a pointer to it, the struct is freed after the method is done calling, but perhaps before the objc method has used the object.
it is also possible your argtypes or restype is not correct, i have not looked into the proper calling convention...
-
As @Phuket2 said in another topic, "it's beyond my comprehension" 😥
I think I'll stop this project, even if it stays only this part of displaying a MKpolyline on the map.
I need to recognize my own limits 😭 -
@JonB
all_points
is declared asglobal
though, so it should stay alive until another script is run. Although you're right, the ctypes array probably has to be put into a global as well, because the structs it contains aren't the same as the ones inall_points
.@cvp See if this works:
def addPolyLineToMap(self): global all_points global all_points_array all_points_array = (CLLocationCoordinate2D * len(all_points))(*all_points) MKPolyline = ObjCClass('MKPolyline') polyline = MKPolyline.polylineWithCoordinates_count_(all_points_array, len(all_points), argtypes=[POINTER(CLLocationCoordinate2D), c_ulong]) self.mk_map_view.addOverlay_(polyline)
-
@dgelessus
I've modified as requested, same crash without anything in faultlog folder.
Thanks to continue so to help me but if you think that it's waste of time, don't hesitate to tell me. I'm really sorry to be unable to solve this problem my-self. -
Hooray for ridiculous fixes...
polyline = ObjCInstance(MKPolyline.polylineWithCoordinates_count_( all_points_array, len(all_points), restype=c_void_p, argtypes=[POINTER(CLLocationCoordinate2D), c_ulong], ))
It turns out that you have to give
restype
andargtypes
together, otherwise they are ignored. If I set therestype
to ac_void_p
and convert that to anObjCInstance
afterwards, everything works fine, because now theargtypes
are used. -
Whaaaaa
If that is a ridiculous fix, I don't find any sufficient word for my knowledge of objective-c...😭
Thanks a lot
It only stays the code for the mapView_rendererForOverlay. -
It functions!
Thanks a lot to @JonB and @dgelessus. Without them I never could have done it.
Here is the full source