How to check if MKCoordinateRegion contains CLLocationCoordinate2D without using MKMapView?
Asked Answered
N

10

20

I need to check if user location belongs to the MKCoordinateRegion. I was surprised not to find simple function for this, something like: CGRectContainsCGPoint(rect, point).

I found following piece of code:

CLLocationCoordinate2D topLeftCoordinate = 
    CLLocationCoordinate2DMake(region.center.latitude 
                               + (region.span.latitudeDelta/2.0), 
                               region.center.longitude 
                               - (region.span.longitudeDelta/2.0));


    CLLocationCoordinate2D bottomRightCoordinate = 
    CLLocationCoordinate2DMake(region.center.latitude 
                               - (region.span.latitudeDelta/2.0), 
                               region.center.longitude 
                               + (region.span.longitudeDelta/2.0));

        if (location.latitude < topLeftCoordinate.latitude || location.latitude > bottomRightCoordinate.latitude || location.longitude < bottomRightCoordinate.longitude || location.longitude > bottomRightCoordinate.longitude) {

    // Coordinate fits into the region

    }

But, I am not sure if it is accurate as documentation does not specify exactly how the region rectangle is calculated.

There must be simpler way to do it. Have I overlooked some function in the MapKit framework documentation?

Newborn answered 11/5, 2012 at 14:13 Comment(0)
N
15

In case there is anybody else confused with latitudes and longitues, here is tested, working solution:

MKCoordinateRegion region = self.mapView.region;

CLLocationCoordinate2D location = user.gpsposition.coordinate;
CLLocationCoordinate2D center   = region.center;
CLLocationCoordinate2D northWestCorner, southEastCorner;

northWestCorner.latitude  = center.latitude  - (region.span.latitudeDelta  / 2.0);
northWestCorner.longitude = center.longitude - (region.span.longitudeDelta / 2.0);
southEastCorner.latitude  = center.latitude  + (region.span.latitudeDelta  / 2.0);
southEastCorner.longitude = center.longitude + (region.span.longitudeDelta / 2.0);

if (
    location.latitude  >= northWestCorner.latitude && 
    location.latitude  <= southEastCorner.latitude &&

    location.longitude >= northWestCorner.longitude && 
    location.longitude <= southEastCorner.longitude
    )
{
    // User location (location) in the region - OK :-)
    NSLog(@"Center (%f, %f) span (%f, %f) user: (%f, %f)| IN!", region.center.latitude, region.center.longitude, region.span.latitudeDelta, region.span.longitudeDelta, location.latitude, location.longitude);

}else {

    // User location (location) out of the region - NOT ok :-(
    NSLog(@"Center (%f, %f) span (%f, %f) user: (%f, %f)| OUT!", region.center.latitude, region.center.longitude, region.span.latitudeDelta, region.span.longitudeDelta, location.latitude, location.longitude);
}
Newborn answered 13/5, 2012 at 18:51 Comment(2)
I doubt this would work: 1. why should location.latitude >= northWestCorner.latitude? Shouldn't it be sounthEastCorner.latitude? 2. What if calculated minimum longitude is -2.0, maximum longitude is 2.0, and your location.longitude is 359.0?Cooley
@Cooley is right that it will fail when tested around 360 degrees. See my answer below for a slightly more correct solution.Oleaginous
O
21

I'm posting this answer as the accepted solution is not valid in my opinion. This answer is also not perfect but it handles the case when coordinates wrap around 360 degrees boundaries, which is enough to be suitable in my situation.

+ (BOOL)coordinate:(CLLocationCoordinate2D)coord inRegion:(MKCoordinateRegion)region
{
    CLLocationCoordinate2D center = region.center;
    MKCoordinateSpan span = region.span;

    BOOL result = YES;
    result &= cos((center.latitude - coord.latitude)*M_PI/180.0) > cos(span.latitudeDelta/2.0*M_PI/180.0);
    result &= cos((center.longitude - coord.longitude)*M_PI/180.0) > cos(span.longitudeDelta/2.0*M_PI/180.0);
    return result;
}
Oleaginous answered 8/5, 2014 at 15:35 Comment(2)
This should be the accepted solution. The cos() function takes care of the 0 to 360 degree issue. Even though it performs a non-linear scale on the distance, it is compared to an equally scaled delta, so it works like a charm.Pyretotherapy
This worked for me for a small, well defined rectangular region in my city. I cannot attest to the general case.Murmansk
L
17

You can convert your location to a point with MKMapPointForCoordinate, then use MKMapRectContainsPoint on the mapview's visibleMapRect. This is completely off the top of my head. Let me know if it works.

Lemuel answered 11/5, 2012 at 14:48 Comment(5)
It feels completely overwhelming to initialize whole MKMapView and set it up just for such a simple check. I need to calculate this outside any view controller.Newborn
Sorry, I thought you were working with a mapview already in place. If you only have that region, you'll have to rely on it to be accurate. Why do you think the region is no good? Where did you get the region from?Lemuel
The region is OK. I am just not sure if I am checking against it correctly. The documentation of MKCoordinateRegion does not specify exactly how the latitude and longitude spans constructs area rectangle.Newborn
The way you're doing it is fine. The region specifies a span which is in degrees, the same as latitude and longitude. They convert directly. I'm not quite sure about the logic of your if statement. Shouldn't they be && instead of ||?Lemuel
This appears to be a great, simple solution. I have to test more, but may be the best solution to this tricky problem.Hostetler
N
15

In case there is anybody else confused with latitudes and longitues, here is tested, working solution:

MKCoordinateRegion region = self.mapView.region;

CLLocationCoordinate2D location = user.gpsposition.coordinate;
CLLocationCoordinate2D center   = region.center;
CLLocationCoordinate2D northWestCorner, southEastCorner;

northWestCorner.latitude  = center.latitude  - (region.span.latitudeDelta  / 2.0);
northWestCorner.longitude = center.longitude - (region.span.longitudeDelta / 2.0);
southEastCorner.latitude  = center.latitude  + (region.span.latitudeDelta  / 2.0);
southEastCorner.longitude = center.longitude + (region.span.longitudeDelta / 2.0);

if (
    location.latitude  >= northWestCorner.latitude && 
    location.latitude  <= southEastCorner.latitude &&

    location.longitude >= northWestCorner.longitude && 
    location.longitude <= southEastCorner.longitude
    )
{
    // User location (location) in the region - OK :-)
    NSLog(@"Center (%f, %f) span (%f, %f) user: (%f, %f)| IN!", region.center.latitude, region.center.longitude, region.span.latitudeDelta, region.span.longitudeDelta, location.latitude, location.longitude);

}else {

    // User location (location) out of the region - NOT ok :-(
    NSLog(@"Center (%f, %f) span (%f, %f) user: (%f, %f)| OUT!", region.center.latitude, region.center.longitude, region.span.latitudeDelta, region.span.longitudeDelta, location.latitude, location.longitude);
}
Newborn answered 13/5, 2012 at 18:51 Comment(2)
I doubt this would work: 1. why should location.latitude >= northWestCorner.latitude? Shouldn't it be sounthEastCorner.latitude? 2. What if calculated minimum longitude is -2.0, maximum longitude is 2.0, and your location.longitude is 359.0?Cooley
@Cooley is right that it will fail when tested around 360 degrees. See my answer below for a slightly more correct solution.Oleaginous
J
7

The other answers all have faults. The accepted answer is a little verbose, and fails near the international dateline. The cosine answer is workable, but fails for very small regions (because delta cosine is sine which tends towards zero near zero, meaning for smaller angular differences we expect zero change) This answer should work correctly for all situations, and is simpler.

Swift:

/* Standardises and angle to [-180 to 180] degrees */
class func standardAngle(var angle: CLLocationDegrees) -> CLLocationDegrees {
    angle %= 360
    return angle < -180 ? -360 - angle : angle > 180 ? 360 - 180 : angle
}

/* confirms that a region contains a location */
class func regionContains(region: MKCoordinateRegion, location: CLLocation) -> Bool {
    let deltaLat = abs(standardAngle(region.center.latitude - location.coordinate.latitude))
    let deltalong = abs(standardAngle(region.center.longitude - location.coordinate.longitude))
    return region.span.latitudeDelta >= deltaLat && region.span.longitudeDelta >= deltalong
}

Objective C:

/* Standardises and angle to [-180 to 180] degrees */
+ (CLLocationDegrees)standardAngle:(CLLocationDegrees)angle {
    angle %= 360
    return angle < -180 ? -360 - angle : angle > 180 ? 360 - 180 : angle
}

/* confirms that a region contains a location */
+ (BOOL)region:(MKCoordinateRegion*)region containsLocation:(CLLocation*)location {
    CLLocationDegrees deltaLat = fabs(standardAngle(region.center.latitude - location.coordinate.latitude))
    CLLocationDegrees deltalong = fabs(standardAngle(region.center.longitude - location.coordinate.longitude))
    return region.span.latitudeDelta >= deltaLat && region.span.longitudeDelta >= deltalong
}

This method fails for regions that include either pole though, but then the coordinate system itself fails at the poles. For most applications, this solution should suffice. (Note, not tested on Objective C)

Jokjakarta answered 22/10, 2014 at 9:18 Comment(2)
In the standardAngle method, for the case where the normalized angle > 180: shouldn't the return value be 360 - angle instead of 360 - 180?Murmansk
You should use span.delta/2 when comparing to latitude or longitude.Mustang
V
6

I've used this code to determine if a coordinate is within a circular region (a coordinate with a radius around it).

- (BOOL)location:(CLLocation *)location isNearCoordinate:(CLLocationCoordinate2D)coordinate withRadius:(CLLocationDistance)radius
{
    CLCircularRegion *circularRegion = [[CLCircularRegion alloc] initWithCenter:location.coordinate radius:radius identifier:@"radiusCheck"];

    return [circularRegion containsCoordinate:coordinate];
}
Votyak answered 6/10, 2014 at 12:32 Comment(0)
U
2

Works for me like a charm (Swift 5)

func check(
    location: CLLocationCoordinate2D,
    contains childLocation: CLLocationCoordinate2D,
    with radius: Double)
    -> Bool
{
    let region = CLCircularRegion(center: location, radius: radius, identifier: "SearchId")
    return region.contains(childLocation)
}
Ulpian answered 21/2, 2020 at 18:57 Comment(0)
S
1

Owen Godfrey, the objective-C code doesn´t work, this is the good code: Fails on Objective-C, this is the good code:

/* Standardises and angle to [-180 to 180] degrees */
- (CLLocationDegrees)standardAngle:(CLLocationDegrees)angle {
    angle=fmod(angle,360);
    return angle < -180 ? -360 - angle : angle > 180 ? 360 - 180 : angle;
}

-(BOOL)thisRegion:(MKCoordinateRegion)region containsLocation:(CLLocation *)location{
    CLLocationDegrees deltaLat =fabs([self standardAngle:(region.center.latitude-location.coordinate.latitude)]);
    CLLocationDegrees deltaLong =fabs([self standardAngle:(region.center.longitude-location.coordinate.longitude)]);
    return region.span.latitudeDelta >= deltaLat && region.span.longitudeDelta >=deltaLong;
}
    CLLocationDegrees deltalong = fabs(standardAngle(region.center.longitude - location.coordinate.longitude));
    return region.span.latitudeDelta >= deltaLat && region.span.longitudeDelta >= deltalong;
}

Thanks!

Springing answered 19/2, 2015 at 10:47 Comment(1)
Use region.span.latitudeDelta/2 and region.span.longitudeDelta in last lineMustang
E
1

I had problem with same calculations. I like conception proposed by Owen Godfrey here, bun even Fernando here missed the fact that latitude is wraped diferently than longitude and has diferent range. To clarify my proposal I post it with tests so you can check it out by your self.

import XCTest
import MapKit

// MARK - The Solution

extension CLLocationDegrees {

    enum WrapingDimension: Double {
        case latitude = 180
        case longitude = 360
    }

    /// Standardises and angle to [-180 to 180] or [-90 to 90] degrees
    func wrapped(diemension: WrapingDimension) -> CLLocationDegrees {
        let length = diemension.rawValue
        let halfLenght = length/2.0
        let angle = self.truncatingRemainder(dividingBy: length)
        switch diemension {
        case .longitude:
            //        return angle < -180.0 ? 360.0 + angle : angle > 180.0 ? -360.0 + angle : angle
            return angle < -halfLenght ? length + angle : angle > halfLenght ? -length + angle : angle
        case .latitude:
            //        return angle < -90.0 ? -180.0 - angle : angle > 90.0 ? 180.0 - angle : angle
            return angle < -halfLenght ? -length - angle : angle > halfLenght ? length - angle : angle
        }
    }
}

extension MKCoordinateRegion {
    /// confirms that a region contains a location
    func contains(_ coordinate: CLLocationCoordinate2D) -> Bool {
        let deltaLat = abs((self.center.latitude - coordinate.latitude).wrapped(diemension: .latitude))
        let deltalong = abs((self.center.longitude - coordinate.longitude).wrapped(diemension: .longitude))
        return self.span.latitudeDelta/2.0 >= deltaLat && self.span.longitudeDelta/2.0 >= deltalong
    }
}

// MARK - Unit tests

class MKCoordinateRegionContaingTests: XCTestCase {

    func testRegionContains() {
        var region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(0, 0), MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1))
        var coords = CLLocationCoordinate2DMake(0, 0)
        XCTAssert(region.contains(coords))

        coords = CLLocationCoordinate2DMake(0.5, 0.5)
        XCTAssert(region.contains(coords))

        coords = CLLocationCoordinate2DMake(-0.5, 0.5)
        XCTAssert(region.contains(coords))
        coords = CLLocationCoordinate2DMake(0.5, 0.5000001)
        XCTAssert(!region.contains(coords)) // NOT Contains
        coords = CLLocationCoordinate2DMake(0.5, -0.5000001)
        XCTAssert(!region.contains(coords)) // NOT Contains
        coords = CLLocationCoordinate2DMake(1, 1)
        XCTAssert(!region.contains(coords)) // NOT Contains

        region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(0, 180), MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1))
        coords = CLLocationCoordinate2DMake(0, 180.5)
        XCTAssert(region.contains(coords))
        coords.longitude = 179.5
        XCTAssert(region.contains(coords))
        coords.longitude = 180.5000001
        XCTAssert(!region.contains(coords)) // NOT Contains
        coords.longitude = 179.5000001
        XCTAssert(region.contains(coords))
        coords.longitude = 179.4999999
        XCTAssert(!region.contains(coords)) // NOT Contains

        region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(90, -180), MKCoordinateSpan(latitudeDelta: 1, longitudeDelta: 1))
        coords = CLLocationCoordinate2DMake(90.5, -180.5)
        XCTAssert(region.contains(coords))

        coords = CLLocationCoordinate2DMake(89.5, -180.5)
        XCTAssert(region.contains(coords))

        coords = CLLocationCoordinate2DMake(90.50000001, -180.5)
        XCTAssert(!region.contains(coords)) // NOT Contains

        coords = CLLocationCoordinate2DMake(89.50000001, -180.5)
        XCTAssert(region.contains(coords))

        coords = CLLocationCoordinate2DMake(89.49999999, -180.5)
        XCTAssert(!region.contains(coords)) // NOT Contains
    }

    func testStandardAngle() {
        var angle = 180.5.wrapped(diemension: .longitude)
        var required = -179.5
        XCTAssert(self.areAngleEqual(angle, required))

        angle = 360.5.wrapped(diemension: .longitude)
        required = 0.5
        XCTAssert(self.areAngleEqual(angle, required))

        angle = 359.5.wrapped(diemension: .longitude)
        required = -0.5
        XCTAssert(self.areAngleEqual(angle, required))

        angle = 179.5.wrapped(diemension: .longitude)
        required = 179.5
        XCTAssert(self.areAngleEqual(angle, required))

        angle = 90.5.wrapped(diemension: .latitude)
        required = 89.5
        XCTAssert(self.areAngleEqual(angle, required))

        angle = 90.5000001.wrapped(diemension: .latitude)
        required = 89.4999999
        XCTAssert(self.areAngleEqual(angle, required))

        angle = -90.5.wrapped(diemension: .latitude)
        required = -89.5
        XCTAssert(self.areAngleEqual(angle, required))

        angle = -90.5000001.wrapped(diemension: .latitude)
        required = -89.4999999
        XCTAssert(self.areAngleEqual(angle, required))
    }

    /// compare doubles with presition to 8 digits after the decimal point
    func areAngleEqual(_ a:Double, _ b:Double) -> Bool {
        let presition = 0.00000001
        let equal = Int(a / presition) == Int(b / presition)
        print(String(format:"%14.9f %@ %14.9f", a, equal ? "==" : "!=", b) )
        return equal
    }
}
Ebonee answered 1/2, 2018 at 14:32 Comment(0)
F
0

Based on Lukasz solution, but in Swift, in case anybody can make use of Swift:

func isInRegion (region : MKCoordinateRegion, coordinate : CLLocationCoordinate2D) -> Bool {

    let center   = region.center;
    let northWestCorner = CLLocationCoordinate2D(latitude: center.latitude  - (region.span.latitudeDelta  / 2.0), longitude: center.longitude - (region.span.longitudeDelta / 2.0))
    let southEastCorner = CLLocationCoordinate2D(latitude: center.latitude  + (region.span.latitudeDelta  / 2.0), longitude: center.longitude + (region.span.longitudeDelta / 2.0))

    return (
        coordinate.latitude  >= northWestCorner.latitude &&
        coordinate.latitude  <= southEastCorner.latitude &&

        coordinate.longitude >= northWestCorner.longitude &&
        coordinate.longitude <= southEastCorner.longitude
    )
}
Fraudulent answered 23/3, 2016 at 21:20 Comment(0)
S
0

MarekR's answer works for me. This is the extension I've put it in:

extension MKCoordinateRegion {
    
    func contains(coordinate:CLLocationCoordinate2D) -> Bool {
        cos((center.latitude - coordinate.latitude) * Double.pi/180) > cos(span.latitudeDelta / 2.0*Double.pi/180) &&
        cos((center.longitude - coordinate.longitude) * Double.pi/180) > cos(span.longitudeDelta / 2.0*Double.pi/180)
    }
    
}
Stanchion answered 9/6, 2022 at 20:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.