2020-05-16 11:21:55 +02:00
//
// D a t a b a s e . s w i f t
// C a p C o l l e c t o r
//
// C r e a t e d b y C h r i s t o p h o n 1 4 . 0 4 . 2 0 .
// C o p y r i g h t © 2 0 2 0 C H . A l l r i g h t s r e s e r v e d .
//
import Foundation
import UIKit
import CoreML
import SQLite
2021-06-13 14:42:49 +02:00
protocol DatabaseDelegate : AnyObject {
2020-05-16 11:21:55 +02:00
func database ( didAddCap cap : Cap )
2020-06-18 22:55:51 +02:00
func database ( didChangeCap cap : Int )
2020-05-16 11:21:55 +02:00
2020-06-18 22:55:51 +02:00
func database ( didLoadImageForCap cap : Int )
2020-05-16 11:21:55 +02:00
2021-01-13 21:43:46 +01:00
func database ( completedBackgroundWorkItem title : String , subtitle : String )
func database ( needsUserConfirmation title : String , body : String , shouldProceed : @ escaping ( Bool ) -> Void )
func database ( didFailBackgroundWork title : String , subtitle : String )
func databaseHasNewClassifier ( )
func databaseDidFinishBackgroundWork ( )
2020-06-18 22:55:51 +02:00
func databaseNeedsFullRefresh ( )
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
private enum BackgroundWorkTaskType : Int , CustomStringConvertible , Comparable {
case downloadCapNames = 9
case downloadCounts = 8
case downloadClassifier = 7
case uploadingCaps = 6
case uploadingImages = 5
case downloadMainImages = 4
case creatingThumbnails = 3
case creatingColors = 2
var description : String {
switch self {
case . downloadCapNames :
return " Downloading names "
case . downloadCounts :
return " Downloading counts "
case . downloadClassifier :
return " Downloading classifier "
case . uploadingCaps :
return " Uploading caps "
case . uploadingImages :
return " Uploading images "
case . downloadMainImages :
return " Downloading images "
case . creatingThumbnails :
return " Creating thumbnails "
case . creatingColors :
return " Creating colors "
}
}
var maximumNumberOfSimultaneousItems : Int {
switch self {
case . downloadMainImages :
return 50
case . creatingThumbnails :
return 10
case . creatingColors :
return 10
default :
return 1
}
}
var nextType : BackgroundWorkTaskType ? {
BackgroundWorkTaskType ( rawValue : rawValue - 1 )
}
static func < ( lhs : BackgroundWorkTaskType , rhs : BackgroundWorkTaskType ) -> Bool {
lhs . rawValue < rhs . rawValue
}
}
2020-05-16 11:21:55 +02:00
final class Database {
// MARK: V a r i a b l e s
let db : Connection
2021-01-13 21:43:46 +01:00
private let upload : Upload
private let download : Download
2020-05-16 11:21:55 +02:00
2021-01-13 21:43:46 +01:00
let storage : Storage
2020-05-16 11:21:55 +02:00
2020-06-18 22:55:51 +02:00
weak var delegate : DatabaseDelegate ?
2021-01-13 21:43:46 +01:00
init ? ( url : URL , server : URL , storageFolder : URL ) {
2020-05-16 11:21:55 +02:00
guard let db = try ? Connection ( url . path ) else {
return nil
}
let upload = Upload ( server : server )
let download = Download ( server : server )
do {
try db . run ( Cap . createQuery )
try db . run ( upload . createQuery )
2020-06-18 22:55:51 +02:00
try db . run ( Database . Colors . createQuery )
try db . run ( Database . TileImage . createQuery )
2020-05-16 11:21:55 +02:00
} catch {
return nil
}
self . db = db
self . upload = upload
self . download = download
2021-01-13 21:43:46 +01:00
self . storage = Storage ( in : storageFolder )
2020-05-16 11:21:55 +02:00
log ( " Database loaded with \( capCount ) caps " )
}
// MARK: C o m p u t e d p r o p e r t i e s
// / A l l c a p s c u r r e n t l y i n t h e d a t a b a s e
var caps : [ Cap ] {
( try ? db . prepare ( Cap . table ) ) ? . map ( Cap . init ) ? ? [ ]
}
2021-01-13 21:43:46 +01:00
// / T h e i d s o f a l l c a p s
var capIds : Set < Int > {
Set ( caps . map { $0 . id } )
}
2020-06-18 22:55:51 +02:00
// / A d i c t i o n a r y o f a l l c a p s , i n d e x e d b y t h e i r i d s
var capDict : [ Int : Cap ] {
caps . reduce ( into : [ : ] ) { $0 [ $1 . id ] = $1 }
}
2020-05-16 11:21:55 +02:00
// / T h e i d s o f t h e c a p s w h i c h w e r e n ' t i n c l u d e d i n t h e l a s t c l a s s i f i c a t i o n
var unmatchedCaps : [ Int ] {
2020-06-18 22:55:51 +02:00
let query = Cap . table . select ( Cap . columnId ) . filter ( Cap . columnMatched = = false )
return ( try ? db . prepare ( query ) . map { $0 [ Cap . columnId ] } ) ? ? [ ]
2020-05-16 11:21:55 +02:00
}
// / T h e n u m b e r o f c a p s w h i c h c o u l d b e r e c o g n i z e d d u r i n g t h e l a s t c l a s s i f i c a t i o n
var recognizedCapCount : Int {
2020-06-18 22:55:51 +02:00
( try ? db . scalar ( Cap . table . filter ( Cap . columnMatched = = true ) . count ) ) ? ? 0
2020-05-16 11:21:55 +02:00
}
// / T h e n u m b e r o f c a p s c u r r e n t l y i n t h e d a t a b a s e
var capCount : Int {
( try ? db . scalar ( Cap . table . count ) ) ? ? 0
}
// / T h e t o t a l n u m b e r o f i m a g e s f o r a l l c a p s
var imageCount : Int {
2020-06-18 22:55:51 +02:00
( try ? db . prepare ( Cap . table ) . reduce ( 0 ) { $0 + $1 [ Cap . columnCount ] } ) ? ? 0
}
2021-01-13 21:43:46 +01:00
var nextPendingCapUpload : Cap ? {
do {
guard let row = try db . pluck ( Cap . table . filter ( Cap . columnUploaded = = false ) . order ( Cap . columnId . asc ) ) else {
return nil
}
return Cap ( row : row )
} catch {
log ( " Failed to get next pending cap upload " )
return nil
}
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
var pendingCapUploadCount : Int {
do {
let query = Cap . table . filter ( Cap . columnUploaded = = false ) . count
return try db . scalar ( query )
} catch {
log ( " Failed to get pending cap upload count " )
return 0
}
2020-06-18 22:55:51 +02:00
}
2021-01-13 21:43:46 +01:00
var nextPendingImageUpload : ( id : Int , version : Int ) ? {
2020-05-16 11:21:55 +02:00
do {
2021-01-13 21:43:46 +01:00
guard let row = try db . pluck ( upload . table ) else {
return nil
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
return ( id : row [ upload . rowCapId ] , version : row [ upload . rowCapVersion ] )
2020-05-16 11:21:55 +02:00
} catch {
2020-06-18 22:55:51 +02:00
log ( " Failed to get pending image uploads " )
2021-01-13 21:43:46 +01:00
return nil
2020-05-16 11:21:55 +02:00
}
}
2021-01-13 21:43:46 +01:00
var capsWithImages : Set < Int > {
capIds . filter { storage . hasImage ( for : $0 ) }
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
var capsWithThumbnails : Set < Int > {
capIds . filter { storage . hasThumbnail ( for : $0 ) }
}
var pendingImageUploadCount : Int {
( ( try ? db . scalar ( upload . table . count ) ) ? ? 0 )
2020-06-18 22:55:51 +02:00
}
2021-01-13 21:43:46 +01:00
// / T h e n u m b e r o f c a p s w i t h o u t a t h u m b n a i l o n d i s k
var pendingCapForThumbnailCreation : Int {
caps . reduce ( 0 ) { $0 + ( storage . hasThumbnail ( for : $1 . id ) ? 0 : 1 ) }
}
var pendingCapsForColorCreation : Int {
2021-01-10 16:11:31 +01:00
do {
2021-01-13 21:43:46 +01:00
return try capCount - db . scalar ( Colors . table . count )
2021-01-10 16:11:31 +01:00
} catch {
2021-01-13 21:43:46 +01:00
log ( " Failed to get count of caps without color: \( error ) " )
return 0
2021-01-10 16:11:31 +01:00
}
}
2021-01-13 21:43:46 +01:00
2020-05-16 11:21:55 +02:00
var classifierVersion : Int {
set {
UserDefaults . standard . set ( newValue , forKey : Classifier . userDefaultsKey )
log ( " Classifier version set to \( newValue ) " )
}
get {
UserDefaults . standard . integer ( forKey : Classifier . userDefaultsKey )
}
}
2020-08-09 21:04:30 +02:00
var isInOfflineMode : Bool {
set {
UserDefaults . standard . set ( newValue , forKey : Upload . offlineKey )
log ( " Offline mode set to \( newValue ) " )
}
get {
UserDefaults . standard . bool ( forKey : Upload . offlineKey )
}
}
2020-05-16 11:21:55 +02:00
// MARK: D a t a u p d a t e s
/* *
Create a new cap with an image .
The cap is inserted into the database , and the name and image will be uploaded to the server .
- parameter image : The main image of the cap
- parameter name : The name of the cap
- note : Must be called on the main queue .
- note : The registered delegates will be informed about the added cap through ` database ( didAddCap : ) `
- returns : ` true ` , if the cap was created .
*/
func createCap ( image : UIImage , name : String ) -> Bool {
2020-06-18 22:55:51 +02:00
let cap = Cap ( name : name , id : capCount + 1 )
2020-05-16 11:21:55 +02:00
guard insert ( cap : cap ) else {
2020-06-18 22:55:51 +02:00
log ( " Cap not inserted " )
2020-05-16 11:21:55 +02:00
return false
}
2021-01-13 21:43:46 +01:00
guard storage . save ( image : image , for : cap . id ) else {
2020-06-18 22:55:51 +02:00
log ( " Cap image not saved " )
2020-05-16 11:21:55 +02:00
return false
}
2021-01-13 21:43:46 +01:00
addPendingUpload ( for : cap . id , version : 0 )
startBackgroundWork ( )
2020-05-16 11:21:55 +02:00
return true
}
/* *
Insert a new cap .
Only inserts the cap into the database , and optionally notifies the delegates .
- note : When a new cap is created , use ` createCap ( image : name : ) ` instead
*/
@ discardableResult
2020-06-18 22:55:51 +02:00
private func insert ( cap : Cap , notify : Bool = true ) -> Bool {
2020-05-16 11:21:55 +02:00
do {
try db . run ( cap . insertQuery )
2020-06-18 22:55:51 +02:00
if notify {
DispatchQueue . main . async {
self . delegate ? . database ( didAddCap : cap )
}
2020-05-16 11:21:55 +02:00
}
return true
} catch {
log ( " Failed to insert cap \( cap . id ) : \( error ) " )
return false
}
}
func add ( image : UIImage , for cap : Int ) -> Bool {
guard let version = count ( for : cap ) else {
log ( " Failed to get count for cap \( cap ) " )
return false
}
2021-01-13 21:43:46 +01:00
guard storage . save ( image : image , for : cap , version : version ) else {
2020-05-16 11:21:55 +02:00
log ( " Failed to save image \( version ) for cap \( cap ) to disk " )
return false
}
guard update ( count : version + 1 , for : cap ) else {
log ( " Failed update count \( version ) for cap \( cap ) " )
return false
}
guard addPendingUpload ( for : cap , version : version ) else {
log ( " Failed to add cap \( cap ) version \( version ) to upload queue " )
return false
}
2021-01-13 21:43:46 +01:00
startBackgroundWork ( )
2020-05-16 11:21:55 +02:00
return true
}
// MARK: U p d a t i n g c a p p r o p e r t i e s
2020-06-18 22:55:51 +02:00
private func update ( _ property : String , for cap : Int , notify : Bool = true , setter : Setter . . . ) -> Bool {
2020-05-16 11:21:55 +02:00
do {
let query = updateQuery ( for : cap ) . update ( setter )
try db . run ( query )
2020-06-18 22:55:51 +02:00
if notify {
DispatchQueue . main . async {
self . delegate ? . database ( didChangeCap : cap )
}
}
2020-05-16 11:21:55 +02:00
return true
} catch {
log ( " Failed to update \( property ) for cap \( cap ) : \( error ) " )
return false
}
}
@ discardableResult
private func update ( uploaded : Bool , for cap : Int ) -> Bool {
2020-06-18 22:55:51 +02:00
update ( " uploaded " , for : cap , setter : Cap . columnUploaded <- uploaded )
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
2020-05-16 11:21:55 +02:00
@ discardableResult
2020-06-18 22:55:51 +02:00
private func update ( count : Int , for cap : Int ) -> Bool {
update ( " count " , for : cap , setter : Cap . columnCount <- count )
2020-05-16 11:21:55 +02:00
}
@ discardableResult
2020-06-18 22:55:51 +02:00
private func update ( matched : Bool , for cap : Int ) -> Bool {
update ( " matched " , for : cap , setter : Cap . columnMatched <- matched )
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
// MARK: E x t e r n a l e d i t i n g
2020-05-16 11:21:55 +02:00
2020-06-18 22:55:51 +02:00
/* *
Update the ` name ` of a cap .
*/
2020-05-16 11:21:55 +02:00
@ discardableResult
2020-06-18 22:55:51 +02:00
func update ( name : String , for cap : Int ) -> Bool {
guard update ( " name " , for : cap , setter : Cap . columnName <- name , Cap . columnUploaded <- false ) else {
return false
}
2021-01-13 21:43:46 +01:00
startBackgroundWork ( )
2020-06-18 22:55:51 +02:00
return true
2020-05-16 11:21:55 +02:00
}
@ discardableResult
2020-06-18 22:55:51 +02:00
private func updateWithoutUpload ( name : String , for cap : Int ) -> Bool {
update ( " name " , for : cap , notify : false , setter : Cap . columnName <- name )
2020-05-16 11:21:55 +02:00
}
func update ( recognizedCaps : Set < Int > ) {
let unrecognized = self . unmatchedCaps
// U p d a t e c a p s w h i c h h a v e n ' t b e e n r e c o g n i z e d b e f o r e
let newlyRecognized = recognizedCaps . intersection ( unrecognized )
let logIndividualMessages = newlyRecognized . count < 10
if ! logIndividualMessages {
log ( " Marking \( newlyRecognized . count ) caps as matched " )
}
for cap in newlyRecognized {
if logIndividualMessages {
log ( " Marking cap \( cap ) as matched " )
}
update ( matched : true , for : cap )
}
// U p d a t e c a p s w h i c h a r e n o l o n g e r r e c o g n i z e d
let missing = Set ( 1. . . capCount ) . subtracting ( recognizedCaps ) . subtracting ( unrecognized )
for cap in missing {
log ( " Marking cap \( cap ) as not matched " )
update ( matched : false , for : cap )
}
}
2020-06-18 22:55:51 +02:00
// MARK: U p l o a d s
2021-01-13 21:43:46 +01:00
@ discardableResult
2020-06-18 22:55:51 +02:00
private func addPendingUpload ( for cap : Int , version : Int ) -> Bool {
2020-05-16 11:21:55 +02:00
do {
2021-01-13 21:43:46 +01:00
guard try db . scalar ( upload . existsQuery ( for : cap , version : version ) ) = = 0 else {
return true
}
2020-05-16 11:21:55 +02:00
try db . run ( upload . insertQuery ( for : cap , version : version ) )
return true
} catch {
log ( " Failed to add pending upload of cap \( cap ) version \( version ) : \( error ) " )
return false
}
}
2021-01-13 21:43:46 +01:00
@ discardableResult
2020-06-18 22:55:51 +02:00
private func removePendingUpload ( for cap : Int , version : Int ) -> Bool {
2020-05-16 11:21:55 +02:00
do {
try db . run ( upload . deleteQuery ( for : cap , version : version ) )
return true
} catch {
log ( " Failed to remove pending upload of cap \( cap ) version \( version ) : \( error ) " )
return false
}
}
// MARK: I n f o r m a t i o n r e t r i e v a l
func cap ( for id : Int ) -> Cap ? {
do {
guard let row = try db . pluck ( updateQuery ( for : id ) ) else {
log ( " No cap with id \( id ) in database " )
return nil
}
return Cap ( row : row )
} catch {
log ( " Failed to get cap \( id ) : \( error ) " )
return nil
}
}
private func count ( for cap : Int ) -> Int ? {
do {
2020-06-18 22:55:51 +02:00
let row = try db . pluck ( updateQuery ( for : cap ) . select ( Cap . columnCount ) )
return row ? [ Cap . columnCount ]
2020-05-16 11:21:55 +02:00
} catch {
log ( " Failed to get count for cap \( cap ) " )
return nil
}
}
func countOfCaps ( withImageCountLessThan limit : Int ) -> Int {
do {
2020-06-18 22:55:51 +02:00
return try db . scalar ( Cap . table . filter ( Cap . columnCount < limit ) . count )
2020-05-16 11:21:55 +02:00
} catch {
log ( " Failed to get caps with less than \( limit ) images " )
return 0
}
}
func lowestImageCountForCaps ( startingAt start : Int ) -> ( count : Int , numberOfCaps : Int ) {
do {
var currentCount = start - 1
var capsFound = 0
repeat {
currentCount += 1
2020-06-18 22:55:51 +02:00
capsFound = try db . scalar ( Cap . table . filter ( Cap . columnCount = = currentCount ) . count )
2020-05-16 11:21:55 +02:00
} while capsFound = = 0
return ( currentCount , capsFound )
} catch {
return ( 0 , 0 )
}
}
func updateQuery ( for cap : Int ) -> Table {
2020-06-18 22:55:51 +02:00
Cap . table . filter ( Cap . columnId = = cap )
2020-05-16 11:21:55 +02:00
}
// MARK: D o w n l o a d s
@ discardableResult
2021-01-10 16:11:31 +01:00
func downloadImage ( for cap : Int , version : Int = 0 , completion : @ escaping ( _ image : UIImage ? ) -> Void ) -> Bool {
2021-01-13 21:43:46 +01:00
let url = storage . localImageUrl ( for : cap , version : version )
return download . image ( for : cap , version : version , to : url ) { success in
if version = = 0 && success {
2021-01-10 16:11:31 +01:00
DispatchQueue . main . async {
self . delegate ? . database ( didLoadImageForCap : cap )
}
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
let image = self . storage . image ( for : cap , version : version )
2021-01-10 16:11:31 +01:00
completion ( image )
2020-05-16 11:21:55 +02:00
}
}
2020-06-18 22:55:51 +02:00
private func update ( names : [ String ] ) {
let notify = capCount > 0
log ( " Downloaded cap names (initialDownload: \( ! notify ) ) " )
let caps = self . capDict
let changed : [ Int ] = names . enumerated ( ) . compactMap { id , name in
let id = id + 1
guard let existingName = caps [ id ] ? . name else {
// I n s e r t c a p
let cap = Cap ( id : id , name : name , count : 0 )
guard insert ( cap : cap , notify : notify ) else {
return nil
}
return id
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
guard existingName != name else {
// N a m e u n c h a n g e d
return nil
}
guard updateWithoutUpload ( name : name , for : id ) else {
return nil
}
return id
}
if ! notify {
log ( " Added \( changed . count ) new caps after initial download " )
delegate ? . databaseNeedsFullRefresh ( )
2020-05-16 11:21:55 +02:00
}
}
2021-01-13 21:43:46 +01:00
var isDoingWorkInBackgound : Bool {
backgroundTaskStatus != nil
}
private var didUpdateBackgroundItems = false
private var backgroundTaskStatus : BackgroundWorkTaskType ? = nil
private var expectedBackgroundWorkStatus : BackgroundWorkTaskType ? = nil
private var nextBackgroundWorkStatus : BackgroundWorkTaskType ? {
guard let oldType = backgroundTaskStatus else {
return expectedBackgroundWorkStatus
}
guard let type = expectedBackgroundWorkStatus else {
return backgroundTaskStatus ? . nextType
}
guard oldType > type else {
return type
}
return oldType . nextType
}
private func setNextBackgroundWorkStatus ( ) -> BackgroundWorkTaskType ? {
backgroundTaskStatus = nextBackgroundWorkStatus
expectedBackgroundWorkStatus = nil
return backgroundTaskStatus
}
private let context = CIContext ( options : [ . workingColorSpace : kCFNull ! ] )
func startInitialDownload ( ) {
startBackgroundWork ( startingWith : . downloadCapNames )
}
func scheduleClassifierDownload ( ) {
startBackgroundWork ( startingWith : . downloadClassifier )
}
func startBackgroundWork ( ) {
startBackgroundWork ( startingWith : . uploadingCaps )
}
private func startBackgroundWork ( startingWith type : BackgroundWorkTaskType ) {
guard ! isDoingWorkInBackgound else {
if expectedBackgroundWorkStatus ? . rawValue ? ? 0 < type . rawValue {
log ( " Background work scheduled: \( type ) " )
expectedBackgroundWorkStatus = type
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
return
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
DispatchQueue . global ( qos : . utility ) . async {
self . performAllBackgroundWorkItems ( allItemsStartingAt : type )
}
}
private func performAllBackgroundWorkItems ( allItemsStartingAt type : BackgroundWorkTaskType ) {
didUpdateBackgroundItems = false
expectedBackgroundWorkStatus = type
log ( " Starting background task " )
while let type = setNextBackgroundWorkStatus ( ) {
log ( " Handling background task: \( type ) " )
guard performAllItems ( for : type ) else {
// I f a n e r r o r o c c u r s , s t o p t h e b a c k g r o u n d t a s k s
backgroundTaskStatus = nil
expectedBackgroundWorkStatus = nil
break
}
}
log ( " Background work completed " )
delegate ? . databaseDidFinishBackgroundWork ( )
}
private func performAllItems ( for type : BackgroundWorkTaskType ) -> Bool {
switch type {
case . downloadCapNames :
return downloadCapNames ( )
case . downloadCounts :
return downloadImageCounts ( )
case . downloadClassifier :
return downloadClassifier ( )
case . uploadingCaps :
return uploadCaps ( )
case . uploadingImages :
return uploadImages ( )
case . downloadMainImages :
return downloadMainImages ( )
case . creatingThumbnails :
return createThumbnails ( )
case . creatingColors :
return createColors ( )
}
}
private func downloadCapNames ( ) -> Bool {
log ( " Downloading cap names " )
let result = DispatchGroup . singleTask { callback in
download . names { names in
guard let names = names else {
callback ( false )
return
}
self . update ( names : names )
callback ( true )
}
}
log ( " Completed download of cap names " )
return result
}
private func downloadImageCounts ( ) -> Bool {
log ( " Downloading cap image counts " )
let result = DispatchGroup . singleTask { callback in
download . imageCounts { counts in
guard let counts = counts else {
self . log ( " Failed to download server image counts " )
callback ( false )
return
}
let newCaps = self . didDownload ( imageCounts : counts )
guard newCaps . count > 0 else {
callback ( true )
return
}
self . log ( " Found \( newCaps . count ) new caps on the server. " )
self . downloadInfo ( for : newCaps ) { success in
callback ( success )
}
}
}
guard result else {
log ( " Failed download of cap image counts " )
return false
}
log ( " Completed download of cap image counts " )
return true
}
private func downloadClassifier ( ) -> Bool {
log ( " Downloading classifier (if needed) " )
let result = DispatchGroup . singleTask { callback in
download . classifierVersion { version in
guard let version = version else {
self . log ( " Failed to download server model version " )
callback ( false )
return
}
let ownVersion = self . classifierVersion
guard ownVersion < version else {
self . log ( " Not updating classifier: Own version \( ownVersion ) , server version \( version ) " )
callback ( true )
return
}
let title = " Download classifier "
let detail = ownVersion = = 0 ?
" A classifier to match caps is available for download (version \( version ) ). Would you like to download it now? " :
" Version \( version ) of the classifier is available for download (You have version \( ownVersion ) ). Would you like to download it now? "
self . delegate ! . database ( needsUserConfirmation : title , body : detail ) { proceed in
guard proceed else {
self . log ( " User skipped classifier download " )
callback ( true )
return
}
self . download . classifier { progress , received , total in
let t = ByteCountFormatter . string ( fromByteCount : total , countStyle : . file )
let r = ByteCountFormatter . string ( fromByteCount : received , countStyle : . file )
let title = String ( format : " %.0f " , progress * 100 ) + " % ( \( r ) / \( t ) ) "
self . delegate ? . database ( completedBackgroundWorkItem : " Downloading classifier " , subtitle : title )
} completion : { url in
guard let url = url else {
self . log ( " Failed to download classifier " )
callback ( false )
return
}
let compiledUrl : URL
do {
compiledUrl = try MLModel . compileModel ( at : url )
} catch {
self . log ( " Failed to compile downloaded classifier: \( error ) " )
callback ( false )
return
}
guard self . storage . save ( recognitionModelAt : compiledUrl ) else {
self . log ( " Failed to save compiled classifier " )
callback ( false )
return
}
callback ( true )
self . classifierVersion = version
}
}
}
}
log ( " Downloaded classifier (if new version existed) " )
return result
}
private func uploadCaps ( ) -> Bool {
var completed = 0
while let cap = nextPendingCapUpload {
guard upload . upload ( cap ) else {
delegate ? . database ( didFailBackgroundWork : " Upload failed " ,
subtitle : " Cap \( cap . id ) not uploaded " )
return false
}
update ( uploaded : true , for : cap . id )
completed += 1
let total = completed + pendingCapUploadCount
2021-06-13 14:42:49 +02:00
delegate ? . database ( completedBackgroundWorkItem : " Uploading caps " , subtitle : " \( completed ) of \( total ) " )
2021-01-13 21:43:46 +01:00
}
return true
}
private func uploadImages ( ) -> Bool {
var completed = 0
while let ( id , version ) = nextPendingImageUpload {
guard let cap = self . cap ( for : id ) else {
log ( " No cap \( id ) to upload image \( version ) " )
removePendingUpload ( for : id , version : version )
continue
}
guard let url = storage . existingImageUrl ( for : cap . id , version : version ) else {
log ( " No image \( version ) of cap \( id ) to upload " )
removePendingUpload ( for : id , version : version )
continue
}
guard let count = upload . upload ( imageAt : url , of : cap . id ) else {
delegate ? . database ( didFailBackgroundWork : " Upload failed " , subtitle : " Image \( version ) of cap \( id ) " )
return false
}
if count > cap . count {
update ( count : count , for : cap . id )
}
removePendingUpload ( for : id , version : version )
completed += 1
let total = completed + pendingImageUploadCount
delegate ? . database ( completedBackgroundWorkItem : " Uploading images " , subtitle : " \( completed + 1 ) of \( total ) " )
}
return true
}
private func downloadMainImages ( ) -> Bool {
let missing = caps . map { $0 . id } . filter { ! storage . hasImage ( for : $0 ) }
let count = missing . count
guard count > 0 else {
2020-05-16 11:21:55 +02:00
log ( " No images to download " )
2021-01-13 21:43:46 +01:00
return true
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
log ( " Starting image downloads " )
2020-05-16 11:21:55 +02:00
let group = DispatchGroup ( )
2021-01-13 21:43:46 +01:00
group . enter ( )
var shouldDownload = true
let title = " Download images "
let detail = " \( count ) caps have no image. Would you like to download them now? (~ \( ByteCountFormatter . string ( fromByteCount : Int64 ( count * 10000 ) , countStyle : . file ) ) ). Grid view is not available until all images are downloaded. "
delegate ? . database ( needsUserConfirmation : title , body : detail ) { proceed in
shouldDownload = proceed
group . leave ( )
}
group . wait ( )
guard shouldDownload else {
log ( " User skipped image download " )
return false
}
group . enter ( )
let queue = DispatchQueue ( label : " images " )
let semaphore = DispatchSemaphore ( value : 5 )
var downloadsAreSuccessful = true
var completed = 0
for cap in missing {
queue . async {
guard downloadsAreSuccessful else {
return
}
semaphore . wait ( )
let url = self . storage . localImageUrl ( for : cap )
self . download . image ( for : cap , to : url , queue : queue ) { success in
defer { semaphore . signal ( ) }
guard success else {
self . delegate ? . database ( didFailBackgroundWork : " Download failed " , subtitle : " Image of cap \( cap ) " )
downloadsAreSuccessful = false
2020-05-16 11:21:55 +02:00
group . leave ( )
2021-01-13 21:43:46 +01:00
return
2020-05-16 11:21:55 +02:00
}
2021-01-13 21:43:46 +01:00
completed += 1
self . delegate ? . database ( completedBackgroundWorkItem : " Downloading images " , subtitle : " \( completed ) of \( missing . count ) " )
if completed = = missing . count {
group . leave ( )
2020-05-16 11:21:55 +02:00
}
}
2021-01-13 21:43:46 +01:00
}
}
guard group . wait ( timeout : . now ( ) + TimeInterval ( missing . count * 2 ) ) = = . success else {
log ( " Timed out downloading images " )
return false
}
log ( " Finished all image downloads " )
return true
}
private func createThumbnails ( ) -> Bool {
let missing = caps . map { $0 . id } . filter { ! storage . hasThumbnail ( for : $0 ) }
guard missing . count > 0 else {
log ( " No thumbnails to create " )
return true
}
log ( " Creating thumbnails " )
let queue = DispatchQueue ( label : " thumbnails " )
let semaphore = DispatchSemaphore ( value : 5 )
let group = DispatchGroup ( )
group . enter ( )
var thumbnailsAreSuccessful = true
var completed = 0
for cap in missing {
queue . async {
guard thumbnailsAreSuccessful else {
return
}
semaphore . wait ( )
defer { semaphore . signal ( ) }
guard let image = self . storage . image ( for : cap ) else {
self . log ( " No image for cap \( cap ) to create thumbnail " )
self . delegate ? . database ( didFailBackgroundWork : " Creation failed " , subtitle : " Thumbnail of cap \( cap ) " )
thumbnailsAreSuccessful = false
group . leave ( )
return
}
let thumb = Cap . thumbnail ( for : image )
guard self . storage . save ( thumbnail : thumb , for : cap ) else {
self . log ( " Failed to save thumbnail for cap \( cap ) " )
self . delegate ? . database ( didFailBackgroundWork : " Image not saved " , subtitle : " Thumbnail of cap \( cap ) " )
thumbnailsAreSuccessful = false
group . leave ( )
return
}
completed += 1
self . delegate ? . database ( completedBackgroundWorkItem : " Creating thumbnails " , subtitle : " \( completed ) of \( missing . count ) " )
if completed = = missing . count {
group . leave ( )
}
}
}
guard group . wait ( timeout : . now ( ) + TimeInterval ( missing . count * 2 ) ) = = . success else {
log ( " Timed out creating thumbnails " )
return false
}
log ( " Finished all thumbnails " )
return true
}
private func createColors ( ) -> Bool {
let missing = capIds . subtracting ( capsWithColors )
guard missing . count > 0 else {
log ( " No colors to create " )
return true
}
log ( " Creating colors " )
let queue = DispatchQueue ( label : " colors " )
let semaphore = DispatchSemaphore ( value : 5 )
let group = DispatchGroup ( )
group . enter ( )
var colorsAreSuccessful = true
var completed = 0
for cap in missing {
queue . async {
guard colorsAreSuccessful else {
return
}
semaphore . wait ( )
defer { semaphore . signal ( ) }
guard let image = self . storage . ciImage ( for : cap ) else {
self . log ( " No image for cap \( cap ) to create color " )
self . delegate ? . database ( didFailBackgroundWork : " No thumbnail found " , subtitle : " Color of cap \( cap ) " )
colorsAreSuccessful = false
group . leave ( )
return
}
defer { self . context . clearCaches ( ) }
guard let color = image . averageColor ( context : self . context ) else {
self . log ( " Failed to create color for cap \( cap ) " )
self . delegate ? . database ( didFailBackgroundWork : " Calculation failed " , subtitle : " Color of cap \( cap ) " )
colorsAreSuccessful = false
group . leave ( )
return
}
guard self . set ( color : color , for : cap ) else {
self . log ( " Failed to save color for cap \( cap ) " )
self . delegate ? . database ( didFailBackgroundWork : " Color not saved " , subtitle : " Color of cap \( cap ) " )
colorsAreSuccessful = false
group . leave ( )
return
}
completed += 1
self . delegate ? . database ( completedBackgroundWorkItem : " Creating colors " , subtitle : " \( completed ) of \( missing . count ) " )
if completed = = missing . count {
group . leave ( )
2020-05-16 11:21:55 +02:00
}
}
}
2021-01-13 21:43:46 +01:00
guard group . wait ( timeout : . now ( ) + TimeInterval ( missing . count * 2 ) ) = = . success else {
log ( " Timed out creating colors " )
return false
}
log ( " Finished all colors " )
return true
2020-05-16 11:21:55 +02:00
}
func hasNewClassifier ( completion : @ escaping ( _ version : Int ? , _ size : Int64 ? ) -> Void ) {
download . classifierVersion { version in
guard let version = version else {
self . log ( " Failed to download server model version " )
completion ( nil , nil )
return
}
let ownVersion = self . classifierVersion
guard ownVersion < version else {
self . log ( " Not updating classifier: Own version \( ownVersion ) , server version \( version ) " )
completion ( nil , nil )
return
}
2020-06-18 22:55:51 +02:00
self . log ( " Getting size of classifier \( version ) " )
2020-05-16 11:21:55 +02:00
self . download . classifierSize { size in
completion ( version , size )
}
}
}
2020-06-18 22:55:51 +02:00
private func didDownload ( imageCounts newCounts : [ Int ] ) -> [ Int : Int ] {
let capsCounts = capDict
2020-05-16 11:21:55 +02:00
if newCounts . count != capsCounts . count {
2020-06-18 22:55:51 +02:00
log ( " Downloaded \( newCounts . count ) image counts, but \( capsCounts . count ) caps stored locally " )
2020-05-16 11:21:55 +02:00
}
2020-06-18 22:55:51 +02:00
var newCaps = [ Int : Int ] ( )
let changed = newCounts . enumerated ( ) . compactMap { id , newCount -> Int ? in
let id = id + 1
guard let oldCount = capsCounts [ id ] ? . count else {
2020-05-16 11:21:55 +02:00
log ( " Received count \( newCount ) for unknown cap \( id ) " )
2020-06-18 22:55:51 +02:00
newCaps [ id ] = newCount
2020-05-16 11:21:55 +02:00
return nil
}
guard oldCount != newCount else {
return nil
}
2020-06-18 22:55:51 +02:00
self . update ( count : newCount , for : id )
2020-05-16 11:21:55 +02:00
return id
}
switch changed . count {
case 0 :
log ( " Refreshed image counts for all caps without changes " )
case 1 :
log ( " Refreshed image counts for caps, changed cap \( changed [ 0 ] ) " )
case 2. . . 10 :
log ( " Refreshed image counts for caps \( changed . map ( String . init ) . joined ( separator : " , " ) ) . " )
default :
log ( " Refreshed image counts for all caps ( \( changed . count ) changed) " )
}
2020-06-18 22:55:51 +02:00
return newCaps
}
private func downloadInfo ( for newCaps : [ Int : Int ] , completion : @ escaping ( _ success : Bool ) -> Void ) {
var success = true
let group = DispatchGroup ( )
for ( id , count ) in newCaps {
group . enter ( )
download . name ( for : id ) { name in
guard let name = name else {
self . log ( " Failed to get name for new cap \( id ) " )
success = false
group . leave ( )
return
}
let cap = Cap ( id : id , name : name , count : count )
self . insert ( cap : cap )
group . leave ( )
}
}
if group . wait ( timeout : . now ( ) + . seconds ( 30 ) ) != . success {
self . log ( " Timed out waiting for images to be downloaded " )
}
completion ( success )
}
func downloadImageCount ( for cap : Int ) {
download . imageCount ( for : cap ) { count in
guard let count = count else {
return
}
self . update ( count : count , for : cap )
}
2020-05-16 11:21:55 +02:00
}
2021-01-10 16:11:31 +01:00
2020-05-16 11:21:55 +02:00
func setMainImage ( of cap : Int , to version : Int ) {
guard version != 0 else {
log ( " No need to switch main image with itself for cap \( cap ) " )
return
}
2020-06-18 22:55:51 +02:00
upload . setMainImage ( for : cap , to : version ) { success in
guard success else {
2020-05-16 11:21:55 +02:00
self . log ( " Could not make \( version ) the main image for cap \( cap ) " )
return
}
2021-01-13 21:43:46 +01:00
guard self . storage . switchMainImage ( to : version , for : cap ) else {
2020-06-18 22:55:51 +02:00
self . log ( " Could not switch \( version ) to main image for cap \( cap ) " )
return
}
DispatchQueue . main . async {
self . delegate ? . database ( didLoadImageForCap : cap )
}
2020-05-16 11:21:55 +02:00
}
}
}
extension Database : Logger { }