Calculate centre coordinate point for CLLocation array - ios

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);
}

Related

GMSOverlay zIndex issue on tap: Swift iOS Google Maps SDK

I have been trying to implement GMSOverlay on map with overlapping overlays. Setting zIndex did work on displaying overlays on top of each other. But when tapping on top overlay, it is not working as it should. The one behind is overtaking tap.
Can someone help me debug and fix this issue:
Here is the code snippet of overlay implementation:
Overlay Custom class
class OrbisMapOverlayMarker: GMSGroundOverlay {
var mapPlaceBaseColor: UIColor!
var mapPlaceId: String!
var placeCoordinate: CLLocationCoordinate2D!
var lastCheckinTimeStamp: Date!
var radius: CLLocationDistance!
var fixedRadius: CLLocationDistance!
var imageView: UIImageView!
private var pulseMaxSize: CLLocationDistance = 100
var increment: CLLocationDistance = 10
private let maxSizeReference: CLLocationDistance = 100
private let referenceRadius: CLLocationDistance = 500
var fixedOpacity: Float = 1
var isProgressing = true
var imageUrlName: String = ""
init(bounds: GMSCoordinateBounds, icon: UIImage?, radius: CLLocationDistance) {
super.init()
self.imageView = UIImageView()
self.bounds = bounds
self.icon = icon
self.radius = radius
self.fixedRadius = radius
pulseMaxSize = (maxSizeReference / referenceRadius) * radius
increment = (maxSizeReference * 0.1 / referenceRadius) * radius
}
}
Implementation:
.
.
.
let placeSize = orbisPlace.calculatedSize
let clDistance = CLLocationDistance(placeSize)
let coordinate = CLLocationCoordinate2D(latitude: orbisPlace.coordinates?.latitude ?? 0, longitude: orbisPlace.coordinates?.longitude ?? 0)
let topLeftCoordinate = CLHelper.coordinate(from: coordinate, distance: -clDistance)
let bottomRightCoordinate = CLHelper.coordinate(from: coordinate, distance: clDistance)
let circleZIndex = Int32(1600 - placeSize)
var color = UIColor(named: AppColors.appBlue.rawValue)
if let colorHex = orbisPlace.placeDominantGroup?.strokeColorHexString {
color = UIColor.hexStringToUIColor(hex: colorHex)
}
let customMarker = OrbisMapOverlayMarker(bounds: GMSCoordinateBounds(coordinate: topLeftCoordinate, coordinate: bottomRightCoordinate), icon: nil, radius: clDistance)
let mapZoomRadius = mapView.getRadius()
customMarker.mapPlaceId = orbisPlace.placeKey
customMarker.position = CLLocationCoordinate2D(latitude: orbisPlace.coordinates?.latitude ?? 0, longitude: orbisPlace.coordinates?.longitude ?? 0)
let placeOpacity = Float(getPlaceCircleOpacity(forCircle: customMarker, withSize: placeSize, mapRadius: mapZoomRadius))
if let activeMarker = self.activeGlowOverlay {
if activeMarker.mapPlaceId == orbisPlace.placeKey {
customMarker.opacity = 1
customMarker.fixedOpacity = Float(1)
}
else {
customMarker.opacity = 0.1
customMarker.fixedOpacity = Float(0.1)
}
}
else {
customMarker.opacity = placeOpacity
customMarker.fixedOpacity = Float(placeOpacity)
}
customMarker.isTappable = true
mapMarkers.append(customMarker)
customMarker.icon = image
customMarker.zIndex = circleZIndex
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1, qos: .userInteractive, flags: .enforceQoS) {
customMarker.map = mapView
}
.
.
.
I have also submitted issue on map sdk repo:
https://github.com/googlemaps/maps-sdk-for-ios-samples/issues/118

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 }
}

Swift - How Can I Show The MKPolyline For A Tracked Event

My problem is that when a jog has been completed, I can not present the recordedMapView with a full polyline that had tracked the location in an after exercise report.
Currently I am able to persist the data from tracking a run in the first view controller appearing in app, then in the detail view controller I fetch the data and unwrap/assign them to the respective variables, however, I am not sure where I go wrong in the code that is not allowing a polyline to appear. The map region does seem to be getting set properly as the map zooms in and out dynamically to fit the entire journey.
What could be the issue for why the polyline is not being presented and is there a solution to the code that I have provided to correct this problem?
var context : NSManagedObjectContext?
var runTimestamp : NSDate?
var runDuration : NSNumber?
var runDistance : NSNumber?
var runLocations : NSOrderedSet?
var locationTimeStamp : NSDate?
var locationLatitude : NSNumber?
var locationLongitude : NSNumber?
override func viewDidLoad()
{
super.viewDidLoad()
guard let context = context, finishedLocations = fetchLocation ( context ), finishedRun = fetchRun ( context ) else { return }
for location in finishedLocations
{
if let timestamp = location.timestamp, latitude = location.latitude, longitude = location.longitude
{
locationTimeStamp = timestamp
locationLatitude = latitude
locationLongitude = longitude
}
}
for run in finishedRun
{
if let timeStamp = run.timestamp, duration = run.duration, distance = run.distance, locations = run.locations
{
runTimestamp = timeStamp
runDuration = duration
runDistance = distance
runLocations = locations
}
}
updateUI ()
}
func loadMapView()
{
if runLocations!.count > 0
{
recordedMapView.region = mapRegion()
let colorSegments = MulticolorPolylineSegment.colorSegments(forLocations: runLocations!.array as! [Location])
recordedMapView.addOverlays(colorSegments)
}
else
{
let alertController = UIAlertController( title: "Error", message: "No Locations Saved", preferredStyle: .Alert )
let alertAction = UIAlertAction ( title: "Error", style : .Default , handler : nil )
alertController.addAction( alertAction )
presentViewController ( alertController, animated: true, completion: nil )
}
}
func mapRegion() -> MKCoordinateRegion
{
let initialLocation = runLocations!.firstObject as! Location
var minLat = initialLocation.latitude! .doubleValue
var minLng = initialLocation.longitude!.doubleValue
var maxLat = minLat
var maxLng = minLng
let locations = runLocations!.array as! [Location]
for location in locations
{
minLat = min( minLat, location.latitude! .doubleValue )
minLng = min( minLng, location.longitude!.doubleValue )
maxLat = max( maxLat, location.latitude! .doubleValue )
maxLng = max( maxLng, location.longitude!.doubleValue )
}
return MKCoordinateRegion(
center: CLLocationCoordinate2D( latitude : ( (minLat + maxLat)/2 ) , longitude : ( (minLng + maxLng)/2 ) ),
span : MKCoordinateSpan ( latitudeDelta: ( (maxLat - minLat)*1.1) , longitudeDelta: ( (maxLng - minLng)*1.1) )
)
}
func polyline() -> MKPolyline
{
var coordinates = [CLLocationCoordinate2D]()
let locations = runLocations!.array as! [Location]
for location in locations
{
coordinates.append( CLLocationCoordinate2D(latitude: (location.latitude!.doubleValue), longitude: (location.longitude!.doubleValue)) )
}
return MKPolyline(coordinates: &coordinates, count: runLocations!.count)
}
func mapView(mapView: MKMapView, rendererForOverlay overlay: MKOverlay) -> MKOverlayRenderer
{
let polyline = overlay as! MulticolorPolylineSegment
let renderer = MKPolylineRenderer(polyline: polyline)
renderer.strokeColor = polyline.color
renderer.lineWidth = 3
return renderer
}
Swift 3.x:
What I see you don't add the polyLine properly into mapView.
Change your polyline function like below;
func polyline()
{
if theJourney != nil && theJourney!.coords != nil{
var coordinates = [CLLocationCoordinate2D]()
let locations = runLocations!.array as! [Location]
for location in locations
{
coordinates.append( CLLocationCoordinate2D(latitude: (location.latitude!.doubleValue), longitude: (location.longitude!.doubleValue)) )
}
}
let polyline = MKPolyline(coordinates: &coordinates, count: runLocations!.count)
mapView.add(polyline)
}
And MKMapView's rendererFor method with code below;
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = UIColor.green
renderer.lineWidth = 3
return renderer
}
That's should work. Please let me know.

Swift 2 MapKit Annotations - How to group annotations?

I have close to 8.000 annotations in my map and I'd like to group them depending of the zoom that the user do in the app.
All the latitudes and longitudes are already into the CoreData as Double.
Case I have two different annotation images in the same point, I'd like to show two groups, one with the cross and one with the heart.
When the user click over the annotation and if this annotation is a grouped annotation, I'd like to show to the user how many annotations has at this location, "forcing" the user to zoom in to see the annotations individually.
Bellow are the images of my app and the current annotations.
Thank you!
I already got it.
var zoomLevel = Double()
var iphoneScaleFactorLatitude = Double()
var iphoneScaleFactorLongitude = Double()
var CanUpdateMap: Bool = false
static func getLatitudeLongitudeLimitsFromMap(mapView: MKMapView) -> [String: Double] {
var coord = [String: Double]()
let MinLat: Double = mapView.region.center.latitude - (mapView.region.span.latitudeDelta / 2)
let MaxLat: Double = mapView.region.center.latitude + (mapView.region.span.latitudeDelta / 2)
let MinLon: Double = mapView.region.center.longitude - (mapView.region.span.longitudeDelta / 2)
let MaxLon: Double = mapView.region.center.longitude + (mapView.region.span.longitudeDelta / 2)
coord["MinLat"] = MinLat
coord["MaxLat"] = MaxLat
coord["MinLon"] = MinLon
coord["MaxLon"] = MaxLon
return coord
}
func LoadMap(mapView: MKMapView) {
// Get the limits after move or resize the map
let coord: [String: Double] = getLatitudeLongitudeLimitsFromMap(mapView)
let MinLat: Double = coord["MinLat"]! as Double
let MaxLat: Double = coord["MaxLat"]! as Double
let MinLon: Double = coord["MinLon"]! as Double
let MaxLon: Double = coord["MaxLon"]! as Double
var arrAnnotations = [MKAnnotation]()
let FilterMinLat = arrDicListPinsWithLatitudeLongitude.filter({
if let item = $0["Latitude"] as? Double {
return item > MinLat
} else {
return false
}
})
let FilterMaxLat = FilterMinLat.filter({
if let item = $0["Latitude"] as? Double {
return item < MaxLat
} else {
return false
}
})
let FilterMinLon = FilterMaxLat.filter({
if let item = $0["Longitude"] as? Double {
return item > MinLon
} else {
return false
}
})
let FilterMaxLon = FilterMinLon.filter({
if let item = $0["Longitude"] as? Double {
return item < MaxLon
} else {
return false
}
})
for Item in FilterMaxLon {
let dic:[String:AnyObject] = Item
var Name = String()
var Address = String()
var IconPNG = String()
if let Latitude = dic["Latitude"] as? Double {
if let Longitude = dic["Longitude"] as? Double {
if let item = dic["Name"] {
Name = item as! String
}
if let item = dic["Address"] {
Address = item as! String
}
if let item = dic["TypeID"] as? Int {
if item == 11 {
IconPNG = "icon-cross.png"
} else {
IconPNG = "icon-heart.png"
}
}
arrAnnotations.append(CreateAnnotation(Address, Title: Name, Latitude: Latitude, Longitude: Longitude, IconPNG: IconPNG))
}
}
}
}
// Show in the map only the annotations from that specific region
iphoneScaleFactorLatitude = mapView.region.center.latitude
iphoneScaleFactorLongitude = mapView.region.center.longitude
if zoomLevel != mapView.region.span.longitudeDelta {
filterAnnotations(arrAnnotations)
zoomLevel = mapView.region.span.longitudeDelta
CanUpdateMap = true
}
}
func filterAnnotations(arrAnnotations: [MKAnnotation]) {
let latDelta: Double = 0.04 / iphoneScaleFactorLatitude
let lonDelta: Double = 0.04 / iphoneScaleFactorLongitude
var shopsToShow = [AnyObject]()
var arrAnnotationsNew = [MKAnnotation]()
for var i = 0; i < arrAnnotations.count; i++ {
let checkingLocation: MKAnnotation = arrAnnotations[i]
let latitude: Double = checkingLocation.coordinate.latitude
let longitude: Double = checkingLocation.coordinate.longitude
var found: Bool = false
for tempPlacemark: MKAnnotation in shopsToShow as! [MKAnnotation] {
if fabs(tempPlacemark.coordinate.latitude - latitude) < fabs(latDelta) && fabs(tempPlacemark.coordinate.longitude - longitude) < fabs(lonDelta) {
found = true
}
}
if !found {
shopsToShow.append(checkingLocation)
arrAnnotationsNew.append(checkingLocation)
}
}
// Clean the map
for item: MKAnnotation in self.mapRedes.annotations {
myMap.removeAnnotation(item)
}
// Add new annotations to the map
for item: MKAnnotation in arrAnnotationsNew {
myMap.addAnnotation(item)
}
}
func mapView(mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// This validation should be added, because it will find all the annotations before the map resize
if CanUpdateMap == true {
LoadMap(mapView)
}
}
Just a correction to my code:
//....
func LoadMap(mapView: MKMapView) {
//....
// Show in the map only the annotations from that specific region
iphoneScaleFactorLatitude = Double(mapView.bounds.size.width / 30) // 30 = width of the annotation
iphoneScaleFactorLongitude = Double(mapView.bounds.size.height / 30) // 30 = height of the annotation
//....
}
func filterAnnotations(mapView: MKMapView, arrAnnotations: [MKAnnotation]) {
let latDelta: Double = mapView.region.span.longitudeDelta / iphoneScaleFactorLatitude
let lonDelta: Double = mapView.region.span.longitudeDelta / iphoneScaleFactorLongitude
//....
}
//....

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

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")
}
}
}
}
}

Resources