我正在处理从 Google Geocoding API 返回的视口(viewport)和边界。对给定坐标进行反向地理编码时,该服务会返回多个具有不同粒度(国家、行政区域、地区、子地区、路线等)的结果。我想根据 map 上的当前可见区域选择最合适的结果。
我已经确定了比较位置视口(viewport)、当前 map 视口(viewport)及其交集(使用 MKMapPoint
函数)的面积比率(在 MKMapRectIntersection
² 中)的算法。只要位置视口(viewport)不跨越 180 度子午线,这就会非常有效。在这种情况下,它们的交集为 0。
我已经开始调查原因,作为调试帮助,我确实显示了 MKPolygon
map 上的叠加层,为我提供有关正在发生的事情的视觉线索。为了避免我的代码在地理坐标和 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];
例如有问题的位置,这里是美国返回的视口(viewport),类型为 country 的最后一个结果,当 geocoding coordinates somewhere in Virginia :
Southwest: 18.9110643, 172.4546967
Northeast: 71.3898880, -66.9453948
请注意位于位置视口(viewport)左下角的西南坐标如何穿过 180 度子午线。当在 map 上显示此位置叠加为多边形时,它会错误地显示在美国边界的右侧(棕色大矩形,仅左下角可见):
同样,显示 location viewport for Russia显示了错误地定位在俄罗斯边界左侧的矩形。
当我将位置视口(viewport)转换为
MKMapPoint
时,这在视觉上证实了存在类似的问题。 s 和 MKMapRect
并发现 map 视口(viewport)(上图中的白色矩形)和位置视口(viewport)之间没有交集。我计算 map rect 的方式类似于这个 SO 问题中的答案:
How to fit a certain bounds consisting of NE and SW coordinates into the visible map view?
...除非坐标跨越第 180 条子午线,否则效果很好。测试
MKMapRect
与 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 条子午线的多边形叠加层的正确方法是什么?
如何正确构建
MKMapRect
跨越第 180 条子午线?
最佳答案
由于该区域的记录严重不足,因此 Map Kit Functions Reference 应修改为:
Warning: All the described functions work fine, as long as you do not cross the 180th meridian.
Here be dragons. You have been warned...
为了解决这个问题,我求助于古老的调查测试。请原谅围绕散文的评论。它们允许您逐字复制 复制和粘贴 下的所有源,以便您可以自己玩。
首先是一个小辅助函数,它将
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
叠加层显示在 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}}
/*
看起来经度以错误的方式交换。亚洲是
{{-12, 25}, {81, -168}}
。使用 MKMapRect
函数生成的 MKMapRectSpans180thMeridian
没有通过测试——我们知道它应该通过!错误尝试
因此,当坐标跨越反子午线时,
MKPolygon
无法正确计算 MKMapRect
。好的,让我们自己创建 map 矩形。以下是 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}}
/*
奇怪的是,我们得到了和以前一样的结果。无论如何,
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
的提示说:For overlays that span the 180th meridian, boundingMapRect should have either a negative MinX or a MaxX that is greater than MKMapSizeWorld.width.
这些条件都不满足。更糟糕的是,
rb.size.width
是 负 1.44 亿。那肯定是错误的。当我们通过反子午线时,我们必须更正 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
。 map 矩形宽度为正。坐标呢? Northeast 的经度为 191.6455
。环绕地球(-360),它是 -168.3545
。 Q.E.D. 我们已经通过满足第二个条件计算了跨越第 180 条子午线的正确
MKMapRect
: MaxX ( rc.origin.x + rc.size.width
= 152870935.0 + 124248035.2 = 276.81 百万) 的世界宽度。如何满足第一个条件,负 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}}
/*
因此,如果我们手动转换为
MKMapPoint
并捏造负原点,即使 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 ,但我离题了......
*/
}
测试函数源代码到此结束。
显示叠加
正如问题中提到的,为了调试问题,我使用
MKPolygon
s 来可视化正在发生的事情。事实证明,跨越反子午线的 MKMapRect
的两种形式在 map 上叠加时显示不同。当您从西半球接近反子午线时,只会显示负起源的那个。同样,当您从东半球接近第 180 条子午线时,会显示正原点形式。 MKPolygonView
不会为您处理第 180 条子午线的跨越。您需要自己调整多边形点。这是从 map 矩形创建多边形的方法:
- (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 条子午线后龙之国的灵魂。
关于ios - MKMapRect 和显示跨越第 180 个子午线的 map 叠加层,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/9022490/