【问题标题】:MKMapRect and displaying map overlays that span 180th meridianMKMapRect 并显示跨越 180 条子午线的地图叠加层
【发布时间】: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

【问题讨论】:

    标签: ios mapkit


    【解决方案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://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.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://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 条子午线后面的龙之国的灵魂。

    【讨论】:

      【解决方案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
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2016-12-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2021-05-29
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多