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

Posted

技术标签:

【中文标题】MKMapRect 并显示跨越 180 条子午线的地图叠加层【英文标题】:MKMapRect and displaying map overlays that span 180th meridian 【发布时间】:2012-01-26 17:29:56 【问题描述】:

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

我已经确定了比较位置视口、当前地图视口及其交点的区域比率(MKMapPoint²)的算法(使用MKMapRectIntersection 函数)。只要位置视口不跨越 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];

例如有问题的位置,这里是为美国返回的视口,类型为国家的最后一个结果,当geocoding coordinates somewhere in Virginia:

Southwest: 18.9110643, 172.4546967  
Northeast: 71.3898880, -66.9453948

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

同样,显示location viewport for Russia 会显示矩形错误地位于俄罗斯边界的左侧。

当我将位置视口转换为MKMapPoints 和MKMapRect 并发现地图视口(上图中的白色矩形)和位置视口之间没有交集时,这在视觉上确认存在类似的问题。

我计算地图矩形的方式类似于这个 SO 问题中的答案:How to fit a certain bounds consisting of NE and SW coordinates into the visible map view? ...效果很好除非坐标跨越第 180 条子午线。用MKMapRectSpans180thMeridian测试MKMapRect返回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

【问题讨论】:

【参考方案1】:

由于这方面的记录严重不足,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://***.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://***.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.3545Q.E.D.

通过满足第二个条件,我们计算出跨越 180 条子午线的正确 MKMapRectMaxX (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.0159Q.E.D.

所以有两种正确的形式来计算跨越 180 条子午线的 MKMapRect。一种具有阳性,一种具有阴性来源

替代方法

上面演示的负起源方法(rd)对应于Anna Karenina在此问题的另一个答案中建议的替代方法获得的结果: */

// https://***.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 条子午线后面的龙之国的灵魂。

【讨论】:

【参考方案2】:

根据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 叠加层也存在类似问题(请参阅 this question)。

【讨论】:

【参考方案3】:

简而言之,如果多边形穿过反子午线,请检查 mapPoints。 如果 mapPoint.x 大于 primeMeridian.x,则从 mapPoint.x 中减去世界的宽度。

这会将地图沿本初子午线拆分。 minX 为负,mapSize 小于世界的宽度。 Palimondo 的回答对弄清楚这一点非常有帮助。

我正在使用 geodesicPolylines 并为此花费了几天时间。终于找到答案了!

/// Note: If both the prime meridian and the antimeridian are crossed, an empty polygon will be returned
func makePolygon(lines: [LineAnnotation]) -> MKPolygon 
    let polylines = lines.map( $0.polyline )
    let sum = polylines.reduce(0,  $0 + $1.pointCount )
    let pointer = UnsafeMutablePointer<MKMapPoint>.allocate(capacity: sum)
    var advance = 0
    let spans180thMeridian = polylines.contains(where:  $0.boundingMapRect.spans180thMeridian )
    let primeMeridianMapPoint = MKMapPoint(CLLocationCoordinate2D(latitude: 0, longitude: 0))
    let spansPrimeMeridian = polylines.contains(where: 
        return $0.boundingMapRect.minX <= primeMeridianMapPoint.x && $0.boundingMapRect.maxX >= primeMeridianMapPoint.x
    )
    guard !(spans180thMeridian && spansPrimeMeridian) else  return MKPolygon() 
    if spans180thMeridian 
        for polyline in polylines 
            // initialize the pointer with a copy of the polyline points, adjusted if needed
            let points = UnsafeMutablePointer<MKMapPoint>.allocate(capacity: polyline.pointCount)
            points.initialize(from: polyline.points(), count: polyline.pointCount)
            for i in 0..<polyline.pointCount 
                let pointPointer = points.advanced(by: i)
                if pointPointer.pointee.x > primeMeridianMapPoint.x 
                    pointPointer.pointee.x -= MKMapSize.world.width
                
            
            pointer.advanced(by: advance).initialize(from: points, count: polyline.pointCount)
            advance += polyline.pointCount
            points.deinitialize(count: polyline.pointCount)
            points.deallocate()
        

     else 
        // initialize the pointer with the polyline points
        for polyline in polylines 
            pointer.advanced(by: advance).initialize(from: polyline.points(), count: polyline.pointCount)
            advance += polyline.pointCount
        
    
    let polygon = MKPolygon(points: pointer, count: sum)
    print(polygon.boundingMapRect)
    pointer.deinitialize(count: sum)
    pointer.deallocate()
    return polygon

【讨论】:

以上是关于MKMapRect 并显示跨越 180 条子午线的地图叠加层的主要内容,如果未能解决你的问题,请参考以下文章

如何正确偏移 MKMapRect

经纬度数据在模型如何运用

如何创建给出了两个百分点MKMapRect,每一个经度和纬度值规定?

将 MKTileOverlayPath 转换为 MKMapRect?

简述高斯-克吕格投影及其特点

经纬度是怎么排列的?