MKMapRect and display overlay maps covering the 180th meridian

I work with viewports and borders returned by the Google geocoding API. When reverse geocoding for a given coordinate, the service returns several results with varying degrees of detail (country, administrative region, locality, publication, route, etc.). I want to choose the most suitable result, given the current visible area on the map.

I settled on an algorithm that compares the relationship of areas (in MKMapPoint²) of the location viewport, the current map viewer and their intersection (using the function MKMapRectIntersection). This works very well if the location view does not cover 180 meridians. In this case, their intersection is 0.

I began to investigate the reason and as a debugging helper application that I display on a map MKPolygonon a map to give me visual clues as to what is happening. To avoid possible errors introduced by my code, which does the conversion between geocoordinates and MKMapRect, I built a polygon overlay using the source coordinates from Google results, for example:

CLLocationCoordinate2D sw, ne, nw, se;
sw = location.viewportSouthWest.coordinate;
ne = location.viewportNorthEast.coordinate;
nw = CLLocationCoordinate2DMake(ne.latitude, sw.longitude);
se = CLLocationCoordinate2DMake(sw.latitude, ne.longitude);
CLLocationCoordinate2D coords[] = {nw, ne, se, sw};
MKPolygon *p = [MKPolygon polygonWithCoordinates:coords count:4];

For example, a problem location, here is the viewport returned for the United States, the last result of a country like when geocoding coordinates somewhere in Virginia :

Southwest: 18.9110643, 172.4546967  
Northeast: 71.3898880, -66.9453948

, - , , 180 . , , ( , ):

Viewport of USA overlayed on mapViewport of Russia overlayed on map

, location viewport , .

, , MKMapPoint MKMapRect ( ) .

, rect, SO:
, NE SW, ?
... , 180- . MKMapRect MKMapRectSpans180thMeridian return false, .

Apple . , , MKOverlay.h:

// boundingMapRect should be the smallest rectangle that completely contains
// the overlay.
// For overlays that span the 180th meridian, boundingMapRect should have 
// either a negative MinX or a MaxX that is greater than MKMapSizeWorld.width.
@property (nonatomic, readonly) MKMapRect boundingMapRect;

, 180- ?
MKMapRect, 180- ?

+5
2

MKOverlay.h, nw sw MKMapPoint, " ".

:

//calculation of the nw, ne, se, and sw coordinates goes here

MKMapPoint points[4];
if (nw.longitude > ne.longitude)  //does it cross 180th?
{
    //Get the mappoint for equivalent distance on
    //the "positive" side of the dateline...
    points[0] = MKMapPointForCoordinate(
                  CLLocationCoordinate2DMake(nw.latitude, -nw.longitude));

    //Reset the mappoint to the correct side of the dateline, 
    //now it will be negative (as per Apple comments)...
    points[0].x = - points[0].x;
}
else
{
    points[0] = MKMapPointForCoordinate(nw);
}
points[1] = MKMapPointForCoordinate(ne);
points[2] = MKMapPointForCoordinate(se);
points[3] = MKMapPointForCoordinate(sw);
points[3].x = points[0].x;    //set to same as NW whether + or -

MKPolygon *p = [MKPolygon polygonWithPoints:points count:4];

[mapView addOverlay:p];

p.boundingMapRect YES MKMapRectSpans180thMeridian ( , , , ).

, , . , , . , .

-, MKPolygonView MKMapRectSpans180thMeridian .

( , ).

MKPolygon , :

if (MKMapRectSpans180thMeridian(p.boundingMapRect))
{
    MKMapRect remainderRect = MKMapRectRemainder(p.boundingMapRect);

    MKMapPoint remPoints[4];
    remPoints[0] = remainderRect.origin;
    remPoints[1] = MKMapPointMake(remainderRect.origin.x + remainderRect.size.width, remainderRect.origin.y);
    remPoints[2] = MKMapPointMake(remainderRect.origin.x + remainderRect.size.width, remainderRect.origin.y + remainderRect.size.height);
    remPoints[3] = MKMapPointMake(remainderRect.origin.x, remainderRect.origin.y + remainderRect.size.height);

    MKPolygon *remPoly = [MKPolygon polygonWithPoints:remPoints count:4];

    [mapView addOverlay:remPoly];
}

, MKPolyline, +/- 180 (. ).

+5

, :

: , 180- .
. ...

, . , . , .

, MKMapRect , :

NSString* MyStringCoordsFromMapRect(MKMapRect rect) {
    MKMapPoint pNE = rect.origin, pSW = rect.origin;
    pNE.x += rect.size.width;
    pSW.y += rect.size.height;

    CLLocationCoordinate2D sw, ne;
    sw = MKCoordinateForMapPoint(pSW);
    ne = MKCoordinateForMapPoint(pNE);

    return [NSString stringWithFormat:@"{{%f, %f}, {%f, %f}}", 
            sw.latitude, sw.longitude, ne.latitude, ne.longitude];
}

/*
,

MapRect Spanning 180- :

*/

- (void)testHowToCreateMapRectSpanning180thMeridian
{

/*
  location viewport , API Google, antimeridian. - - (-180,0):
*/

CLLocationCoordinate2D sw, ne, nw, se;
sw = CLLocationCoordinate2DMake(-12.9403000, 25.0159000);
ne = CLLocationCoordinate2DMake(81.6691780, -168.3545000);
nw = CLLocationCoordinate2DMake(ne.latitude, sw.longitude);
se = CLLocationCoordinate2DMake(sw.latitude, ne.longitude);

/*
, 268 , MKMapPoints. , , , ± 85 . -180 180 .
*/

NSLog(@"\nMKMapRectWorld: %@\n => %@",
      MKStringFromMapRect(MKMapRectWorld), 
      MyStringCoordsFromMapRect(MKMapRectWorld));
// MKMapRectWorld: {{0.0, 0.0}, {268435456.0, 268435456.0}}
//  => {{-85.051129, -180.000000}, {85.051129, 180.000000}}

/*
MKPolygon, , ?
*/

// MKPolygon bounds
CLLocationCoordinate2D coords[] = {nw, ne, se, sw};
MKPolygon *p = [MKPolygon polygonWithCoordinates:coords count:4];
MKMapRect rp = p.boundingMapRect;
STAssertFalse(MKMapRectSpans180thMeridian(rp), nil); // Incorrect!!!
NSLog(@"\n rp: %@\n => %@",
      MKStringFromMapRect(rp), 
      MyStringCoordsFromMapRect(rp));
// rp: {{8683514.2, 22298949.6}, {144187420.8, 121650857.5}}
//  => {{-12.940300, -168.354500}, {81.669178, 25.015900}}

/*
, . {{-12, 25}, {81, -168}}. MKMapRect MKMapRectSpans180thMeridian, , !

, MKPolygon MKMapRect, . , . , , NE SW, ?

... - MKMapRectUnion. MKMapRect , , :

*/

// https://stackoverflow.com/a/8496988/41307
MKMapPoint pNE = MKMapPointForCoordinate(ne);
MKMapPoint pSW = MKMapPointForCoordinate(sw);
MKMapRect ru = MKMapRectUnion(MKMapRectMake(pNE.x, pNE.y, 0, 0),
                              MKMapRectMake(pSW.x, pSW.y, 0, 0));
STAssertFalse(MKMapRectSpans180thMeridian(ru), nil); // Incorrect!!!
STAssertEquals(ru, rp, nil);
NSLog(@"\n ru: %@\n => %@",
      MKStringFromMapRect(ru), 
      MyStringCoordsFromMapRect(ru));
// ru: {{8683514.2, 22298949.6}, {144187420.8, 121650857.5}}
//  => {{-12.940300, -168.354500}, {81.669178, 25.015900}}

/*
 , , . , MKPolygon , , MKRectUnion.

. , MapRect , .
*/

// https://stackoverflow.com/a/8500002/41307
MKMapRect ra = MKMapRectMake(MIN(pNE.x, pSW.x), MIN(pNE.y, pSW.y), 
                             ABS(pNE.x - pSW.x), ABS(pNE.y - pSW.y));
STAssertFalse(MKMapRectSpans180thMeridian(ru), nil); // Incorrect!!!
STAssertEquals(ra, ru, nil);
NSLog(@"\n ra: %@\n => %@",
      MKStringFromMapRect(ra), 
      MyStringCoordsFromMapRect(ra));
// ra: {{8683514.2, 22298949.6}, {144187420.8, 121650857.5}}
//  => {{-12.940300, -168.354500}, {81.669178, 25.015900}}

/*
! , . , , . , MKMapRectUnion. ...
*/

// Let put the coordinates manually in proper slots
MKMapRect rb = MKMapRectMake(pSW.x, pNE.y, 
                             (pNE.x - pSW.x), (pSW.y - pNE.y));
STAssertFalse(MKMapRectSpans180thMeridian(rb), nil); // Incorrect!!! Still :-(
NSLog(@"\n rb: %@\n => %@",
      MKStringFromMapRect(rb), 
      MyStringCoordsFromMapRect(rb));
// rb: {{152870935.0, 22298949.6}, {-144187420.8, 121650857.5}}
//  => {{-12.940300, 25.015900}, {81.669178, -168.354500}}

/*
, {{-12, 25}, {81, -168}}. , MKMapRect MKMapRectSpans180thMeridian. ...?!

MKOverlay.h :

, 180- , boundingMapRect MinX, MaxX, , MKMapSizeWorld.width.

. , rb.size.width 144 . .

, :
*/

// Let correct for crossing 180th meridian
double antimeridianOveflow = 
  (ne.longitude > sw.longitude) ? 0 : MKMapSizeWorld.width;    
MKMapRect rc = MKMapRectMake(pSW.x, pNE.y, 
                             (pNE.x - pSW.x) + antimeridianOveflow, 
                             (pSW.y - pNE.y));
STAssertTrue(MKMapRectSpans180thMeridian(rc), nil); // YES. FINALLY!
NSLog(@"\n rc: %@\n => %@",
      MKStringFromMapRect(rc), 
      MyStringCoordsFromMapRect(rc));
// rc: {{152870935.0, 22298949.6}, {124248035.2, 121650857.5}}
//  => {{-12.940300, 25.015900}, {81.669178, 191.645500}}

/*
, MKMapRectSpans180thMeridian. . ? - 191.6455. (-360), -168.3545. Q.E.D.

MKMapRect, 180- , : MaxX (rc.origin.x + rc.size.width= 152870935.0 + 124248035.2 = 277118970.2) ( 268 ).

, MinX === origin.x?
*/

// Let correct for crossing 180th meridian another way
MKMapRect rd = MKMapRectMake(pSW.x - antimeridianOveflow, pNE.y, 
                             (pNE.x - pSW.x) + antimeridianOveflow, 
                             (pSW.y - pNE.y));
STAssertTrue(MKMapRectSpans180thMeridian(rd), nil); // YES. AGAIN!
NSLog(@"\n rd: %@\n => %@",
      MKStringFromMapRect(rd), 
      MyStringCoordsFromMapRect(rd));
// rd: {{-115564521.0, 22298949.6}, {124248035.2, 121650857.5}}
//  => {{-12.940300, -334.984100}, {81.669178, -168.354500}}

STAssertFalse(MKMapRectEqualToRect(rc, rd), nil);

/*
MKMapRectSpans180thMeridian. , - : -334.9841. (+360), 25.0159. Q.E.D.

, MKMapRect, 180- . .

, (rd), , , :
*/

// https://stackoverflow.com/a/9023921/41307
MKMapPoint points[4];
if (nw.longitude > ne.longitude) {
    points[0] = MKMapPointForCoordinate(
                  CLLocationCoordinate2DMake(nw.latitude, -nw.longitude));
    points[0].x = - points[0].x;
}
else
    points[0] = MKMapPointForCoordinate(nw);
points[1] = MKMapPointForCoordinate(ne);
points[2] = MKMapPointForCoordinate(se);
points[3] = MKMapPointForCoordinate(sw);
points[3].x = points[0].x;
MKPolygon *p2 = [MKPolygon polygonWithPoints:points count:4];
MKMapRect rp2 = p2.boundingMapRect;
STAssertTrue(MKMapRectSpans180thMeridian(rp2), nil); // Also GOOD!
NSLog(@"\n rp2: %@\n => %@",
      MKStringFromMapRect(rp2), 
      MyStringCoordsFromMapRect(rp2));
// rp2: {{-115564521.0, 22298949.6}, {124248035.2, 121650857.5}}
//  => {{-12.940300, -334.984100}, {81.669178, -168.354500}}

/*
, MKMapPoint , MKPolygon boundingMapRect. rect (rd).
*/

STAssertTrue([MKStringFromMapRect(rp2) isEqualToString:
              MKStringFromMapRect(rd)], nil);

/*
... , :
*/

// STAssertEquals(rp2, rd, nil); // Sure, shouldn't compare floats byte-wise!
// STAssertTrue(MKMapRectEqualToRect(rp2, rd), nil);

/*
, , , ...
*/

}

.

, MKPolygon . , MKMapRect, , - . , , . , , 180- . MKPolygonView 180- . .

rect:

- (MKPolygon *)polygonFor:(MKMapRect)r 
{
    MKMapPoint p1 = r.origin, p2 = r.origin, p3 = r.origin, p4 = r.origin;
    p2.x += r.size.width;
    p3.x += r.size.width; p3.y += r.size.height;
    p4.y += r.size.height;
    MKMapPoint points[] = {p1, p2, p3, p4};
    return [MKPolygon polygonWithPoints:points count:4];
}

.

for (GGeocodeResult *location in locations) {
    MKMapRect r = location.mapRect;
    [self.debugLocationBounds addObject:[self polygonFor:r]];

    if (MKMapRectSpans180thMeridian(r)) {
        r.origin.x -= MKMapSizeWorld.width;
        [self.debugLocationBounds addObject:[self polygonFor:r]];
    }
}            
[self.mapView addOverlays:self.debugLocationBounds]; 

, , 180- .

+17

All Articles