Swift: Draw a Semi-Sphere in Mkmapview

How to draw custom shapes on MKMapView

After some RND I came across a library curvyRoute & used it to draw arch on MKMapView.

// Adds arch overlay to the mapView
private func addArcOverlays() {
let pointA = CLLocationCoordinate2D(latitude: 15.463157486154865, longitude: 73.78846049308775)
let pointB = CLLocationCoordinate2D(latitude: 15.495608080208948, longitude: 73.83418584279791)

mapView.addOverlay(LineOverlay(origin: pointA, destination: pointB))
let style = LineOverlayStyle(strokeColor: .red, lineWidth: 4, alpha: 1)
let arc = ArcOverlay(origin: pointA, destination: pointB, style: style)
arc.radiusMultiplier = 0.5
mapView.addOverlay(arc)
}

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
switch overlay {
case let lineOverlay as LineOverlay:
let linerender = MapLineOverlayRenderer(lineOverlay)
setVisibleMapRect(linerender.overlay, animated: true)
return linerender

case let polyline as MKPolyline:
let renderer = MKPolylineRenderer(overlay: polyline)
renderer.strokeColor = UIColor.yellow.withAlphaComponent(0.5)
renderer.lineWidth = 4
return renderer

default:
return MKOverlayRenderer()
}
}

How to draw circle overlay on MapKit that surrounds several annotations/coordinates?

I came up with MKCoordinateRegion initializer, which provides the region of the coordinates, the extension has a computed property to provide the radius of the region.

extension MKCoordinateRegion {

init?(from coordinates: [CLLocationCoordinate2D]) {
guard coordinates.count > 1 else { return nil }

let a = MKCoordinateRegion.region(coordinates, fix: { $0 }, fix2: { $0 })
let b = MKCoordinateRegion.region(coordinates, fix: MKCoordinateRegion.fixMeridianNegativeLongitude, fix2: MKCoordinateRegion.fixMeridian180thLongitude)

guard (a != nil || b != nil) else { return nil }
guard (a != nil && b != nil) else {
self = a ?? b!
return
}

self = [a!, b!].min(by: { $0.span.longitudeDelta < $1.span.longitudeDelta }) ?? a!
}

var radius: CLLocationDistance {
let furthest = CLLocation(latitude: self.center.latitude + (span.latitudeDelta / 2),
longitude: center.longitude + (span.longitudeDelta / 2))
return CLLocation(latitude: center.latitude, longitude: center.longitude).distance(from: furthest)
}

// MARK: - Private
private static func region(_ coordinates: [CLLocationCoordinate2D],
fix: (CLLocationCoordinate2D) -> CLLocationCoordinate2D,
fix2: (CLLocationCoordinate2D) -> CLLocationCoordinate2D) -> MKCoordinateRegion? {
let t = coordinates.map(fix)
let min = CLLocationCoordinate2D(latitude: t.min { $0.latitude < $1.latitude }!.latitude,
longitude: t.min { $0.longitude < $1.longitude }!.longitude)
let max = CLLocationCoordinate2D(latitude: t.max { $0.latitude < $1.latitude }!.latitude,
longitude: t.max { $0.longitude < $1.longitude }!.longitude)

// find span
let span = MKCoordinateSpanMake(max.latitude - min.latitude, max.longitude - min.longitude)

// find center
let center = CLLocationCoordinate2D(latitude: max.latitude - span.latitudeDelta / 2,
longitude: max.longitude - span.longitudeDelta / 2)

return MKCoordinateRegion(center: fix2(center), span: span)
}

private static func fixMeridianNegativeLongitude(coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
guard (coordinate.longitude < 0) else { return coordinate }

let fixedLng = 360 + coordinate.longitude
return CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: fixedLng)
}

private static func fixMeridian180thLongitude(coordinate: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
guard (coordinate.longitude > 180) else { return coordinate }

let fixedLng = -360 + coordinate.longitude
return CLLocationCoordinate2D(latitude: coordinate.latitude, longitude: fixedLng)
}

}

Usage:

let coordinates: [CLLocationCoordinate2D] = self.mapView.annotations.map{ $0.coordinate }
if let region = MKCoordinateRegion(from: coordinates) {
self.mapView.add(MKCircle(center: region.center, radius: region.radius))
}

Result is exactly what I want, with ability to handle coordinates crossing 180th meridian:

Sample Image

Is there any way to move MKCircle overlay with MKUserLocation without flicking or blinking?

There are two options:

  1. I’ve found that, in general, the flickering effect is diminished if you add the new overlay before removing the old one.

  2. You might consider making the circle an custom annotation view rather than an overlay. That way, you can just adjust the coordinate without adding/removing.

By putting the circle in the annotation, itself, it’s seamless, both with user tracking on:

Sample Image

I turned off user tracking half way through, so you could see both patterns.

class CirclePointerAnnotationView: MKAnnotationView {
let circleShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.lightGray.withAlphaComponent(0.25).cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
return shapeLayer
}()

let pinShapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.blue.cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
return shapeLayer
}()

let imageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.image = UIImage(named: "woman")
imageView.clipsToBounds = true
return imageView
}()

var pinHeight: CGFloat = 100
var pinRadius: CGFloat = 30
var annotationViewSize = CGSize(width: 300, height: 300)

override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)

layer.addSublayer(circleShapeLayer)
layer.addSublayer(pinShapeLayer)
addSubview(imageView)

bounds.size = annotationViewSize
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func layoutSubviews() {
let radius = min(bounds.width, bounds.height) / 2
let center = CGPoint(x: bounds.midX, y: bounds.midY)
circleShapeLayer.path = UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: 2 * .pi, clockwise: true).cgPath

let angle = asin(pinRadius / (pinHeight - pinRadius))
let pinCenter = CGPoint(x: center.x, y: center.y - (pinHeight - pinRadius))
let path = UIBezierPath()
path.move(to: center)
path.addArc(withCenter: pinCenter, radius: pinRadius, startAngle: .pi - angle, endAngle: angle, clockwise: true)
path.close()
pinShapeLayer.path = path.cgPath

let imageViewDimension = pinRadius * 2 - 15
imageView.bounds.size = CGSize(width: imageViewDimension, height: imageViewDimension)
imageView.center = pinCenter
imageView.layer.cornerRadius = imageViewDimension / 2
}
}

How to draw UIBezierPath overlay on MKMapView?

According to Anna's answer you should use [self pointForMapPoint:points[i]] instead of points[i]

- (void)createPath
{
MKPolyline *line = (id)self.overlay;

MKMapPoint *points = line.points;
NSUInteger pointCount = line.pointCount;

CGMutablePathRef path = CGPathCreateMutable();
CGPoint point = [self pointForMapPoint:points[0]];
CGPathMoveToPoint(path, NULL, point.x, point.y);

for (int i = 1; i < pointCount; i++) {
point = [self pointForMapPoint:points[i]];
CGPathAddLineToPoint(path, NULL, point.x, point.y);
}
[self setPath:path];
}


Related Topics



Leave a reply



Submit