感谢翻译小组成员崩月姐姐热心翻译。本篇文章是我们每周推荐优秀国外的技术类文章的其中一篇。如果您有不错的原创或译文,欢迎提交给我们,更欢迎其他朋友加入我们的翻译小组(联系qq:2408167315)。
 
如何在iOS地图上以用户可以理解并乐于接受的方式来处理和显示大量数据?这个教程将会给大家进行示例说明。
 
我们要开发一款iOS的app应用,这个应用包含有87000个旅馆的信息,每个旅馆的信息中包括有一个坐标值,一个旅馆名跟一个电话号码。这款app可以在用户拖动、放大缩小地图时更新旅馆数据,而不需要用户重新进行搜索。
 
为了达到这个目的,我们需要构造一个可快速检索的数据结构。C语言的性能高,所以我们用C语言来构造这个数据结构。为了确保大量的数据不会让用户感到迷惑,所以我们还需要想出一个合并数据的解决方案。最后,为了更好的适应市场,我们需要把app做的更完善一些。
 
完成这个教学后,你将学到这款app的所有核心内容。
 
 
数据结构
首先我们先来分析下数据,搞清我们要如何处理数据。旅馆数据中包含了一系列的坐标点(包括纬度和经度),我们需要根据这些坐标点在地图上进行标注。地图可 以任意的拖动并放大缩小,所以我们不需要把所有的点都全部绘制出来,我们只需要绘制可以显示在屏幕上的点。核心问题是:我们需要查询出显示在屏幕上的所有 的点,所以我们要想出一个查找算法,查找存在于一个矩形范围内的所有点。
 
一个简单的解决方式就是遍历所有的点,然后判断(xMin<=x<=xMax并且yMin<=y<=yMax),很不幸,这是一个复杂度为O(N)的算法,显然不适合我们的情况。
 
这儿有个更好的解决方法,就是我们可以利用对称性来减少我们的查询范围。那么如何能通过查询的每一次的迭代来减少查询的范围呢?我们可以在每个区域内都加 索引,这样可以有效减少查询的范围。这种区域索引的方式可以用四叉树来实现,查询复杂度为O(H)(H是查询的那个点所在的树的高度)
 
四叉树
四叉树是一个数据结构,由一系列的结点(node)构成。每个结点包含一个桶(bucket)跟一个包围框(boundingbox)。每个桶里面有一系 列的点(point)。如果一个点包含在一个外包围框A中,就会被添加到A所在结点的桶(bucket)中。一旦这个结点的桶满了,这个结点就会分裂成四 个子结点,每个子节点的包围框分别是当前结点包围框的1/4。分裂之后那些本来要放到当前结点桶中的点就都会放到子容器的桶中。
 
那么我们该如何来对四叉树进行编码呢?
 
我们先来定义基本的结构:
 
  1. typedef struct TBQuadTreeNodeData {
  2. double x;
  3. double y;
  4. void* data;
  5. } TBQuadTreeNodeData;
  6. TBQuadTreeNodeData TBQuadTreeNodeDataMake(double x, double y, void* data);
  7. typedef struct TBBoundingBox {
  8. double x0; double y0;
  9. double xf; double yf;
  10. } TBBoundingBox;
  11. TBBoundingBox TBBoundingBoxMake(double x0, double y0, double xf, double yf);
  12. typedef struct quadTreeNode {
  13. struct quadTreeNode* northWest;
  14. struct quadTreeNode* northEast;
  15. struct quadTreeNode* southWest;
  16. struct quadTreeNode* southEast;
  17. TBBoundingBox boundingBox;
  18. int bucketCapacity;
  19. TBQuadTreeNodeData *points;
  20. int count;
  21. } TBQuadTreeNode;
  22. TBQuadTreeNode* TBQuadTreeNodeMake(TBBoundingBox boundary, int bucketCapacity);
 
TBQuadTreeNodeData结构包含了坐标点(纬度,经度)。void*data是一个普通的指针,用来存储我们需要的其他信息,如旅馆名跟电 话号码。TBBoundingBox代表一个用于范围查询的长方形,也就是之前谈到(xMin<=x<=xMax&& yMin<=y<=yMax)查询的那个长方形。左上角是(xMin,yMin),右下角是(xMax,yMax)。
 
最后,我们看下TBQuadTreeNode结构,这个结构包含了四个指针,每个指针分别指向这个结点的四个子节点。它还有一个外包围框和一个数组(数组中就是那个包含一系列坐标点的桶)。
 
在我们建立完四叉树的同时,空间上的索引也就同时形成了。这是生成四叉树的演示动画。
 
 
下面的代码准确描述了以上动画的过程:
 
  1. void TBQuadTreeNodeSubdivide(TBQuadTreeNode* node)
  2. {
  3. TBBoundingBox box = node->boundingBox;
  4. double xMid = (box.xf + box.x0) / 2.0;
  5. double yMid = (box.yf + box.y0) / 2.0;
  6. TBBoundingBox northWest = TBBoundingBoxMake(box.x0, box.y0, xMid, yMid);
  7. node->northWest = TBQuadTreeNodeMake(northWest, node->bucketCapacity);
  8. TBBoundingBox northEast = TBBoundingBoxMake(xMid, box.y0, box.xf, yMid);
  9. node->northEast = TBQuadTreeNodeMake(northEast, node->bucketCapacity);
  10. TBBoundingBox southWest = TBBoundingBoxMake(box.x0, yMid, xMid, box.yf);
  11. node->southWest = TBQuadTreeNodeMake(southWest, node->bucketCapacity);
  12. TBBoundingBox southEast = TBBoundingBoxMake(xMid, yMid, box.xf, box.yf);
  13. node->southEast = TBQuadTreeNodeMake(southEast, node->bucketCapacity);
  14. }
  15. bool TBQuadTreeNodeInsertData(TBQuadTreeNode* node, TBQuadTreeNodeData data)
  16. {
  17. // Bail if our coordinate is not in the boundingBox
  18. if (!TBBoundingBoxContainsData(node->boundingBox, data)) {
  19. return false;
  20. }
  21. // Add the coordinate to the points array
  22. if (node->count < node->bucketCapacity) {
  23. node->points[node->count++] = data;
  24. return true;
  25. }
  26. // Check to see if the current node is a leaf, if it is, split
  27. if (node->northWest == NULL) {
  28. TBQuadTreeNodeSubdivide(node);
  29. }
  30. // Traverse the tree
  31. if (TBQuadTreeNodeInsertData(node->northWest, data)) return true;
  32. if (TBQuadTreeNodeInsertData(node->northEast, data)) return true;
  33. if (TBQuadTreeNodeInsertData(node->southWest, data)) return true;
  34. if (TBQuadTreeNodeInsertData(node->southEast, data)) return true;
  35. return false;
  36. }
 
现在我们已经完成了四叉树的构造,我们还需要在四叉树上进行区域范围查询并返回TBQuadTreeNodeData结构。以下是区域范围查询的演示动画,在浅蓝区域内的是所有的标注点。当标注点被查询到在指定的区域范围内,则会被标注为绿色。
 
 
以下是查询代码:
 
  1. typedef void(^TBDataReturnBlock)(TBQuadTreeNodeData data);
  2. void TBQuadTreeGatherDataInRange(TBQuadTreeNode* node, TBBoundingBox range, TBDataReturnBlock block)
  3. {
  4. // If range is not contained in the node's boundingBox then bail
  5. if (!TBBoundingBoxIntersectsBoundingBox(node->boundingBox, range)) {
  6. return;
  7. }
  8. for (int i = 0; i < node->count; i++) {
  9. // Gather points contained in range
  10. if (TBBoundingBoxContainsData(range, node->points[i])) {
  11. block(node->points[i]);
  12. }
  13. }
  14. // Bail if node is leaf
  15. if (node->northWest == NULL) {
  16. return;
  17. }
  18. // Otherwise traverse down the tree
  19. TBQuadTreeGatherDataInRange(node->northWest, range, block);
  20. TBQuadTreeGatherDataInRange(node->northEast, range, block);
  21. TBQuadTreeGatherDataInRange(node->southWest, range, block);
  22. TBQuadTreeGatherDataInRange(node->southEast, range, block);
  23. }
 
用四叉树这种结构可以进行快速的查询。在一个包含成百上千条数据的数据库中,可以以60fps的速度查询上百条数据。
 
 
用旅馆数据来填充四叉树
旅馆的数据来自于POIplaza这个网站,而且已经格式化成csv文件。我们要从硬盘中读取出数据并对数据进行转换,最后用数据来填充四叉树。
 
创建四叉树的代码在TBCoordinateQuadTree类中:
 
  1. typedef struct TBHotelInfo {
  2. char* hotelName;
  3. char* hotelPhoneNumber;
  4. } TBHotelInfo;
  5. TBQuadTreeNodeData TBDataFromLine(NSString *line)
  6. {
  7. // Example line:
  8. // -80.26262, 25.81015, Everglades Motel, USA-United States, +1 305-888-8797
  9. NSArray *components = [line componentsSeparatedByString:@","];
  10. double latitude = [components[1] doubleValue];
  11. double longitude = [components[0] doubleValue];
  12. TBHotelInfo* hotelInfo = malloc(sizeof(TBHotelInfo));
  13. NSString *hotelName = [components[2] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  14. hotelInfo->hotelName = malloc(sizeof(char) * hotelName.length + 1);
  15. strncpy(hotelInfo->hotelName, [hotelName UTF8String], hotelName.length + 1);
  16. NSString *hotelPhoneNumber = [[components lastObject] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
  17. hotelInfo->hotelPhoneNumber = malloc(sizeof(char) * hotelPhoneNumber.length + 1);
  18. strncpy(hotelInfo->hotelPhoneNumber, [hotelPhoneNumber UTF8String], hotelPhoneNumber.length + 1);
  19. return TBQuadTreeNodeDataMake(latitude, longitude, hotelInfo);
  20. }
  21. - (void)buildTree
  22. {
  23. NSString *data = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"USA-HotelMotel" ofType:@"csv"] encoding:NSASCIIStringEncoding error:nil];
  24. NSArray *lines = [data componentsSeparatedByString:@"\n"];
  25. NSInteger count = lines.count - 1;
  26. TBQuadTreeNodeData *dataArray = malloc(sizeof(TBQuadTreeNodeData) * count);
  27. for (NSInteger i = 0; i < count; i++) {
  28. dataArray[i] = TBDataFromLine(lines[i]);
  29. }
  30. TBBoundingBox world = TBBoundingBoxMake(19, -166, 72, -53);
  31. _root = TBQuadTreeBuildWithData(dataArray, count, world, 4);
  32. }
 
现在我们用iPhone上预加载的数据创建了一个四叉树。接下来我们将处理app的下一个部分:合并数据(clustering)。
 
合并数据(clustering)
现在我们有了一个装满旅馆数据的四叉树,可以用来解决合并数据的问题了。首先,让我们来探索下合并数据的原因。我们合并数据是因为我们不想因为数据过于庞 大而使用户迷惑。实际上有很多种方式可以解决这个问题。GoogleMaps根据地图的缩放等级(zoomlevel)来显示搜索结果数据中的一部分数 据。地图放的越大,就越能清晰的看到更细节的标注,直到你能看到所有有效的标注。我们将采用这种合并数据的方式,只显示出来旅馆的个数,而不在地图上显示 出所有的旅馆信息。
 
最终呈现的标注是一个中心显示旅馆个数的小圆圈。实现的原理跟如何把图片缩小的原理差不多。我们先在地图上画一个格子。每个格子中包含了很多个小单元格, 每个小单元格中的所有旅馆数据合并出一个标注。然后通过每个小单元格中所有旅馆的坐标值的平均值来决定合并后这个标注的坐标值。
 
这是以上处理的演示动画。
 
 
以下是代码实现过程。在TBCoordinateQuadTree类中添加了一个方法。
 
  1. - (NSArray *)clusteredAnnotationsWithinMapRect:(MKMapRect)rect withZoomScale:(double)zoomScale
  2. {
  3. double TBCellSize = TBCellSizeForZoomScale(zoomScale);
  4. double scaleFactor = zoomScale / TBCellSize;
  5. NSInteger minX = floor(MKMapRectGetMinX(rect) * scaleFactor);
  6. NSInteger maxX = floor(MKMapRectGetMaxX(rect) * scaleFactor);
  7. NSInteger minY = floor(MKMapRectGetMinY(rect) * scaleFactor);
  8. NSInteger maxY = floor(MKMapRectGetMaxY(rect) * scaleFactor);
  9. NSMutableArray *clusteredAnnotations = [[NSMutableArray alloc] init];
  10. for (NSInteger x = minX; x <= maxX; x++) {
  11. for (NSInteger y = minY; y <= maxY; y++) {
  12. MKMapRect mapRect = MKMapRectMake(x / scaleFactor, y / scaleFactor, 1.0 / scaleFactor, 1.0 / scaleFactor);
  13. __block double totalX = 0;
  14. __block double totalY = 0;
  15. __block int count = 0;
  16. TBQuadTreeGatherDataInRange(self.root, TBBoundingBoxForMapRect(mapRect), ^(TBQuadTreeNodeData data) {
  17. totalX += data.x;
  18. totalY += data.y;
  19. count++;
  20. });
  21. if (count >= 1) {
  22. CLLocationCoordinate2D coordinate = CLLocationCoordinate2DMake(totalX / count, totalY / count);
  23. TBClusterAnnotation *annotation = [[TBClusterAnnotation alloc] initWithCoordinate:coordinate count:count];
  24. [clusteredAnnotations addObject:annotation];
  25. }
  26. }
  27. }
  28. return [NSArray arrayWithArray:clusteredAnnotations];
  29. }
 
上面的方法在指定小单元格大小的前提下合并数据生成了最终的标注。现在我们需要做的就是把这些标注绘制到MKMapView上。首先我们创建一个 UIViewController的子类,然后用MKMapView作为它的view视图。在可视区域改变的情况下,我们需要实时更新标注的显示,所以我 们要实现mapView:regionDidChangeAnimated:的协议方法。
 
  1. - (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated
  2. {
  3. [[NSOperationQueue new] addOperationWithBlock:^{
  4. double zoomScale = self.mapView.bounds.size.width / self.mapView.visibleMapRect.size.width;
  5. NSArray *annotations = [self.coordinateQuadTree clusteredAnnotationsWithinMapRect:mapView.visibleMapRect withZoomScale:zoomScale];
  6. [self updateMapViewAnnotationsWithAnnotations:annotations];
  7. }];
  8. }
 
 
只添加必要的标注
在主线程中我们期望尽可能花费较少时间来做运算,这意味着我们要尽可能的把所有内容都放到后台的线程中。为了在主线程中花费更少的时间来做计算,我们只需要绘制一些必要的标注。这可以避免用户滑动过程中感到很卡,从而保证流畅的用户体验。
 
开始之前,我们看一下下面的图片:
 
 
左边的屏幕截图是地图进行滑动前的地图快照。这个快照中的标注就是目前mapView中的标注,我们称这个为“before集合”。
 
右边的屏幕截图是地图进行滑动后的地图快照。这个快照中的标注就是从clusteredAnnotationsWithinMapRect:withZoomScale:这个函数中得到的返回值。我们称这个为“after集合”。
 
我们期望保留两个快照中都存在的标注点(即重合的那些标注点),去除在“after集合”中不存在的那些标注点,同时添加那些新的标注点。
 
  1. - (void)updateMapViewAnnotationsWithAnnotations:(NSArray *)annotations
  2. {
  3. NSMutableSet *before = [NSMutableSet setWithArray:self.mapView.annotations];
  4. NSSet *after = [NSSet setWithArray:annotations];
  5. // Annotations circled in blue shared by both sets
  6. NSMutableSet *toKeep = [NSMutableSet setWithSet:before];
  7. [toKeep intersectSet:after];
  8. // Annotations circled in green
  9. NSMutableSet *toAdd = [NSMutableSet setWithSet:after];
  10. [toAdd minusSet:toKeep];
  11. // Annotations circled in red
  12. NSMutableSet *toRemove = [NSMutableSet setWithSet:before];
  13. [toRemove minusSet:after];
  14. // These two methods must be called on the main thread
  15. [[NSOperationQueue mainQueue] addOperationWithBlock:^{
  16. [self.mapView addAnnotations:[toAdd allObjects]];
  17. [self.mapView removeAnnotations:[toRemove allObjects]];
  18. }];
  19. }
 
这样我们尽可能的确保在主线程上做少量的工作,从而提升地图滑动的流畅性。
 
接下来我们来看下如何绘制标注,并且在标注上显示出来旅馆的个数。最后我们给标注加上点击事件,这样使得app从头到脚都可以表现的非常完美。
 
绘制标注
由于我们在地图上并没有完全显示出全部旅馆,所以我们需要在剩余的这些标注上表现出真实的旅馆总量。
 
首先创建一个圆形的标注,中间显示合并后的个数,也就是旅馆的真实总量。这个圆形的大小同样可以反映出合并后的个数。
 
为了实现这个需求,我们要找出一个方程式,允许我们在1到500+的数值中生成一个缩小后的数值。用这个数值来作为标注的大小。我们将用到以下的方程式。
 
 
x值较低的时候f(x)增长的比较快,x在值变大的时候f(x)增长变缓慢,β值用来控制f(x)趋于1的速度。α值影响最小值(在我们的项目中,我们的最小合并值(也就是1)能占总共最大值的60%)。
 
  1. static CGFloat const TBScaleFactorAlpha = 0.3;
  2. static CGFloat const TBScaleFactorBeta = 0.4;
  3. CGFloat TBScaledValueForValue(CGFloat value)
  4. {
  5. return 1.0 / (1.0 + expf(-1 * TBScaleFactorAlpha * powf(value, TBScaleFactorBeta)));
  6. }
  7. - (void)setCount:(NSUInteger)count
  8. {
  9. _count = count;
  10. // Our max size is (44,44)
  11. CGRect newBounds = CGRectMake(0, 0, roundf(44 * TBScaledValueForValue(count)), roundf(44 * TBScaledValueForValue(count)));
  12. self.frame = TBCenterRect(newBounds, self.center);
  13. CGRect newLabelBounds = CGRectMake(0, 0, newBounds.size.width / 1.3, newBounds.size.height / 1.3);
  14. self.countLabel.frame = TBCenterRect(newLabelBounds, TBRectCenter(newBounds));
  15. self.countLabel.text = [@(_count) stringValue];
  16. [self setNeedsDisplay];
  17. }
 
现在标注的大小已经OK了。让我们再来把这个标注做漂亮些。
 
  1. - (void)setupLabel
  2. {
  3. _countLabel = [[UILabel alloc] initWithFrame:self.frame];
  4. _countLabel.backgroundColor = [UIColor clearColor];
  5. _countLabel.textColor = [UIColor whiteColor];
  6. _countLabel.textAlignment = NSTextAlignmentCenter;
  7. _countLabel.shadowColor = [UIColor colorWithWhite:0.0 alpha:0.75];
  8. _countLabel.shadowOffset = CGSizeMake(0, -1);
  9. _countLabel.adjustsFontSizeToFitWidth = YES;
  10. _countLabel.numberOfLines = 1;
  11. _countLabel.font = [UIFont boldSystemFontOfSize:12];
  12. _countLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters;
  13. [self addSubview:_countLabel];
  14. }
  15. - (void)drawRect:(CGRect)rect
  16. {
  17. CGContextRef context = UIGraphicsGetCurrentContext();
  18. CGContextSetAllowsAntialiasing(context, true);
  19. UIColor *outerCircleStrokeColor = [UIColor colorWithWhite:0 alpha:0.25];
  20. UIColor *innerCircleStrokeColor = [UIColor whiteColor];
  21. UIColor *innerCircleFillColor = [UIColor colorWithRed:(255.0 / 255.0) green:(95 / 255.0) blue:(42 / 255.0) alpha:1.0];
  22. CGRect circleFrame = CGRectInset(rect, 4, 4);
  23. [outerCircleStrokeColor setStroke];
  24. CGContextSetLineWidth(context, 5.0);
  25. CGContextStrokeEllipseInRect(context, circleFrame);
  26. [innerCircleStrokeColor setStroke];
  27. CGContextSetLineWidth(context, 4);
  28. CGContextStrokeEllipseInRect(context, circleFrame);
  29. [innerCircleFillColor setFill];
  30. CGContextFillEllipseInRect(context, circleFrame);
  31. }
 
添加最后的touch事件
目前的标注可以很好的呈现出我们的数据了,让我们最后添加一些touch事件来让我们的app用起来更有趣。
 
首先,我们需要为新添加到地图上的标注做一个动画。如果没有添加动画的话,新的标注就会在地图上突然出现,体验效果将会大打折扣。
 
  1. - (void)addBounceAnnimationToView:(UIView *)view
  2. {
  3. CAKeyframeAnimation *bounceAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform.scale"];
  4. bounceAnimation.values = @[@(0.05), @(1.1), @(0.9), @(1)];
  5. bounceAnimation.duration = 0.6;
  6. NSMutableArray *timingFunctions = [[NSMutableArray alloc] init];
  7. for (NSInteger i = 0; i < 4; i++) {
  8. [timingFunctions addObject:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
  9. }
  10. [bounceAnimation setTimingFunctions:timingFunctions.copy];
  11. bounceAnimation.removedOnCompletion = NO;
  12. [view.layer addAnimation:bounceAnimation forKey:@"bounce"];
  13. }
  14. - (void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views
  15. {
  16. for (UIView *view in views) {
  17. [self addBounceAnnimationToView:view];
  18. }
  19. }
 
接下来,我们想要根据地图的缩放比例来改变在合并时的小单元格(cell)的大小。在地图进行放大时,小单元格变小。所以我们需要定义一下当前地图的缩放 比例。也就是scale=mapView.bounds.size.width/mapView.visibleMapRect.size.width:
 
  1. NSInteger TBZoomScaleToZoomLevel(MKZoomScale scale)
  2. {
  3. double totalTilesAtMaxZoom = MKMapSizeWorld.width / 256.0;
  4. NSInteger zoomLevelAtMaxZoom = log2(totalTilesAtMaxZoom);
  5. NSInteger zoomLevel = MAX(0, zoomLevelAtMaxZoom + floor(log2f(scale) + 0.5));
  6. return zoomLevel;
  7. }
 
我们为每个地图缩放的比例都定义一个常量。
 
  1. float TBCellSizeForZoomScale(MKZoomScale zoomScale)
  2. {
  3. NSInteger zoomLevel = TBZoomScaleToZoomLevel(zoomScale);
  4. switch (zoomLevel) {
  5. case 13:
  6. case 14:
  7. case 15:
  8. return 64;
  9. case 16:
  10. case 17:
  11. case 18:
  12. return 32;
  13. case 19:
  14. return 16;
  15. default:
  16. return 88;
  17. }
  18. }
 
现在我们放大地图,我们将看到逐渐变小的标注,直到最后我们能看到代表每个旅馆的那个标注。
 

[ios3-地图] 如何在iOS地图上高效的显示大量数据 [转]的更多相关文章

  1. 如何在iOS地图上高效的显示大量数据

    2016-01-13 / 23:02:13 刚才在微信上看到这篇由cocoachina翻译小组成员翻译的文章,觉得还是挺值得参考的,因此转载至此,原文请移步:http://robots.thought ...

  2. 如何在iOS手机上进行自动化测试

    版权声明:允许转载,但转载必须保留原链接:请勿用作商业或者非法用途 Airtest支持iOS自动化测试,在Mac上为iOS手机部署iOS-Tagent之后,就可以使用AirtestIDE连接设备,像连 ...

  3. fir.im Weekly - 如何在 iOS 上构建 TensorFlow 应用

    本期 fir.im Weekly 收集了最近新鲜出炉的 iOS /Android 技术分享,包括 iOS 系统开发 TensorFlow 教程.iOS 新架构.iOS Notifications 推送 ...

  4. iOS 地图定位及大头针的基本使用

    地图 Part1 - 定位及大头针的基本使用 一.MapKit 作用 : 用于地图展示 如大头针,路线,覆盖层展示等(着重界面展示) 使用步骤 导入头文件 #import <MapKit/Map ...

  5. 【高德API】如何利用MapKit开发全英文检索的iOS地图

    原文:[高德API]如何利用MapKit开发全英文检索的iOS地图 制作全英文地图的展示并不困难,但是要制作全英文的数据检索列表,全英文的信息窗口,你就没办法了吧.告诉你,我有妙招!使用iOS自带的M ...

  6. 【iOS地图开发】巧妙打造中英文全球地图

    地图开发的同学们经常遇到这样的问题,国内版地图开发,用高德或者百度就行了.但是,国外的地图怎么办?这里告诉大家,如果利用iOS地图,打造中英文的,国内国外都能用的,全球地图. 制作全英文地图的展示并不 ...

  7. iOS 地图相关

    参考博文:https://blog.csdn.net/zhengang007/article/details/52858198?utm_source=blogxgwz7 1.坐标系 目前常见的坐标系有 ...

  8. Swift - 使用MapKit显示地图,并在地图上做标记

    通过使用MapKit可以将地图嵌入到视图中,MapKit框架除了可以显示地图,还支持在地图上做标记. 1,通过mapType属性,可以设置地图的显示类型 MKMapType.Standard :标准地 ...

  9. 从底层谈WebGIS 原理设计与实现(六):WebGIS中地图瓦片在Canvas上的拼接显示原理

    从底层谈WebGIS 原理设计与实现(六):WebGIS中地图瓦片在Canvas上的拼接显示原理 作者:naaoveGI…    文章来源:naaoveGIS    点击数:1145    更新时间: ...

随机推荐

  1. Android单元测试Junit (一)

    1.在eclips中建立一个Android工程,具体信息如下: 2.配置单元测试环境,打开AndroidManifest.xml,具体代码如下所示: <?xml version="1. ...

  2. 【蜗牛—漫漫IT路之大学篇(九) 】

    再来一篇叨叨的博客 近期,状态还是那个状态,人还是那个人. 前两天,感冒了,可能是宿舍阴面的事吧.然后,中午睡觉的时候穿着短袖披了一件外套,然后鼻子就不通气了.只是,前天晚上,我骑着崔国强的车子跑了不 ...

  3. js预解析问题总结

    //示例 1 alert(a) // undefind. alert(fn) // function 整个函数块. var a = 1; function fn(){ return falss; }; ...

  4. struts2 &lt;s: select 标签值

    JSP页面: <s:select label="家长导航"  value="id" name="navson.pid" list=&q ...

  5. SpringMVC格式化显示

    SpringMVC学习系列(7) 之 格式化显示 在系列(6)中我们介绍了如何验证提交的数据的正确性,当数据验证通过后就会被我们保存起来.保存的数据会用于以后的展示,这才是保存的价值.那么在展示的时候 ...

  6. C#的Task和Java的Future

    C#的Task和Java的Future 自从项目中语言换成Java后就很久没有看C#了,但说实话我是身在曹营心在汉啊.早就知道.NET4.5新增了async和await但一直没有用过,今天看到这篇文章 ...

  7. Orchard中的Host和Tenant

      Orchard的多个子站点特性 Orchard中可以支持多个子站点.当你运行Orchard的时候,通常一个网站运行在一个应用程序域中.这也是一般ASP.NET应用程序区分两个网站的方法,也就是说两 ...

  8. 自然语言处理(NLP)常用开源工具总结(转)

    ..................................内容纯转发+收藏................................... 学习自然语言这一段时间以来接触和听说了好多开 ...

  9. 多模块分布式系统的简单服务访问 - OSGI原形(.NET)

    多模块分布式系统的简单服务访问 - OSGI原形(.NET) 先描述一下本篇描述的适用场景(3台server, 各个模块分布在各个Server上,分布式模块互相依赖.交互的场景): 多个OSIG引擎交 ...

  10. Redis系统学习 一、基础知识

    1.数据库 select 1  select 0 2.命令.关键字和值 redis不仅仅是一种简单的关键字-值型存储,从其核心概念来看,Redsi的5种数据结构中的每一个都至少有一个关键字和一个值.在 ...