diff --git a/MapCache/Classes/DiskCache/DiskCache.swift b/MapCache/Classes/DiskCache/DiskCache.swift index 0f31a1f..98c30f1 100644 --- a/MapCache/Classes/DiskCache/DiskCache.swift +++ b/MapCache/Classes/DiskCache/DiskCache.swift @@ -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 diff --git a/MapCache/Classes/MapCache.swift b/MapCache/Classes/MapCache.swift index 46419ca..769d3e7 100644 --- a/MapCache/Classes/MapCache.swift +++ b/MapCache/Classes/MapCache.swift @@ -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) @@ -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 diff --git a/MapCache/Classes/MapCacheProtocol.swift b/MapCache/Classes/MapCacheProtocol.swift index d741022..469514f 100644 --- a/MapCache/Classes/MapCacheProtocol.swift +++ b/MapCache/Classes/MapCacheProtocol.swift @@ -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) + } diff --git a/MapCache/Classes/RegionDownloader.swift b/MapCache/Classes/RegionDownloader.swift index fedcffa..d5d668a 100644 --- a/MapCache/Classes/RegionDownloader.swift +++ b/MapCache/Classes/RegionDownloader.swift @@ -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 { @@ -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 { @@ -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 @@ -158,11 +168,23 @@ 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 @@ -170,6 +192,7 @@ import MapKit // 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)%") } @@ -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)) + } +} diff --git a/MapCache/Classes/TileRange.swift b/MapCache/Classes/TileRange.swift index ba9a0ed..a4620cb 100644 --- a/MapCache/Classes/TileRange.swift +++ b/MapCache/Classes/TileRange.swift @@ -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 @@ -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 + } } diff --git a/MapCache/Classes/ZoomRange.swift b/MapCache/Classes/ZoomRange.swift index 3c2a0ec..a629d1b 100644 --- a/MapCache/Classes/ZoomRange.swift +++ b/MapCache/Classes/ZoomRange.swift @@ -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 + } }