MKMapRect and displaying map overlays that span 180th meridian

…衆ロ難τιáo~ 提交于 2019-12-02 21:16:35

According to that comment in MKOverlay.h, if the nw and sw corners were specified as negative MKMapPoint values, the overlay should be "drawn correctly".

If we try this:

//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's whether + or -

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

[mapView addOverlay:p];

The resulting p.boundingMapRect does return YES for MKMapRectSpans180thMeridian (but the code already figured that out from the coordinates since it didn't have the maprect to begin with).

Unfortunately, however, creating the maprect with the negative values fixes only half the problem. The half of the polygon that is east of the dateline is now drawn correctly. However, the other half on the west of the dateline does not get drawn at all.

Apparently, the built-in MKPolygonView does not call MKMapRectSpans180thMeridian and draw the polygon in two parts.

You can create a custom overlay view and do this drawing yourself (you'd create one overlay but the view would draw two polygons).

Or, you could just create two MKPolygon overlays and let the map view draw them by adding the following after the above code:

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

By the way, there is a similar issue with drawing MKPolyline overlays that cross +/-180 (see this question).

Palimondo

As this area is woefully under-documented, the Map Kit Functions Reference should be amended with:

Warning: All the described functions work fine, as long as you do not cross the 180th meridian.
Here be dragons. You have been warned...

To solve this question, I have resorted to the good old investigative testing. Please excuse the comments around the prose. They allow you to copy & paste all source below verbatim, so that you can play with it yourself.

First a little helper function that converts the corner points of MKMapRect back into coordinate space, so that we can compare results of our conversions with the starting coordinates:

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

/*
And now, let's test

How To Create MapRect Spanning 180th Meridian:

*/

- (void)testHowToCreateMapRectSpanning180thMeridian
{

/*
We'll use location viewport of Asia, as returned by Google Geocoding API, because it spans the antimeridian. The northeast corner lies already in western hemisphere—longitudal range (-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);

/*
For the reference, here are the bounds of the whole projected world, some 268 million, after converting to MKMapPoints. Our little helper function shows us that the Mercator projection used here is unable to express latitudes above ±85 degrees. Longitude spans nicely from -180 to 180 degrees.
*/

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

/*
Why was the MKPolygon overlay, created using the geo-coordinates, displayed in the wrong place on the map?
*/

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

/*
It looks like the longitudes got swapped the wrong way. Asia is {{-12, 25}, {81, -168}}. The resulting MKMapRect does not pass the test using the MKMapRectSpans180thMeridian function —and we know it should!

False Attempts

So the MKPolygon does not compute the MKMapRect correctly, when the coordinates span the antimeridian. OK, let's create the map rect ourselves. Here are two methods suggested in answers to How to fit a certain bounds consisting of NE and SW coordinates into the visible map view?

... quick way is a slight trick using the MKMapRectUnion function. Create a zero-size MKMapRect from each coordinate and then merge the two rects into one big rect using the function:

*/

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

/*
Curiously, we have the same result as before. It makes sense that MKPolygon should probably compute its bounds using MKRectUnion, anyway.

Now I've done the next one myself, too. Compute the MapRect's origin, width and hight manually, while trying to be fancy and not worry about the correct ordering of the corners.
*/

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

/*
Hey! It is the same result as before. This is how the latitudes get swapped, when the coordinates cross the antimeridian. And it is probably how the MKMapRectUnion works, too. Not good...
*/

// Let's 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}}

/*
Remember, the Asia is {{-12, 25}, {81, -168}}. We are getting back the right coordinates, but the MKMapRect does not span the antimeridian according to MKMapRectSpans180thMeridian. What the...?!

The Solution

The hint from MKOverlay.h said:

For overlays that span the 180th meridian, boundingMapRect should have either a negative MinX or a MaxX that is greater than MKMapSizeWorld.width.

None of those conditions is met. What's worse, the rb.size.width is negative 144 million. That's definitely wrong.

We have to correct the rect values when we pass the antimeridian, so that one of those conditions is met:
*/

// Let's 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}}

/*
Finally we have satisfied the MKMapRectSpans180thMeridian. Map rect width is positive. What about the coordinates? Northeast has longitude of 191.6455. Wrapped around the globe (-360), it is -168.3545. Q.E.D.

We have computed the correct MKMapRect that spans the 180th meridian by satisfying the second condition: the MaxX (rc.origin.x + rc.size.width = 152870935.0 + 124248035.2 = 277118970.2) is greater then width of the world (268 million).

What about satisfying the first condition, negative MinX === origin.x?
*/

// Let's 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);

/*
This also passes the MKMapRectSpans180thMeridian test. And the reverse conversion to geo-coordinates gives us match, except for the southwest longitude: -334.9841. But wrapped around the world (+360), it is 25.0159. Q.E.D.

So there are two correct forms to compute the MKMapRect that spans 180th meridian. One with positive and one with negative origin.

Alternative Method

The negative origin method demonstrated above (rd) corresponds to the result obtained by alternative method suggested by Anna Karenina in another answer to this question:
*/

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

/*
So if we manually convert to MKMapPoints and fudge the negative origin, even the MKPolygon can compute the boundingMapRect correctly. Resulting map rect is equivalent to the nagative origin method above (rd).
*/

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

/*
Or should I say almost equivalent... because curiously, the following assertions would fail:
*/

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

/*
One would guess they know how to compare floating point numbers, but I digress...
*/

}

This concludes the test function source code.

Displaying Overlay

As mentioned in the question, to debug the problem I've used MKPolygons to visualize what was going on. It turns out that the two forms of MKMapRects that span antimeridian are displayed differently when overlayed on the map. When you approach the antimeridian from the west hemisphere, only the one with negative origin gets displayed. Likewise, the positive origin form is displayed when you approach the 180th meridian from the eastern hemisphere. The MKPolygonView does not handle the spanning of 180th meridian for you. You need to adjust the polygon points yourself.

This is how to create polygon from the map 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];
}

I have simply used brute force and added the polygon twice—one in each form.

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

I hope this helps other souls that wander in to the land of the dragons behind the 180th meridian.

In short, if the polygon crosses the antimeridian, check the mapPoints. If the mapPoint.x is greater than the primeMeridian.x, subtract the width of the world from the mapPoint.x.

This splits the map down the prime meridian. The minX is negative and the mapSize is smaller than the width of the world. Palimondo's answer was hugely helpful figuring this out.

I am working with geodesicPolylines and spent a couple days on this. Finally found an answer!

/// Note: If both the prime meridian and the antimeridian are crossed, an empty polygon will be returned
func makePolygon(lines: [LineAnnotation]) -> MKPolygon {
    let polylines = lines.map({ $0.polyline })
    let sum = polylines.reduce(0, { $0 + $1.pointCount })
    let pointer = UnsafeMutablePointer<MKMapPoint>.allocate(capacity: sum)
    var advance = 0
    let spans180thMeridian = polylines.contains(where: { $0.boundingMapRect.spans180thMeridian })
    let primeMeridianMapPoint = MKMapPoint(CLLocationCoordinate2D(latitude: 0, longitude: 0))
    let spansPrimeMeridian = polylines.contains(where: {
        return $0.boundingMapRect.minX <= primeMeridianMapPoint.x && $0.boundingMapRect.maxX >= primeMeridianMapPoint.x
    })
    guard !(spans180thMeridian && spansPrimeMeridian) else { return MKPolygon() }
    if spans180thMeridian {
        for polyline in polylines {
            // initialize the pointer with a copy of the polyline points, adjusted if needed
            let points = UnsafeMutablePointer<MKMapPoint>.allocate(capacity: polyline.pointCount)
            points.initialize(from: polyline.points(), count: polyline.pointCount)
            for i in 0..<polyline.pointCount {
                let pointPointer = points.advanced(by: i)
                if pointPointer.pointee.x > primeMeridianMapPoint.x {
                    pointPointer.pointee.x -= MKMapSize.world.width
                }
            }
            pointer.advanced(by: advance).initialize(from: points, count: polyline.pointCount)
            advance += polyline.pointCount
            points.deinitialize(count: polyline.pointCount)
            points.deallocate()
        }

    } else {
        // initialize the pointer with the polyline points
        for polyline in polylines {
            pointer.advanced(by: advance).initialize(from: polyline.points(), count: polyline.pointCount)
            advance += polyline.pointCount
        }
    }
    let polygon = MKPolygon(points: pointer, count: sum)
    print(polygon.boundingMapRect)
    pointer.deinitialize(count: sum)
    pointer.deallocate()
    return polygon
}
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!