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边框的右侧(大的棕色矩形,只有左下角可见):
类似地,显示俄罗斯的位置视口显示矩形位于俄罗斯边界左侧的错误位置。
当我将位置视口转换为MKMapPoint
和MKMapRect
并且在地图视口(上图中的白色矩形)和位置视口之间没有找到交叉点时,这在视觉上确认存在类似的问题。
我计算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}}
。 我们正在回到正确的坐标,但根据MKMapRect
, 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}}
/ *
最后我们对MKMapRectSpans180thMeridian
感到满意。 地图矩形宽度为正。 坐标怎么样? 东北经度为191.6455
。 环绕全球(-360),它是-168.3545
。 QED
我们通过满足第二个条件计算了跨越第180个子午线的正确MKMapRect
: 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);
/ *
这也通过了MKMapRectSpans180thMeridian
测试。 除了西南经-334.9841
,反向转换为地理坐标给我们匹配: -334.9841
。 但环绕世界( 25.0159
),它是25.0159
。 QED
因此有两种正确的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}}
/ *
因此,如果我们手动转换为MKMapPoint
并MKMapPoint
负面原点,即使是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个子午线后面的龙之地。