由于这方面的记录严重不足,Map Kit Functions Reference 应修改为:
警告:只要您不越过 180 度经线,所有描述的功能都可以正常工作。
Here be dragons。您已被警告...
为了解决这个问题,我采用了古老的调查测试。请原谅散文周围的cmets。它们允许您复制和粘贴 以下所有源代码,以便您自己玩。
首先是一个小辅助函数,将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 Geocoding API 返回的location viewport of Asia,因为它跨越了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);
/*
作为参考,这里是整个投影世界的边界,大约 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}}
/*
为什么使用地理坐标创建的 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。好的,让我们自己创建地图矩形。以下是How to fit a certain bounds consisting of NE and SW coordinates into the visible map view?的答案中建议的两种方法
... 快速方法是使用 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'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 不跨越根据MKMapRectSpans180thMeridian 的反子午线。什么...?!
解决方案
来自MKOverlay.h的提示说:
对于跨越 180 度子午线的叠加层,boundingMapRect 应该具有大于 MKMapSizeWorld.width 的负 MinX 或 MaxX。
这些条件都不满足。更糟糕的是,rb.size.width 是负 1.44 亿。这绝对是错误的。
当我们通过反子午线时,我们必须纠正矩形值,以便满足其中一个条件:
*/
// 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。 Q.E.D.
通过满足第二个条件,我们计算出跨越 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。但是环游世界(+360),它是25.0159。 Q.E.D.
所以有两种正确的形式来计算跨越 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}}
/*
因此,如果我们手动转换为MKMapPoints 并捏造负原点,即使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);
/*
有人会猜他们知道如何compare floating point numbers,但我离题了...
*/
}
测试函数源码到此结束。
显示叠加层
如问题中所述,为了调试问题,我使用MKPolygons 来可视化正在发生的事情。事实证明,跨越反子午线的MKMapRects 的两种形式在地图上叠加时显示不同。当您从西半球接近逆子午线时,只会显示具有负原点的逆子午线。同样,当您从东半球接近 180 度经线时,会显示正原点形式。 MKPolygonView 不为您处理第 180 条子午线的跨度。您需要自己调整多边形点。
这是从地图矩形创建多边形的方法:
- (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 条子午线后面的龙之国的灵魂。