How to detect taps on MKPolylines/Overlays like Maps.app? - ios

When displaying directions on the built-in Maps.app on the iPhone you can "select" one of the usually 3 route alternatives that are displayed by tapping on it. I wan't to replicate this functionality and check if a tap lies within a given MKPolyline.
Currently I detect taps on the MapView like this:
// Add Gesture Recognizer to MapView to detect taps
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleMapTap:)];
// we require all gesture recognizer except other single-tap gesture recognizers to fail
for (UIGestureRecognizer *gesture in self.gestureRecognizers) {
if ([gesture isKindOfClass:[UITapGestureRecognizer class]]) {
UITapGestureRecognizer *systemTap = (UITapGestureRecognizer *)gesture;
if (systemTap.numberOfTapsRequired > 1) {
[tap requireGestureRecognizerToFail:systemTap];
}
} else {
[tap requireGestureRecognizerToFail:gesture];
}
}
[self addGestureRecognizer:tap];
I handle the taps as follows:
- (void)handleMapTap:(UITapGestureRecognizer *)tap {
if ((tap.state & UIGestureRecognizerStateRecognized) == UIGestureRecognizerStateRecognized) {
// Check if the overlay got tapped
if (overlayView != nil) {
// Get view frame rect in the mapView's coordinate system
CGRect viewFrameInMapView = [overlayView.superview convertRect:overlayView.frame toView:self];
// Get touch point in the mapView's coordinate system
CGPoint point = [tap locationInView:self];
// Check if the touch is within the view bounds
if (CGRectContainsPoint(viewFrameInMapView, point)) {
[overlayView handleTapAtPoint:[tap locationInView:self.directionsOverlayView]];
}
}
}
}
This works as expected, now I need to check if the tap lies within the given MKPolyline overlayView (not strict, I the user taps somewhere near the polyline this should be handled as a hit).
What's a good way to do this?
- (void)handleTapAtPoint:(CGPoint)point {
MKPolyline *polyline = self.polyline;
// TODO: detect if point lies withing polyline with some margin
}
Thanks!

The question is rather old, but my answer may be useful to other people looking for a solution for this problem.
This code detects touches on poly lines with a maximum distance of 22 pixels in every zoom level. Just point your UITapGestureRecognizer to handleTap:
/** Returns the distance of |pt| to |poly| in meters
*
* from http://paulbourke.net/geometry/pointlineplane/DistancePoint.java
*
*/
- (double)distanceOfPoint:(MKMapPoint)pt toPoly:(MKPolyline *)poly
{
double distance = MAXFLOAT;
for (int n = 0; n < poly.pointCount - 1; n++) {
MKMapPoint ptA = poly.points[n];
MKMapPoint ptB = poly.points[n + 1];
double xDelta = ptB.x - ptA.x;
double yDelta = ptB.y - ptA.y;
if (xDelta == 0.0 && yDelta == 0.0) {
// Points must not be equal
continue;
}
double u = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta);
MKMapPoint ptClosest;
if (u < 0.0) {
ptClosest = ptA;
}
else if (u > 1.0) {
ptClosest = ptB;
}
else {
ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta);
}
distance = MIN(distance, MKMetersBetweenMapPoints(ptClosest, pt));
}
return distance;
}
/** Converts |px| to meters at location |pt| */
- (double)metersFromPixel:(NSUInteger)px atPoint:(CGPoint)pt
{
CGPoint ptB = CGPointMake(pt.x + px, pt.y);
CLLocationCoordinate2D coordA = [mapView convertPoint:pt toCoordinateFromView:mapView];
CLLocationCoordinate2D coordB = [mapView convertPoint:ptB toCoordinateFromView:mapView];
return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB));
}
#define MAX_DISTANCE_PX 22.0f
- (void)handleTap:(UITapGestureRecognizer *)tap
{
if ((tap.state & UIGestureRecognizerStateRecognized) == UIGestureRecognizerStateRecognized) {
// Get map coordinate from touch point
CGPoint touchPt = [tap locationInView:mapView];
CLLocationCoordinate2D coord = [mapView convertPoint:touchPt toCoordinateFromView:mapView];
double maxMeters = [self metersFromPixel:MAX_DISTANCE_PX atPoint:touchPt];
float nearestDistance = MAXFLOAT;
MKPolyline *nearestPoly = nil;
// for every overlay ...
for (id <MKOverlay> overlay in mapView.overlays) {
// .. if MKPolyline ...
if ([overlay isKindOfClass:[MKPolyline class]]) {
// ... get the distance ...
float distance = [self distanceOfPoint:MKMapPointForCoordinate(coord)
toPoly:overlay];
// ... and find the nearest one
if (distance < nearestDistance) {
nearestDistance = distance;
nearestPoly = overlay;
}
}
}
if (nearestDistance <= maxMeters) {
NSLog(#"Touched poly: %#\n"
" distance: %f", nearestPoly, nearestDistance);
}
}
}

#Jensemanns answer in Swift 4, which by the way was the only solution that I found that worked for me to detect clicks on a MKPolyline:
let map = MKMapView()
let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
map.addGestureRecognizer(mapTap)
func mapTapped(_ tap: UITapGestureRecognizer) {
if tap.state == .recognized {
// Get map coordinate from touch point
let touchPt: CGPoint = tap.location(in: map)
let coord: CLLocationCoordinate2D = map.convert(touchPt, toCoordinateFrom: map)
let maxMeters: Double = meters(fromPixel: 22, at: touchPt)
var nearestDistance: Float = MAXFLOAT
var nearestPoly: MKPolyline? = nil
// for every overlay ...
for overlay: MKOverlay in map.overlays {
// .. if MKPolyline ...
if (overlay is MKPolyline) {
// ... get the distance ...
let distance: Float = Float(distanceOf(pt: MKMapPointForCoordinate(coord), toPoly: overlay as! MKPolyline))
// ... and find the nearest one
if distance < nearestDistance {
nearestDistance = distance
nearestPoly = overlay as! MKPolyline
}
}
}
if Double(nearestDistance) <= maxMeters {
print("Touched poly: \(nearestPoly) distance: \(nearestDistance)")
}
}
}
func distanceOf(pt: MKMapPoint, toPoly poly: MKPolyline) -> Double {
var distance: Double = Double(MAXFLOAT)
for n in 0..<poly.pointCount - 1 {
let ptA = poly.points()[n]
let ptB = poly.points()[n + 1]
let xDelta: Double = ptB.x - ptA.x
let yDelta: Double = ptB.y - ptA.y
if xDelta == 0.0 && yDelta == 0.0 {
// Points must not be equal
continue
}
let u: Double = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta)
var ptClosest: MKMapPoint
if u < 0.0 {
ptClosest = ptA
}
else if u > 1.0 {
ptClosest = ptB
}
else {
ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta)
}
distance = min(distance, MKMetersBetweenMapPoints(ptClosest, pt))
}
return distance
}
func meters(fromPixel px: Int, at pt: CGPoint) -> Double {
let ptB = CGPoint(x: pt.x + CGFloat(px), y: pt.y)
let coordA: CLLocationCoordinate2D = map.convert(pt, toCoordinateFrom: map)
let coordB: CLLocationCoordinate2D = map.convert(ptB, toCoordinateFrom: map)
return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB))
}
Swift 5.x version
let map = MKMapView()
let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped))
map.addGestureRecognizer(mapTap)
#objc func mapTapped(_ tap: UITapGestureRecognizer) {
if tap.state == .recognized {
// Get map coordinate from touch point
let touchPt: CGPoint = tap.location(in: map)
let coord: CLLocationCoordinate2D = map.convert(touchPt, toCoordinateFrom: map)
let maxMeters: Double = meters(fromPixel: 22, at: touchPt)
var nearestDistance: Float = MAXFLOAT
var nearestPoly: MKPolyline? = nil
// for every overlay ...
for overlay: MKOverlay in map.overlays {
// .. if MKPolyline ...
if (overlay is MKPolyline) {
// ... get the distance ...
let distance: Float = Float(distanceOf(pt: MKMapPoint(coord), toPoly: overlay as! MKPolyline))
// ... and find the nearest one
if distance < nearestDistance {
nearestDistance = distance
nearestPoly = overlay as? MKPolyline
}
}
}
if Double(nearestDistance) <= maxMeters {
print("Touched poly: \(String(describing: nearestPoly)) distance: \(nearestDistance)")
}
}
}
private func distanceOf(pt: MKMapPoint, toPoly poly: MKPolyline) -> Double {
var distance: Double = Double(MAXFLOAT)
for n in 0..<poly.pointCount - 1 {
let ptA = poly.points()[n]
let ptB = poly.points()[n + 1]
let xDelta: Double = ptB.x - ptA.x
let yDelta: Double = ptB.y - ptA.y
if xDelta == 0.0 && yDelta == 0.0 {
// Points must not be equal
continue
}
let u: Double = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta)
var ptClosest: MKMapPoint
if u < 0.0 {
ptClosest = ptA
}
else if u > 1.0 {
ptClosest = ptB
}
else {
ptClosest = MKMapPoint(x: ptA.x + u * xDelta, y: ptA.y + u * yDelta)
}
distance = min(distance, ptClosest.distance(to: pt))
}
return distance
}
private func meters(fromPixel px: Int, at pt: CGPoint) -> Double {
let ptB = CGPoint(x: pt.x + CGFloat(px), y: pt.y)
let coordA: CLLocationCoordinate2D = map.convert(pt, toCoordinateFrom: map)
let coordB: CLLocationCoordinate2D = map.convert(ptB, toCoordinateFrom: map)
return MKMapPoint(coordA).distance(to: MKMapPoint(coordB))
}

You can refer my answer may it will help you to find desired solution.
I've added gesture on my MKMapView.
[mapV addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(mapTapped:)]];
This is how i handled my gesture and find out whether the tap is on Overlay view or not.
- (void)mapTapped:(UITapGestureRecognizer *)recognizer
{
MKMapView *mapView = (MKMapView *)recognizer.view;
CGPoint tapPoint = [recognizer locationInView:mapView];
NSLog(#"tapPoint = %f,%f",tapPoint.x, tapPoint.y);
//convert screen CGPoint tapPoint to CLLocationCoordinate2D...
CLLocationCoordinate2D tapCoordinate = [mapView convertPoint:tapPoint toCoordinateFromView:mapView];
//convert CLLocationCoordinate2D tapCoordinate to MKMapPoint...
MKMapPoint point = MKMapPointForCoordinate(tapCoordinate);
if (mapView.overlays.count > 0 ) {
for (id<MKOverlay> overlay in mapView.overlays)
{
if ([overlay isKindOfClass:[MKCircle class]])
{
MKCircle *circle = overlay;
MKCircleRenderer *circleRenderer = (MKCircleRenderer *)[mapView rendererForOverlay:circle];
//convert MKMapPoint tapMapPoint to point in renderer's context...
CGPoint datpoint = [circleRenderer pointForMapPoint:point];
[circleRenderer invalidatePath];
if (CGPathContainsPoint(circleRenderer.path, nil, datpoint, false)){
NSLog(#"tapped on overlay");
break;
}
}
}
}
}
Thanks.
This may help you hopefully.

The solution proposed below by Jensemann is working great. See below code adapted for Swift 2, tested successfully on IOS 8 and 9 (XCode 7.1).
func didTapMap(gestureRecognizer: UIGestureRecognizer) {
tapPoint = gestureRecognizer.locationInView(mapView)
NSLog("tapPoint = %f,%f",tapPoint.x, tapPoint.y)
//convert screen CGPoint tapPoint to CLLocationCoordinate2D...
let tapCoordinate = mapView.convertPoint(tapPoint, toCoordinateFromView: mapView)
let tapMapPoint = MKMapPointForCoordinate(tapCoordinate)
print("tap coordinates = \(tapCoordinate)")
print("tap map point = \(tapMapPoint)")
// Now we test to see if one of the overlay MKPolyline paths were tapped
var nearestDistance = Double(MAXFLOAT)
let minDistance = 2000 // in meters, adjust as needed
var nearestPoly = MKPolyline()
// arrayPolyline below is an array of MKPolyline overlaid on the mapView
for poly in arrayPolyline {
// ... get the distance ...
let distance = distanceOfPoint(tapMapPoint, poly: poly)
print("distance = \(distance)")
// ... and find the nearest one
if (distance < nearestDistance) {
nearestDistance = distance
nearestPoly = poly
}
}
if (nearestDistance <= minDistance) {
NSLog("Touched poly: %#\n distance: %f", nearestPoly, nearestDistance);
}
}
func distanceOfPoint(pt: MKMapPoint, poly: MKPolyline) -> Double {
var distance: Double = Double(MAXFLOAT)
var linePoints: [MKMapPoint] = []
var polyPoints = UnsafeMutablePointer<MKMapPoint>.alloc(poly.pointCount)
for point in UnsafeBufferPointer(start: poly.points(), count: poly.pointCount) {
linePoints.append(point)
print("point: \(point.x),\(point.y)")
}
for n in 0...linePoints.count - 2 {
let ptA = linePoints[n]
let ptB = linePoints[n+1]
let xDelta = ptB.x - ptA.x
let yDelta = ptB.y - ptA.y
if (xDelta == 0.0 && yDelta == 0.0) {
// Points must not be equal
continue
}
let u: Double = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta)
var ptClosest = MKMapPoint()
if (u < 0.0) {
ptClosest = ptA
} else if (u > 1.0) {
ptClosest = ptB
} else {
ptClosest = MKMapPointMake(ptA.x + u * xDelta, ptA.y + u * yDelta);
}
distance = min(distance, MKMetersBetweenMapPoints(ptClosest, pt))
}
return distance
}

Updated for Swift 3
func isTappedOnPolygon(with tapGesture:UITapGestureRecognizer, on mapView: MKMapView) -> Bool {
let tappedMapView = tapGesture.view
let tappedPoint = tapGesture.location(in: tappedMapView)
let tappedCoordinates = mapView.convert(tappedPoint, toCoordinateFrom: tappedMapView)
let point:MKMapPoint = MKMapPointForCoordinate(tappedCoordinates)
let overlays = mapView.overlays.filter { o in
o is MKPolygon
}
for overlay in overlays {
let polygonRenderer = MKPolygonRenderer(overlay: overlay)
let datPoint = polygonRenderer.point(for: point)
polygonRenderer.invalidatePath()
return polygonRenderer.path.contains(datPoint)
}
return false
}

#Rashwan L : Updated his answer to Swift 4.2
let map = MKMapView()
let mapTap = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
map.addGestureRecognizer(mapTap)
#objc private func mapTapped(_ tap: UITapGestureRecognizer) {
if tap.state == .recognized && tap.state == .recognized {
// Get map coordinate from touch point
let touchPt: CGPoint = tap.location(in: skyMap)
let coord: CLLocationCoordinate2D = skyMap.convert(touchPt, toCoordinateFrom: skyMap)
let maxMeters: Double = meters(fromPixel: 22, at: touchPt)
var nearestDistance: Float = MAXFLOAT
var nearestPoly: MKPolyline? = nil
// for every overlay ...
for overlay: MKOverlay in skyMap.overlays {
// .. if MKPolyline ...
if (overlay is MKPolyline) {
// ... get the distance ...
let distance: Float = Float(distanceOf(pt: MKMapPoint(coord), toPoly: overlay as! MKPolyline))
// ... and find the nearest one
if distance < nearestDistance {
nearestDistance = distance
nearestPoly = overlay as? MKPolyline
}
}
}
if Double(nearestDistance) <= maxMeters {
print("Touched poly: \(nearestPoly) distance: \(nearestDistance)")
}
}
}
private func distanceOf(pt: MKMapPoint, toPoly poly: MKPolyline) -> Double {
var distance: Double = Double(MAXFLOAT)
for n in 0..<poly.pointCount - 1 {
let ptA = poly.points()[n]
let ptB = poly.points()[n + 1]
let xDelta: Double = ptB.x - ptA.x
let yDelta: Double = ptB.y - ptA.y
if xDelta == 0.0 && yDelta == 0.0 {
// Points must not be equal
continue
}
let u: Double = ((pt.x - ptA.x) * xDelta + (pt.y - ptA.y) * yDelta) / (xDelta * xDelta + yDelta * yDelta)
var ptClosest: MKMapPoint
if u < 0.0 {
ptClosest = ptA
}
else if u > 1.0 {
ptClosest = ptB
}
else {
ptClosest = MKMapPoint(x: ptA.x + u * xDelta, y: ptA.y + u * yDelta)
}
distance = min(distance, ptClosest.distance(to: pt))
}
return distance
}
private func meters(fromPixel px: Int, at pt: CGPoint) -> Double {
let ptB = CGPoint(x: pt.x + CGFloat(px), y: pt.y)
let coordA: CLLocationCoordinate2D = skyMap.convert(pt, toCoordinateFrom: skyMap)
let coordB: CLLocationCoordinate2D = skyMap.convert(ptB, toCoordinateFrom: skyMap)
return MKMapPoint(coordA).distance(to: MKMapPoint(coordB))
}

The real "cookie" in this code is the point -> line distance function. I was so happy to find it and it worked great (swift 4, iOS 11). Thanks to everyone, especially #Jensemann. Here is my refactoring of it:
public extension MKPolyline {
// Return the point on the polyline that is the closest to the given point
// along with the distance between that closest point and the given point.
//
// Thanks to:
// http://paulbourke.net/geometry/pointlineplane/
// https://stackoverflow.com/questions/11713788/how-to-detect-taps-on-mkpolylines-overlays-like-maps-app
public func closestPoint(to: MKMapPoint) -> (point: MKMapPoint, distance: CLLocationDistance) {
var closestPoint = MKMapPoint()
var distanceTo = CLLocationDistance.infinity
let points = self.points()
for i in 0 ..< pointCount - 1 {
let endPointA = points[i]
let endPointB = points[i + 1]
let deltaX: Double = endPointB.x - endPointA.x
let deltaY: Double = endPointB.y - endPointA.y
if deltaX == 0.0 && deltaY == 0.0 { continue } // Points must not be equal
let u: Double = ((to.x - endPointA.x) * deltaX + (to.y - endPointA.y) * deltaY) / (deltaX * deltaX + deltaY * deltaY) // The magic sauce. See the Paul Bourke link above.
let closest: MKMapPoint
if u < 0.0 { closest = endPointA }
else if u > 1.0 { closest = endPointB }
else { closest = MKMapPointMake(endPointA.x + u * deltaX, endPointA.y + u * deltaY) }
let distance = MKMetersBetweenMapPoints(closest, to)
if distance < distanceTo {
closestPoint = closest
distanceTo = distance
}
}
return (closestPoint, distanceTo)
}
}

It's an old thread however I found a different way which may help anyone. Tested on multiple routes overlay in Swift 4.2.
#IBAction func didTapGesture(_ sender: UITapGestureRecognizer) {
let touchPoint = sender.location(in: mapView)
let touchCoordinate = mapView.convert(touchPoint, toCoordinateFrom: mapView)
let mapPoint = MKMapPoint(touchCoordinate)
for overlay in mapView.overlays {
if overlay is MKPolyline {
if let polylineRenderer = mapView.renderer(for: overlay) as? MKPolylineRenderer {
let polylinePoint = polylineRenderer.point(for: mapPoint)
if polylineRenderer.path.contains(polylinePoint) {
print("polyline was tapped")
}
}
}
}
}

Related

Swift iOS MapKit Polylines - creating a grid

I am attempting to draw a grid using polylines in the viewable area of the map. However, although the lines are drawn they seem to flicker and disappear at times. I have been through different approaches but all seem to suffer the same problem.
I have done this through the map view delegate mapViewDidChangeVisibleRegion, if existing poly lines are their then I remove them and draw new ones across the viewable region.
Below is the delegate code I just can't seem to pin point why the lines disappear.
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
DispatchQueue.main.async {
let zoomLevel = mapView.getZoomLevel()
if(mapView.overlays.count > 0)
{
for line in mapView.overlays
{
mapView.removeOverlay(line)
}
}
let northEast = mapView.convert(CGPoint(x: mapView.bounds.width, y: 0), toCoordinateFrom: mapView)
let southWest = mapView.convert(CGPoint(x: 0, y: mapView.bounds.height), toCoordinateFrom: mapView)
let minLon = 10 * Int((floor(southWest.longitude) / 10.0).rounded())
let maxLon = 10 * Int((floor(northEast.longitude) / 10.0).rounded())
let minLat = 10 * Int((floor(southWest.latitude) / 10.0).rounded())
let maxLat = 10 * Int((floor(northEast.latitude) / 10.0).rounded())
if(zoomLevel < 6)
{
for latIndex in stride(from: minLat, through: maxLat, by: 10)
{
var pointsLon : [CLLocationCoordinate2D] = [CLLocationCoordinate2D]()
if(minLon < 0 && maxLon < 0)
{
let startPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(minLon))
pointsLon.append(startPos)
let endPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(maxLon))
pointsLon.append(endPos)
}
if(minLon > 0 && maxLon > 0)
{
let startPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(minLon))
pointsLon.append(startPos)
let endPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(maxLon))
pointsLon.append(endPos)
}
if(minLon < 0 && maxLon > 0 )
{
let startPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(maxLon))
pointsLon.append(startPos)
let midPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(0))
pointsLon.append(midPos)
let endPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(minLon))
pointsLon.append(endPos)
}
if(minLon > 0 && maxLon < 0 )
{
let sPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(maxLon))
pointsLon.append(sPos)
let mPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(-180))
pointsLon.append(mPos)
let polyline = MKPolyline(coordinates: pointsLon, count: pointsLon.count)
self.polylines.append(polyline)
mapView.addOverlay(polyline, level: .aboveRoads )
pointsLon.removeAll()
let startPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(minLon))
pointsLon.append(startPos)
let midPos = CLLocationCoordinate2DMake(CLLocationDegrees(latIndex), CLLocationDegrees(180))
pointsLon.append(midPos)
}
let polyline = MKPolyline(coordinates: pointsLon, count: pointsLon.count)
self.polylines.append(polyline)
mapView.addOverlay(polyline, level: .aboveRoads )
}
}
else if(zoomLevel < 12)
{
//to be implemented
}
else{
//to be implemented
}
}
}
Any ideas as to why is gratefully recieved. Running on iOS 12.1 and iPhone 8 and emulator.
Thanks in advance.

Movement of marker in google map

I am creating an Uber like iOS app(swift).I have integrated google map and added marker.Also i am receiving current latitude and longitude of the vehicle from backend API. Now i want to show the movement of vehicle in my app. I have written some code and it is working. But there is some issue with movement of the vehicle. There is some difference between position of marker in my app and the actual location of vehicle. Accuracy is not good. Here i am sharing my code.I calls the backend API in each 5 seconds.
func moveCab()
{
let oldCoordinate: CLLocationCoordinate2D? = CLLocationCoordinate2DMake(Double(oldLatitude)!,Double(oldLongitude)!)
let newCoordinate: CLLocationCoordinate2D? = CLLocationCoordinate2DMake(Double(currentLatitude)!,Double(currentLongitude)!)
mMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
mMarker.rotation = CLLocationDegrees(getHeadingForDirection(fromCoordinate: oldCoordinate!, toCoordinate: newCoordinate!))
//found bearing value by calculation when marker add
mMarker.position = oldCoordinate!
//this can be old position to make car movement to new position
mMarker.map = mapView
//marker movement animation
CATransaction.begin()
CATransaction.setValue(Int(2.0), forKey: kCATransactionAnimationDuration)
CATransaction.setCompletionBlock({() -> Void in
self.mMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
// mMarker.rotation = CDouble(data.value(forKey: "bearing"))
//New bearing value from backend after car movement is done
})
mMarker.position = newCoordinate!
mMarker.map = mapView
mMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
mMarker.rotation = CLLocationDegrees(getHeadingForDirection(fromCoordinate: oldCoordinate!, toCoordinate: newCoordinate!))
//found bearing value by calculation
CATransaction.commit()
oldLatitude = currentLatitude
oldLongitude = currentLongitude
}
func getHeadingForDirection(fromCoordinate fromLoc: CLLocationCoordinate2D, toCoordinate toLoc: CLLocationCoordinate2D) -> Float {
let fLat: Float = Float((fromLoc.latitude).degreesToRadians)
let fLng: Float = Float((fromLoc.longitude).degreesToRadians)
let tLat: Float = Float((toLoc.latitude).degreesToRadians)
let tLng: Float = Float((toLoc.longitude).degreesToRadians)
let degree: Float = (atan2(sin(tLng - fLng) * cos(tLat), cos(fLat) * sin(tLat) - sin(fLat) * cos(tLat) * cos(tLng - fLng))).radiansToDegrees
if degree >= 0 {
return degree
}
else {
return 360 + degree
}
extension Int {
var degreesToRadians: Double { return Double(self) * .pi / 180 }
}
extension FloatingPoint {
var degreesToRadians: Self { return self * .pi / 180 }
var radiansToDegrees: Self { return self * 180 / .pi }
}

How to draw an arc on Google Maps in iOS?

How to draw an arc between two coordinate points in Google Maps like in this image and same like facebook post in iOS ?
Before using the below function, don't forget to import GoogleMaps
Credits: xomena
func drawArcPolyline(startLocation: CLLocationCoordinate2D?, endLocation: CLLocationCoordinate2D?) {
if let startLocation = startLocation, let endLocation = endLocation {
//swap the startLocation & endLocation if you want to reverse the direction of polyline arc formed.
let mapView = GMSMapView()
let path = GMSMutablePath()
path.add(startLocation)
path.add(endLocation)
// Curve Line
let k: Double = 0.2 //try between 0.5 to 0.2 for better results that suits you
let d = GMSGeometryDistance(startLocation, endLocation)
let h = GMSGeometryHeading(startLocation, endLocation)
//Midpoint position
let p = GMSGeometryOffset(startLocation, d * 0.5, h)
//Apply some mathematics to calculate position of the circle center
let x = (1 - k * k) * d * 0.5 / (2 * k)
let r = (1 + k * k) * d * 0.5 / (2 * k)
let c = GMSGeometryOffset(p, x, h + 90.0)
//Polyline options
//Calculate heading between circle center and two points
let h1 = GMSGeometryHeading(c, startLocation)
let h2 = GMSGeometryHeading(c, endLocation)
//Calculate positions of points on circle border and add them to polyline options
let numpoints = 100.0
let step = ((h2 - h1) / Double(numpoints))
for i in stride(from: 0.0, to: numpoints, by: 1) {
let pi = GMSGeometryOffset(c, r, h1 + i * step)
path.add(pi)
}
//Draw polyline
let polyline = GMSPolyline(path: path)
polyline.map = mapView // Assign GMSMapView as map
polyline.strokeWidth = 3.0
let styles = [GMSStrokeStyle.solidColor(UIColor.black), GMSStrokeStyle.solidColor(UIColor.clear)]
let lengths = [20, 20] // Play with this for dotted line
polyline.spans = GMSStyleSpans(polyline.path!, styles, lengths as [NSNumber], .rhumb)
let bounds = GMSCoordinateBounds(coordinate: startLocation, coordinate: endLocation)
let insets = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20)
let camera = mapView.camera(for: bounds, insets: insets)!
mapView.animate(to: camera)
}
}
I used Bezier quadratic equation to draw curved lines. You can have a look on to the implementation. Here is the sample code.
func bezierPath(from startLocation: CLLocationCoordinate2D, to endLocation: CLLocationCoordinate2D) -> GMSMutablePath {
let distance = GMSGeometryDistance(startLocation, endLocation)
let midPoint = GMSGeometryInterpolate(startLocation, endLocation, 0.5)
let midToStartLocHeading = GMSGeometryHeading(midPoint, startLocation)
let controlPointAngle = 360.0 - (90.0 - midToStartLocHeading)
let controlPoint = GMSGeometryOffset(midPoint, distance / 2.0 , controlPointAngle)
let path = GMSMutablePath()
let stepper = 0.05
let range = stride(from: 0.0, through: 1.0, by: stepper)// t = [0,1]
func calculatePoint(when t: Double) -> CLLocationCoordinate2D {
let t1 = (1.0 - t)
let latitude = t1 * t1 * startLocation.latitude + 2 * t1 * t * controlPoint.latitude + t * t * endLocation.latitude
let longitude = t1 * t1 * startLocation.longitude + 2 * t1 * t * controlPoint.longitude + t * t * endLocation.longitude
let point = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return point
}
range.map { calculatePoint(when: $0) }.forEach { path.add($0) }
return path
}
None of the answers mentioned is a full proof solution. For a few locations, it draws a circle instead of a polyline.
To resolve this we will calculate bearing(degrees clockwise from true north) and if it is less than zero, swap the start and end location.
func createArc(
startLocation: CLLocationCoordinate2D,
endLocation: CLLocationCoordinate2D) -> GMSPolyline {
var start = startLocation
var end = endLocation
if start.bearing(to: end) < 0.0 {
start = endLocation
end = startLocation
}
let angle = start.bearing(to: end) * Double.pi / 180.0
let k = abs(0.3 * sin(angle))
let path = GMSMutablePath()
let d = GMSGeometryDistance(start, end)
let h = GMSGeometryHeading(start, end)
let p = GMSGeometryOffset(start, d * 0.5, h)
let x = (1 - k * k) * d * 0.5 / (2 * k)
let r = (1 + k * k) * d * 0.5 / (2 * k)
let c = GMSGeometryOffset(p, x, h + 90.0)
var h1 = GMSGeometryHeading(c, start)
var h2 = GMSGeometryHeading(c, end)
if (h1 > 180) {
h1 = h1 - 360
}
if (h2 > 180) {
h2 = h2 - 360
}
let numpoints = 100.0
let step = ((h2 - h1) / Double(numpoints))
for i in stride(from: 0.0, to: numpoints, by: 1) {
let pi = GMSGeometryOffset(c, r, h1 + i * step)
path.add(pi)
}
path.add(end)
let polyline = GMSPolyline(path: path)
polyline.strokeWidth = 3.0
polyline.spans = GMSStyleSpans(
polyline.path!,
[GMSStrokeStyle.solidColor(UIColor(hex: "#2962ff"))],
[20, 20], .rhumb
)
return polyline
}
The bearing is the direction in which a vertical line on the map points, measured in degrees clockwise from north.
func bearing(to point: CLLocationCoordinate2D) -> Double {
func degreesToRadians(_ degrees: Double) -> Double { return degrees * Double.pi / 180.0 }
func radiansToDegrees(_ radians: Double) -> Double { return radians * 180.0 / Double.pi }
let lat1 = degreesToRadians(latitude)
let lon1 = degreesToRadians(longitude)
let lat2 = degreesToRadians(point.latitude);
let lon2 = degreesToRadians(point.longitude);
let dLon = lon2 - lon1;
let y = sin(dLon) * cos(lat2);
let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon);
let radiansBearing = atan2(y, x);
return radiansToDegrees(radiansBearing)
}
The answer above does not handle all the corner cases, here is one that draws the arcs nicely:
func drawArcPolyline(startLocation: CLLocationCoordinate2D?, endLocation: CLLocationCoordinate2D?) {
if let _ = startLocation, let _ = endLocation {
//swap the startLocation & endLocation if you want to reverse the direction of polyline arc formed.
var start = startLocation!
var end = endLocation!
var gradientColors = GMSStrokeStyle.gradient(
from: UIColor(red: 11.0/255, green: 211.0/255, blue: 200.0/255, alpha: 1),
to: UIColor(red: 0/255, green: 44.0/255, blue: 66.0/255, alpha: 1))
if startLocation!.heading(to: endLocation!) < 0.0 {
// need to reverse the start and end, and reverse the color
start = endLocation!
end = startLocation!
gradientColors = GMSStrokeStyle.gradient(
from: UIColor(red: 0/255, green: 44.0/255, blue: 66.0/255, alpha: 1),
to: UIColor(red: 11.0/255, green: 211.0/255, blue: 200.0/255, alpha: 1))
}
let path = GMSMutablePath()
// Curve Line
let k = abs(0.3 * sin((start.heading(to: end)).degreesToRadians)) // was 0.3
let d = GMSGeometryDistance(start, end)
let h = GMSGeometryHeading(start, end)
//Midpoint position
let p = GMSGeometryOffset(start, d * 0.5, h)
//Apply some mathematics to calculate position of the circle center
let x = (1-k*k)*d*0.5/(2*k);
let r = (1+k*k)*d*0.5/(2*k);
let c = GMSGeometryOffset(p, x, h + 90.0)
//Polyline options
//Calculate heading between circle center and two points
var h1 = GMSGeometryHeading(c, start)
var h2 = GMSGeometryHeading(c, end)
if(h1>180){
h1 = h1 - 360
}
if(h2>180){
h2 = h2 - 360
}
//Calculate positions of points on circle border and add them to polyline options
let numpoints = 100.0
let step = (h2 - h1) / numpoints
for i in stride(from: 0.0, to: numpoints, by: 1) {
let pi = GMSGeometryOffset(c, r, h1 + i * step)
path.add(pi)
}
path.add(end)
//Draw polyline
let polyline = GMSPolyline(path: path)
polyline.map = mapView // Assign GMSMapView as map
polyline.strokeWidth = 5.0
polyline.spans = [GMSStyleSpan(style: gradientColors)]
}
}
Objective-C version #Rouny answer
- (void)DrawCurvedPolylineOnMapFrom:(CLLocationCoordinate2D)startLocation To:(CLLocationCoordinate2D)endLocation
{
GMSMutablePath * path = [[GMSMutablePath alloc]init];
[path addCoordinate:startLocation];
[path addCoordinate:endLocation];
// Curve Line
double k = 0.2; //try between 0.5 to 0.2 for better results that suits you
CLLocationDistance d = GMSGeometryDistance(startLocation, endLocation);
float h = GMSGeometryHeading(startLocation , endLocation);
//Midpoint position
CLLocationCoordinate2D p = GMSGeometryOffset(startLocation, d * 0.5, h);
//Apply some mathematics to calculate position of the circle center
float x = (1-k*k)*d*0.5/(2*k);
float r = (1+k*k)*d*0.5/(2*k);
CLLocationCoordinate2D c = GMSGeometryOffset(p, x, h + -90.0);
//Polyline options
//Calculate heading between circle center and two points
float h1 = GMSGeometryHeading(c, startLocation);
float h2 = GMSGeometryHeading(c, endLocation);
//Calculate positions of points on circle border and add them to polyline options
float numpoints = 100;
float step = ((h2 - h1) / numpoints);
for (int i = 0; i < numpoints; i++) {
CLLocationCoordinate2D pi = GMSGeometryOffset(c, r, h1 + i * step);
[path addCoordinate:pi];
}
//Draw polyline
GMSPolyline * polyline = [GMSPolyline polylineWithPath:path];
polyline.map = mapView;
polyline.strokeWidth = 3.0;
NSArray *styles = #[[GMSStrokeStyle solidColor:kBaseColor],
[GMSStrokeStyle solidColor:[UIColor clearColor]]];
NSArray *lengths = #[#5, #5];
polyline.spans = GMSStyleSpans(polyline.path, styles, lengths, kGMSLengthRhumb);
GMSCoordinateBounds * bounds = [[GMSCoordinateBounds alloc]initWithCoordinate:startLocation coordinate:endLocation];
UIEdgeInsets insets = UIEdgeInsetsMake(20, 20, 20, 20);
GMSCameraPosition * camera = [mapView cameraForBounds:bounds insets:insets ];
[mapView animateToCameraPosition:camera];
}
Swift 5+
Very easy and Smooth way
//MARK: - Usage
let path = self.bezierPath(from: CLLocationCoordinate2D(latitude: kLatitude, longitude: kLongtitude), to: CLLocationCoordinate2D(latitude: self.restaurantLat, longitude: self.restaurantLong))
let polyline = GMSPolyline(path: path)
polyline.strokeWidth = 5.0
polyline.strokeColor = appClr
polyline.map = self.googleMapView // Google MapView
Simple Function
func drawArcPolyline(from startLocation: CLLocationCoordinate2D, to endLocation: CLLocationCoordinate2D) -> GMSMutablePath {
let distance = GMSGeometryDistance(startLocation, endLocation)
let midPoint = GMSGeometryInterpolate(startLocation, endLocation, 0.5)
let midToStartLocHeading = GMSGeometryHeading(midPoint, startLocation)
let controlPointAngle = 360.0 - (90.0 - midToStartLocHeading)
let controlPoint = GMSGeometryOffset(midPoint, distance / 2.0 , controlPointAngle)
let path = GMSMutablePath()
let stepper = 0.05
let range = stride(from: 0.0, through: 1.0, by: stepper)// t = [0,1]
func calculatePoint(when t: Double) -> CLLocationCoordinate2D {
let t1 = (1.0 - t)
let latitude = t1 * t1 * startLocation.latitude + 2 * t1 * t * controlPoint.latitude + t * t * endLocation.latitude
let longitude = t1 * t1 * startLocation.longitude + 2 * t1 * t * controlPoint.longitude + t * t * endLocation.longitude
let point = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
return point
}
range.map { calculatePoint(when: $0) }.forEach { path.add($0) }
return path
}

Calculate centre coordinate point for CLLocation array

How to calculate the centre point for MapView if we have multiple CLLocation points?
func zoomToFitPolyLine(pointsArr : NSArray) {
if (pointsArr.count == 0) {
return
}
var topLeftCoord : CLLocationCoordinate2D? = CLLocationCoordinate2DMake(-90, 180 )
var bottomRightCoord : CLLocationCoordinate2D? = CLLocationCoordinate2DMake(90, -180 )
for (index,_) in (pointsArr.enumerated()) {
let tempLoc : CLLocation? = pointsArr.object(at: index) as? CLLocation
topLeftCoord?.latitude = fmax((topLeftCoord?.latitude)!, (tempLoc?.coordinate.latitude)!)
topLeftCoord?.longitude = fmin((topLeftCoord?.longitude)!, (tempLoc?.coordinate.longitude)!)
bottomRightCoord?.latitude = fmin((bottomRightCoord?.latitude)!, (tempLoc?.coordinate.latitude)!)
bottomRightCoord?.longitude = fmax((bottomRightCoord?.longitude)!, (tempLoc?.coordinate.longitude)!)
}
var centerCoordinate : CLLocationCoordinate2D? = CLLocationCoordinate2DMake(0, 0)
centerCoordinate?.latitude = (topLeftCoord?.latitude)! - ((topLeftCoord?.latitude)! - (bottomRightCoord?.latitude)!) * 0.5;
centerCoordinate?.longitude = (topLeftCoord?.longitude)! + ((bottomRightCoord?.longitude)! - (topLeftCoord?.longitude)!) * 0.5;
self.zoomToRegion(location: centerCoordinate!)
}
func zoomToRegion(location:CLLocationCoordinate2D)
{
let camera : MKMapCamera = MKMapCamera.init(lookingAtCenter: location, fromEyeCoordinate: location, eyeAltitude: CLLocationDegrees(10));
myMap?.setCamera(camera, animated: true);
}

draw line on MKMapView with pattern image

I try to draw a line on a MKMapView with a pattern image.
The drawing is done by adding a custom MKMapOverlay view.
I'm able to get the line drawn, but it seems that the drawing is done using only the left, topmost pixel of the pattern image, instead of the whole image.
Here is my drawing routine:
void drawPatternCellCallback(void *info, CGContextRef cgContext)
{
UIImage *patternImage = [UIImage imageNamed:#"tmpLine"];
CGContextDrawImage(cgContext, CGRectMake(0, 0, patternImage.size.width, patternImage.size.height), patternImage.CGImage);
}
- (void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale
inContext:(CGContextRef)context
{
float alpha = 1;
float tileW = 6.0f;
float tileH = 4.0f;
CGFloat lineWidth = MKRoadWidthAtZoomScale(zoomScale)*2;
CGMutablePathRef path = CGPathCreateMutable();
if (path != nil)
{
//setup styles
CGContextSetRGBStrokeColor(context, 0.0f, 0.0f, 1.0f, 0.5f);
const CGPatternCallbacks kPatternCallbacks = {0, drawPatternCellCallback, NULL};
CGPatternRef strokePattern = CGPatternCreate(
NULL,
CGRectMake(0, 0, tileW, tileH),
CGAffineTransformIdentity,
tileW, // horizontal spacing
tileH,// vertical spacing
kCGPatternTilingConstantSpacing,
true,
&kPatternCallbacks);
//color sapce
CGColorSpaceRef patternSpace = CGColorSpaceCreatePattern(NULL);
CGContextSetStrokeColorSpace(context, patternSpace);
//pattern
CGContextSetStrokePattern(context, strokePattern, &alpha);
//joins/ends
CGContextSetLineJoin(context, kCGLineJoinMiter);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextSetLineWidth(context, lineWidth);
//OK, let's draw it
CGPoint firstCGPoint = [self pointForMapPoint:self.point1];
CGPoint lastCGPoint = [self pointForMapPoint:self.point2];
CGPathMoveToPoint(path, NULL, lastCGPoint.x, lastCGPoint.y);
CGPathAddLineToPoint(path, NULL, firstCGPoint.x, firstCGPoint.y);
CGContextAddPath(context, path);
CGContextStrokePath(context);
//house hold
CGPathRelease(path);
CGPatternRelease(strokePattern);
CGColorSpaceRelease(patternSpace);
}
}
Any idea what's wrong?
Thanx!
I ended up with a completely different strategy.
Instead of adding by own overlay I now rely on MKPolyLine.
Using the following code I'm able to add a pseudo animated line from point A to point B on a MKMapView.
The code adds several overlays to the MKMapViews with a slight delay, giving the impression of an animation
Not the most beautiful solution! - but it looks pretty good in action :-)
/*start the animation*/
-(void)plotRouteOnMap
{
[self.mapView removeOverlays:self.mapView.overlays];
//calculate a number locations between the two locations
self.points = [self getPointsOnRouteFrom:<FROM_LOCATION>
to:<TO_LOCATION>
onMapView:self.mapView];
[self addOverlaysFromPointsWithStartFrom:[NSNumber numberWithInt:1]];
}
/*convert a CGPoint to a CLLocation according to a mapView*/
- (CLLocation*)pointToLocation:(MKMapView *)mapView fromPoint:(CGPoint)fromPoint
{
CLLocationCoordinate2D coord = [mapView convertPoint:fromPoint toCoordinateFromView:mapView];
return [[[CLLocation alloc] initWithLatitude:coord.latitude longitude:coord.longitude] autorelease];
}
/*get a list of Location objects between from and to*/
-(NSArray*)getPointsOnRouteFrom:(CLLocation*)from to:(CLLocation*)to onMapView:(MKMapView*)mapView
{
int NUMBER_OF_PIXELS_TO_SKIP =10; //lower number will give a more smooth animation, but will result in more layers
NSMutableArray *ret = [NSMutableArray array];
CGPoint fromPoint = [mapView convertCoordinate:from.coordinate toPointToView:mapView];
CGPoint toPoint = [mapView convertCoordinate:to.coordinate toPointToView:mapView];
NSArray *allPixels = [self getAllPointsFromPoint:fromPoint toPoint:toPoint];
for (int i = 0 ; i < [allPixels count] ; i+=NUMBER_OF_PIXELS_TO_SKIP) {
NSValue *pointVal = [allPixels objectAtIndex:i];
[ret addObject:[self pointToLocation:mapView fromPoint:[pointVal CGPointValue]]];
}
[ret addObject:[self pointToLocation:mapView fromPoint:toPoint]];
return ret;
}
/*calulate alle pixels from point to toPint*/
-(NSArray*)getAllPointsFromPoint:(CGPoint)fPoint toPoint:(CGPoint)tPoint
{
/*Simplyfied implementation of Bresenham's line algoritme */
NSMutableArray *ret = [NSMutableArray array];
float deltaX = fabsf(tPoint.x - fPoint.x);
float deltaY = fabsf(tPoint.y - fPoint.y);
float x = fPoint.x;
float y = fPoint.y;
float err = deltaX-deltaY;
float sx = -0.5;
float sy = -0.5;
if(fPoint.x<tPoint.x)
sx = 0.5;
if(fPoint.y<tPoint.y)
sy = 0.5;
do {
[ret addObject:[NSValue valueWithCGPoint:CGPointMake(x, y)]];
float e = 2*err;
if(e > -deltaY)
{
err -=deltaY;
x +=sx;
}
if(e < deltaX)
{
err +=deltaX;
y+=sy;
}
} while (round(x) != round(tPoint.x) && round(y) != round(tPoint.y));
[ret addObject:[NSValue valueWithCGPoint:tPoint]];//add final point
return ret;
}
/*add a poly line overlay to mapview which start at position 0 and end in 'end' in the array points*/
-(void)addOverlaysFromPointsWithStartFrom:(NSNumber*)end
{
int intEnd = [end intValue];
//construct polyline view from start
CLLocationCoordinate2D *locations = malloc(sizeof(CLLocationCoordinate2D)*2);
CLLocation *loc1 = (CLLocation*)[points objectAtIndex:0];
CLLocation *loc2= (CLLocation*)[points objectAtIndex:intEnd];
locations[0] = loc1.coordinate;
locations[1] = loc2.coordinate;
MKPolyline *line = [MKPolyline polylineWithCoordinates:locations count:2];
[self.mapView addOverlay:line];
if((intEnd+1) < [points count])//add more overlays after delays unless this is the endpoint
{
[self performSelector:#selector(addOverlaysFromPointsWithStartFrom:) withObject:[NSNumber numberWithInt:intEnd + 1] afterDelay:0.01];
}
}
Swift version of accepted answer
Add image as overlay using MKOverlayRenderer
func addLayersOfAnimatingOverlay() {
let sourcePoint = // enter as CLLocation
let destinationPoint = // enter as CLLocation
let pointsCoordinatesArray = self.getLocationArrayFrom(startLocation: sourcePoint, endLocation: destinationPoint)
//add overlay on above coordinates
DispatchQueue.main.async{
self.addDirectionOverlayInMap(locationArray: self.pointsCoordinates1, title: "1")
}
To get coordinates in a MKPolyline
func getLocationArrayFrom(startLocation: CLLocation, endLocation: CLLocation) -> [CLLocationCoordinate2D] {
var coordinatesArray: [CLLocationCoordinate2D] = []
if let points = helperClass.getPointsOnRoute(from: startLocation, to: endLocation, on: mapView) {
for point in points {
let coordinate = point.coordinate
coordinatesArray.append(coordinate)
}
}
return coordinatesArray
}
//MARK: get cordinates from line
func getPointsOnRoute(from: CLLocation?, to: CLLocation?, on mapView: MKMapView?) -> [CLLocation]? {
let NUMBER_OF_PIXELS_TO_SKIP: Int = 120
//lower number will give a more smooth animation, but will result in more layers
var ret = [Any]()
var fromPoint: CGPoint? = nil
if let aCoordinate = from?.coordinate {
fromPoint = mapView?.convert(aCoordinate, toPointTo: mapView)
}
var toPoint: CGPoint? = nil
if let aCoordinate = to?.coordinate {
toPoint = mapView?.convert(aCoordinate, toPointTo: mapView)
}
let allPixels = getAllPoints(from: fromPoint!, to: toPoint!)
var i = 0
while i < (allPixels?.count)! {
let pointVal = allPixels![i] as? NSValue
ret.append(point(toLocation: mapView, from: (pointVal?.cgPointValue)!)!)
i += NUMBER_OF_PIXELS_TO_SKIP
}
ret.append(point(toLocation: mapView, from: toPoint!)!)
return ret as? [CLLocation]
}
/**convert a CGPoint to a CLLocation according to a mapView*/
func point(toLocation mapView: MKMapView?, from fromPoint: CGPoint) -> CLLocation? {
let coord: CLLocationCoordinate2D? = mapView?.convert(fromPoint, toCoordinateFrom: mapView)
return CLLocation(latitude: coord?.latitude ?? 0, longitude: coord?.longitude ?? 0)
}
func getAllPoints(from fPoint: CGPoint, to tPoint: CGPoint) -> [Any]? {
/*Simplyfied implementation of Bresenham's line algoritme */
var ret = [AnyHashable]()
let deltaX: Float = fabsf(Float(tPoint.x - fPoint.x))
let deltaY: Float = fabsf(Float(tPoint.y - fPoint.y))
var x: Float = Float(fPoint.x)
var y: Float = Float(fPoint.y)
var err: Float = deltaX - deltaY
var sx: Float = -0.5
var sy: Float = -0.5
if fPoint.x < tPoint.x {
sx = 0.5
}
if fPoint.y < tPoint.y {
sy = 0.5
}
repeat {
ret.append(NSValue(cgPoint: CGPoint(x: CGFloat(x), y: CGFloat(y))))
let e: Float = 2 * err
if e > -deltaY {
err -= deltaY
x += sx
}
if e < deltaX {
err += deltaX
y += sy
}
} while round(Float(x)) != round(Float(tPoint.x)) && round(Float(y)) != round(Float(tPoint.y))
ret.append(NSValue(cgPoint: tPoint))
//add final point
return ret
}
This will give the following effect (without animation)
The project can be found here

Resources