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.
Map API, need suggestions for how to execute ideas
-
@RocketBlaster05 try this quick and dirty example.
Press the map button to show or hide the map
Long press add a new location, temporary as "dropped" then as "user point" at next long press.
Locations are saved in a file (here a.loc) and when you restart the script, previous points are shown on the map as green pin's, dropped is red.
Hoping this helps (only if I have correctly understood your request, maybe I could be wrong as usual 🤔)
Edit: source has just been modified for pin's color#!python2 ''' NOTE: This requires the latest beta of Pythonista 1.6 (build 160022) Demo of a custom ui.View subclass that embeds a native map view using MapKit (via objc_util). Tap and hold the map to drop a pin. The MapView class is designed to be reusable, but it doesn't implement *everything* you might need. I hope that the existing methods give you a basic idea of how to add new capabilities though. For reference, here's Apple's documentation about the underlying MKMapView class: http://developer.apple.com/library/ios/documentation/MapKit/reference/MKMapView_Class/index.html If you make any changes to the OMMapViewDelegate class, you need to restart the app. Because this requires creating a new Objective-C class, the code can basically only run once per session (it's not safe to delete an Objective-C class at runtime as long as instances of the class potentially exist). ''' from objc_util import * import ast import ctypes import ui import location import os import time import weakref MKPointAnnotation = ObjCClass('MKPointAnnotation') MKPinAnnotationView = ObjCClass('MKPinAnnotationView') UIColor = ObjCClass('UIColor') # used to set pin color def mapView_viewForAnnotation_(self,cmd,mk_mapview,mk_annotation): global map_pin_type, map_pin_color, map_addr_color, map_pin_size, map_pin_radius, map_pin_borderwidth, map_pin_bordercolor, contacts_photos 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 = tit pinView = mapView.dequeueReusableAnnotationViewWithIdentifier_(id) if not pinView: # Modify pin color: use MKPinAnnotationView pinView = MKPinAnnotationView.alloc().initWithAnnotation_reuseIdentifier_(annotation,id) pinView.canShowCallout = True # tap-> show title pinView.animatesDrop = True # Animated pin falls like a drop if tit == 'Dropped Pin': pinView.pinColor = 0 # 0=red 1=green 2=purple else: pinView.pinColor = 1 # 0=red 1=green 2=purple 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) # Build method of MKMapView Delegate methods = [mapView_viewForAnnotation_] protocols = ['MKMapViewDelegate'] try: MyMapViewDelegate = ObjCClass('MyMapViewDelegate') except Exception as e: MyMapViewDelegate = create_objc_class('MyMapViewDelegate', methods=methods, protocols=protocols) # _map_delegate_cache is used to get a reference to the MapView from the (Objective-C) delegate callback. The keys are memory addresses of `OMMapViewDelegate` (Obj-C) objects, the values are `ObjCInstance` (Python) objects. This mapping is necessary because `ObjCInstance` doesn't guarantee that you get the same object every time when you instantiate it with a pointer (this may change in future betas). MapView stores a weak reference to itself in the specific `ObjCInstance` that it creates for its delegate. _map_delegate_cache = weakref.WeakValueDictionary() 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)] 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) #print(dir(self.mk_map_view.region())) 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() self.long_press_action = None self.scroll_action = None #NOTE: The button is only used as a convenient action target for the gesture recognizer. While this isn't documented, the underlying UIButton object has an `-invokeAction:` method that takes care of calling the associated Python action. self.gesture_recognizer_target = ui.Button() self.gesture_recognizer_target.action = self.long_press UILongPressGestureRecognizer = ObjCClass('UILongPressGestureRecognizer') self.recognizer = UILongPressGestureRecognizer.alloc().initWithTarget_action_(self.gesture_recognizer_target, sel('invokeAction:')).autorelease() self.mk_map_view.addGestureRecognizer_(self.recognizer) self.long_press_location = ui.Point(0, 0) self.map_delegate = MyMapViewDelegate.alloc().init()#.autorelease() self.mk_map_view.setDelegate_(self.map_delegate) self.map_delegate.map_view_ref = weakref.ref(self) _map_delegate_cache[self.map_delegate.ptr] = self.map_delegate def long_press(self, sender): #NOTE: The `sender` argument will always be the dummy ui.Button that's used as the gesture recognizer's target, just ignore it... gesture_state = self.recognizer.state() if gesture_state == 1 and callable(self.long_press_action): loc = self.recognizer.locationInView_(self.mk_map_view) self.long_press_location = ui.Point(loc.x, loc.y) self.long_press_action(self) @on_main_thread def add_pin(self, lat, lon, title, subtitle=None, select=False): '''Add a pin annotation to the map''' MKPointAnnotation = ObjCClass('MKPointAnnotation') coord = CLLocationCoordinate2D(lat, lon) annotation = MKPointAnnotation.alloc().init().autorelease() annotation.setTitle_(title) if subtitle: annotation.setSubtitle_(subtitle) annotation.setCoordinate_(coord, restype=None, argtypes=[CLLocationCoordinate2D]) self.mk_map_view.addAnnotation_(annotation) if select: self.mk_map_view.selectAnnotation_animated_(annotation, True) @on_main_thread def remove_all_pins(self): '''Remove all annotations (pins) from the map''' self.mk_map_view.removeAnnotations_(self.mk_map_view.annotations()) @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 set_center_coordinate(self, lat, lon, animated=False): '''Set latitude/longitude without changing the zoom level''' coordinate = CLLocationCoordinate2D(lat, lon) self.mk_map_view.setCenterCoordinate_animated_(coordinate, animated, restype=None, argtypes=[CLLocationCoordinate2D, c_bool]) @on_main_thread def get_center_coordinate(self): '''Return the current center coordinate as a (latitude, longitude) tuple''' coordinate = self.mk_map_view.centerCoordinate(restype=CLLocationCoordinate2D, argtypes=[]) return coordinate.latitude, coordinate.longitude @on_main_thread def point_to_coordinate(self, point): '''Convert from a point in the view (e.g. touch location) to a latitude/longitude''' coordinate = self.mk_map_view.convertPoint_toCoordinateFromView_(CGPoint(*point), self._objc_ptr, restype=CLLocationCoordinate2D, argtypes=[CGPoint, c_void_p]) return coordinate.latitude, coordinate.longitude def _notify_region_changed(self): if callable(self.scroll_action): self.scroll_action(self) # -------------------------------------- # DEMO: def long_press_action(sender): global locs,path # Add a pin when the MapView recognizes a long-press c = sender.point_to_coordinate(sender.long_press_location) # this of only to special process asked in forum # https://forum.omz-software.com/topic/7077/removing-custom-pins-with-map-api for annotation in sender.mk_map_view.annotations(): if str(annotation.title()) == 'Dropped Pin': sender.mk_map_view.removeAnnotation_(annotation) prev_lat,prev_lon = locs[-1] sender.add_pin(prev_lat, prev_lon, 'user point', str((prev_lat, prev_lon))) else: sender.add_pin(c[0], c[1], 'Dropped Pin', str(c), select=True) sender.set_center_coordinate(c[0], c[1], animated=True) locs.append(c) with open(path,mode='wt') as f: content = str(locs) f.write(content) def scroll_action(sender): # Show the current center coordinate in the title bar after the map is scrolled/zoomed: sender.name = 'lat/long: %.2f, %.2f' % sender.get_center_coordinate() def main(): global locs,path # create main view mv = ui.View() mv.name = 'Map for RocketBlaster05' mv.background_color = 'white' mv.present('fullscreen') w,h = ui.get_screen_size() # Create and present a MapView: v = MapView(frame=(0,0,w,h-76)) v.hidden = True mv.add_subview(v) bmap = ui.ButtonItem() bmap.image = ui.Image.named('iob:map_32') mv.right_button_items = (bmap,) def bmap_action(sender): v.hidden = not v.hidden bmap.action = bmap_action v.long_press_action = long_press_action v.scroll_action = scroll_action path = 'a.loc' if not os.path.exists(path): locs = [] else: with open(path,mode='rt') as f: content = f.read() locs = ast.literal_eval(content) for lat,lon in locs: v.add_pin(lat, lon, 'user point', str((lat, lon))) if __name__ == '__main__': main()
-
@cvp Yes this is very helpful. I will need to adjust the button function but that is really easy, and I'll change the presentation to "sheet". I will need to modify the code so that it automatically gets centered on the user's location like omz's second version of the map. If you could guide me towards how to do that, that would be amazing.
-
@RocketBlaster05 That will not be for today...sorry
Don't you want to center the map on the center of all previous pin's? -
@RocketBlaster05 did you try several runs with multiple pin's, like
-
#!python2 ??
-
@ccc old line of omz
-
@ccc works without the line
-
@ccc you know my standard response to your advices: you're always right 😇
-
@RocketBlaster05 said:
automatically gets centered on the user's location
def main(): . . . # center on user location import location location.start_updates() time.sleep(1) loc = location.get_location() location.stop_updates() if loc: lat, lon = loc['latitude'], loc['longitude'] v.set_region(lat, lon, 0.05, 0.05, animated=True) if __name__ == '__main__': main()
Next lesson : zoom so all previous pin's are visible, interested?
-
@cvp yes! that would be very helpful. I am very thankful you are willing to help out!
-
def compute_region_param(l): # Compute min and max of latitude and longitude min_lat = min(l,key = lambda x:x[0])[0] max_lat = max(l,key = lambda x:x[0])[0] min_lon = min(l,key = lambda x:x[1])[1] max_lon = max(l,key = lambda x:x[1])[1] d_lat = 1.2*(max_lat-min_lat) d_lon = 1.2*(max_lon-min_lon) return min_lat,min_lon,max_lat,max_lon,d_lat,d_lon def main(): . . . # center on user location import location location.start_updates() time.sleep(1) loc = location.get_location() location.stop_updates() if loc: lat, lon = loc['latitude'], loc['longitude'] if locs: min_lat,min_lon,max_lat,max_lon,d_lat,d_lon = compute_region_param(locs) v.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) else: v.set_region(lat, lon, 0.05, 0.05, animated=True)
-
@cvp this is very good. One more thing for now: I want to make the user's location the purple pin instead of the green/red so that it stands out. I have the location as 'Current Location' for the pin. Any ideas? Thanks
-
@RocketBlaster05 said:
user's location the purple pin
Be careful: if you change something in the mapView_viewForAnnotation_ def, you need to restart Pythonista because the class is already built and kept in memory (restart means remove it from the tasks list and relaunch it)
Did you remark that if you tap on a pin, you get its title and location.
Édit: try to avoid to have in the .loc file a point with the same coordinates as the user location,
because you could have a purple and a green pin at exactly same position and only see one, even with a big zoom.def mapView_viewForAnnotation_(self,cmd,mk_mapview,mk_annotation): . . . if tit == 'Dropped Pin': pinView.pinColor = 0 # 0=red 1=green 2=purple elif tit == 'Current Location': pinView.pinColor = 2 # 0=red 1=green 2=purple else: pinView.pinColor = 1 # 0=red 1=green 2=purple . . . loc = location.get_location() location.stop_updates() if loc: lat, lon = loc['latitude'], loc['longitude'] # add a purple pin for user's location v.add_pin(lat, lon, 'Current Location', str((lat, lon))) # don't save it in file but add it in locs for centering zoom locs.append((lat,lon)) if locs: min_lat,min_lon,max_lat,max_lon,d_lat,d_lon = compute_region_param(locs) v.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) else: v.set_region(lat, lon, 0.05, 0.05, animated=True)
-
pin_colors = { "Dropped Pin": 0, # red "Current Location": 2, # purple } … pin_color = pin_colors.get(title, 1) # default to green
-
@ccc said:
pin_color = pin_colors.get(title, 1) # default to green
pinView.pin_color = pin_colors.get(tit, 1) # default to green
-
@RocketBlaster05 and tell me if you want to see your own photo instead of a purple pin
-
@RocketBlaster05 or, if you prefer,
Or, with with some drops of blood 😂
-
@RocketBlaster05 and if you want to be able to set view type (satellite, flyover...), you can find this here
See two buttons above right of the map
-
@cvp said:
@RocketBlaster05 and tell me if you want to see your own photo instead of a purple pin
This is a great idea. I would like to incorporate this to make the map feel more personal. This may be a big request but can you also show me how to go about making the map start automatically instead of having to press the button? I would like to make it so the button will mark somebody’s location with an image of like a trash bag. Also, is there a simple command I can use to make the map update somebody’s position? I haven’t tried moving around with the map open so if it already moves automatically then that’s my bad for not checking. Thanks in advance.
I would like for it to show the face on a little pin, like you showed with the blood. I would like for the memoji to be able to have the trash bag I mentioned before just below it like the blood drop. If that is possible, that would be exactly what I need for my pins.
-
An image of like a trash bag… 🚮🗑♻️☠️🤢💰💩🤮