2019-03-15 13:19:19 +01:00
|
|
|
//
|
|
|
|
// Cap.swift
|
|
|
|
// CapCollector
|
|
|
|
//
|
|
|
|
// Created by Christoph on 19.11.18.
|
|
|
|
// Copyright © 2018 CH. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import UIKit
|
2019-07-17 11:10:07 +02:00
|
|
|
import CoreImage
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
import SQLite
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
struct Cap {
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
// MARK: - Static variables
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let sufficientImageCount = 10
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let imageWidth = 299 // New for XCode models, 227/229 for turicreate
|
|
|
|
|
|
|
|
static let imageSize = CGSize(width: imageWidth, height: imageWidth)
|
2019-03-15 13:19:19 +01:00
|
|
|
|
|
|
|
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
|
2020-05-16 11:21:55 +02:00
|
|
|
let tile: Int
|
2019-03-15 13:19:19 +01:00
|
|
|
|
|
|
|
/// The name of the cap
|
2020-05-16 11:21:55 +02:00
|
|
|
let name: String
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
/// The name of the cap without special characters
|
|
|
|
let cleanName: String
|
2019-03-15 13:19:19 +01:00
|
|
|
|
|
|
|
/// The number of images existing for the cap
|
2020-05-16 11:21:55 +02:00
|
|
|
let count: Int
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2019-07-17 11:10:07 +02:00
|
|
|
/// The average color of the cap
|
2020-05-16 11:21:55 +02:00
|
|
|
let color: UIColor
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
/// Indicate if the cap can be found by the recognition model
|
|
|
|
let matched: Bool
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
/// Indicate if the cap is present on the server
|
|
|
|
let uploaded: Bool
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
// MARK: Init
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
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
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
// MARK: SQLite
|
2019-07-17 11:10:07 +02:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let table = Table("data")
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
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)
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
2020-05-16 11:21:55 +02:00
|
|
|
}()
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let rowId = Expression<Int>("id")
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let rowName = Expression<String>("name")
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let rowCount = Expression<Int>("count")
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let rowTile = Expression<Int>("tile")
|
2019-07-17 11:10:07 +02:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let rowRed = Expression<Int>("red")
|
|
|
|
static let rowGreen = Expression<Int>("green")
|
|
|
|
static let rowBlue = Expression<Int>("blue")
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let rowMatched = Expression<Bool>("matched")
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static let rowUploaded = Expression<Bool>("uploaded")
|
2019-03-15 13:19:19 +01:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
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)
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
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
|
2019-07-17 11:10:07 +02:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
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)
|
2019-07-17 11:10:07 +02:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
// MARK: Text
|
2019-07-17 11:10:07 +02:00
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
func matchDescription(match: Float?) -> String {
|
|
|
|
guard let match = match else {
|
|
|
|
return hasSufficientImages ? "" : "⚠️"
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
2020-05-16 11:21:55 +02:00
|
|
|
let percent = Int((match * 100).rounded())
|
|
|
|
return String(format: "%d %%", arguments: [percent])
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
/// The cap id and the number of images
|
|
|
|
var subtitle: String {
|
|
|
|
guard count != 1 else {
|
|
|
|
return "\(id) (1 image)"
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
2020-05-16 11:21:55 +02:00
|
|
|
return "\(id) (\(count) images)"
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
// MARK: - Images
|
|
|
|
|
|
|
|
var hasSufficientImages: Bool {
|
|
|
|
count > Cap.sufficientImageCount
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
var hasImage: Bool {
|
|
|
|
app.storage.hasImage(for: id)
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
/// 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)
|
2019-04-12 13:46:18 +02:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
static func thumbnail(for image: UIImage) -> UIImage {
|
|
|
|
let len = GridViewController.len * 2
|
|
|
|
return image.resize(to: CGSize.init(width: len, height: len))
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
func updateLocalThumbnail() {
|
|
|
|
guard let img = image else {
|
2019-03-15 13:19:19 +01:00
|
|
|
return
|
|
|
|
}
|
2020-05-16 11:21:55 +02:00
|
|
|
let thumbnail = Cap.thumbnail(for: img)
|
|
|
|
guard app.storage.save(thumbnail: thumbnail, for: id) else {
|
|
|
|
error("Failed to save thumbnail")
|
2019-03-15 13:19:19 +01:00
|
|
|
return
|
|
|
|
}
|
2020-05-16 11:21:55 +02:00
|
|
|
log("Created thumbnail for cap \(id)")
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
func updateLocalColor() {
|
|
|
|
guard let color = image?.averageColor else {
|
2019-07-17 11:10:07 +02:00
|
|
|
return
|
|
|
|
}
|
2020-05-16 11:21:55 +02:00
|
|
|
app.database.update(color: color, for: id)
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-16 11:21:55 +02:00
|
|
|
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.
|
2019-03-15 13:19:19 +01:00
|
|
|
*/
|
2020-05-16 11:21:55 +02:00
|
|
|
@discardableResult
|
|
|
|
func downloadMainImage(completion: @escaping (_ image: UIImage?) -> Void) -> Bool {
|
|
|
|
app.database.downloadMainImage(for: id, completion: completion)
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
/**
|
|
|
|
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)
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
2019-07-17 11:10:07 +02:00
|
|
|
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 {
|
2020-05-16 11:21:55 +02:00
|
|
|
let rgb = color.rgb
|
|
|
|
return String(format: "%04d", id) + ";\(name);\(count);\(tile);\(rgb.red);\(rgb.green);\(rgb.blue)\n"
|
2019-03-15 13:19:19 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - Protocol Logger
|
|
|
|
|
2020-05-16 11:21:55 +02:00
|
|
|
extension Cap: Logger { }
|
2019-03-15 13:19:19 +01:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|