Caps-iOS/CapCollector/Data/Cap.swift
2020-05-16 11:21:55 +02:00

277 lines
7.2 KiB
Swift

//
// 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<Int>("id")
static let rowName = Expression<String>("name")
static let rowCount = Expression<Int>("count")
static let rowTile = Expression<Int>("tile")
static let rowRed = Expression<Int>("red")
static let rowGreen = Expression<Int>("green")
static let rowBlue = Expression<Int>("blue")
static let rowMatched = Expression<Bool>("matched")
static let rowUploaded = Expression<Bool>("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
}
}