MKMapRect并显示跨越第180个子午线的地图叠加层

Pal*_*ndo 7 mapkit ios

我正在处理从Google Geocoding API返回的视口和边界.当对给定坐标进行反向地理编码时,服务返回具有各种粒度(国家,行政区域,地点,子地点,路线等)的若干结果.我想在给定地图上当前可见区域的结果上选择最合适的.

我已经确定了比较MKMapPoint位置视口,当前地图视口和它们的交点(使用MKMapRectIntersection函数)的面积比(²)的算法.只要位置视口不跨越180子午线,这就可以很好地工作.在那种情况下,他们的交集是0.

我已经开始调查原因并作为调试辅助工具我MKPolygon在地图上显示叠加层,以便为我提供有关正在发生的事情的视觉线索.为了避免我的代码在地理坐标之间进行转换而引入的错误MKMapRect,我使用Google结果中的原始坐标构建了多边形叠加,如下所示:

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];
Run Code Online (Sandbox Code Playgroud)

例如,有问题的位置,这里是为美国返回的视口,类型为country的最后结果,当在弗吉尼亚州的某处进行地理编码坐标时:

Southwest: 18.9110643, 172.4546967  
Northeast: 71.3898880, -66.9453948
Run Code Online (Sandbox Code Playgroud)

注意位于视口左下角的西南坐标是如何跨越180子午线的.当在地图上显示覆盖为多边形的此位置时,它会错误地显示在USA边框的右侧(大的棕色矩形,只有左下角可见):

美国的视口覆盖在地图上 俄罗斯的视口覆盖在地图上

类似地,显示俄罗斯的位置视口显示矩形位于俄罗斯边界左侧的错误位置.

当我将位置视口转换为MKMapPoints并且MKMapRect在地图视口(上图中的白色矩形)和位置视口之间没有找到交叉点时,这在视觉上确认存在类似的问题.

我计算地图rect的方式类似于这个SO问题中的答案:
如何将由NE和SW坐标组成的特定边界拟合到可见地图视图中?
... 除非坐标跨越第180个子午线,否则工作正常.测试MKMapRectMKMapRectSpans180thMeridian返回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;
Run Code Online (Sandbox Code Playgroud)

显示跨越第180个子午线的多边形叠加层的正确方法是什么?
如何正确构建MKMapRect跨越180度子午线?

Pal*_*ndo 18

由于该区域严重缺乏文档,因此应修改Map Kit函数参考:

警告:只要您没有越过第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];
}
Run Code Online (Sandbox Code Playgroud)

/*
现在,让我们来测试吧

如何创建跨越第180个子午线的MapRect:

*/

- (void)testHowToCreateMapRectSpanning180thMeridian
{
Run Code Online (Sandbox Code Playgroud)

/*
我们将使用Google地理编码API返回亚洲位置视口,因为它跨越反默认.在东北角落在于已经在西半球,longitudal范围(-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);
Run Code Online (Sandbox Code Playgroud)

/*
作为参考,这里是整个预计世界的范围,大约2.68亿,转换为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}}
Run Code Online (Sandbox Code Playgroud)

/*
为什么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}}
Run Code Online (Sandbox Code Playgroud)

/*
看起来经度是以错误的方式交换的.亚洲是{{-12, 25}, {81, -168}}.结果MKMapRect没有通过使用MKMapRectSpans180thMeridian函数的测试- 我们知道它应该!

虚假尝试

因此,当坐标跨越antimeridian时,MKPolygon不能MKMapRect正确计算.好的,让我们自己创建地图.以下是如何将包含NE和SW坐标的特定边界拟合到可见地图视图中的两种方法

...快速方式是使用MKMapRectUnion函数的一个小技巧.从每个坐标创建一个零大小的MKMapRect,然后使用以下函数将两个rects合并为一个大的rect:

*/

// 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}}
Run Code Online (Sandbox Code Playgroud)

/*
奇怪的是,我们有与以前相同的结果.无论如何,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}}
Run Code Online (Sandbox Code Playgroud)

/*
嘿!这与以前的结果相同.当坐标穿过反射影像时,这就是纬度交换的方式.它也可能是如何MKMapRectUnion工作的.不好......
*/

// 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}}
Run Code Online (Sandbox Code Playgroud)

/*
记住,亚洲是{{-12, 25}, {81, -168}}.我们正在回到正确的坐标,但是MKMapRect并没有按照反对的方式跨越MKMapRectSpans180thMeridian.什么......?!

解决方案

提示MKOverlay.h说:

对于跨越第180个子午线的叠加层,boundingMapRect应该具有负MinX或大于MKMapSizeWorld.width的MaxX.

没有满足这些条件.更糟糕的rb.size.width是,这是 1.44亿.这绝对是错的.

当我们通过antimeridian时,我们必须更正rect值,以便满足其中一个条件:
*/

// 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}}
Run Code Online (Sandbox Code Playgroud)

/*
最后我们满意了MKMapRectSpans180thMeridian.地图矩形宽度为正.坐标怎么样?东北有经度191.6455.缠绕在全球(-360),它是-168.3545.QED

我们MKMapRect通过满足第二个条件来计算跨越第180个子午线的正确:MaxX(rc.origin.x + rc.size.width= 152870935.0 + 124248035.2 = 277118970.2)大于世界的宽度(2.68亿).

如何满足第一个条件,负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);
Run Code Online (Sandbox Code Playgroud)

/*
这也通过了MKMapRectSpans180thMeridian测试.除了西南经度之外,反向转换为地理坐标给我们匹配:-334.9841.但它包裹着世界(+360)25.0159.QED

因此有两种正确的形式来计算跨越180度子午线的MKMapRect.一个是积极的,一个是阴性的.

替代方法

上面演示的负面起源方法(rd)对应于Anna Karenina在另一个答案中提出的替代方法所获得的结果:
*/

// 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}}
Run Code Online (Sandbox Code Playgroud)

/*
因此,如果我们手动转换为MKMapPoints并捏造负面原点,即使是MKPolygon可以boundingMapRect正确计算.得到的map rect相当于上面的(rd)的nagative origin方法.
*/

STAssertTrue([MKStringFromMapRect(rp2) isEqualToString:
              MKStringFromMapRect(rd)], nil);
Run Code Online (Sandbox Code Playgroud)

/*
或者我应该说几乎相同......因为奇怪的是,以下断言会失败:
*/

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

/*
有人会猜他们知道如何比较浮点数,但我离题了...
*/

}
Run Code Online (Sandbox Code Playgroud)

测试功能源代码到此结束.

显示叠加

正如在问题中提到的,为了调试问题,我用MKPolygons来可视化正在发生的事情.事实证明,MKMapRect跨地图的两种形式的s在地图上重叠时显示不同.当您从西半球接近antimeridian时,只显示具有负原点的那个.同样,当您从东半球接近第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];
}
Run Code Online (Sandbox Code Playgroud)

我只是简单地使用蛮力并在每种形式中添加了两次多边形.

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]; 
Run Code Online (Sandbox Code Playgroud)

我希望这能帮助其他灵魂进入第180个子午线后面的龙之地.


小智 5

根据该评论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's whether + or -

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

[mapView addOverlay:p];
Run Code Online (Sandbox Code Playgroud)

所产生的p.boundingMapRect并返回YESMKMapRectSpans180thMeridian(但代码已经想通了这一点,从坐标,因为它没有与开始maprect).

但遗憾的是,使用负值创建maprect只能解决问题的一半.现在可以正确绘制日期线以东的多边形的一半.但是,日期线以西的另一半根本没有被绘制.

显然,内置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];
}
Run Code Online (Sandbox Code Playgroud)

顺便说一句,绘图MKPolyline覆盖的问题与+/- 180 相似(请参阅此问题).