Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Map View
# coding: utf-8
NOTE: This requires Pythonista 3
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:
adapted from original code by omz
from objc_util import *
import ctypes
import ui
import location
import time
import weakref
# _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()
MKAnnotationView = ObjCClass('MKAnnotationView')
# Create a new Objective-C class to act as the MKMapView's delegate...
def mapView_regionDidChangeAnimated_(self, cmd, mk_mapview, animated):
# Resolve weak reference from delegate to mapview:
map_view = _map_delegate_cache[self].map_view_ref()
if map_view:
def mapView_viewForAnnotation_(self, cmd, mapview, annotation):
av= ObjCInstance(mapview).dequeueReusableAnnotationViewWithIdentifier('MyAnnotation')
if not av:
#if title is an image name, set image
if img:
return av.ptr
else: #None defaults to pin
return None
#new simplified class creation using protocols
methods=[mapView_regionDidChangeAnimated_, mapView_viewForAnnotation_],
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):
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_objc = ObjCInstance(self)
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.long_press_location = ui.Point(0, 0)
self.map_delegate = OMMapViewDelegate.alloc().init()#.autorelease()
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)
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()
if subtitle:
annotation.setCoordinate_(coord, restype=None, argtypes=[CLLocationCoordinate2D])
if select:
self.mk_map_view.selectAnnotation_animated_(annotation, True)
def remove_all_pins(self):
'''Remove all annotations (pins) from the map'''
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 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])
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
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):
# --------------------------------------
def long_press_action(sender):
# Add a pin when the MapView recognizes a long-press
c = sender.point_to_coordinate(sender.long_press_location)
sender.add_pin(c[0], c[1], 'Dropped Pin', str(c), select=True)
sender.set_center_coordinate(c[0], c[1], animated=True)
def scroll_action(sender):
# Show the current center coordinate in the title bar after the map is scrolled/zoomed: = 'lat/long: %.2f, %.2f' % sender.get_center_coordinate()
if __name__ == '__main__':
# Create and present a MapView:
v = MapView(frame=(0, 0, 500, 500))
v.long_press_action = long_press_action
v.scroll_action = scroll_action
# Add a pin with the current location (if available), and zoom to that location:
import location
loc = location.get_location()
if loc:
lat, lon = loc['latitude'], loc['longitude']
v.set_region(lat, lon, 0.05, 0.05, animated=True)
v.add_pin(lat, lon, 'Current Location', str((lat, lon)))
v.add_pin(lat+.01, lon, 'plc:Tree_Short','a')
v.add_pin(lat-.011, lon+.01, 'plc:Character_Pink_Girl','b')
v.add_pin(lat+.003, lon+.005, 'plc:Tree_Ugly','a')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment