반응형
출처 카페 > 맥부기 애플(Apple OS.. | 퓨리넬
원문 http://cafe.naver.com/mcbugi/326999
구글맵 SDK 를 이용한 OpenStreet Map 오프라인 맵 구현하기.

안녕하세요. 퓨리넬입니다.

제가 예전에 구글맵 지도 다운로드 기능 구현에 대해 질문을 올린적이 있었는데요, 답이 없길래 직접 구현해 봤습니다.
아주 간단하게 만들 수 있었던 것인데 너무 어렵게 생각했었나봐요. 

1. Tile
Tile 이라는 개념이 있습니다. 구글맵 SDK에서 어떻게 사용하는지 https://developers.google.com/maps/documentation/ios-sdk/tiles 에 나와 있습니다.
현재 화면에 보여지는 지도 화면의 경도, 위도, zoom level 값으로 zoom, x, y 좌표를 구하고 이 좌표에 대한 Tile 데이터를 불러옵니다.

이 때 개발자는 TileLayer 의 클래스를 상속받아 자신이 원하는 타일 데이터를 가져올 수 있습니다.
GMSSyncTileLayer 와 GMSURLTileLayer 가 있는데 저는 GMSSyncTileLayer 를 상속받아 사용하였습니다.
GMSURLTileLayer 는 상속받아 클래스를 만들어 사용하는것이 아니라 GMSTileURLConstructor 라는 block을 만들어 x, y, zoom 좌표를 가지고 원하는 URL을 만들어 요청할 수 있게 합니다. 단순히 URL만 바꿀 수 있고 상속받아 사용할 수 없도록 만들어졌습니다.(GMSURLTileLayer is a concrete class that cannot be subclassed.)

2. Map Data
타일 레이어를 이용하여 원하는 지도 데이터를 불러와야 겠는데 일단 구글맵은 약관에 지도데이터를 따로 저장하여 사용하는것을 금지하고 있다고 합니다.
그래서 무료로 사용할 수 있는 맵을 찾아보니 Open Street Map(http://www.openstreetmap.org/) 과 Open Cycle Map(http://www.opencyclemap.org/) 이 있었습니다.
zoom level, x, y 로 만들어진 Tile은 이들 맵 역시 마찬가지 였습니다.
해당 내용은 http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames 에서 'Zoom levels' 항목을 보시면 되겠습니다.

그리고 Tile servers 에 OpenStreetMap, OpenCycleMap등의 지도 데이터 요청 URL이 있기 때문에 이것을 사용하면 되겠습니다.
OpenStreetMap 의 경우 http://[abc].tile.openstreetmap.org/{zoom}/{x}/{y}.png 입니다. 앞의 [abc] 는 안붙이고 http://tile.로 해도 되었습니다.


3. Code
저는 단순히 지도 데이터를 Open Street Map으로 불러오는 것이 아니라 이미지 데이터를 저장도 해야 하는 것이었습니다. 그래서 GMSURLTileLayer를 사용할 이유는 없었습니다.
GMSSyncTileLayer 를 상속받으면 

- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom;

를 구현해 줍니다.

저는 일단 [NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex: 0]; 로 Document 디렉토리를 불러와
zoom, x, y 좌표에 해당하는 지도 이미지를 불러오도록 합니다.
NSString *fileName = [NSString stringWithFormat:@"%@/osm/%zd/%zd/%zd.png", _documentPath, zoom, x, y];
UIImage *img = [UIImage imageWithContentsOfFile:fileName];

그런데 처음에는 당연이 img는 nil일테니, 서버에 요청을 해야 하겠습니다.


처음에는 온라인 상태에서 지도를 보는 용도로 GMSURLTileLayer 를 사용하였고, 저장된 지도를 볼 때에는 GMSSyncTileLayer 를 상속받는 클래스를 사용하였습니다.
이렇게 두 개가 나뉘어졌기 때문에 쓸데없이 코드를 분기하고, GMSURLTileLayer 를 사용하여 Open Street Map 을 볼 때 다운받은 지도에 대한 데이터를 저장할 수 없었습니다.
지도 데이터는 사용자가 별도로 저장을 실행하면 다운받게 할 수 있지만, 일반적인 캐싱을 하는 것처럶 이왕이면 한 번 받은 지도 데이터를 저장하여 굳이 다시 서버에 요청을 하거나 지도 저장 실행시 다시 다운받지 않도록 하고 싶었습니다.
그러나 GMSURLTileLayer 는 상속받아 사용할 수 없고, GMSTileURLConstructor 라는 block 은 NSURL 만 리턴해줄 뿐이기 때문에 다운받은 지도 데이터는 어떻게 해야할 지 고민이었습니다.

그런데 GMSURLTileLayer 는 아예 사용할 필요가 없었습니다.
GMSSyncTileLayer 상속받은 클래스에서 - (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom; 가 실행될 때 메인스레드에서 호출되는것이 아니었습니다.
이미 서브스레드에서 실행되고 있기 때문에 여기서 동기로 데이터를 받아 리턴해주면 되겠다고 생각하였습니다.

그래서 대충 다음과 같이 작성하였습니다.
if(img == nil) {
  NSString *stringUrl = [NSString stringWithFormat:@"http://tile.openstreetmap.org/%zd/%zd/%zd.png", zoom, x, y];
  NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:stringUrl]];
  img = [UIImage imageWithData:imgData];
  [imgData writeToFile:fileName atomically:NO];
}
return img;

그리고 이 타일 레이어 클래스는
_googleMap.mapType = kGMSTypeNone;
self.offlineTileLayer = [[GSOfflineTileLayer alloc] init];
_offlineTileLayer.map = _googleMap;

으로 사용하게 됩니다.
구글맵의 mapType 을 none으로 해야 구글의 지도가 나오지 않습니다.
그리고 다시 구글맵을 표시할 때에는 [_offlineTileLayer clearTileCache]; 을 해주고 제거하면 되겠습니다.

이렇게 하면 구글맵 SDK 를 이용하여 구글맵을 이용하는 한편, 사용자가 원할 경우 Open Street map 등의 다른 지도를 이용하면서 오프라인 맵 기능을 사용할 수 있습니다.
혹은 사용자가 미리 iTunes를 이용하여 document에 직접 넣어 사용할 수도 있겠습니다.

4. Download

화면에 보이고 있는 지도화면에서 더 높은 zoom level 의 지도 데이터까지 한 번에 다운로드 할 경우에는
보여지고 있는 화면의 가장 위 오른쪽, 가장 아래 왼쪽 의 경도, 위도 와 최소 zoom, 최대 zoom 값이 필요합니다.

그리고 두 좌표와 min/max 를 이용하여 몇 개의 tile를 다운받는지 총 count를 먼저 해야겠습니다.

#define M_PI        3.14159265358979323846264338327950288


- (NSUInteger)tileCountForSouthWest:(CLLocationCoordinate2D)southWest northEast:(CLLocationCoordinate2D)northEast minZoom:(NSUInteger)minZoom maxZoom:(NSUInteger)maxZoom

{

    NSUInteger minCacheZoom = minZoom;

    NSUInteger maxCacheZoom = maxZoom;


    CLLocationDegrees minCacheLat = southWest.latitude;

    CLLocationDegrees maxCacheLat = northEast.latitude;

    CLLocationDegrees minCacheLon = southWest.longitude;

    CLLocationDegrees maxCacheLon = northEast.longitude;


    NSAssert(minCacheZoom <= maxCacheZoom, @"Minimum zoom should be less than or equal to maximum zoom");

    NSAssert(maxCacheLat  >  minCacheLat,  @"Northernmost bounds should exceed southernmost bounds");

    NSAssert(maxCacheLon  >  minCacheLon,  @"Easternmost bounds should exceed westernmost bounds");


    NSUInteger n, xMin, yMax, xMax, yMin;


    NSUInteger totalTiles = 0;


    for (NSUInteger zoom = minCacheZoom; zoom <= maxCacheZoom; zoom++)

    {

        n = pow(2.0, zoom);

        xMin = floor(((minCacheLon + 180.0) / 360.0) * n);

        yMax = floor((1.0 - (logf(tanf(minCacheLat * M_PI / 180.0) + 1.0 / cosf(minCacheLat * M_PI / 180.0)) / M_PI)) / 2.0 * n);

        xMax = floor(((maxCacheLon + 180.0) / 360.0) * n);

        yMin = floor((1.0 - (logf(tanf(maxCacheLat * M_PI / 180.0) + 1.0 / cosf(maxCacheLat * M_PI / 180.0)) / M_PI)) / 2.0 * n);


        totalTiles += (xMax + 1 - xMin) * (yMax + 1 - yMin);

    }


    return totalTiles;

}


그리고 한번에 다운받는 부분은 Mapbox Offine 샘플에서 가져왔는데 대략적인것만 붙이겠습니다.


- (void)mapImageDownload:(NSInteger)totalCount {


    CGSize size = self.bounds.size;

    CGPoint point = {00};

    point.x = size.width;

    CLLocationCoordinate2D northEast = [_googleMap.projection coordinateForPoint:point];

    point.x = 0;

    point.y = size.height;

    CLLocationCoordinate2D southWest = [_googleMap.projection coordinateForPoint:point];

    NSUInteger minZoom = zoomLevel;

    NSUInteger maxZoom = 19;

    NSUInteger minCacheZoom = minZoom;

    NSUInteger maxCacheZoom = maxZoom;

    

    CLLocationDegrees minCacheLat = southWest.latitude;

    CLLocationDegrees maxCacheLat = northEast.latitude;

    CLLocationDegrees minCacheLon = southWest.longitude;

    CLLocationDegrees maxCacheLon = northEast.longitude;


    dispatch_queue_t dQueue = dispatch_queue_create("MapDownload"NULL);

    dispatch_async(dQueue, ^{

        NSUInteger n, xMin, yMax, xMax, yMin;

        NSString *documentPath = [NSSearchPathForDirectoriesInDomains (NSDocumentDirectoryNSUserDomainMaskYESobjectAtIndex0];

        NSFileManager *fileManager = [NSFileManager defaultManager];


        for (NSUInteger zoom = minCacheZoom; zoom <= maxCacheZoom; zoom++) {

            n = pow(2.0, zoom);

            xMin = floor(((minCacheLon + 180.0) / 360.0) * n);

            yMax = floor((1.0 - (logf(tanf(minCacheLat * M_PI / 180.0) + 1.0 / cosf(minCacheLat * M_PI / 180.0)) / M_PI)) / 2.0 * n);

            xMax = floor(((maxCacheLon + 180.0) / 360.0) * n);

            yMin = floor((1.0 - (logf(tanf(maxCacheLat * M_PI / 180.0) + 1.0 / cosf(maxCacheLat * M_PI / 180.0)) / M_PI)) / 2.0 * n);

            

            for (NSUInteger x = xMin; x <= xMax; x++) {

                for (NSUInteger y = yMin; y <= yMax; y++) {

NSString *stringUrl = [NSString stringWithFormat:@"http://tile.openstreetmap.org/%zd/%zd/%zd.png", zoom, x, y];

                        NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:stringUrl]];



어쩌고 저쩌고 하면 되겠습니다.


제가 이것 때문에 고민이 많았는데 누군가에게 도움이 되었으면 좋겠습니다.


아 그런데 이렇게 하면 메모리를 좀 더 많이 쓰는 느낌이 드네요. ㅎㅎ


의견 있으시면 댓글 부탁드립니다.


반응형
블로그 이미지

앱스페이스

,