How to check if MKCoordinateRegion contains CLLoca

2019-01-21 17:35发布

问题:

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?

回答1:

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


回答2:

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.



回答3:

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


回答4:

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)



回答5:

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


回答6:

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!



回答7:

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


回答8:

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