ios - MKMapRect 和显示跨越第 180 个子午线的 map 叠加层

标签 ios mapkit

我正在处理从 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 上显示此位置叠加为多边形时,它会错误地显示在美国边界的右侧(棕色大矩形,仅左下角可见):

Viewport of USA overlayed on map
Viewport of Russia overlayed on 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 条子午线,否则效果很好。测试 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;

显示跨越第 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/

相关文章:

iOS ionic 2 : How to send http request to different port than 80/443 (https)

iOS 强制音频输出仅至耳机插孔

ios - 变量不在 View Controller 之间共享

iOS:设备上的自定义位置

ios - 使用 MKPolyline 在 MKMapView 中绘制多组线

iphone - Mapkit 绘制一个圆

swift - 如何在 swiftUI 中添加返回用户位置按钮?

ios - 您可以使用 Mapkit 引用 Apple map 中的地点吗?

ios - 如何在同一注释 View (swift3)下更改mapkit中的引脚颜色

ios - 如果我不需要持久存储,那么以类似数据库的方式存储数据的最佳做法是什么? NSMutableArray?