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

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

我已经确定了比较位置视口,当前地图视口和它们的交点(使用MKMapRectIntersection函数)的面积比(在MKMapPoint )的算法。 只要位置视口不跨越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]; 

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

 Southwest: 18.9110643, 172.4546967 Northeast: 71.3898880, -66.9453948 

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

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

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

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

我计算map rect的方式类似于这个SO问题中的答案:
如何将由NE和SW坐标组成的特定边界拟合到可见地图视图中?
…… 除非坐标跨越第180个子午线, 否则工作正常。 使用MKMapRectSpans180thMeridian测试MKMapRectSpans180thMeridian返回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度子午线的多边形叠加层的正确方法是什么?
如何正确构建跨越180度子午线的MKMapRect

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

生成的p.boundingMapRect确实为MKMapRectSpans180thMeridian返回YES (但是代码已经从坐标中找出,因为它没有以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]; } 

顺便说一句,绘制跨越+/- 180的MKPolyline叠加层也存在类似问题(请参阅此问题 )。

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

警告:只要您没有越过第180个子午线,所有描述的function都可以正常工作。
这里是龙 。 你被警告了…

为了解决这个问题,我采取了良好的旧调查测试。 请原谅散文的评论。 它们允许您逐字 复制和粘贴 所有来源 ,以便您可以自己玩。

首先是一个小帮助函数, 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]; } 

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

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

* /

 - (void)testHowToCreateMapRectSpanning180thMeridian { 

/ *
我们将使用Google地理编码API返回的亚洲位置视口 ,因为它跨越了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); 

/ *
作为参考,在转换为MKMapPoints之后,这里是整个预测世界的边界,大约2.68亿 。 我们的小助手function向我们展示了这里使用的墨卡托投影无法表达高于±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函数通过测试 – 我们知道它应该!

虚假尝试

因此,当坐标跨越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}} 

/ *
奇怪的是,我们有与以前相同的结果。 无论如何, MKPolygon应该使用MKRectUnion计算其边界是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}} 

/ *
嘿! 这与以前的结果相同。 当坐标穿过antimeridian时,这就是纬度交换的方式。 它也许就是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}} 

/ *
请记住,亚洲是{{-12, 25}, {81, -168}} 。 我们正在回到正确的坐标,但根据MKMapRectMKMapRect并没有跨越反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}} 

/ *
最后我们对MKMapRectSpans180thMeridian感到满意。 地图矩形宽度为正。 坐标怎么样? 东北经度为191.6455 。 环绕全球(-360),它是-168.3545QED

我们通过满足第二个条件计算了跨越第180个子午线的正确MKMapRectMaxXrc.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); 

/ *
这也通过了MKMapRectSpans180thMeridian测试。 除了西南经-334.9841 ,反向转换为地理坐标给我们匹配: -334.9841 。 但环绕世界( 25.0159 ),它是25.0159QED

因此有两种正确的forms来计算跨越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}} 

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

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

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

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

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

 } 

测试function源代码到此结束。

显示叠加

正如问题所述,为了调试问题,我使用MKPolygon来可视化正在发生的事情。 事实certificate,跨地图的两种forms的MKMapRect在地图上叠加时的显示方式不同。 当您从西半球接近反导数时,只会显示具有负原点的反物质。 同样,当您从东半球接近第180个子午线时,会显示正原点forms。 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]; } 

我只是使用蛮力并在每种forms中添加了两次多边形。

 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个子午线后面的龙之地。