iOS开发之地图篇(三):历史轨迹记录


月落乌啼霜满天,江枫渔火对愁眠。
姑苏城外寒山寺,夜半钟声到客船。

前言

  • 。。。
  • (过了一万年。。。)

  • 最近一直在做一个宠物定位的项目,本来以为只需要参考官方文档接入地图实现功能就可以winner winner chicken dinner了,但是等到我掉以轻心的做到后面再加上历史轨迹疯狂改需求的时候,又双叒叕加上还要百度和谷歌同步实现功能,心中开始有一万只神兽在奔腾,于是、、就有了这篇文章的由来了。

关于历史轨迹

  • 补一张效果图
    历史轨迹效果

  • 历史轨迹历经了好几个版本的在几个方案之间来回拉锯,现在终于是。。。还没有定下来具体的方案了🤣

    • 方案一:选时间段查询历史轨迹数据后全部显示在地图上面,然后可以点击播放按钮回放历史轨迹并画线;或者拖动一个UISlideBar滑动条可以切换每一轨迹点并显示轨迹详细信息弹框。
    • 方案二:选时间段查询历史轨迹数据后显示第一个点在地图上面,然后可以通过四个操作按钮(起点,上一个,下一个,终点)切换轨迹点画线并显示轨迹详细信息弹框;或者手动点击切换每一轨迹点并显示轨迹详细信息弹框。
    • 方案三:选时间段查询历史轨迹数据后全部显示在地图上面,然后可以通过四个操作按钮(起点,上一个,下一个,终点)切换轨迹点并显示轨迹详细信息弹框;或者手动点击切换每一轨迹点并显示轨迹详细信息弹框。
    • 方案四五六七八九…待客户和老板定。
  • 问题

    • 方案一:查询轨迹过多导致获取数据比较耗时,显示点比较多画线乱。
    • 方案二:筛选所选时间段的估计点(抽取平均10个点出来,后来又改为5个,在在后来改为服务器配置),画线功能由服务器配置,选择时间段间隔服务器配置。
    • 方案三:在方案二的基础上,分页获取所有数据并全部显示
    • 方案四五六七八九…未知

百度地图历史轨迹

开始挖坑

  • 历史古迹无非就是多个位置点构成,所以和上一篇的添加坐标点大头针一样,只是添加多个而已,考虑到控件的重用机制,所以有了下面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ... 遍历历史古迹数据 创建地图大头针
// ... 对每一个坐标反地理编码
// ... 在反地理编码里面刷新对应的模型信息地址 并刷新自定义弹框paopaoView
- (BMKAnnotationView *)mapView:(BMKMapView *)mapView viewForAnnotation:(id <BMKAnnotation>)annotation
{
if (![annotation isKindOfClass:[XSPointAnnotation class]]) { // 不是自定义大头针模型
return nil;
}
BMKAnnotationView *annotationView = nil;
XSPointAnnotation *customAnnotation = (XSPointAnnotation *)annotation;
if (customAnnotation.annotationType == XSAnnotationTypeLocationPoint) { // 当前位置点类型
annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"LocationAnnotation"];
if (annotationView == nil) {
annotationView = [[BMKPinAnnotationView alloc] initWithAnnotation:customAnnotation reuseIdentifier:@"LocationAnnotation"];
}
annotationView.image = [UIImage imageNamed:[customAnnotation.locationDetailsModel.deviceBindingTypeImageName stringByAppendingString:@"_small"]];
annotationView.draggable = NO;
annotationView.canShowCallout = YES;
self.petAnnotationView.locationDetails = customAnnotation.locationDetailsModel;
self.petAnnotationView.isHiddenNoNeeds = YES;
self.petAnnotationView.width = 270;
annotationView.paopaoView = [[BMKActionPaopaoView alloc] initWithCustomView:self.petAnnotationView];
[annotationView setSelected:YES animated:NO]; // 选中当前的
}
return annotationView;
}

掉坑里了

  • 如果所有的大头针都共用一个自定义的paopaoView(petAnnotationView)的话,就会导致selectAnnotation:animated:方法没有效果
  • 并且只有最后添加的那一个轨迹带你点击才会调用selectAnnotation:animated: 其他大头针点击都会失效
  • 如果要控制切换轨迹点刷新显示弹框信息,那就需要根据索引先移除当前点,再重新添加重新渲染,而且切换不流畅,会出现莫名的问题

怎么从坑里跳出来?

  • 1、找大牛
  • 2、查资料
  • 3、习惯有坑,顺其自然

  • 但是

  • 作为一个视这世界上只有两件事是真理:人都会死;程序永远有bug为座右铭的程序猿,当然是选择3啊,但是考虑到这样有可能会被打死,而求人又不如求己的理念,我就开始苦逼的查资料,看官方demo和官方论坛(屎一样的论坛,永远回复:在处理中、感谢你的反馈)
  • 经过我的反复查询资料,不断尝试,最后终于在我的灵机一动下解决了,再次印证了爱迪生的话但那1%的灵感是最重要的,甚至比那99%的汗水都要重要。😂

    • 既然一个轨迹点可以正常的点击显示和隐藏,是不是需要每一个轨迹点大头针都需要绑定一个paopaoView呢
    • 本着实践是检验真理的唯一标准,于是有了以下代码

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      - (BMKAnnotationView *)mapView:(BMKMapView *)mapView viewForAnnotation:(id <BMKAnnotation>)annotation
      {
      if (![annotation isKindOfClass:[XSPointAnnotation class]]) { // 不是自定义大头针模型
      return nil;
      }
      BMKAnnotationView *annotationView = nil;
      XSPointAnnotation *customAnnotation = (XSPointAnnotation *)annotation;
      if (customAnnotation.annotationType == XSAnnotationTypeLocationPoint) { // 当前位置点类型
      annotationView = [mapView dequeueReusableAnnotationViewWithIdentifier:@"XSTracksLocationAnnotation"];
      if (annotationView == nil) {
      annotationView = [[BMKPinAnnotationView alloc] initWithAnnotation:customAnnotation reuseIdentifier:@"XSTracksLocationAnnotation"];
      }
      XSLocationDetails *locationDetails = customAnnotation.locationDetailsModel;
      XSPetAnnotationView *petAnnotationView = [XSPetAnnotationView petAnnotationView];
      annotationView.image = [UIImage imageNamed:[customAnnotation.locationDetailsModel.deviceBindingTypeImageName stringByAppendingString:@"_small"]];
      annotationView.draggable = NO;
      annotationView.canShowCallout = YES;
      petAnnotationView.isHiddenNoNeeds = YES;
      petAnnotationView.width = 270;
      petAnnotationView.locationDetails = locationDetails;
      annotationView.paopaoView = [[BMKActionPaopaoView alloc] initWithCustomView:petAnnotationView];
      self.petAnnotationView = petAnnotationView;
      }
      return annotationView;
      }
  • 是骡子是马拉出来溜溜,编译运行,轨迹点都可以点击了,哇,开心的就像一个几十岁的孩子拍了拍肚皮,正准备去喝杯Coffee舒爽一下呢,发现地址信息没有

  • 于是开始考虑现在是一个轨迹点对应一个自定义的paopaoView,那么在反地理编码的代理回调里面怎么把地址信息对应的轨迹点模型更新呢
    • 根据代理回调里面location坐标和当前的所有历史轨迹数据坐标对比, 开心的打印一下,竟然和传递的转换前的坐标有误差;
    • 仿谷歌地图反地理编码,改代理回调为block回调?直接动手
1
2
3
4
5
6
7
8
9
// XSBaiduGeocoder.h
typedef void (^XSReverseGeocodeHandler)(BMKReverseGeoCodeResult *result);

@interface XSBaiduGeocoder : NSObject

+ (instancetype)geocoder;
- (void)reverseGeocodeCoordinate:(CLLocationCoordinate2D)coordinate completionHandler:(XSReverseGeocodeHandler)handler;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// XSBaiduGeocoder.m
@interface XSBaiduGeocoder () <BMKGeoCodeSearchDelegate>

@property (nonatomic, copy) XSReverseGeocodeHandler reverseGeocodeHandler;

@end

@implementation XSBaiduGeocoder

+ (instancetype)geocoder
{
static XSBaiduGeocoder *helper;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
helper = [[XSBaiduGeocoder alloc] init];
});
return helper;
}

- (void)reverseGeocodeCoordinate:(CLLocationCoordinate2D)coordinate completionHandler:(XSReverseGeocodeHandler)handler
{
XSLogFunc
self.reverseGeocodeHandler = handler;
// 解析地址
BMKReverseGeoCodeOption *reverseGeocodeSearchOption = [[BMKReverseGeoCodeOption alloc] init];
reverseGeocodeSearchOption.reverseGeoPoint = coordinate;
BMKGeoCodeSearch *geoCodeSearch = [[XSGeoCodeSearch alloc] init];
geoCodeSearch.delegate = self;
if (![geoCodeSearch reverseGeoCode:reverseGeocodeSearchOption]) {
XSLog(@"百度反地理编码获取地址失败");
} else {
XSLog(@"百度反地理编码获取地址成功");
}
}

- (void)onGetReverseGeoCodeResult:(BMKGeoCodeSearch *)searcher result:(BMKReverseGeoCodeResult *)result errorCode:(BMKSearchErrorCode)error
{
XSLogFunc
if (self.reverseGeocodeHandler) {
self.reverseGeocodeHandler(result);
}
}
@end
  • BMKGeoCodeSearch这个一定要调用一次创建一次,否则只会转换一次地址信息
  • 但是此时虽然轨迹点地址信息都转换了,但是只有点击最后一个轨迹点有地址信息,其他都是空的,这个问题卡了好久脑子没转过来,一直在block捕获方向考虑,后来突然醒悟
    • self.reverseGeocodeHandler = handlerblock是每次都进来赋值,而地址转换完成的代理回调方法又调用延迟,那在代理回调方法里面回调临时保存的self.reverseGeocodeHandler肯定是最后一个,所以就只会更新最后一个轨迹坐标的模型地址数据
    • 既然BMKGeoCodeSearch是每一个坐标对应一个,那个完全可以自定义XSGeoCodeSearch继承BMKGeoCodeSearch添加对应的回调handler,等地址转换完成的代理回调时候,根据XSGeoCodeSearch保存的handler回调回去,于是代码修改如下
1
2
3
4
5
6
7
8
9
10
// XSGeoCodeSearch.h
@class BMKReverseGeoCodeResult;

typedef void (^XSReverseGeocodeHandler)(BMKReverseGeoCodeResult *result);

@interface XSGeoCodeSearch : BMKGeoCodeSearch

@property (nonatomic, copy) XSReverseGeocodeHandler reverseGeocodeHandler;

@end
1
2
3
4
5
6
7
// XSBaiduGeocoder.h
@interface XSBaiduGeocoder : NSObject

+ (instancetype)geocoder;
- (void)reverseGeocodeCoordinate:(CLLocationCoordinate2D)coordinate completionHandler:(XSReverseGeocodeHandler)handler;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// XSBaiduGeocoder.m
@interface XSBaiduGeocoder () <BMKGeoCodeSearchDelegate>

@end

@implementation XSBaiduGeocoder

+ (instancetype)geocoder
{
static XSBaiduGeocoder *helper;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
helper = [[XSBaiduGeocoder alloc] init];
});
return helper;
}

- (void)reverseGeocodeCoordinate:(CLLocationCoordinate2D)coordinate completionHandler:(XSReverseGeocodeHandler)handler
{
XSLogFunc
// 解析地址
BMKReverseGeoCodeOption *reverseGeocodeSearchOption = [[BMKReverseGeoCodeOption alloc] init];
reverseGeocodeSearchOption.reverseGeoPoint = coordinate;
XSGeoCodeSearch *geoCodeSearch = [[XSGeoCodeSearch alloc] init];
geoCodeSearch.reverseGeocodeHandler = handler;
geoCodeSearch.delegate = self;
if (![geoCodeSearch reverseGeoCode:reverseGeocodeSearchOption]) {
XSLog(@"百度反地理编码获取地址失败");
} else {
XSLog(@"百度反地理编码获取地址成功");
}
}

- (void)onGetReverseGeoCodeResult:(BMKGeoCodeSearch *)searcher result:(BMKReverseGeoCodeResult *)result errorCode:(BMKSearchErrorCode)error
{
XSLogFunc
// 查询searcher 对应的block 然后回调
XSGeoCodeSearch *codeSearcher = (XSGeoCodeSearch *)searcher;
if (codeSearcher.reverseGeocodeHandler) {
codeSearcher.reverseGeocodeHandler(result);
}
}
@end
  • 再运行,点击切换轨迹点,世界都美好了,操作轨迹点的切换也可以直接使用selectAnnotation:animated:弹出详情自定义弹框了

轨迹点区域控制

  • 如果想控制所有的估计点都在屏幕范围内显示,最早使用的上一篇里面的方法,后来查资料看这一种也可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)baiduMapViewFitAnnotationsWithCoordinates:(NSArray *)coordsModels count:(NSUInteger)count
{
//创建两个数组,用来存所有的经度和纬度
if (count < 2) return;
NSMutableArray *latArr = [[NSMutableArray alloc] init];
NSMutableArray *lonArr = [[NSMutableArray alloc] init];
for (XSLocationDetails *locationDetails in coordsModels) {
[latArr addObject:@(locationDetails.coordinateBD09ll.latitude)];
[lonArr addObject:@(locationDetails.coordinateBD09ll.longitude)];
}
NSNumber *latMax = [latArr valueForKeyPath:@"@max.floatValue"];//最大纬度
NSNumber *latMin = [latArr valueForKeyPath:@"@min.floatValue"];//最小纬度
NSNumber *lonMax = [lonArr valueForKeyPath:@"@max.floatValue"];//最大经度
NSNumber *lonMin = [lonArr valueForKeyPath:@"@min.floatValue"];//最小经度

BMKCoordinateRegion region;
region.center.latitude = ([latMax doubleValue] + [latMin doubleValue]) / 2;
region.center.longitude = ([lonMax doubleValue] + [lonMin doubleValue]) / 2;
region.span.latitudeDelta = 0.0055; // 数字越小 缩放等级越大
region.span.longitudeDelta = 0.0055;
region = [self.baiduMapView regionThatFits:region];
[self.baiduMapView setRegion:region animated:YES];
}
  • 当然还有更简单的方法,所以一定要多看各种SDK的头文件,看都有哪些功能
1
2
3
4
5
/**
*设置地图使显示区域显示所有annotations,如果数组中只有一个则直接设置地图中心为annotation的位置
*@param annotations 指定的标注
*@param animated 是否启动动画
*/- (void)showAnnotations:(NSArray *)annotations animated:(BOOL)animated;

谷歌地图历史轨迹

  • 关于谷歌地图历史轨迹,基本上就没什么说的了,接口相比百度更规范和清晰
  • 谷歌地图添加Marker(相当于百度地图Annotation)直接配置大头针各种属性然后marker.map = self.googleMapView就等于添加了一个大头针,没有回调
  • 谷歌地图触发paopaoView的时候,调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (UIView *)mapView:(GMSMapView *)mapView markerInfoWindow:(GMSMarker *)marker
{
XSMarker *myMarker = (XSMarker *)marker;
XSLocationDetails *locationModel = myMarker.locationDetailsModel;
XSPetAnnotationView *petAnnotationView = [XSPetAnnotationView petAnnotationView];
petAnnotationView.isHiddenNoNeeds = YES;
[[GMSGeocoder geocoder] reverseGeocodeCoordinate:locationModel.locationCoordinate2D completionHandler:^(GMSReverseGeocodeResponse * _Nullable response, NSError * _Nullable error) {
// 获取第一个位置信息
GMSAddress *addressModel = response.firstResult;
NSString *firstString = addressModel.lines.firstObject;
NSString *lastString = addressModel.lines.lastObject;
NSString *address = [NSString stringWithFormat:@"%@, %@", firstString, lastString];
locationModel.deviceAddress = address;
petAnnotationView.locationDetails = locationModel; // 地址转换处理重新刷新界面
}];
petAnnotationView.width = 270;
petAnnotationView.locationDetails = locationModel;
return petAnnotationView;
}
  • 谷歌地图貌似没有可以获取地图上所有Marker的方法(可能我没找到),需要添加的时候自己维护一个数组
  • 谷歌地图的反地理编码本来就是block回调
  • 谷歌地图控制所有轨迹点范围的实现
1
2
3
4
5
6
7
8
- (void)googleMapViewFitAnnotationsWithCoordinates:(NSArray *)coordsModels count:(NSUInteger)count
{
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] init];
for (XSLocationDetails *locationDetails in coordsModels) {
bounds = [bounds includingCoordinate:locationDetails.locationCoordinate2D];
}
[self.googleMapView animateWithCameraUpdate:[GMSCameraUpdate fitBounds:bounds withPadding:30.0f]];
}
要不要鼓励一下😘