I'm attempting to have a UIView follow a Polyline using the angle of the Polyline. The problem I'm facing is the rotation is never right unless the angle is lower than 50 degrees, which I'm thinking is just a coincidence. How can I get the rotation to follow the line?
Here's the code I'm using to detect the rotation.
func heading(from: MSGeopoint, to: MSGeopoint) -> Double {
let a = CLLocationCoordinate2D(latitude: from.position.latitude, longitude: from.position.longitude)
let b = CLLocationCoordinate2D(latitude: to.position.latitude, longitude: to.position.longitude)
let lat1 = a.latitude.degreesToRadians
let lon1 = a.longitude.degreesToRadians
let lat2 = b.latitude.degreesToRadians
let lon2 = b.longitude.degreesToRadians
let dLon = lon2 - lon1
let y = sin(dLon) * cos(lat2)
let x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(dLon)
let headingDegrees = atan2(y, x).radiansToDegrees
if headingDegrees >= 0 {
print("\(headingDegrees)")
return headingDegrees
} else {
print("\(headingDegrees + 360)")
return headingDegrees + 360
}
}
The MSGeoPoint is primarily a wrapper for CoreLocation using Bing maps but here's how you instantiate the from and to.
let fromGeoPoint = MSGeopoint(latitude: from.latitude, longitude: from.longitude)
let toGeoPoint = MSGeopoint(latitude: to.latitude, longitude: to.longitude)
And the degreesToRadians.
extension BinaryInteger {
var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
Here's also how I'm creating the view.
func createDistanceView() -> UIView {
let distanceView: DistanceView = UIView.fromNib()
distanceView.layer.borderColor = UIColor(named: "Blue")?.cgColor ?? UIColor.blue.cgColor
distanceView.layer.borderWidth = 3
distanceView.layer.masksToBounds = true
let heading = heading(from: from, to: to).rounded(.toNearestOrAwayFromZero)
distanceView.distanceLabel.text = "Your almost there"
return distanceView
}
I am integrating Mapbox iOS SDK into my app.
Right now I am stuck at a point where I want to achieve car tracking feature like Uber app.
I used to have that feature with Google Maps SDK but I cant make it work with Mapbox SDK.
I am adding MGLPointAnnotation object to add it on map and want to move it from point A to point B with animation.
I am doing it using
UIView.animate(withDuration: TimeInterval(duration), animations: {
// Update annotation coordinate to be the destination coordinate
point.coordinate = newCoordinate
}, completion: nil)
But for MGLPointAnnotation I can't change its image because when there's a turn I want to rotate the image(Annotation).
If I use MGLAnnotationView object I can change the image but I cant change its coordinate because its readonly.
What should I do here to achieve that functionality?
Several years ago I wrote smth similar, I used GoggleMaps, look through the code, maybe it will be helpful, I really don't remember what are all that numbers in angle counting
extension Double {
var degrees: Double {
return self * 180.0 / Double.pi
}
}
extension CLLocationCoordinate2D {
func angleToPosition(position : CLLocationCoordinate2D) -> CLLocationDegrees {
let bearingRadians = atan2(Double(position.latitude - latitude), Double(position.longitude - longitude))
var bearingDegrees = bearingRadians.degrees
// print("\(bearingDegrees)")
var roundDegrees = 360.0
if bearingDegrees < 0 {
if bearingDegrees > -90 {
roundDegrees = 350
}
if bearingDegrees < -90 && bearingDegrees > -180 {
roundDegrees = 370
}
bearingDegrees += roundDegrees
return 360 - bearingDegrees
}
roundDegrees = bearingDegrees < 90 ? 350 : 360
if bearingDegrees > 90 && bearingDegrees < 180 {
roundDegrees = 370
}
UserDefaults.standard.set(bearingDegrees, forKey: "bearingDegrees")
return roundDegrees - bearingDegrees
}
func duration(toDestination destination: CLLocationCoordinate2D, withSpeed speed : Double) -> Double {
let distance = GMSGeometryDistance(self, destination)
return distance / (speed * (1000 / 3600))
}
}
And this is the func which do the rotation, you can use it as soon as you receive new coordinates, or call it in for loop if you have certain polyline
func animateCarDrive(info: [String: Any]) {
let speed = info["speed"] as? Double ?? 40 // if your car's speed is changeable
let position = info["position"] as? CLLocationCoordinate2D // new point position
let duration = marker.position.duration(toDestination: position, withSpeed: speed)
marker.rotation = marker.position.angleToPosition(position: position)
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
marker.position = position
CATransaction.commit()
}
I am trying to develop an application based on Uber and Ola like concept. So for this i need to integrate the Google Map for the pick up and drop location ion iOS. So please tell me how to achieve the Moving annotation(car)animation in iOS using the Google map.
using Swift 3.1
var oldCoodinate: CLLocationCoordinate2D? = CLLocationCoordinate2DMake(CDouble((data.value(forKey: "lat") as? CLLocationCoordinate2D)), CDouble((data.value(forKey: "lng") as? CLLocationCoordinate2D)))
var newCoodinate: CLLocationCoordinate2D? = CLLocationCoordinate2DMake(CDouble((data.value(forKey: "lat") as? CLLocationCoordinate2D)), CDouble((data.value(forKey: "lng") as? CLLocationCoordinate2D)))
driverMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
driverMarker.rotation = getHeadingForDirection(fromCoordinate: oldCoodinate, toCoordinate: newCoodinate)
//found bearing value by calculation when marker add
driverMarker.position = oldCoodinate
//this can be old position to make car movement to new position
driverMarker.map = mapView_
//marker movement animation
CATransaction.begin()
CATransaction.setValue(Int(2.0), forKey: kCATransactionAnimationDuration)
CATransaction.setCompletionBlock({() -> Void in
driverMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
driverMarker.rotation = CDouble(data.value(forKey: "bearing"))
//New bearing value from backend after car movement is done
})
driverMarker.position = newCoodinate
//this can be new position after car moved from old position to new position with animation
driverMarker.map = mapView_
driverMarker.groundAnchor = CGPoint(x: CGFloat(0.5), y: CGFloat(0.5))
driverMarker.rotation = getHeadingForDirection(fromCoordinate: oldCoodinate, toCoordinate: newCoodinate)
//found bearing value by calculation
CATransaction.commit()
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 }
}
method for get bearing value from old and new coordinates
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
}
}
for Objective C code: Move GMSMarker on Google Map Like UBER
For github: ARCarMovement
I am trying to get the coordinates of a point, that is on a set distance from a starting position, but the end result is wrong.
First, I calculate the angle between the starting position and the desired destination:
private func calculateAngleBetweenLocations(currentLocation: CLLocationCoordinate2D, targetLocation: CLLocationCoordinate2D) -> Double {
let fLat = self.degreesToRadians(currentLocation.latitude);
let fLng = self.degreesToRadians(currentLocation.longitude);
let tLat = self.degreesToRadians(targetLocation.latitude);
let tLng = self.degreesToRadians(targetLocation.longitude);
let deltaLng = tLng - fLng
let y = sin(deltaLng) * cos(tLat)
let x = cos(fLat) * sin(tLat) - sin(fLat) * cos(tLat) * cos(deltaLng)
let bearing = atan2(y, x)
return self.radiansToDegrees(bearing)
}
Then, I calculate the new point, given a distance:
private func coordinatesForMovement(endLocation: CLLocationCoordinate2D, distance: Double) -> CLLocationCoordinate2D {
let angle = self.calculateAngleBetweenLocations(self.currentLocation, targetLocation: endLocation)
let x = self.currentLocation.latitude + distance * cos(angle)
let y = self.currentLocation.longitude + distance * sin(angle)
return CLLocationCoordinate2D(latitude: x, longitude: y)
}
And this is the result (the feet are the starting position, the blue marker is the destination and the red marker is where the new calculated point is). I've tried passing the distance in meters and kilometers and every other floating point position, but never got the correct result. Any ideas?
Ok, after some more digging, I found this answer, which resolves my problem. Here is my complete solution in swift:
internal func moveToLocation(location: CLLocationCoordinate2D, distance: Double) {
let angle = self.calculateAngleBetweenLocations(self.currentLocation, targetLocation: location)
let newLocation = self.coordinates(self.currentLocation, atDistance: distance, atAngle: angle)
self.moveUser(newLocation: newLocation)
}
private func coordinates(startingCoordinates: CLLocationCoordinate2D, atDistance: Double, atAngle: Double) -> CLLocationCoordinate2D {
let distanceRadians = atDistance / 6371
let bearingRadians = self.degreesToRadians(atAngle)
let fromLatRadians = self.degreesToRadians(startingCoordinates.latitude)
let fromLonRadians = self.degreesToRadians(startingCoordinates.longitude)
let toLatRadians = asin(sin(fromLatRadians) * cos(distanceRadians) + cos(fromLatRadians) * sin(distanceRadians) * cos(bearingRadians))
var toLonRadians = fromLonRadians + atan2(sin(bearingRadians) * sin(distanceRadians) * cos(fromLatRadians), cos(distanceRadians) - sin(fromLatRadians) * sin(toLatRadians));
toLonRadians = fmod((toLonRadians + 3 * M_PI), (2 * M_PI)) - M_PI
let lat = self.radiansToDegrees(toLatRadians)
let lon = self.radiansToDegrees(toLonRadians)
return CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
private func calculateAngleBetweenLocations(currentLocation: CLLocationCoordinate2D, targetLocation: CLLocationCoordinate2D) -> Double {
let fLat = self.degreesToRadians(currentLocation.latitude);
let fLng = self.degreesToRadians(currentLocation.longitude);
let tLat = self.degreesToRadians(targetLocation.latitude);
let tLng = self.degreesToRadians(targetLocation.longitude);
let deltaLng = tLng - fLng
let y = sin(deltaLng) * cos(tLat)
let x = cos(fLat) * sin(tLat) - sin(fLat) * cos(tLat) * cos(deltaLng)
let bearing = atan2(y, x)
return self.radiansToDegrees(bearing)
}
private func degreesToRadians(x: Double) -> Double {
return M_PI * x / 180.0
}
private func radiansToDegrees(x: Double) -> Double {
return x * 180.0 / M_PI
}
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")
}
}
}
}
}