// // Cap.swift // CapCollector // // Created by Christoph on 19.11.18. // Copyright © 2018 CH. All rights reserved. // import Foundation import UIKit import CoreImage import SQLite struct Cap { // MARK: - Static variables static let sufficientImageCount = 10 static let imageWidth = 299 // New for XCode models, 227/229 for turicreate static let imageSize = CGSize(width: imageWidth, height: imageWidth) static let jpgQuality: CGFloat = 0.3 private static let mosaicColumns = 40 static let mosaicCellSize: CGFloat = 60 private static let mosaicRowHeight = mosaicCellSize * 0.866 private static let mosaicMargin = mosaicCellSize - mosaicRowHeight // MARK: - Variables /// The unique number of the cap let id: Int /// The tile position of the cap let tile: Int /// The name of the cap let name: String /// The name of the cap without special characters let cleanName: String /// The number of images existing for the cap let count: Int /// The average color of the cap let color: UIColor /// Indicate if the cap can be found by the recognition model let matched: Bool /// Indicate if the cap is present on the server let uploaded: Bool // MARK: Init init(name: String, id: Int, color: UIColor) { self.id = id self.count = 1 self.name = name self.cleanName = "" self.tile = id self.color = color self.matched = false self.uploaded = false } // MARK: SQLite static let table = Table("data") static let createQuery: String = { table.create(ifNotExists: true) { t in t.column(rowId, primaryKey: true) t.column(rowName) t.column(rowCount) t.column(rowTile) t.column(rowRed) t.column(rowGreen) t.column(rowBlue) t.column(rowMatched) t.column(rowUploaded) } }() static let rowId = Expression("id") static let rowName = Expression("name") static let rowCount = Expression("count") static let rowTile = Expression("tile") static let rowRed = Expression("red") static let rowGreen = Expression("green") static let rowBlue = Expression("blue") static let rowMatched = Expression("matched") static let rowUploaded = Expression("uploaded") init(row: Row) { self.id = row[Cap.rowId] self.name = row[Cap.rowName] self.count = row[Cap.rowCount] self.tile = row[Cap.rowTile] self.cleanName = name.clean self.matched = row[Cap.rowMatched] self.uploaded = row[Cap.rowUploaded] let r = CGFloat(row[Cap.rowRed]) / 255 let g = CGFloat(row[Cap.rowGreen]) / 255 let b = CGFloat(row[Cap.rowBlue]) / 255 self.color = UIColor(red: r, green: g, blue: b, alpha: 1.0) } init(id: Int, name: String, count: Int) { self.id = id self.name = name self.count = count self.tile = id - 1 self.cleanName = name.clean self.matched = false self.color = UIColor.gray self.uploaded = false } var insertQuery: Insert { let colors = color.rgb return Cap.table.insert( Cap.rowId <- id, Cap.rowName <- name, Cap.rowCount <- count, Cap.rowTile <- tile, Cap.rowRed <- colors.red, Cap.rowGreen <- colors.green, Cap.rowBlue <- colors.blue, Cap.rowMatched <- matched, Cap.rowUploaded <- uploaded) } // MARK: Text func matchDescription(match: Float?) -> String { guard let match = match else { return hasSufficientImages ? "" : "⚠️" } let percent = Int((match * 100).rounded()) return String(format: "%d %%", arguments: [percent]) } /// The cap id and the number of images var subtitle: String { guard count != 1 else { return "\(id) (1 image)" } return "\(id) (\(count) images)" } // MARK: - Images var hasSufficientImages: Bool { count > Cap.sufficientImageCount } var hasImage: Bool { app.storage.hasImage(for: id) } /// The main image of the cap var image: UIImage? { app.storage.image(for: id) } /// The main image of the cap var thumbnail: UIImage? { app.storage.thumbnail(for: id) } static func thumbnail(for image: UIImage) -> UIImage { let len = GridViewController.len * 2 return image.resize(to: CGSize.init(width: len, height: len)) } func updateLocalThumbnail() { guard let img = image else { return } let thumbnail = Cap.thumbnail(for: img) guard app.storage.save(thumbnail: thumbnail, for: id) else { error("Failed to save thumbnail") return } log("Created thumbnail for cap \(id)") } func updateLocalColor() { guard let color = image?.averageColor else { return } app.database.update(color: color, for: id) } /** Download the main image of the cap. - Note: The downloaded image is automatically saved to disk - returns: `true`, if the image will be downloaded, `false`, if the image is already being downloaded. */ @discardableResult func downloadMainImage(completion: @escaping (_ image: UIImage?) -> Void) -> Bool { app.database.downloadMainImage(for: id, completion: completion) } /** Download a specified image of the cap. - parameter number: The number of the image - parameter completion: The completion handler, called with the image if successful - parameter image: The image, if the download was successful, or nil on error - returns: `true`, if the image will be downloaded, `false`, if the image is already being downloaded. */ func downloadImage(_ number: Int, completion: @escaping (_ image: UIImage?) -> Void) -> Bool { app.database.downloadImage(for: id, version: number, completion: completion) } } // MARK: - Protocol Hashable extension Cap: Hashable { static func == (lhs: Cap, rhs: Cap) -> Bool { return lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(id) } } // MARK: - Protocol CustomStringConvertible extension Cap: CustomStringConvertible { var description: String { let rgb = color.rgb return String(format: "%04d", id) + ";\(name);\(count);\(tile);\(rgb.red);\(rgb.green);\(rgb.blue)\n" } } // MARK: - Protocol Logger extension Cap: Logger { } // MARK: - String extension extension String { var clean: String { return lowercased().replacingOccurrences(of: "[^a-z0-9 ]", with: "", options: .regularExpression) } } // MARK: - Int extension private extension Int { var isEven: Bool { return self % 2 == 0 } }