Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve "Download a full region of the map" #41

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions MapCache/Classes/DiskCache/DiskCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ open class DiskCache {
})
}

/// Determine if the tile has been cached
open func exists(forKey key: String) -> Bool {
let path = self.path(forKey: key)
let fileManager = FileManager.default
return fileManager.fileExists(atPath: path)
}

/// Calculates the size used by all the files in the cache.
public func calculateDiskSize() -> UInt64 {
let fileManager = FileManager.default
Expand Down
84 changes: 83 additions & 1 deletion MapCache/Classes/MapCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ open class MapCache : MapCacheProtocol {
///
/// - SeeAlso: `LoadTileMode`
///
public func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, Error?) -> Void) {
open func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, Error?) -> Void) {

let key = cacheKey(forPath: path)

Expand Down Expand Up @@ -161,6 +161,88 @@ open class MapCache : MapCacheProtocol {
}
}

/// Load cached tile identification information
/// - Parameter path: the tile path
/// - Returns: etag if present
open func loadETag(forPath path: MKTileOverlayPath) -> String? {
return nil
}

/// Stores the identification information of the tile
/// - Parameter path: the tile path
/// - Parameter etag: the identification information of the tile, If nil will delete old information
open func saveETag(forPath path: MKTileOverlayPath, etag: String?) {
}

/// Cache specified tiles
/// - Parameters:
/// - path: the path of the tile to be cache
/// - update: indicates to re-download from the server even if the cache already contains this tile
/// - result: result is the closure that will be run once the tile or an error is received.
open func cacheTile(at path: MKTileOverlayPath, update: Bool, result: @escaping (_ size: Int, Error?) -> Void) {

let key = cacheKey(forPath: path)
let exists = diskCache.exists(forKey: key)

if !update && exists {
result(0, nil)
return
}

print ("MapCache::cacheTileFromServer:: key=\(key)" )
let url = self.url(forTilePath: path)
var req = URLRequest(url: url)
if exists {
if let eTag = loadETag(forPath: path) {
req.addValue(eTag, forHTTPHeaderField: "If-None-Match")
}
}

let task = URLSession.shared.dataTask(with: req) {(data, response, error) in
if error != nil {
print("!!! MapCache::cacheTileFromServer Error for url= \(url) \(error.debugDescription)")
result(0, error)
return
}
guard let httpResponse = response as? HTTPURLResponse else {
print("!!! MapCache::cacheTileFromServer No data url= \(url)")
result(0, nil)
return
}

if httpResponse.statusCode == 304 {
print("MapCache::cacheTileFromServer unmodified for url= \(url)")
result(0, nil)
return
}

guard let data = data else {
print("!!! MapCache::cacheTileFromServer No data for url= \(url)")
result(0, nil)
return
}

guard (200...299).contains(httpResponse.statusCode) else {
print("!!! MapCache::cacheTileFromServer statusCode != 2xx url= \(url)")
result(0, nil)
return
}
self.diskCache.setData(data, forKey: key)
print ("MapCache::cacheTileFromServer:: Data received saved cacheKey=\(key)" )
var etag: String? = nil
if #available(iOS 13.0, *) {
etag = httpResponse.value(forHTTPHeaderField: "etag")
} else {
etag = httpResponse.allHeaderFields["Etag"] as? String
}
self.saveETag(forPath: path, etag: etag)
result(data.count, nil)
}
task.resume()


}

//TODO review why does it have two ways of retrieving the cache size.

/// Currently size of the cache
Expand Down
7 changes: 7 additions & 0 deletions MapCache/Classes/MapCacheProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,11 @@ public protocol MapCacheProtocol {
/// - SeeAlso [MapKit.MkTileOverlay](https://developer.apple.com/documentation/mapkit/mktileoverlay)
func loadTile(at path: MKTileOverlayPath, result: @escaping (Data?, Error?) -> Void)

/// Cache specified tile
/// - Parameters:
/// - path: the path of the tile to be cache
/// - update: indicates to re-download from the server even if the cache already contains this tile
/// - result: result is the closure that will be run once the tile or an error is received.
func cacheTile(at path: MKTileOverlayPath, update: Bool, result: @escaping (_ size: Int, Error?) -> Void)

}
39 changes: 37 additions & 2 deletions MapCache/Classes/RegionDownloader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ import MapKit
/// Cache that is going to be used for saving/loading the files.
public let mapCache: MapCacheProtocol

/// Whether to check and update downloaded tiles
public let update: Bool

/// Total number of tiles to be downloaded.
public var totalTilesToDownload: TileNumber {
get {
Expand All @@ -51,6 +54,10 @@ import MapKit
/// The variable that actually keeps the count of the downloaded bytes.
private var _downloadedBytes: UInt64 = 0


/// Indicates that the download has been canceled
private var _stoped = false

/// Total number of downloaded data bytes.
public var downloadedBytes: UInt64 {
get {
Expand Down Expand Up @@ -138,14 +145,17 @@ import MapKit
///
/// - Parameter forRegion: the region to be downloaded.
/// - Parameter mapCache: the `MapCache` implementation used to download and store the downloaded data
/// - Parameter update: whether to check and update downloaded tiles
///
public init(forRegion region: TileCoordsRegion, mapCache: MapCacheProtocol) {
public init(forRegion region: TileCoordsRegion, mapCache: MapCacheProtocol, update: Bool = false) {
self.region = region
self.mapCache = mapCache
self.update = update
}

/// Resets downloader counters.
public func resetCounters() {
_stoped = false
_downloadedBytes = 0
_successfulTileDownloads = 0
_failedTileDownloads = 0
Expand All @@ -158,18 +168,31 @@ import MapKit
//Downloads stuff
resetCounters()
downloaderQueue.async {
/// Limit the number of tasks
let semaphore = DispatchSemaphore(value: 30)
for range: TileRange in self.region.tileRanges() ?? [] {
for tileCoords: TileCoords in range {
if self._stoped {
return
}
while semaphore.wait(timeout: DispatchTime(after: 10)) == .timedOut {
if self._stoped {
return
}
}

///Add to the download queue.
let mktileOverlayPath = MKTileOverlayPath(tileCoords: tileCoords)
self.mapCache.loadTile(at: mktileOverlayPath, result: {data,error in
self.mapCache.cacheTile(at: mktileOverlayPath, update: self.update, result: { size, error in
semaphore.signal()
if error != nil {
print(error?.localizedDescription ?? "Error downloading tile")
self._failedTileDownloads += 1
// TODO add to an array of tiles not downloaded
// so a retry can be performed
} else {
self._successfulTileDownloads += 1
self._downloadedBytes += UInt64(size)
print("RegionDownloader:: Donwloaded zoom: \(tileCoords.zoom) (x:\(tileCoords.tileX),y:\(tileCoords.tileY)) \(self.downloadedTiles)/\(self.totalTilesToDownload) \(self.downloadedPercentage)%")

}
Expand All @@ -191,9 +214,21 @@ import MapKit
}
}

/// Stop download.
public func stop() {
_stoped = true
}

/// Returns an estimation of the total number of bytes the whole region may occupy.
/// Again, it is an estimation.
public func estimateRegionByteSize() -> UInt64 {
return RegionDownloader.defaultAverageTileSizeBytes * self.region.count
}
}


public extension DispatchTime {
init(after: TimeInterval) {
self.init(uptimeNanoseconds:DispatchTime.now().uptimeNanoseconds + UInt64(after * 1000000000))
}
}
11 changes: 10 additions & 1 deletion MapCache/Classes/TileRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public enum TileRangeError: Error {
public struct TileRange: Sequence {

/// Zoom level.
var zoom: Zoom
public internal(set) var zoom: Zoom

/// Min value of tile in X axis.
var minTileX: TileNumber
Expand Down Expand Up @@ -85,5 +85,14 @@ public struct TileRange: Sequence {
public func makeIterator() -> TileRangeIterator {
return TileRangeIterator(self)
}

/// Check tile are included in this area
/// - Parameter tile: Tile that need to be checked
/// - Returns: true If the tile is contained in this area
public func contains(_ tile: TileCoords) -> Bool {
return tile.zoom == zoom
&& minTileX <= tile.tileX && tile.tileX <= maxTileX
&& minTileY <= tile.tileY && tile.tileY <= maxTileY
}
}

4 changes: 4 additions & 0 deletions MapCache/Classes/ZoomRange.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,8 @@ public struct ZoomRange : Sequence {
public func makeIterator() -> ZoomRangeIterator{
return ZoomRangeIterator(self)
}

public func contains(_ zoom: Zoom) -> Bool {
return min <= zoom && zoom <= max
}
}