I'm working on the OpenStreetMap for display polygons. Here I'm using MKMapView and set tile of OpenStreetMap.
OSM tile is http://tile.openstreetmap.org/\(path.z)/\(path.x)/\(path.y).png
It's working fine and draw the polygon on the accurate position. (please find the attachment below) I have added the UITapGestureRecognizer on the polygon which is drawn on the map for getting click event. The only issue is when I tap on the polygon it detects silently down. please check the video here.
Code of tile:
func setupTileRenderer() {
let overlay = AdventureMapOverlay()
overlay.canReplaceMapContent = true
mapView.addOverlay(overlay, level: MKOverlayLevel.aboveLabels)
tileRenderer = MKTileOverlayRenderer(tileOverlay: overlay)
overlay.minimumZ = 13
overlay.maximumZ = 16
}
extension MapViewVC : MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? myPolygon {
let renderer = MKPolygonRenderer(polygon: overlay)
renderer.fillColor = overlay.color //UIColor.black.withAlphaComponent(0.5)
renderer.strokeColor = UIColor.orange
renderer.lineWidth = 2
return renderer
}
return tileRenderer
}
func removeMapOverlay(){
self.mapView.removeOverlays(self.arrPolygons.compactMap({$0.mkOverlay}))
}
}
Tap event:
#objc func mapTapped(_ gesture: UITapGestureRecognizer){
let point = gesture.location(in: self.mapView)
let coordinate = self.mapView.convert(point, toCoordinateFrom: nil)
//print("Tap \(coordinate)")
for overlay in self.mapView.overlays {
if let polygon = overlay as? MKPolygon {
guard (self.mapView.renderer(for: polygon) as? MKPolygonRenderer) != nil else { continue }
if polygon.contain(coor: coordinate){
print("Tap was inside this polygon \(coordinate)")
break
}
continue
}
}
}
extension MKPolygon {
func contain(coor: CLLocationCoordinate2D) -> Bool {
let polygonRenderer = MKPolygonRenderer(polygon: self)
let currentMapPoint: MKMapPoint = MKMapPoint(coor)
let polygonViewPoint: CGPoint = polygonRenderer.point(for: currentMapPoint)
return polygonRenderer.path.contains(polygonViewPoint)
}
}
P.S.: I'm also finding way to render OpenStreetMap offline line same as MKMapView.
Any help will be appreciated. Thank you. :)
Related
Hi everyone I've been working for a few days to show a straight line on my map. I use swiftUI and mapkit to render the map. what I want to achieve is a straight line between the two annotations, these are shown on the map.
Dit is de code die ik op dit moment heb. Ik hoop dut jullie mij kunnen helpen want ik kom er niet uit.
import MapKit
struct MapViewWorldDetail: UIViewRepresentable {
var StartCoordinate: CLLocationCoordinate2D
var EndCoordinate: CLLocationCoordinate2D
var name: String
#Binding var region: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 50, longitudeDelta: 50)
let region = MKCoordinateRegion(center: self.region, span: span)
// view.mapType = MKMapType.satelliteFlyover;
view.setRegion(region, animated: true)
let annotation = MKPointAnnotation()
annotation.coordinate = StartCoordinate
annotation.title = name
view.addAnnotation(annotation)
let annotationEnd = MKPointAnnotation()
annotationEnd.coordinate = EndCoordinate
annotationEnd.title = name
view.addAnnotation(annotationEnd)
let aPolyline = MKGeodesicPolyline(coordinates: [StartCoordinate, EndCoordinate], count: 2)
view.addOverlay(aPolyline)
}
}
Note on the straight line you are drawing: The line that you are drawing using MKGeodesicPolyline has this note in the Apple Developer Documentation:
MKGeodesicPolyline - When displayed on a two-dimensional map view, the line segment between any two points may appear curved.
Example of working code
In SwiftUI, you'll need to implement the MKMapViewDelegate in a Coordinator class, which is where the Overlay handling is taken care of:
Add this to func updateUIView
func updateUIView(_ view: MKMapView, context: UIViewRepresentableContext<MapView>) {
// Stuff you already had in updateUIView
// :
// adding this at the end is sufficient
mapView.delegate = context.coordinator
}
Add this to your struct, struct MapViewWorldDetail
// MARK: - Coordinator for using UIKit inside SwiftUI.
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
// MARK: - Managing the Display of Overlays
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
print("mapView(_:rendererFor:)")
if let polyline = overlay as? MKPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: polyline)
polylineRenderer.strokeColor = .red
polylineRenderer.lineWidth = 3
return polylineRenderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
Good references to review:
Sample project that implements the Coordinator class & delegates (but does not do overlays)
https://github.com/roblabs/ios-map-ui/tree/master/MapKit-SwiftUI-for-iOS-macOS
Medium posts by others
https://medium.com/better-programming/exploring-mapkit-on-ios-13-1a7a1439e3b6
https://medium.com/flawless-app-stories/mapkit-in-swiftui-c0cc2b07c28a
I'm developing an app that request multiple directions (MKDirectionsRequest) and draw the routes in a mapView, all ok.
But I'm facing a problem: I want to draw each route with different color.
The first idea was simple: use title/subtitle to 'tag' different MKPolyline so I can set the color I want in the delegate function:
mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer
but I don't like this solution because it's 'ugly' and I will have to parse a string the day I will have to pass different params (traffic ..)
The second simple solution was to subclass MKPolyline, yeh simple ..
as MKPolyline doesn't have a designated initializer, this is impossible (is it ?)
[edit]: I want to create a subclass of MKPolyline to copy "on it" the already created MKPolyline returned by MKDirectionsRequest.routes but I can't figure out how to override the read-only params (Apple says that we should override them in the subclass and add the setter, but I have an infinite loop in the setter witch is .. normal)
if was using objC, it will be simple to "inject" code at runtime and add my params, but I'm using swift.
could anyone help on this, thanks.
Simpler method not requiring a custom renderer :
import UIKit
import MapKit
class CustomPolyline : MKPolyline {
var color: UIColor?
}
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// setup mapView
mapView.delegate = self
// set map view region
let location : CLLocationCoordinate2D = CLLocationCoordinate2DMake(51.4987, 0.007);
let viewRegion = MKCoordinateRegionMakeWithDistance(location, 400, 400)
mapView.setRegion(viewRegion, animated:true )
// add red line
let coords1 = [CLLocationCoordinate2D(latitude: 51.499526, longitude: 0.004785),CLLocationCoordinate2D(latitude: 51.500007, longitude: 0.005493)]
let polyline1 = CustomPolyline(coordinates: coords1, count: coords1.count)
polyline1.color = UIColor.red
mapView.add(polyline1)
// add blue line
let coords2 = [CLLocationCoordinate2D(latitude: 51.498103, longitude: 0.007574), CLLocationCoordinate2D(latitude: 51.498190, longitude: 0.009677)]
let polyline2 = CustomPolyline(coordinates: coords2, count: coords2.count)
polyline2.color = UIColor.blue
mapView.add(polyline2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is CustomPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: overlay)
polylineRenderer.strokeColor = (overlay as! CustomPolyline).color
polylineRenderer.lineWidth = 4
return polylineRenderer
}
return MKOverlayRenderer()
}
}
Why do you want to set color to your custom MKOverlay, couldnt you simply set the desired color to MKOverlayRenderer ? It should be quite simpler then.
You could also easily subclass MKPolyline. I dont see why you were not able to do that.
If you want to be able to create a custom MKOverlay and set custom properties, then, draw it into the map, you would also need to create your own renderer. That seems like a big job. But, here is an example how you could do that.
import UIKit
import MapKit
public class CustomOverlay: MKPolyline {
public var customColor: UIColor?
}
public class CustomRenderer: MKPolylineRenderer {
override public func strokePath(_ path: CGPath, in context: CGContext) {
guard let overlayColor = self.overlay as? CustomOverlay, let color = overlayColor.customColor else {
super.strokePath(path, in: context)
return
}
context.saveGState()
context.setStrokeColor(color.cgColor)
context.addPath(path)
context.drawPath(using: .stroke)
context.restoreGState()
}
}
public extension CLLocationCoordinate2D {
static let Salo = CLLocationCoordinate2DMake(60.3909, 23.1355)
static let Turku = CLLocationCoordinate2DMake(60.454510, 22.264824)
static let Helsinki = CLLocationCoordinate2DMake(60.170833, 24.9375)
}
public class ViewController: UIViewController, MKMapViewDelegate {
private var mapView: MKMapView!
override public func viewDidLoad() {
super.viewDidLoad()
createMapView()
setupMapView()
}
private func createMapView() {
mapView = MKMapView(frame: .zero)
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.delegate = self
view.addSubview(mapView)
[mapView.topAnchor.constraint(equalTo: view.topAnchor),
mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor)].forEach { $0.isActive = true }
}
private func setupMapView() {
let coordinates: [CLLocationCoordinate2D] = [.Helsinki, .Turku]
let customPolyLine = CustomOverlay(coordinates: coordinates, count: coordinates.count)
customPolyLine.customColor = UIColor.red
mapView.add(customPolyLine)
let coordinateSpan = MKCoordinateSpan(latitudeDelta: 3, longitudeDelta: 3)
let region = MKCoordinateRegion(center: .Salo, span: coordinateSpan)
mapView.setRegion(region, animated: true)
}
// MARK: MKMapViewDelegate
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
/* return a simple renderer */
// let renderer = MKPolylineRenderer(overlay:overlay)
// renderer.lineWidth = 2
// renderer.lineDashPattern = [1, 2, 1]
// renderer.strokeColor = UIColor.red
// return renderer
/* a custom renderer */
let customRenderer = CustomRenderer(overlay: overlay)
customRenderer.lineWidth = 2
customRenderer.strokeColor = UIColor.green // this color is not used, since we apply color from overlay inside strokePath(:inContext:) method for custom renderer
customRenderer.lineDashPattern = [1, 2, 1]
return customRenderer
}
}
The below code works and gives me a draggable annotation view. However, I've noticed that the annotation view is not draggable from the very beginning on, but rather only after the finger has rested for a short moment on the annotation view. When directly going into a drag movement the dragging doesn't affect the annotation view but instead pans the map. It certainly doesn't feel like drag'n'drop. Both on the device and in the simulator.
ViewController (Delegate of the MapView)
override func viewDidLoad() {
/// ...
let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(addPin))
mapView.addGestureRecognizer(gestureRecognizer)
}
func addPin(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state != UIGestureRecognizerState.Began {
return
}
for annotation in mapView.annotations {
mapView.removeAnnotation(annotation)
}
let touchLocationInView = gestureRecognizer.locationInView(mapView)
let coordinate = mapView.convertPoint(touchLocationInView, toCoordinateFromView: mapView)
let annotation = DragAnnotation(coordinate: coordinate, title: "draggable", subtitle: "")
mapView.addAnnotation(annotation)
}
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKindOfClass(DragAnnotation) {
let reuseIdentifier = "DragAnnotationIdentifier"
var annotationView: MKAnnotationView!
if let dequeued = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseIdentifier) {
annotationView = dequeued
} else {
annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
}
annotationView.annotation = annotation
annotationView.image = UIImage(named: "bluedisk2")
annotationView.canShowCallout = false
annotationView.draggable = true
return annotationView
}
return nil
}
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, didChangeDragState newState: MKAnnotationViewDragState, fromOldState oldState: MKAnnotationViewDragState) {
switch (newState) {
case .Starting:
view.dragState = .Dragging
case .Ending, .Canceling:
view.dragState = .None
default: break
}
}
DragAnnotation
import UIKit
import MapKit
class DragAnnotation : NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D {
didSet {
print(coordinate)
}
}
var title: String?
var subtitle: String?
init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
}
}
I don't think you can change the draggable delay, but you could disable it and add your own drag gesture that has no delay (or a shorter delay):
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is MKUserLocation { return nil }
var view = mapView.dequeueReusableAnnotationViewWithIdentifier("Foo")
if view == nil {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "Foo")
view?.draggable = false
let drag = UILongPressGestureRecognizer(target: self, action: #selector(handleDrag(_:)))
drag.minimumPressDuration = 0 // set this to whatever you want
drag.allowableMovement = .max
view?.addGestureRecognizer(drag)
} else {
view?.annotation = annotation
}
return view
}
private var startLocation = CGPointZero
func handleDrag(gesture: UILongPressGestureRecognizer) {
let location = gesture.locationInView(mapView)
if gesture.state == .Began {
startLocation = location
} else if gesture.state == .Changed {
gesture.view?.transform = CGAffineTransformMakeTranslation(location.x - startLocation.x, location.y - startLocation.y)
} else if gesture.state == .Ended || gesture.state == .Cancelled {
let annotationView = gesture.view as! MKAnnotationView
let annotation = annotationView.annotation as! DragAnnotation
let translate = CGPoint(x: location.x - startLocation.x, y: location.y - startLocation.y)
let originalLocation = mapView.convertCoordinate(annotation.coordinate, toPointToView: mapView)
let updatedLocation = CGPoint(x: originalLocation.x + translate.x, y: originalLocation.y + translate.y)
annotationView.transform = CGAffineTransformIdentity
annotation.coordinate = mapView.convertPoint(updatedLocation, toCoordinateFromView: mapView)
}
}
By the way, if you're going to change the coordinate of an annotation, you will want to add dynamic keyword to your custom class coordinate declaration so that the appropriate KVO notifications will be issued.
class DragAnnotation: NSObject, MKAnnotation {
dynamic var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
init(coordinate: CLLocationCoordinate2D, title: String? = nil, subtitle: String? = nil) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
super.init()
}
}
Note, in this example, I set the delay to zero. That means that if you tap on the annotation (the typical UI for "show the callout"), this may prevent that from working correctly. It will treat this as a very short drag. This is why the standard UI requires a delay in the long press before dragging.
So, for this reason, I personally would hesitate to override this behavior shown above because it will be different then standard map behavior that the end user is familiar with. If your map behaves differently than any other maps on the user's device, this could be a source of frustration.
I am currently trying to allow the user to add pins to the map which will then draw a polygon connecting those pins. However I want to extend it to allow the user to be able to drag the pins and the polygons will be updated accordingly. MKMapView draws the polygon from the array of coordinates according to their arrangement in the array (if I am not mistaken). The problem I am facing now is how do I update the polygons after the user repositioned the pins.
var touchCoordinatesWithOrder: [(coordinate: CLLocationCoordinate2D, order: Int)] = []
var counter = 0
func addLongPressGesture() {
let longPressRecogniser = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressRecogniser.minimumPressDuration = 1.0
mapView.addGestureRecognizer(longPressRecogniser)
}
func handleLongPress(gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state != .Began {
return
}
let touchPoint = gestureRecognizer.locationInView(self.mapView)
let touchMapCoordinate = mapView.convertPoint(touchPoint, toCoordinateFromView: mapView)
let annotation = MKPointAnnotation()
annotation.coordinate = touchMapCoordinate
mapView.addAnnotation(annotation)
touchCoordinatesWithOrder.append((coordinate: touchMapCoordinate, order: counter))
counter += 1
}
#IBAction func drawAction(sender: AnyObject) {
if touchCoordinatesWithOrder.count <= 2 {
print("Not enough coordinates")
return
}
var coords = [CLLocationCoordinate2D]()
for i in 0..<touchCoordinatesWithOrder.count {
coords.append(touchCoordinatesWithOrder[i].coordinate)
}
let polygon = MKPolygon(coordinates: &coords, count: coords.count)
mapView.addOverlay(polygon)
counter = 0
}
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, didChangeDragState newState: MKAnnotationViewDragState, fromOldState oldState: MKAnnotationViewDragState) {
// if the user repositioned pin number2 then how to I update my array?
}
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolygon {
let polygonView = MKPolygonRenderer(overlay: overlay)
polygonView.strokeColor = UIColor.blackColor()
polygonView.lineWidth = 0.5
return polygonView
}
return MKPolylineRenderer()
}
To make the pins draggable, you need to set draggable = true on the MKAnnotationView. Implement the viewForAnnotation and dequeue or create the annotation, then set draggable = true. Ensure that the MKMapView delegate is set otherwise none of the delegate methods will be called.
You may also find it easier to store the annotations in an array, rather than just storing the coordinates. The map view retains a reference to the annotations in the array, so when the point is moved in the map, the annotation is automatically updated.
Your question did not say whether you need to draw a path around the points, or through the points. If you want to draw an overlay which surrounds the points, then you also need to calculate the convex hull for the coordinates. The code example does this, although it's easily removed.
Example:
class MapAnnotationsOverlayViewController: UIViewController, MKMapViewDelegate {
#IBOutlet var mapView: MKMapView!
// Array of annotations - modified when the points are changed.
var annotations = [MKPointAnnotation]()
// Current polygon displayed in the overlay.
var polygon: MKPolygon?
override func viewDidLoad() {
super.viewDidLoad()
mapView.delegate = self
addLongPressGesture()
}
func addLongPressGesture() {
let longPressRecogniser = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress))
longPressRecogniser.minimumPressDuration = 0.25
mapView.addGestureRecognizer(longPressRecogniser)
}
func handleLongPress(gestureRecognizer: UIGestureRecognizer) {
guard gestureRecognizer.state == .Began else {
return
}
let touchPoint = gestureRecognizer.locationInView(self.mapView)
let touchMapCoordinate = mapView.convertPoint(touchPoint, toCoordinateFromView: mapView)
let annotation = MKPointAnnotation()
// The annotation must have a title in order for it to be selectable.
// Without a title the annotation is not selectable, and therefore not draggable.
annotation.title = "Point \(annotations.count)"
annotation.coordinate = touchMapCoordinate
mapView.addAnnotation(annotation)
// Add the new annotation to the list.
annotations.append(annotation)
// Redraw the overlay.
updateOverlay()
}
#IBAction func drawAction(sender: AnyObject) {
updateOverlay()
}
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView? {
var view = mapView.dequeueReusableAnnotationViewWithIdentifier("pin")
if let view = view {
view.annotation = annotation
}
else {
view = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "pin")
// Allow the pin to be repositioned.
view?.draggable = true
}
return view
}
func mapView(mapView: MKMapView, annotationView view: MKAnnotationView, didChangeDragState newState: MKAnnotationViewDragState, fromOldState oldState: MKAnnotationViewDragState) {
// The map view retains a reference to the same annotations in the array.
// The annotation in the array is automatically updated when the pin is moved.
updateOverlay()
}
func updateOverlay() {
// Remove existing overlay.
if let polygon = self.polygon {
mapView.removeOverlay(polygon)
}
self.polygon = nil
if annotations.count < 3 {
print("Not enough coordinates")
return
}
// Create coordinates for new overlay.
let coordinates = annotations.map({ $0.coordinate })
// Sort the coordinates to create a path surrounding the points.
// Remove this if you only want to draw lines between the points.
var hull = sortConvex(coordinates)
let polygon = MKPolygon(coordinates: &hull, count: hull.count)
mapView.addOverlay(polygon)
self.polygon = polygon
}
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolygon {
let polygonView = MKPolygonRenderer(overlay: overlay)
polygonView.strokeColor = UIColor.blackColor()
polygonView.lineWidth = 0.5
return polygonView
}
return MKPolylineRenderer()
}
}
Here is the convex hull sorting algorithm (adapted from this Gist on GitHub).
func sortConvex(input: [CLLocationCoordinate2D]) -> [CLLocationCoordinate2D] {
// X = longitude
// Y = latitude
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
// Returns a positive value, if OAB makes a counter-clockwise turn,
// negative for clockwise turn, and zero if the points are collinear.
func cross(P: CLLocationCoordinate2D, _ A: CLLocationCoordinate2D, _ B: CLLocationCoordinate2D) -> Double {
let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude)
let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude)
return part1 - part2;
}
// Sort points lexicographically
let points = input.sort() {
$0.longitude == $1.longitude ? $0.latitude < $1.latitude : $0.longitude < $1.longitude
}
// Build the lower hull
var lower: [CLLocationCoordinate2D] = []
for p in points {
while lower.count >= 2 && cross(lower[lower.count-2], lower[lower.count-1], p) <= 0 {
lower.removeLast()
}
lower.append(p)
}
// Build upper hull
var upper: [CLLocationCoordinate2D] = []
for p in points.reverse() {
while upper.count >= 2 && cross(upper[upper.count-2], upper[upper.count-1], p) <= 0 {
upper.removeLast()
}
upper.append(p)
}
// Last point of upper list is omitted because it is repeated at the
// beginning of the lower list.
upper.removeLast()
// Concatenation of the lower and upper hulls gives the convex hull.
return (upper + lower)
}
This is how it would look with the convex hull sorting (path drawn around points):
This is how it looks without sorting (path drawn from point to point in sequence):
Thank #Luke.
I update Luke's sortConvex function,
so make it compiled successfully by Swift 5.
func sortConvex(input: [CLLocationCoordinate2D]) -> [CLLocationCoordinate2D] {
// X = longitude
// Y = latitude
// 2D cross product of OA and OB vectors, i.e. z-component of their 3D cross product.
// Returns a positive value, if OAB makes a counter-clockwise turn,
// negative for clockwise turn, and zero if the points are collinear.
func cross(P: CLLocationCoordinate2D, _ A: CLLocationCoordinate2D, _ B: CLLocationCoordinate2D) -> Double {
let part1 = (A.longitude - P.longitude) * (B.latitude - P.latitude)
let part2 = (A.latitude - P.latitude) * (B.longitude - P.longitude)
return part1 - part2;
}
// Sort points lexicographically
let points: [CLLocationCoordinate2D] = input.sorted { a, b in
a.longitude < b.longitude || a.longitude == b.longitude && a.longitude < b.longitude
}
// Build the lower hull
var lower: [CLLocationCoordinate2D] = []
for p in points {
while lower.count >= 2 {
let a = lower[lower.count - 2]
let b = lower[lower.count - 1]
if cross(P: p, a, b) > 0 { break }
lower.removeLast()
}
lower.append(p)
}
// Build upper hull
var upper: [CLLocationCoordinate2D] = []
for p in points.lazy.reversed() {
while upper.count >= 2 {
let a = upper[upper.count - 2]
let b = upper[upper.count - 1]
if cross(P: p, a, b) > 0 { break }
upper.removeLast()
}
upper.append(p)
}
// Last point of upper list is omitted because it is repeated at the
// beginning of the lower list.
upper.removeLast()
// Concatenation of the lower and upper hulls gives the convex hull.
return (upper + lower)
}
I am trying to learn MapKit and am now trying to add an image (not a polygon, circle, rectangle, etc.) as an overlay to the map view. I can't seem to find any sample code to help explain how to do this.
So far my code in ViewController.swift is:
import UIKit
import MapKit
import CoreLocation
class ViewController: UIViewController, MKMapViewDelegate, CLLocationManagerDelegate {
#IBOutlet var mapView: MKMapView!
var locationManager: CLLocationManager!
var mapOverlay: MKOverlay!
override func viewDidLoad() {
super.viewDidLoad()
//Setup our Location Manager
locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
//Setup our Map View
mapView.delegate = self
mapView.mapType = MKMapType.Satellite
mapView.showsUserLocation = true
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
I know I need to use:
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer
and the drawMapRect function but I don't know what to put in them. Also, it would be useful if all the code is in ViewController.swift
I only need one overlay so I don't need a class. I need it to be an image overlay (so it can resize), not an annotation. I also tried to follow the tutorial at http://www.raywenderlich.com/30001/overlay-images-and-overlay-views-with-mapkit-tutorial but there is not a section with all the code and I find it hard to follow along. Can you people please help me how to create image MKOverlays and add them to MKMapKit?
Thanks in advance...
Swift 3
The following works for me in Swift 3.
class ImageOverlay : NSObject, MKOverlay {
let image:UIImage
let boundingMapRect: MKMapRect
let coordinate:CLLocationCoordinate2D
init(image: UIImage, rect: MKMapRect) {
self.image = image
self.boundingMapRect = rect
self.coordinate = rect.centerCoordinate
}
}
class ImageOverlayRenderer : MKOverlayRenderer {
override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
guard let overlay = self.overlay as? ImageOverlay else {
return
}
let rect = self.rect(for: overlay.boundingMapRect)
UIGraphicsPushContext(context)
overlay.image.draw(in: rect)
UIGraphicsPopContext()
}
}
Then as usual, in your MapView delegate:
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is ImageOverlay {
return ImageOverlayRenderer(overlay: overlay)
}
...
}
And whenever you need to add an image to your map:
let overlay = ImageOverlay(image: myImage, rect: desiredLocationAsMapRect)
mapView.add(overlay)
Note, I always ensure that the rect has the same aspect ratio as the image. Otherwise, I suspect image distortion will result.
Apple do have some sample code for displaying overlays but it is in Objective-C so you will have to transpose it to Swift.
Essentially there are two things you need to do (and both can be done in your view controller). The first is to create the overlay and add it to the map i.e. somewhere in viewDidLoad():
var points = [CLLocationCoordinate2D(latitude: -29.8122, longitude: 148.6351),
CLLocationCoordinate2D(latitude: -27.9307, longitude: 148.6351),
CLLocationCoordinate2D(latitude: -27.9307, longitude: 150.9909),
CLLocationCoordinate2D(latitude: -29.8122, longitude: 150.9909)]
let tile = MKPolygon(coordinates: &points, count: points.count)
tile.title = "Moree"
mapView.addOverlay(tile)
Note that points has to be a var as you are dereferencing it for the tile creation.
The second step is then to define the renderer:
// mapView delegate function
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer {
if overlay.isKindOfClass(MKPolygon) {
let renderer = MKPolygonRenderer(overlay: overlay)
renderer.fillColor = UIColor.cyanColor().colorWithAlphaComponent(0.2)
renderer.strokeColor = UIColor.blueColor().colorWithAlphaComponent(0.7)
renderer.lineWidth = 3
return renderer
}
fatalError()
}
Using Swift 2.1/Xcode 7.2, this renders a tile for me in a location in Australia. So you may want to tweak the lat/lons to give you an overlay in your map - map defaults are to your continent.