高德地图SDK实时绘制跑步路径-Swift实现

-_-

跑步运动软件最基本的功能之一,就是对运动中的用户进行实时定位,并绘制出运动路径。本文主要内容,就是用高德的SDK实现简单的路径绘制。(苹果内置的地图用的也是高德,当然,你也可以用百度的。)

首先,xcode创建一个新的工程,选择Single View Application,语言选择Swift

1.jpg

工程创建完毕,下一步是导入高德地图的framework。可以选择用CocoaPods,手动安装方式如下:

下载高德地图SDK,申请API Key。相关网页
(撰写本文时高德SDK的最新版本为3.3.0,需要加-Objc,否则相关的接口调用会直接崩溃,Demo使用的版本为3.2.0,不需要加标记,版本之间一些接口和变量会存在差异。)

<h3>导入SDK</h3>

左侧目录中选中工程名,在 TARGETS->Build Phases-> Link Binary With Libaries 中点击“+”按钮,在弹出的窗口中点击“Add Other”按钮,选择下载好的 MAMapKit.framework 文件添加到工程中。

2.jpg

<h3>引入AMap.bundle资源文件</h3>

AMap.bundle资源文件中存储了定位、默认大头针标注视图等图片,可利用这些资源图片进行开发。

左侧目录中选中工程名,在右键菜单中选择Add Files to “工程名”…,从 MAMapKit.framework->Resources 文件夹中选择 AMap.bundle文件,并勾选“Copy items if needed”复选框,单击“Add”按钮,将资源文件添加到工程中。

(MAMapKit.framework最好放在工程文件下,否则有可能导致编译不过。)

<h3>引入依赖库</h3>

左侧目录中选中工程名,在TARGETS->Build Settings-> Link Binary With Libaries中点击“+”按钮,在弹出的窗口中查找并选择所需的库(见下),单击“Add”按钮,将库文件添加到工程中。

libz.tbd

libstdc++.6.0.9.tbd

Security.framework

SystemConfiguration.framework

CoreTelephony.framework

OpenGLES.framework

CoreLocation.framework

CoreGraphics.framework

Foundation.framework

UIKit.framework

引入完成后如下图所示:

3.jpg

<h3>Info.plist添加属性</h3>

添加NSLocationAlwaysUsageDescription。(不添加这货的话会导致无法进行定位,被这个问题坑了下。。)

添加Required background modes,并给它添加App registers for location updates属性,使得应用在进入后台时也能继续接受定位信息,MAMapView有一个属性的设置和这个相关,后续提到。

App Transport Security Settings添加上Allow Arbitrary Loads,并设为YES,iOS9之后网络请求默认改为https,不修改会报警告

修改后info.plist如下所示

4.jpg

<h2>接下来,开始代码的实现</h2>

<h3>新建桥接头文件</h3>

SDK为第三方OC项目,在Swift中调用需要创建桥接头文件。新建头文件,命名为MapPath-Bridging-Header.h,然后设置好对应的路径,如下图:

5.jpg

Ps.先设置麻烦此时可以新建一个OC类,这时xcode会自动弹出一个是否创建桥接头文件提示框,选择Create Bridging Header即可自动生成并完成工程的对应设置。

6.jpg

文件代码实现如下

// Create-Bridging-Header.h
#import <MAMapKit/MAMapKit.h>

<h3>设置ApiKey</h3>

使用地图需要先设置ApiKey,否则会报ApiKey为空的警告。在项目启动的时候设置,所以在AppDelegatedidFinishLaunchingWithOptions进行处理

//  AppDelegate.swift
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
    // Override point for customization after application launch.
    
    // 设置apiKey
    MAMapServices.sharedServices().apiKey = "e8be3a280e6fc3c2651ebef77e7a0fa5"
    return true
}

新建一个类RunningViewController,继承自UIViewController,遵守MAMapViewDelegate,该类用来专门展示跑步过程中的地图界面,同时创建RunningViewController.xib文件

class RunningViewController: UIViewController, MAMapViewDelegate

声明属性

@IBOutlet weak var mapView: MAMapView!
var coordinateArray: [CLLocationCoordinate2D] = []

将一个UIView拖到RunningViewController.xib的视图上,设置四边具体父视图的间距为0,并链接上mapView。(懒得写代码添加视图了。)

coordinateArray数组用来存储每次获取的定位更新数据,绘制路径时需要用到。

viewDidLoad的时候进行地图初始化的设置

override func viewDidLoad() {
    super.viewDidLoad()

    initMapView()
}

func initMapView()
{
    mapView.delegate = self
    mapView.zoomLevel = 15.5
    mapView.distanceFilter = 3.0
    mapView.desiredAccuracy = kCLLocationAccuracyBestForNavigation
}

将视图控制器设置为mapView的代理,当位置变化更新时通过代理进行回调,绘制路径时也需要通过代理进行属性的设置

zoomLevel为当前地图缩放的比例

distanceFilter为定位的最小更新距离,当移动距离超过设定的值时便会有位置的更新回调

desiredAccuracy为定位精度,默认为最高精度,一般直接用这个就好

当视图显示后,开始进行定位

override func viewDidAppear(animated: Bool) {
    super.viewDidAppear(animated)
    startLocation()
}

func startLocation()
{
    mapView.showsUserLocation = true
    mapView.userTrackingMode = MAUserTrackingMode.Follow
    mapView.pausesLocationUpdatesAutomatically = false
    mapView.allowsBackgroundLocationUpdates = true
}

showsUserLocation设为true后便开始进行定位,设为false则定位停止。定位本身会比较耗点,应用在不需要定位时记得将定位功能关闭,否则你的手机电量会消耗很快滴。

userTrackingMode为定位的方式,查看接口可知有三种形式,这里采用跟随用户位置移动的定位方式

allowsBackgroundLocationUpdates设置为true表示允许进行后台定位,保证之前有设置过App registers for location updates属性,否则会崩溃。

<h3>代理的实现</h3>

每次有位置更新变会调用mapView(mapView: MAMapView, didUpdateUserLocation userLocation: MAUserLocation, updatingLocation: Bool)函数

// MARK: MAMapViewDelegate

func mapView(mapView: MAMapView, didUpdateUserLocation userLocation: MAUserLocation, updatingLocation: Bool)
{
    // 地图每次有位置更新时的回调
    
    if updatingLocation {
        // 获取新的定位数据
        let coordinate = userLocation.coordinate
        
        // 添加到保存定位点的数组
        self.coordinateArray.append(coordinate)
       
        updatePath()
    }
}

通过updatePath函数进行路径的绘制

 func updatePath () {
    
    // 每次获取到新的定位点重新绘制路径
    
    // 移除掉除之前的overlay
    let overlays = self.mapView.overlays
    self.mapView.removeOverlays(overlays)
    
    let polyline = MAPolyline(coordinates: &self.coordinateArray, count: UInt(self.coordinateArray.count))
    self.mapView.addOverlay(polyline)
    
    // 将最新的点定位到界面正中间显示
    let lastCoord = self.coordinateArray[self.coordinateArray.count - 1]
    self.mapView.setCenterCoordinate(lastCoord, animated: true)
}

之前最开始的实现方式是用最新的点和上一次点进行连接,但这种方式会有一个问题,在将地图放大到最大缩放比后会发现,通过两点两点连接的线段并不能构成一条完整的线,而是一段一段不连贯的线段。所以现在改为直接拿所有的点重新绘制整条路径。绘制之前记得将已经存在的路径移除掉,否则每次绘制的路径会堆积在一起,导致路径线条变粗长生毛刺。

removeOverlays用来移除掉之前的路径。

addOverlay添加重新绘制的路径。

setCenterCoordinate将指定的坐标点显示在地图中间,保证在用户跑了很长的距离之后也不会超出地图的显示范围。

通过addOverlay绘制路径时,会有一个函数回调:

func mapView(mapView: MAMapView!, viewForOverlay overlay: MAOverlay!) -> MAOverlayView! {
    if overlay.isKindOfClass(MAPolyline) {
        let polylineView = MAPolylineView(overlay: overlay)
        polylineView.lineWidth = 6
        polylineView.strokeColor = UIColor(red: 4 / 255.0, green:  181 / 255.0, blue:  108 / 255.0, alpha: 1.0)
        
        return polylineView
    }
    
    return nil
}

判断是否为MAPolyline类型,然后设置路径宽度和颜色。(除了线条以外,地图上贴图片等一些操作也在这个回调中进行处理)

大体实现如上。接下来可以把Demo跑起来了。

程序大体样子如下,打开时,界面中间有个按钮,点击之后便进入地图界面

7.jpg
8.jpg

此时,你可能迫不及待地想编好真机然后到外面跑跑测试下效果,其实模拟器提供了一个功能,能够直接模拟位置的变化。选中模拟器的Debug,点Location,里面有几个选项,有几种改变位置的方式,也可以通过自己设置经纬度进行定位,其中Freeway Drive运动的速度比较快~:)

绘制运动路径的效果如下图:

9.jpg

(模拟器模拟的效果地图上不会显示其他信息,真机上则会跟你实际位置相对应,地图内容是完整显示周围信息的)

具体实现见源码

<h3>最后再有个后记</h3>

iOS系统的定位采用的是混合定位的方式,通过GPS、Wifi、手机基站信号共同定位的方式来提高定位精度,虽说如此,但偶尔出现某个点的定位误差依然是难以避免的,当出现较大偏差时,会导致路径上有某个明显凸出的点,或整条路径毛刺现象严重,即使长时间在一个位置不动,也会出现定位点在附近导出乱飘的情况。这些问题只能通过算法分析来修正。

常用的处理算法为卡尔曼滤波,相关的内容对数学功底要求极高,有兴趣可自行Google研究。

之前无意在github上找到了份相关的源码实现,用的是java,改成iOS后直接拿来用发现对结果的修正确实起到一定的帮助,能将绘制的路径进行平滑处理,滤掉了明显的毛刺。

下面为源码对测试数据处理前后的结果显示

10.png
11.png

有兴趣的自己看源码修改来用的,地址见下方:

KalmanFilter的Java实现

<完> :)

推荐阅读更多精彩内容