-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement caching tiles with kingfisher
- Loading branch information
1 parent
0cf2f41
commit 6799776
Showing
9 changed files
with
265 additions
and
189 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
// | ||
// DataSourceTileProvider.swift | ||
// Marlin | ||
// | ||
// Created by Daniel Barela on 12/19/22. | ||
// | ||
|
||
import Foundation | ||
import Kingfisher | ||
import CoreData | ||
import MapKit | ||
import sf_proj_ios | ||
import sf_ios | ||
|
||
enum DataTileError: Error { | ||
case zeroObjects | ||
case notFound | ||
case unexpected(code: Int) | ||
} | ||
|
||
extension DataTileError: CustomStringConvertible { | ||
public var description: String { | ||
switch self { | ||
case .zeroObjects: | ||
return "There were no objects for this image." | ||
case .notFound: | ||
return "The specified item could not be found." | ||
case .unexpected(_): | ||
return "An unexpected error occurred." | ||
} | ||
} | ||
} | ||
|
||
extension DataTileError: LocalizedError { | ||
public var errorDescription: String? { | ||
switch self { | ||
case .zeroObjects: | ||
return NSLocalizedString( | ||
"There were no objects for this image.", | ||
comment: "Zero Objects" | ||
) | ||
case .notFound: | ||
return NSLocalizedString( | ||
"The specified item could not be found.", | ||
comment: "Resource Not Found" | ||
) | ||
case .unexpected(_): | ||
return NSLocalizedString( | ||
"An unexpected error occurred.", | ||
comment: "Unexpected Error" | ||
) | ||
} | ||
} | ||
} | ||
|
||
struct DataSourceTileProvider<T : MapImage>: ImageDataProvider { | ||
var cacheKey: String { | ||
var key = "\(T.self.key)/\(path.z)/\(path.x)/\(path.y)" | ||
if let predicate = predicate { | ||
key = "\(key)/\(predicate.debugDescription)" | ||
} | ||
return key | ||
} | ||
let predicate: NSPredicate? | ||
let sortDescriptors: [NSSortDescriptor]? | ||
let path: MKTileOverlayPath | ||
let tileSize: CGSize | ||
let objects: [T]? | ||
|
||
init(path: MKTileOverlayPath, predicate: NSPredicate?, sortDescriptors: [NSSortDescriptor]?, objects: [T]? = nil, tileSize: CGSize) { | ||
self.path = path | ||
self.predicate = predicate | ||
self.sortDescriptors = sortDescriptors | ||
self.tileSize = tileSize | ||
self.objects = objects | ||
} | ||
|
||
func data(handler: @escaping (Result<Data, Error>) -> Void) { | ||
DispatchQueue.global(qos: .userInitiated).async { | ||
let zoomLevel = path.z | ||
|
||
let minTileLon = longitude(x: path.x, zoom: path.z) | ||
let maxTileLon = longitude(x: path.x+1, zoom: path.z) | ||
let minTileLat = latitude(y: path.y+1, zoom: path.z) | ||
let maxTileLat = latitude(y: path.y, zoom: path.z) | ||
|
||
guard let neCorner3857 = SFGeometryUtils.degreesToMetersWith(x: maxTileLon, andY: maxTileLat), | ||
let swCorner3857 = SFGeometryUtils.degreesToMetersWith(x: minTileLon, andY: minTileLat) else { | ||
return | ||
} | ||
|
||
let minTileX = swCorner3857.x.doubleValue | ||
let minTileY = swCorner3857.y.doubleValue | ||
let maxTileX = neCorner3857.x.doubleValue | ||
let maxTileY = neCorner3857.y.doubleValue | ||
|
||
// border the tile by 40 miles since that is as far as any light i have seen. if that is wrong, update | ||
// border has to be at least 30 pixels as well | ||
let nauticalMilesMeasurement = NSMeasurement(doubleValue: 40.0, unit: UnitLength.nauticalMiles) | ||
let metersMeasurement = nauticalMilesMeasurement.converting(to: UnitLength.meters).value | ||
|
||
let tolerance = max(metersMeasurement, ((maxTileY - minTileY) / self.tileSize.width) * 30.0) | ||
|
||
guard let neCornerTolerance = SFGeometryUtils.metersToDegreesWith(x: maxTileX + tolerance, andY: maxTileY + tolerance), | ||
let swCornerTolerance = SFGeometryUtils.metersToDegreesWith(x: minTileX - tolerance, andY:minTileY - tolerance) else { | ||
return | ||
} | ||
|
||
drawTile(tileBounds3857: MapBoundingBox(swCorner: (x: swCorner3857.x.doubleValue, y: swCorner3857.y.doubleValue), neCorner: (x: neCorner3857.x.doubleValue, y: neCorner3857.y.doubleValue)), queryBounds: MapBoundingBox(swCorner: (x: swCornerTolerance.x.doubleValue, y: swCornerTolerance.y.doubleValue), neCorner: (x: neCornerTolerance.x.doubleValue, y: neCornerTolerance.y.doubleValue)), zoomLevel: zoomLevel, cacheKey: cacheKey, handler: handler) | ||
} | ||
} | ||
|
||
func drawTile(tileBounds3857: MapBoundingBox, queryBounds: MapBoundingBox, zoomLevel: Int, cacheKey: String, handler: @escaping (Result<Data, Error>) -> Void) { | ||
|
||
var boundsPredicate: NSPredicate? | ||
|
||
if queryBounds.swCorner.x < -180 { | ||
boundsPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [ | ||
NSPredicate(format: "latitude >= %lf AND latitude <= %lf AND longitude >= %lf AND longitude <= %lf", queryBounds.swCorner.y, queryBounds.neCorner.y, -180.0, queryBounds.neCorner.x), | ||
NSPredicate(format: "latitude >= %lf AND latitude <= %lf AND longitude >= %lf AND longitude <= %lf", queryBounds.swCorner.y, queryBounds.neCorner.y, queryBounds.swCorner.x + 360.0, 180.0) | ||
]) | ||
} else if queryBounds.neCorner.x > 180 { | ||
boundsPredicate = NSCompoundPredicate(orPredicateWithSubpredicates: [ | ||
NSPredicate(format: "latitude >= %lf AND latitude <= %lf AND longitude >= %lf AND longitude <= %lf", queryBounds.swCorner.y, queryBounds.neCorner.y, queryBounds.swCorner.x, 180.0), | ||
NSPredicate(format: "latitude >= %lf AND latitude <= %lf AND longitude >= %lf AND longitude <= %lf", queryBounds.swCorner.y, queryBounds.neCorner.y, -180.0, queryBounds.neCorner.x - 360.0) | ||
]) | ||
} else { | ||
boundsPredicate = NSPredicate( | ||
format: "latitude >= %lf AND latitude <= %lf AND longitude >= %lf AND longitude <= %lf", queryBounds.swCorner.y, queryBounds.neCorner.y, queryBounds.swCorner.x, queryBounds.neCorner.x | ||
) | ||
} | ||
|
||
guard let boundsPredicate = boundsPredicate else { | ||
return | ||
} | ||
|
||
var finalPredicate: NSPredicate = boundsPredicate | ||
|
||
if let predicate = predicate { | ||
finalPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, boundsPredicate]) | ||
} | ||
|
||
let objects = getMatchingObjects(predicate: finalPredicate) | ||
if objects == nil || objects?.count == 0 { | ||
handler(.failure(DataTileError.zeroObjects)) | ||
return | ||
} | ||
|
||
UIGraphicsBeginImageContext(self.tileSize) | ||
|
||
if let objects = objects { | ||
for object in objects { | ||
let mapImages = object.mapImage(marker: false, zoomLevel: zoomLevel, tileBounds3857: tileBounds3857, context: UIGraphicsGetCurrentContext()) | ||
for mapImage in mapImages { | ||
let object3857Location = coord4326To3857(longitude: object.longitude, latitude: object.latitude) | ||
let xPosition = (((object3857Location.x - tileBounds3857.swCorner.x) / (tileBounds3857.neCorner.x - tileBounds3857.swCorner.x)) * self.tileSize.width) | ||
let yPosition = self.tileSize.height - (((object3857Location.y - tileBounds3857.swCorner.y) / (tileBounds3857.neCorner.y - tileBounds3857.swCorner.y)) * self.tileSize.height) | ||
mapImage.draw(in: CGRect(x: (xPosition - (mapImage.size.width / 2)), y: (yPosition - (mapImage.size.height / 2)), width: mapImage.size.width, height: mapImage.size.height)) | ||
} | ||
} | ||
} | ||
|
||
let newImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()! | ||
|
||
UIGraphicsEndImageContext() | ||
guard let cgImage = newImage.cgImage else { | ||
handler(.failure(DataTileError.notFound)) | ||
return | ||
} | ||
let data = UIImage(cgImage: cgImage).pngData() | ||
if let data = data { | ||
handler(.success(data)) | ||
} else { | ||
handler(.failure(DataTileError.notFound)) | ||
} | ||
|
||
} | ||
|
||
func getMatchingObjects(predicate: NSPredicate) -> [T]? { | ||
if let objects = objects { | ||
|
||
let filteredObjects: [T] = objects.filter { object in | ||
return predicate.evaluate(with: object) | ||
} | ||
|
||
return filteredObjects | ||
} | ||
|
||
if let M = T.self as? NSManagedObject.Type { | ||
|
||
let tileFetchRequest = M.fetchRequest() | ||
tileFetchRequest.sortDescriptors = sortDescriptors | ||
|
||
tileFetchRequest.predicate = predicate | ||
|
||
let context = PersistenceController.current.newTaskContext() | ||
let objects = try? context.fetch(tileFetchRequest) | ||
return objects as? [T] | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func coord4326To3857(longitude: Double, latitude: Double) -> (x: Double, y: Double) { | ||
let a = 6378137.0 | ||
let lambda = longitude / 180 * Double.pi; | ||
let phi = latitude / 180 * Double.pi; | ||
let x = a * lambda; | ||
let y = a * log(tan(Double.pi / 4 + phi / 2)); | ||
|
||
return (x:x, y:y); | ||
} | ||
|
||
func coord3857To4326(y: Double, x: Double) -> (lat: Double, lon: Double) { | ||
let a = 6378137.0 | ||
let d = -y / a | ||
let phi = Double.pi / 2 - 2 * atan(exp(d)) | ||
let lambda = x / a | ||
let lat = phi / Double.pi * 180 | ||
let lon = lambda / Double.pi * 180 | ||
|
||
return (lat: lat, lon: lon) | ||
} | ||
|
||
func longitude(x: Int, zoom: Int) -> Double { | ||
return Double(x) / pow(2.0, Double(zoom)) * 360.0 - 180.0 | ||
} | ||
|
||
func latitude(y: Int, zoom: Int) -> Double { | ||
let n = Double.pi - 2.0 * Double.pi * Double(y) / pow(2.0, Double(zoom)) | ||
return 180.0 / Double.pi * atan(0.5 * (exp(n) - exp(-n))) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.