经常有人问我,ArcGIS怎么弄,怎么弄。
作为当年在厦门搞过商业级天地图项目的我来说,是时候展现一波技术了,就把干货告诉大家,让大家好好搞。不仅会介绍API,还会介绍思路,以及分析过程等等。
前公司项目:天地图·厦门,已发布在App Store,欢迎下载查看。
天地图·厦门 首页
[AGSRuntimeEnvironment setClientID:clientID error:&error];
用来设置认证,只有通过认证,地图才不会显示开发版。
self.mapView.locationDisplay.showsPing = NO;//取消闪烁的光圈
self.mapView.locationDisplay.showsAccuracy = NO;//取消闪烁的光圈
通过这两段代码可以取消ArcGIS闪烁的光圈。
self.mapView.locationDisplay.location.accuracy = 10;
设置定位精度,单位是米。
self.mapView.layerDelegate = self;
设置层代理,主要用到了mapViewDidLoad的代理方法,当地图加载完成时调用。
self.mapView.touchDelegate = self;
设置触碰代理,主要用到了didClickAtPoint的代理方法,当点击地图时调用。
self.mapView.minScale = 100000000;
self.mapView.maxScale = 1000;
设置地图的放大倍数和缩小倍数,ArcGIS不会自己缩小到很小,需要你来设置,通过设置这个参数,可以看到门牌号。
[self.mapView enableWrapAround];
设置允许地图环绕,如果不设置这个参数,左右滑动地图时会到底。比如向左滑动到美国时就停止了,设置了这个参数,滑动到美国,可以继续左滑到英国,再回到美国,形成循环。
AGSSpatialReference *sr = [AGSSpatialReference spatialReferenceWithWKID:4490];
AGSEnvelope *env = [AGSEnvelope envelopeWithXmin:117.85362348365
ymin:24.398242072050003
xmax:118.48276347535
ymax:24.93150561495
spatialReference:sr];
[self.mapView zoomToEnvelope:env animated:NO];
设置初始定位区域,我将它定位在厦门。
以上方法我放在viewDidLoad方法中执行。
[self.mapView.locationDisplay startDataSource];
在地图上显示设备位置。
[self.mapView.locationDisplay addObserver:self
forKeyPath:@"autoPanMode"
options:(NSKeyValueObservingOptionNew)
context:NULL];
监听地图的定位情况,用来控制当前是否处于地图的中心
[self.mapView addObserver:self
forKeyPath:@"mapScale"
options:(NSKeyValueObservingOptionNew)
context:NULL];
地图缩放系数改变时调用,我用来取消搜索框键盘。
[self.mapView addObserver:self
forKeyPath:@"visibleAreaEnvelope"
options:(NSKeyValueObservingOptionNew)
context:NULL];
地图可见区域监测,实时获取地图中心位置,纠错的时候可以用到。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(mapViewDidEndZooming:)
name:AGSMapViewDidEndZoomingNotification
object:nil];
地图停止缩放时触发,用来判断是否需要移除全球地图,提高加载速度。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(mapViewDidEndPanning:)
name:AGSMapViewDidEndPanningNotification
object:nil];
地图停止平移时触发。
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self
action:@selector(handleMapPan:)];
[self.mapView addGestureRecognizer:panGestureRecognizer];
处理地图平移。
以上方法我放在mapViewDidLoad方法中执行。
[self.mapView zoomIn:YES];
[self.mapView zoomOut:YES];
地图的放大和缩小操作。
TDTTiledServiceLayer *tianDiTuLyr = [[TDTTiledServiceLayer alloc] initWithLayerType:type LocalServiceURL:nil error:&err];
添加全球地图操作,TDTTiledServiceLayer是我自己自定义的类,用来加载全球地图用的。遗憾的是,ArcGIS并没有使用一个类来处理加载这一类型全球底图的操作,所以要自己去实现,而这个实现过程过于复杂,我当初也是花了不少时间,篇幅的限制,就不在这里细细展开,因为细细展开的话,文章太长太长了。
[self.mapView insertMapLayer:tianDiTuLyr withName:name atIndex:0];
在某个层级插入地图。地图上放置一张底图,如果上面有什么信息的话,就往上面加一层,需要什么样的信息,就叠加什么样的层,层级处理的理论基础与iOS视图或者cocos 2D都是一样的。
id tiledLayer = [AGSLocalTiledLayer localTiledLayerWithPath:fileName];
加载本地图层,如果本地有tpk文件,可以直接加载。tpk文件和离线地图有紧密的关系,因为离线地图下载下来的就是tpk文件,tpk文件是离线地图数据格式,之后会说到离线地图。
id tiledLayer = [MDTiledMapServiceLayer tiledMapServiceLayerWithURL:url];
MDTiledMapServiceLayer是我自己定义的,继承于AGSTiledMapServiceLayer,根据需要,可能要重写urlForTileKey方法,并配置好column、row、level等信息,而level信息要与服务端约定好。
以上内容是图层的初始化和叠加操作。
接下来介绍专题图的加载。
所谓的专题图,无非是各种功能或者数据显示图,比如在地图上显示自行车位置,显示水坝位置,显示公园位置,这些都是一个个图层,需要往地图上叠加。而加载这些图层用到的地图类也不尽相同。比如:
AGSDynamicMapServiceLayer layer = [AGSDynamicMapServiceLayer dynamicMapServiceLayerWithURL:url];
用来加载动态图层。
AGSGraphicsLayer *graphicsLayer = [AGSGraphicsLayer graphicsLayer];
用来加载图像层。
我目前用到这两个,当然还有其他很多图层,之前都有研究过,走过不少弯路。
AGSCredential *credential = [[AGSCredential alloc] initWithToken:data.appToken referer:data.appReferer];
地图凭证,保证系统的安全和对用户的控制。
AGSQueryTask *queryTask = [AGSQueryTask queryTaskWithURL:url credential:credential];
queryTask.delegate = self;
创建查询任务。
AGSQuery *query = [AGSQuery query];
query.whereClause = data.whereClause;
query.outFields = [NSArray arrayWithObject:@"*"];
query.returnGeometry = YES;
query.outSpatialReference = self.mapView.spatialReference;
[queryTask executeWithQuery:query];
创建查询参数。
查询任务和查询参数用于显示图层上已经标出的指定的点。有些需求可能需要对这些点进行抽稀,我在抽稀过程中使用了冒泡算法,加了点动态规划。
AGSIdentifyTask *identifyTask = [AGSIdentifyTask identifyTaskWithURL:url];
identifyTask.delegate = self;
创建一个Id任务,之前使用这个去加载,因为内容定制度较差,后来改用服务端进行。
但无论是查询任务或者Id任务,它们都需要设置代理,然后实现代理的方法,确保操作得到响应。
接下来介绍测距和测面。
AGSSketchGraphicsLayer *measureSketchLayer = [AGSSketchGraphicsLayer graphicsLayer];
需要使用到该图层,在这图层上进行绘制。
然后你需要理解图层上几个相关变量的含义,比如:
midVertexSymbol
allowHitTest
vertexSymbol
selectedVertexSymbol
mainSymbol
geometry
特别是geometry,几种几何图形不同的创建方式如下:
//创建线
measureSketchLayer.geometry = [[AGSMutablePolyline alloc] initWithSpatialReference:self.mapView.spatialReference];
//创建面
measureSketchLayer.geometry = [[AGSMutablePolygon alloc] initWithSpatialReference:self.mapView.spatialReference];
记得设置一下地图的触摸代理。
self.mapView.touchDelegate = measureSketchLayer;
下面的通知,AGSSketchGraphicsLayerGeometryDidChangeNotification,是用来监测几何体的形状变化。
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(respondToGeomChanged:)
name:AGSSketchGraphicsLayerGeometryDidChangeNotification
object:nil];
在这个方法里面处理距离变化或者面积变化,以及处理之后要介绍的标绘内容。
介绍一下距离和面积的计算方法:
AGSGeometry *sketchGeometry = measureSketchLayer.geometry;
AGSGeometryEngine *geometryEngine = [AGSGeometryEngine defaultGeometryEngine];
self.distance = [geometryEngine geodesicLengthOfGeometry:sketchGeometry inUnit:AGSSRUnitMeter];
距离计算,单位是米。
AGSGeometry *sketchGeometry = measureSketchLayer.geometry;
AGSGeometryEngine *geometryEngine = [AGSGeometryEngine defaultGeometryEngine];
self.area = [geometryEngine shapePreservingAreaOfGeometry:sketchGeometry inUnit:AGSAreaUnitsSquareMeters];
面积计算,单位是平方米。
接下来介绍标绘,包括点标绘、线标绘、面标绘、文字标绘。
创建方式:
//创建点
measureSketchLayer.geometry = [[AGSMutablePoint alloc] initWithX:NAN y:NAN spatialReference:self.mapView.spatialReference];
//创建线
measureSketchLayer.geometry = [[AGSMutablePolyline alloc] initWithSpatialReference:self.mapView.spatialReference];
//创建面
measureSketchLayer.geometry = [[AGSMutablePolygon alloc] initWithSpatialReference:self.mapView.spatialReference];
在AGSSketchGraphicsLayerGeometryDidChangeNotification的通知方法中,更新点的信息。
如果你希望在点上面增加自定义视图,可以这么写:
self.mapView.callout.customView = antherView;
其中antherView,是我自己定义的一个视图,比如在视图上放个删除按钮,放个label显示数据等等,都可以。
如果你想创建文字标绘,你需要这么写:
AGSTextSymbol *textSymbol = [[AGSTextSymbol alloc] initWithText:self.plottingData.title
color:[UIColor redColor]];
textSymbol.fontFamily = @"Heiti SC";
textSymbol.vAlignment = AGSTextSymbolVAlignmentMiddle;
textSymbol.hAlignment = AGSTextSymbolHAlignmentCenter;
textSymbol.fontSize = 13;
textSymbol.offset = CGPointMake(0, 0);
AGSCompositeSymbol *compositeSymbol = [AGSCompositeSymbol compositeSymbol];
[compositeSymbol addSymbol:textSymbol];
AGSPoint *point_ags = [[AGSPoint alloc] initWithX:point.x y:point.y spatialReference:self.mapView.spatialReference];
AGSGraphic *graphic = [AGSGraphic graphicWithGeometry:point_ags symbol:compositeSymbol attributes:nil];
其中各种参数的含义,你通过英文的字面意思或者实际操作,应该都能理解。
有一个业务需求提到需要保存标绘数据,研究发现AGSGeometry实现了AGSCoding协议,可以将AGSGeometry对象转成NSDictionary,这样就可以使用归档保存在本地,归档类NSKeyedArchiver。
接下来介绍纠错,路线规划,运动,搜索,离线地图。
纠错需要让地图标识符一直处于地图的中点,调用的函数为:
CGPoint point = self.view.center;
AGSPoint *mapPoint = [self.mapView toMapPoint:point];
配合之前说的visibleAreaEnvelope就可以实现了。
路线规划涉及到画路线轨迹,先创建一个图像层AGSGraphicsLayer,然后创建直线,或者在直线终点创建点。
//创建直线,添加路径
AGSMutablePolyline *polyline = [[AGSMutablePolyline alloc] initWithSpatialReference:self.mapView.spatialReference];
[polyline addPathToPolyline];
//创建点,添加点路径
AGSPoint *point = [AGSPoint pointWithX:x y:y spatialReference:self.mapView.spatialReference];
[polyline addPointToPath:point];
起点和终点的符号是由图片和文字组成,你需要这么写:
//创建一个图片符号
AGSPictureMarkerSymbol *marker = [AGSPictureMarkerSymbol pictureMarkerSymbolWithImage:image];
//创建一个文字符号
AGSTextSymbol *textSymbol = [[AGSTextSymbol alloc] initWithText:text
color:[UIColor whiteColor]];
//然后创建一个AGSCompositeSymbol来加入这两个符号,如:
AGSCompositeSymbol *compositeSymbol = [AGSCompositeSymbol compositeSymbol];
[compositeSymbol addSymbol:marker];
[compositeSymbol addSymbol:textSymbol];
路线规划的数据是通过请求服务端获取的,获取的数据有一定的格式,一般为分段的一系列点的集合,需要根据约定好的规范进行适配。
路线画出来了,那么如何跳到路线所在位置的中心,你可以这么写:
AGSMutableEnvelope *envelope = [routeGraphicsLayer.fullEnvelope mutableCopy];
[envelope expandByFactor:2.5];
[self.mapView zoomToEnvelope:envelope animated:YES];
说说运动:
运动中需要计步,计步采用加速度传感器实现,封装了一个StepManager来管理步数,具体的算法就不细细展开,非本篇所讲内容。
运动需要在后台长时间运行,需要申请后台权限,上架的时候在备注中讲清楚这个事,态度诚恳,必要的时候叫苹果几声爹,一般就没什么问题了。
double distance = [geometryEngine geodesicLengthOfGeometry:lineGraphic.geometry inUnit:AGSSRUnitMeter];
运动采用的也是距离函数,保存上一点和下一点,然后规定一下距离超过5米或者10米时才画直线。
接下来说搜索,
[self.mapView zoomToScale:self.mapView.mapScale withCenterPoint:(AGSPoint *)graphic.geometry animated:YES];
当点击搜索出来的红色点时,或者点击表格数据时,相应的红点要位于中心。
搜索时需要与路线规划对接,记住在程序设计的时候,多抽离模块代码,避免相同模块写了两份相同的代码。
离线地图比较简单,就没什么好说的了。需要注意的是这是个在后台发起的下载任务,而不是属于某个控制器的下载任务,判断好总大小,控制好节点数据,处理好暂停、继续,就没什么问题了。
总结:ArcGIS是定位最准确的地图,比高德和百度还准确。但是缺点也是很明显的,API封装臃肿,内存消耗大,线程处理不够优美,地图块状切割太大,用户体验不够好等等。比起高德和百度地图来说,还是差了一截。但是,处理地图的方式基本都是一致的,可以作为参考。
尾记:当年研究的热血涌上心头,不断地自我突破,它是我1个人花了1个多月的时间做出来,同样的Deadline,安卓是两个人做的,还不算上我调通接口的时间。而且,论体验和Bug率来说,都属上层,最终让地测院的人赞不绝口。
当然,当初的架构还没有使用到我文章列表的“iOS架构”,代码的结构也非常的不优美,但是注释还是都有的。之所以出现这样的问题,还是那句话:Deadline不是第一生产力,只是给了你把一堆Shit交上去的勇气。
看完了,不点个赞吗?哈哈哈,随便你,开心就好。