Skip to content

Commit

Permalink
implement caching tiles with kingfisher
Browse files Browse the repository at this point in the history
  • Loading branch information
danielbarela committed Dec 20, 2022
1 parent 0cf2f41 commit 6799776
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 189 deletions.
4 changes: 4 additions & 0 deletions Marlin/Marlin.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
F72EAADF291EECF400AC6026 /* NTMActionBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EAADE291EECF400AC6026 /* NTMActionBar.swift */; };
F72EAAE1291F040D00AC6026 /* NoticeToMarinersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EAAE0291F040D00AC6026 /* NoticeToMarinersView.swift */; };
F73ABC80294D2E240085EDC5 /* LightRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73ABC7F294D2E240085EDC5 /* LightRange.swift */; };
F73ABC82295102480085EDC5 /* DataSourceTileProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73ABC81295102480085EDC5 /* DataSourceTileProvider.swift */; };
F740AA77289D68D800754293 /* MarlinCompactWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = F740AA76289D68D800754293 /* MarlinCompactWidth.swift */; };
F740AA79289D6DED00754293 /* MarlinRegularWidth.swift in Sources */ = {isa = PBXBuildFile; fileRef = F740AA78289D6DED00754293 /* MarlinRegularWidth.swift */; };
F74B9B0728B7CF650097D448 /* RadioBeacon+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F74B9B0628B7CF650097D448 /* RadioBeacon+CoreDataClass.swift */; };
Expand Down Expand Up @@ -404,6 +405,7 @@
F72EAADE291EECF400AC6026 /* NTMActionBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NTMActionBar.swift; sourceTree = "<group>"; };
F72EAAE0291F040D00AC6026 /* NoticeToMarinersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeToMarinersView.swift; sourceTree = "<group>"; };
F73ABC7F294D2E240085EDC5 /* LightRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LightRange.swift; sourceTree = "<group>"; };
F73ABC81295102480085EDC5 /* DataSourceTileProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataSourceTileProvider.swift; sourceTree = "<group>"; };
F740AA76289D68D800754293 /* MarlinCompactWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarlinCompactWidth.swift; sourceTree = "<group>"; };
F740AA78289D6DED00754293 /* MarlinRegularWidth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarlinRegularWidth.swift; sourceTree = "<group>"; };
F74B9B0628B7CF650097D448 /* RadioBeacon+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RadioBeacon+CoreDataClass.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -778,6 +780,7 @@
F707BA8E287DC999006A94C4 /* FetchRequestTileOverlay.swift */,
F71D287A28C1401400F1775C /* FetchRequestMap.swift */,
F7FFB33928D9EFDB00C31AA3 /* SearchView.swift */,
F73ABC81295102480085EDC5 /* DataSourceTileProvider.swift */,
);
path = Map;
sourceTree = "<group>";
Expand Down Expand Up @@ -1854,6 +1857,7 @@
F71D286A28BE467200F1775C /* DifferentialGPSStationDetailView.swift in Sources */,
F71DF4AF285927D600686951 /* MarlinScheme.swift in Sources */,
F7A54CD32848149700E5F565 /* Marlin.xcdatamodeld in Sources */,
F73ABC82295102480085EDC5 /* DataSourceTileProvider.swift in Sources */,
F707BA91287F1767006A94C4 /* RaconImage.swift in Sources */,
F7D671EF287475BC00758F1C /* DataSource.swift in Sources */,
F71DF4992858DA4700686951 /* AsamMap.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
Expand Down
8 changes: 3 additions & 5 deletions Marlin/Marlin/DataSources/Light/Light+CoreDataClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import CoreData
import MapKit
import OSLog
import SwiftUI
import Kingfisher

struct LightVolume {
var volumeQuery: String
Expand Down Expand Up @@ -43,18 +44,15 @@ class Light: NSManagedObject, LightProtocol {
static let raconColor = UIColor(argbValue: 0xffb52bb5)

static func postProcess() {
DispatchQueue.global(qos: .background).async {
DispatchQueue.global(qos: .utility).async {
let fetchRequest = NSFetchRequest<Light>(entityName: "Light")
fetchRequest.predicate = NSPredicate(format: "requiresPostProcessing == true")
let context = PersistenceController.current.newTaskContext()

if let objects = try? context.fetch(fetchRequest) {
// print("xxx post process \(objects.count) lights")
context.perform {

context.performAndWait {
for light in objects {
var ranges: [LightRange] = []
// print("\(light.volumeNumber) - \(light.featureNumber) range is: \(light.range)")
light.requiresPostProcessing = false
if let rangeString = light.range {
for rangeSplit in rangeString.components(separatedBy: CharacterSet(charactersIn: ";\n")) {
Expand Down
2 changes: 2 additions & 0 deletions Marlin/Marlin/DataSources/Light/Views/LightMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class LightMap<T: LightProtocol & MapImage>: FetchRequestMap<T> {
super.setupMixin(marlinMap: marlinMap, mapView: mapView)

userDefaultsShowLightRangesPublisher?
.dropFirst()
.removeDuplicates()
.handleEvents(receiveOutput: { showLightRanges in
print("Show light ranges: \(showLightRanges)")
Expand All @@ -42,6 +43,7 @@ class LightMap<T: LightProtocol & MapImage>: FetchRequestMap<T> {

userDefaultsShowLightSectorRangesPublisher?
.removeDuplicates()
.dropFirst()
.handleEvents(receiveOutput: { showLightSectorRanges in
print("Show light sector ranges: \(showLightSectorRanges)")
})
Expand Down
11 changes: 7 additions & 4 deletions Marlin/Marlin/Map/Annotation/MapImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation
import UIKit
import MapKit
import Kingfisher

protocol MapImage {
func mapImage(marker: Bool, zoomLevel: Int, tileBounds3857: MapBoundingBox?, context: CGContext?) -> [UIImage]
Expand All @@ -17,9 +18,14 @@ protocol MapImage {
var TILE_SIZE: Double { get }
static var key: String { get }
static var cacheTiles: Bool { get }
static var imageCache: Kingfisher.ImageCache { get }
}

extension MapImage {
static var imageCache: Kingfisher.ImageCache {
Kingfisher.ImageCache(name: key)
}

var TILE_SIZE: Double {
return 512.0
}
Expand All @@ -41,10 +47,7 @@ extension MapImage {
circle.stroke()
dataSource.color.setFill()
circle.fill()
if let cachedImage = type(of:dataSource).cachedImage(zoomLevel: zoomLevel) {
cachedImage.draw(at: CGPoint(x: pixel.x - cachedImage.size.width / 2.0, y: pixel.y - cachedImage.size.height / 2.0))
} else if let dataSourceImage = type(of:dataSource).image?.aspectResize(to: CGSize(width: radius * 2.0 / 1.5, height: radius * 2.0 / 1.5)).withRenderingMode(.alwaysTemplate).maskWithColor(color: UIColor.white){
type(of:dataSource).cacheImage(zoomLevel: zoomLevel, image: dataSourceImage)
if let dataSourceImage = type(of:dataSource).image?.aspectResize(to: CGSize(width: radius * 2.0 / 1.5, height: radius * 2.0 / 1.5)).withRenderingMode(.alwaysTemplate).maskWithColor(color: UIColor.white){
dataSourceImage.draw(at: CGPoint(x: pixel.x - dataSourceImage.size.width / 2.0, y: pixel.y - dataSourceImage.size.height / 2.0))
}
} else {
Expand Down
233 changes: 233 additions & 0 deletions Marlin/Marlin/Map/DataSourceTileProvider.swift
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)))
}
}
2 changes: 1 addition & 1 deletion Marlin/Marlin/Map/FetchRequestMap.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class FetchRequestMap<T: MapImage>: NSObject, MapMixin {
self.showAsTiles = showAsTiles
self.fetchPredicate = fetchPredicate
self.objects = objects
imageCache = Kingfisher.ImageCache(name: T.key)
imageCache = T.imageCache
}

func getFetchRequest(mapState: MapState) -> NSFetchRequest<NSManagedObject>? {
Expand Down
Loading

0 comments on commit 6799776

Please sign in to comment.