Start version 2
This commit is contained in:
@@ -1,202 +1,159 @@
|
||||
//
|
||||
// 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 constants
|
||||
|
||||
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 name of the cap
|
||||
let name: String
|
||||
|
||||
var name: String
|
||||
|
||||
/// The name of the cap without special characters
|
||||
let cleanName: String
|
||||
|
||||
var cleanName: String
|
||||
|
||||
/// The number of images existing for the cap
|
||||
let count: Int
|
||||
|
||||
/// 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) {
|
||||
self.id = id
|
||||
self.count = 1
|
||||
self.name = name
|
||||
self.cleanName = ""
|
||||
self.matched = false
|
||||
self.uploaded = false
|
||||
var imageCount: Int
|
||||
|
||||
/// The index of the main image for the cap
|
||||
var mainImage: Int
|
||||
|
||||
/// The version of the first classifier capable of recognizing the cap
|
||||
var classifierVersion: Int?
|
||||
|
||||
var color: Color?
|
||||
|
||||
/// The subpath to the main image on the server
|
||||
var mainImagePath: String {
|
||||
String(format: "images/%04d/%04d-%02d.jpg", id, id, mainImage)
|
||||
}
|
||||
|
||||
init(id: Int, name: String, count: Int) {
|
||||
|
||||
/**
|
||||
Create a new cap.
|
||||
- Parameter id: The unique id of the cap
|
||||
- Parameter name: The name associated with the cap
|
||||
*/
|
||||
init(id: Int, name: String, classifier: Int? = nil) {
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.count = count
|
||||
self.cleanName = name.clean
|
||||
self.matched = false
|
||||
self.uploaded = true
|
||||
self.imageCount = 1
|
||||
self.mainImage = 0
|
||||
self.classifierVersion = classifier
|
||||
}
|
||||
|
||||
func renamed(to name: String) -> Cap {
|
||||
Cap(from: self, renamed: name)
|
||||
}
|
||||
|
||||
init(from cap: Cap, renamed newName: String) {
|
||||
self.id = cap.id
|
||||
self.count = cap.count
|
||||
self.name = newName
|
||||
self.cleanName = newName.clean
|
||||
self.matched = cap.matched
|
||||
self.uploaded = cap.uploaded
|
||||
}
|
||||
|
||||
// MARK: SQLite
|
||||
|
||||
init(row: Row) {
|
||||
self.id = row[Cap.columnId]
|
||||
self.name = row[Cap.columnName]
|
||||
self.count = row[Cap.columnCount]
|
||||
self.cleanName = name.clean
|
||||
self.matched = row[Cap.columnMatched]
|
||||
self.uploaded = row[Cap.columnUploaded]
|
||||
|
||||
init(data: CapData) {
|
||||
self.id = data.id
|
||||
self.name = data.name
|
||||
self.cleanName = data.name.clean
|
||||
self.imageCount = data.count
|
||||
self.mainImage = data.mainImage
|
||||
self.classifierVersion = data.classifierVersion
|
||||
}
|
||||
|
||||
static let table = Table("data")
|
||||
|
||||
static var createQuery: String {
|
||||
table.create(ifNotExists: true) { t in
|
||||
t.column(columnId, primaryKey: true)
|
||||
t.column(columnName)
|
||||
t.column(columnCount)
|
||||
t.column(columnMatched)
|
||||
t.column(columnUploaded)
|
||||
}
|
||||
}
|
||||
|
||||
static let columnId = Expression<Int>("id")
|
||||
|
||||
static let columnName = Expression<String>("name")
|
||||
|
||||
static let columnCount = Expression<Int>("count")
|
||||
|
||||
static let columnMatched = Expression<Bool>("matched")
|
||||
|
||||
static let columnUploaded = Expression<Bool>("uploaded")
|
||||
|
||||
var insertQuery: Insert {
|
||||
return Cap.table.insert(
|
||||
Cap.columnId <- id,
|
||||
Cap.columnName <- name,
|
||||
Cap.columnCount <- count,
|
||||
Cap.columnMatched <- matched,
|
||||
Cap.columnUploaded <- uploaded)
|
||||
}
|
||||
|
||||
// MARK: Display
|
||||
|
||||
func matchLabelText(match: Float?, appIsUnlocked: Bool) -> String {
|
||||
if let match = match {
|
||||
let percent = Int((match * 100).rounded())
|
||||
return String(format: "%d %%", arguments: [percent])
|
||||
}
|
||||
|
||||
guard matched else {
|
||||
return "📵"
|
||||
}
|
||||
guard appIsUnlocked, !hasSufficientImages else {
|
||||
return ""
|
||||
}
|
||||
return "⚠️"
|
||||
}
|
||||
|
||||
func countLabelText(appIsUnlocked: Bool) -> String {
|
||||
guard appIsUnlocked else {
|
||||
return "\(id)"
|
||||
}
|
||||
guard count != 1 else {
|
||||
return "\(id) (1 image)"
|
||||
}
|
||||
return "\(id) (\(count) images)"
|
||||
}
|
||||
|
||||
// MARK: Images
|
||||
|
||||
var hasSufficientImages: Bool {
|
||||
count >= Cap.sufficientImageCount
|
||||
mutating func update(with data: CapData) {
|
||||
self.name = data.name
|
||||
self.cleanName = data.name.clean
|
||||
self.imageCount = data.count
|
||||
self.mainImage = data.mainImage
|
||||
self.classifierVersion = data.classifierVersion
|
||||
}
|
||||
|
||||
static func thumbnail(for image: UIImage) -> UIImage {
|
||||
let len = GridViewController.len * 2
|
||||
return image.resize(to: CGSize.init(width: len, height: len))
|
||||
|
||||
static func ==(lhs: Cap, rhs: CapData) -> Bool {
|
||||
lhs.id == rhs.id &&
|
||||
lhs.name == rhs.name &&
|
||||
lhs.imageCount == rhs.count &&
|
||||
lhs.mainImage == rhs.mainImage &&
|
||||
lhs.classifierVersion == rhs.classifierVersion
|
||||
}
|
||||
|
||||
static func !=(lhs: Cap, rhs: CapData) -> Bool {
|
||||
!(lhs == rhs)
|
||||
}
|
||||
|
||||
func classifiable(by classifierVersion: Int?) -> Bool {
|
||||
guard let version = classifierVersion else {
|
||||
return false
|
||||
}
|
||||
guard let own = self.classifierVersion else {
|
||||
return false
|
||||
}
|
||||
return version >= own
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Hashable
|
||||
extension Cap {
|
||||
|
||||
struct Color: Codable, Equatable {
|
||||
|
||||
let r: Int
|
||||
|
||||
let g: Int
|
||||
|
||||
let b: Int
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Protocol Identifiable
|
||||
|
||||
extension Cap: Identifiable {
|
||||
|
||||
}
|
||||
|
||||
// MARK: Protocol Comparable
|
||||
|
||||
extension Cap: Codable {
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "u"
|
||||
case name = "n"
|
||||
case cleanName = "c"
|
||||
case imageCount = "i"
|
||||
case mainImage = "m"
|
||||
case classifierVersion = "v"
|
||||
case color = "f"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Protocol Comparable
|
||||
|
||||
extension Cap: Comparable {
|
||||
|
||||
static func < (lhs: Cap, rhs: Cap) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Protocol Equatable
|
||||
|
||||
extension Cap: Equatable {
|
||||
|
||||
extension Cap: Hashable {
|
||||
|
||||
static func == (lhs: Cap, rhs: Cap) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Protocol Hashable
|
||||
|
||||
extension Cap: Hashable {
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Logger
|
||||
// MARK: String extension
|
||||
|
||||
extension Cap: Logger { }
|
||||
private extension String {
|
||||
|
||||
// MARK: - String extension
|
||||
|
||||
extension String {
|
||||
|
||||
var clean: String {
|
||||
return lowercased().replacingOccurrences(of: "[^a-z0-9 ]", with: "", options: .regularExpression)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Int extension
|
||||
// MARK: Int extension
|
||||
|
||||
private extension Int {
|
||||
|
||||
|
||||
var isEven: Bool {
|
||||
return self % 2 == 0
|
||||
}
|
||||
|
32
Caps/Data/CapData.swift
Normal file
32
Caps/Data/CapData.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import Foundation
|
||||
|
||||
struct CapData: Codable {
|
||||
|
||||
let id: Int
|
||||
|
||||
var name: String
|
||||
|
||||
var count: Int
|
||||
|
||||
var mainImage: Int
|
||||
|
||||
var classifierVersion: Int?
|
||||
|
||||
var color: Cap.Color?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id = "i"
|
||||
case name = "n"
|
||||
case count = "c"
|
||||
case mainImage = "m"
|
||||
case classifierVersion = "v"
|
||||
case color = "f"
|
||||
}
|
||||
}
|
||||
|
||||
extension CapData: Comparable {
|
||||
|
||||
static func < (lhs: CapData, rhs: CapData) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
@@ -1,11 +1,3 @@
|
||||
//
|
||||
// VisionHandler.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 12.02.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Vision
|
||||
import CoreML
|
||||
@@ -13,28 +5,24 @@ import UIKit
|
||||
|
||||
/// Recognise categories in images
|
||||
class Classifier: Logger {
|
||||
|
||||
|
||||
static let userDefaultsKey = "classifier.version"
|
||||
|
||||
|
||||
let model: VNCoreMLModel
|
||||
|
||||
|
||||
init(model: VNCoreMLModel) {
|
||||
self.model = model
|
||||
}
|
||||
/**
|
||||
Classify an image
|
||||
- Parameter image: The image to classify
|
||||
- Parameter completion: The callback with the match results
|
||||
- Parameter matches: A dictionary with a map from cap id to classifier match
|
||||
- Note: This method should not be scheduled on the main thread.
|
||||
*/
|
||||
func recognize(image: UIImage, completion: @escaping (_ matches: [Int: Float]?) -> Void) {
|
||||
guard let ciImage = CIImage(image: image) else {
|
||||
error("Unable to create CIImage")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let orientation = CGImagePropertyOrientation(image.imageOrientation)
|
||||
let handler = VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
|
||||
func recognize(image: CGImage, completion: @escaping (_ matches: [Int: Float]?) -> Void) {
|
||||
let image = CIImage(cgImage: image)
|
||||
let handler = VNImageRequestHandler(ciImage: image, orientation: .up)
|
||||
let request = VNCoreMLRequest(model: model) { request, error in
|
||||
let matches = self.process(request: request, error: error)
|
||||
completion(matches)
|
||||
@@ -46,7 +34,7 @@ class Classifier: Logger {
|
||||
self.error("Failed to perform classification: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func process(request: VNRequest, error: Error?) -> [Int : Float]? {
|
||||
if let e = error {
|
||||
self.error("Unable to classify image: \(e.localizedDescription)")
|
||||
@@ -57,7 +45,7 @@ class Classifier: Logger {
|
||||
return nil
|
||||
}
|
||||
let matches = result.reduce(into: [:]) { $0[Int($1.identifier)!] = $1.confidence }
|
||||
|
||||
|
||||
log("Classifed image with \(matches.count) classes")
|
||||
return matches
|
||||
}
|
||||
|
@@ -1,132 +0,0 @@
|
||||
//
|
||||
// Colors.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 26.05.20.
|
||||
// Copyright © 2020 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SQLite
|
||||
|
||||
extension Database {
|
||||
|
||||
enum Colors {
|
||||
|
||||
static let table = Table("colors")
|
||||
|
||||
static let columnRed = Expression<Double>("red")
|
||||
|
||||
static let columnGreen = Expression<Double>("green")
|
||||
|
||||
static let columnBlue = Expression<Double>("blue")
|
||||
|
||||
static var createQuery: String {
|
||||
table.create(ifNotExists: true) { t in
|
||||
t.column(Cap.columnId, primaryKey: true)
|
||||
t.column(columnRed)
|
||||
t.column(columnGreen)
|
||||
t.column(columnBlue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var colors: [Int : UIColor] {
|
||||
do {
|
||||
let rows = try db.prepare(Database.Colors.table)
|
||||
return rows.reduce(into: [:]) { dict, row in
|
||||
let id = row[Cap.columnId]
|
||||
let r = CGFloat(row[Database.Colors.columnRed])
|
||||
let g = CGFloat(row[Database.Colors.columnGreen])
|
||||
let b = CGFloat(row[Database.Colors.columnBlue])
|
||||
dict[id] = UIColor(red: r, green: g, blue: b, alpha: 1.0)
|
||||
}
|
||||
} catch {
|
||||
log("Failed to load cap colors: \(error)")
|
||||
return [:]
|
||||
}
|
||||
}
|
||||
|
||||
var capsWithColors: Set<Int> {
|
||||
do {
|
||||
let rows = try db.prepare(Database.Colors.table.select(Cap.columnId))
|
||||
return Set(rows.map { $0[Cap.columnId]})
|
||||
} catch {
|
||||
log("Failed to load caps with colors: \(error)")
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
var capsWithoutColors: Set<Int> {
|
||||
Set(1...capCount).subtracting(capsWithColors)
|
||||
}
|
||||
|
||||
func removeColor(for cap: Int) -> Bool {
|
||||
do {
|
||||
try db.run(Colors.table.filter(Cap.columnId == cap).delete())
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to delete cap color \(cap): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func set(color: UIColor, for cap: Int) -> Bool {
|
||||
guard let _ = row(for: cap) else {
|
||||
return insert(color: color, for: cap)
|
||||
}
|
||||
return update(color: color, for: cap)
|
||||
}
|
||||
|
||||
private func insert(color: UIColor, for cap: Int) -> Bool {
|
||||
let (red, green, blue) = color.rgb
|
||||
let query = Database.Colors.table.insert(
|
||||
Cap.columnId <- cap,
|
||||
Database.Colors.columnRed <- red,
|
||||
Database.Colors.columnGreen <- green,
|
||||
Database.Colors.columnBlue <- blue)
|
||||
|
||||
do {
|
||||
try db.run(query)
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to insert color for cap \(cap): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func update(color: UIColor, for cap: Int) -> Bool {
|
||||
let (red, green, blue) = color.rgb
|
||||
let query = Database.Colors.table.filter(Cap.columnId == cap).update(
|
||||
Database.Colors.columnRed <- red,
|
||||
Database.Colors.columnGreen <- green,
|
||||
Database.Colors.columnBlue <- blue)
|
||||
|
||||
do {
|
||||
try db.run(query)
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to update color for cap \(cap): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func row(for cap: Int) -> Row? {
|
||||
do {
|
||||
return try db.pluck(Database.Colors.table.filter(Cap.columnId == cap))
|
||||
} catch {
|
||||
log("Failed to get color for cap \(cap): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func color(for cap: Int) -> UIColor? {
|
||||
guard let row = self.row(for: cap) else {
|
||||
return nil
|
||||
}
|
||||
let r = CGFloat(row[Database.Colors.columnRed])
|
||||
let g = CGFloat(row[Database.Colors.columnGreen])
|
||||
let b = CGFloat(row[Database.Colors.columnBlue])
|
||||
return UIColor(red: r, green: g, blue: b, alpha: 1.0)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,358 +0,0 @@
|
||||
//
|
||||
// Download.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 26.04.20.
|
||||
// Copyright © 2020 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class Download {
|
||||
|
||||
let serverUrl: URL
|
||||
|
||||
let session: URLSession
|
||||
|
||||
let delegate: Delegate
|
||||
|
||||
private var downloadingMainImages = Set<Int>()
|
||||
|
||||
init(server: URL) {
|
||||
let delegate = Delegate()
|
||||
|
||||
self.serverUrl = server
|
||||
self.session = URLSession(configuration: .ephemeral, delegate: delegate, delegateQueue: nil)
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
var serverNameListUrl: URL {
|
||||
Download.serverNameListUrl(server: serverUrl)
|
||||
}
|
||||
|
||||
private static func serverNameListUrl(server: URL) -> URL {
|
||||
server.appendingPathComponent("names.txt")
|
||||
}
|
||||
|
||||
private var serverClassifierVersionUrl: URL {
|
||||
serverUrl.appendingPathComponent("classifier.version")
|
||||
}
|
||||
|
||||
private var serverRecognitionModelUrl: URL {
|
||||
serverUrl.appendingPathComponent("classifier.mlmodel")
|
||||
}
|
||||
|
||||
private var serverAllCountsUrl: URL {
|
||||
serverUrl.appendingPathComponent("counts")
|
||||
}
|
||||
|
||||
var serverImageUrl: URL {
|
||||
serverUrl.appendingPathComponent("images")
|
||||
}
|
||||
|
||||
private func serverImageUrl(for cap: Int, version: Int = 0) -> URL {
|
||||
serverImageUrl.appendingPathComponent(String(format: "%04d/%04d-%02d.jpg", cap, cap, version))
|
||||
}
|
||||
|
||||
private func serverNameUrl(for cap: Int) -> URL {
|
||||
serverUrl.appendingPathComponent("name/\(cap)")
|
||||
}
|
||||
|
||||
private func serverImageCountUrl(for cap: Int) -> URL {
|
||||
serverUrl.appendingPathComponent("count/\(cap)")
|
||||
}
|
||||
|
||||
// MARK: Delegate
|
||||
|
||||
final class Delegate: NSObject, URLSessionDownloadDelegate {
|
||||
|
||||
typealias ProgressHandler = (_ progress: Float, _ bytesWritten: Int64, _ totalBytes: Int64) -> Void
|
||||
|
||||
typealias CompletionHandler = (_ url: URL?) -> Void
|
||||
|
||||
private var progress = [URLSessionDownloadTask : Float]()
|
||||
|
||||
private var callbacks = [URLSessionDownloadTask : ProgressHandler]()
|
||||
|
||||
private var completions = [URLSessionDownloadTask : CompletionHandler]()
|
||||
|
||||
func registerForProgress(_ downloadTask: URLSessionDownloadTask, callback: ProgressHandler?, completion: @escaping CompletionHandler) {
|
||||
progress[downloadTask] = 0
|
||||
callbacks[downloadTask] = callback
|
||||
completions[downloadTask] = completion
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
|
||||
completions[downloadTask]?(location)
|
||||
callbacks[downloadTask] = nil
|
||||
progress[downloadTask] = nil
|
||||
completions[downloadTask] = nil
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
|
||||
let ratio = totalBytesExpectedToWrite > 0 ? Float(totalBytesWritten) / Float(totalBytesExpectedToWrite) : 0
|
||||
progress[downloadTask] = ratio
|
||||
callbacks[downloadTask]?(ratio, totalBytesWritten, totalBytesExpectedToWrite)
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
|
||||
guard let downloadTask = task as? URLSessionDownloadTask else {
|
||||
return
|
||||
}
|
||||
completions[downloadTask]?(nil)
|
||||
callbacks[downloadTask] = nil
|
||||
progress[downloadTask] = nil
|
||||
completions[downloadTask] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Downloading data
|
||||
|
||||
func image(for cap: Int, to url: URL, timeout: TimeInterval = 30) -> Bool {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var result = true
|
||||
let success = image(for: cap, version: 0, to: url) { success in
|
||||
result = success
|
||||
group.leave()
|
||||
}
|
||||
guard success else {
|
||||
log("Already downloading image for cap \(cap)")
|
||||
return false
|
||||
}
|
||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
||||
log("Timed out downloading image for cap \(cap)")
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
Download an image for a cap.
|
||||
- Parameter cap: The id of the cap.
|
||||
- Parameter version: The image version to download.
|
||||
- Parameter completion: A closure with the resulting image
|
||||
- Returns: `true`, of the file download was started, `false`, if the image is already downloading.
|
||||
*/
|
||||
@discardableResult
|
||||
func image(for cap: Int, version: Int = 0, to url: URL, queue: DispatchQueue = .main, completion: @escaping (Bool) -> Void) -> Bool {
|
||||
// Check if main image, and already being downloaded
|
||||
if version == 0 {
|
||||
guard !downloadingMainImages.contains(cap) else {
|
||||
return false
|
||||
}
|
||||
downloadingMainImages.insert(cap)
|
||||
}
|
||||
let serverUrl = serverImageUrl(for: cap, version: version)
|
||||
let query = "Image of cap \(cap) version \(version)"
|
||||
let task = session.downloadTask(with: serverUrl) { fileUrl, response, error in
|
||||
if version == 0 {
|
||||
queue.async {
|
||||
self.downloadingMainImages.remove(cap)
|
||||
}
|
||||
}
|
||||
guard let fileUrl = self.convertResponse(to: query, fileUrl, response, error) else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
try FileManager.default.moveItem(at: fileUrl, to: url)
|
||||
} catch {
|
||||
self.log("Failed to move downloaded image for cap \(cap): \(error)")
|
||||
completion(false)
|
||||
}
|
||||
completion(true)
|
||||
}
|
||||
task.resume()
|
||||
return true
|
||||
}
|
||||
|
||||
func imageCount(for cap: Int, completion: @escaping (_ count: Int?) -> Void) {
|
||||
let url = serverImageCountUrl(for: cap)
|
||||
let query = "Image count for cap \(cap)"
|
||||
session.startTaskExpectingInt(with: url, query: query, completion: completion)
|
||||
}
|
||||
|
||||
func name(for cap: Int, completion: @escaping (_ name: String?) -> Void) {
|
||||
let url = serverNameUrl(for: cap)
|
||||
let query = "Name for cap \(cap)"
|
||||
session.startTaskExpectingString(with: url, query: query, completion: completion)
|
||||
}
|
||||
|
||||
func imageCounts(completion: @escaping ([Int]?) -> Void) {
|
||||
let query = "Image count of all caps"
|
||||
session.startTaskExpectingData(with: serverAllCountsUrl, query: query) { data in
|
||||
guard let data = data else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(data.map(Int.init))
|
||||
}
|
||||
}
|
||||
|
||||
func names(completion: @escaping ([String]?) -> Void) {
|
||||
let query = "Download of server database"
|
||||
session.startTaskExpectingString(with: serverNameListUrl, query: query) { string in
|
||||
completion(string?.trimmingCharacters(in: .whitespacesAndNewlines).components(separatedBy: "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func databaseSize(completion: @escaping (_ size: Int64?) -> Void) {
|
||||
size(of: "database size", to: serverNameListUrl, completion: completion)
|
||||
}
|
||||
|
||||
func classifierVersion(completion: @escaping (Int?) -> Void) {
|
||||
let query = "Server classifier version"
|
||||
session.startTaskExpectingInt(with: serverClassifierVersionUrl, query: query, completion: completion)
|
||||
}
|
||||
|
||||
func classifierSize(completion: @escaping (Int64?) -> Void) {
|
||||
size(of: "classifier size", to: serverRecognitionModelUrl, completion: completion)
|
||||
}
|
||||
|
||||
func classifier(progress: Delegate.ProgressHandler? = nil, completion: @escaping (URL?) -> Void) {
|
||||
let task = session.downloadTask(with: serverRecognitionModelUrl)
|
||||
delegate.registerForProgress(task, callback: progress) { url in
|
||||
self.log("Classifier download complete")
|
||||
completion(url)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
// MARK: Requests
|
||||
|
||||
private func size(of query: String, to url: URL, completion: @escaping (_ size: Int64?) -> Void) {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "HEAD"
|
||||
let task = session.dataTask(with: request) { _, response, _ in
|
||||
guard let r = response else {
|
||||
self.log("Request '\(query)' received no response")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(r.expectedContentLength)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
private func convertIntResponse(to query: String, _ data: Data?, _ response: URLResponse?, _ error: Error?) -> Int? {
|
||||
guard let string = self.convertStringResponse(to: query, data, response, error) else {
|
||||
return nil
|
||||
}
|
||||
guard let int = Int(string.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
||||
self.log("Request '\(query)' received an invalid value '\(string)'")
|
||||
return nil
|
||||
}
|
||||
return int
|
||||
}
|
||||
|
||||
private func convertStringResponse(to query: String, _ data: Data?, _ response: URLResponse?, _ error: Error?) -> String? {
|
||||
guard let data = self.convertResponse(to: query, data, response, error) else {
|
||||
return nil
|
||||
}
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
self.log("Request '\(query)' received invalid data (not a string)")
|
||||
return nil
|
||||
}
|
||||
return string
|
||||
}
|
||||
|
||||
private func convertResponse<T>(to query: String, _ result: T?, _ response: URLResponse?, _ error: Error?) -> T? {
|
||||
if let error = error {
|
||||
log("Request '\(query)' produced an error: \(error)")
|
||||
return nil
|
||||
}
|
||||
guard let response = response else {
|
||||
log("Request '\(query)' received no response")
|
||||
return nil
|
||||
}
|
||||
guard let urlResponse = response as? HTTPURLResponse else {
|
||||
log("Request '\(query)' received an invalid response: \(response)")
|
||||
return nil
|
||||
}
|
||||
guard urlResponse.statusCode == 200 else {
|
||||
log("Request '\(query)' failed with status code \(urlResponse.statusCode)")
|
||||
return nil
|
||||
}
|
||||
guard let r = result else {
|
||||
log("Request '\(query)' received no item")
|
||||
return nil
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension Download: Logger { }
|
||||
|
||||
extension URLSession {
|
||||
|
||||
func startTaskExpectingData(with url: URL, query: String, completion: @escaping (Data?) -> Void) {
|
||||
let task = dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
log("Request '\(query)' produced an error: \(error)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let response = response else {
|
||||
log("Request '\(query)' received no response")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let urlResponse = response as? HTTPURLResponse else {
|
||||
log("Request '\(query)' received an invalid response: \(response)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard urlResponse.statusCode == 200 else {
|
||||
log("Request '\(query)' failed with status code \(urlResponse.statusCode)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let d = data else {
|
||||
log("Request '\(query)' received no data")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(d)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func startTaskExpectingString(with url: URL, query: String, completion: @escaping (String?) -> Void) {
|
||||
startTaskExpectingData(with: url, query: query) { data in
|
||||
guard let data = data else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
log("Request '\(query)' received invalid data (not a string)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(string)
|
||||
}
|
||||
}
|
||||
|
||||
func startTaskExpectingInt(with url: URL, query: String, completion: @escaping (Int?) -> Void) {
|
||||
startTaskExpectingString(with: url, query: query) { string in
|
||||
guard let string = string else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let int = Int(string.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
||||
log("Request '\(query)' received an invalid value '\(string)'")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(int)
|
||||
}
|
||||
}
|
||||
}
|
28
Caps/Data/SortCriteria.swift
Normal file
28
Caps/Data/SortCriteria.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
enum SortCriteria: Int, CaseIterable {
|
||||
case id = 0
|
||||
case name = 1
|
||||
case count = 2
|
||||
case match = 3
|
||||
|
||||
var text: String {
|
||||
switch self {
|
||||
case .id:
|
||||
return "Id"
|
||||
case .name:
|
||||
return "Name"
|
||||
case .count:
|
||||
return "Count"
|
||||
case .match:
|
||||
return "Match"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SortCriteria: Identifiable {
|
||||
|
||||
var id: Int {
|
||||
rawValue
|
||||
}
|
||||
}
|
@@ -1,374 +0,0 @@
|
||||
//
|
||||
// DiskManager.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 23.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import CoreML
|
||||
import Vision
|
||||
|
||||
|
||||
|
||||
protocol ImageProvider: AnyObject {
|
||||
|
||||
func image(for cap: Int) -> UIImage?
|
||||
|
||||
func image(for cap: Int, version: Int) -> UIImage?
|
||||
|
||||
func ciImage(for cap: Int) -> CIImage?
|
||||
}
|
||||
|
||||
protocol ThumbnailCreationDelegate {
|
||||
|
||||
func thumbnailCreation(progress: Int, total: Int)
|
||||
|
||||
func thumbnailCreationFailed()
|
||||
|
||||
func thumbnailCreationIsMissingImages()
|
||||
|
||||
func thumbnailCreationCompleted()
|
||||
}
|
||||
|
||||
|
||||
final class Storage: ImageProvider {
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
let fm = FileManager.default
|
||||
|
||||
let baseUrl: URL
|
||||
|
||||
// MARK: INIT
|
||||
|
||||
init(in folder: URL) {
|
||||
self.baseUrl = folder
|
||||
}
|
||||
|
||||
// MARK: File/folder urls
|
||||
|
||||
var dbUrl: URL {
|
||||
baseUrl.appendingPathComponent("db.sqlite3")
|
||||
}
|
||||
|
||||
var modelUrl: URL {
|
||||
baseUrl.appendingPathComponent("model.mlmodel")
|
||||
}
|
||||
|
||||
func localImageUrl(for cap: Int, version: Int = 0) -> URL {
|
||||
baseUrl.appendingPathComponent("\(cap)-\(version).jpg")
|
||||
}
|
||||
|
||||
private func thumbnailUrl(for cap: Int) -> URL {
|
||||
baseUrl.appendingPathComponent("\(cap)-thumb.jpg")
|
||||
}
|
||||
|
||||
private func tileImageUrl(for image: String) -> URL {
|
||||
baseUrl.appendingPathComponent(image.clean + ".tile")
|
||||
}
|
||||
|
||||
// MARK: Storage
|
||||
|
||||
/**
|
||||
Save an image to disk
|
||||
- parameter url: The url where the downloaded image is stored
|
||||
- parameter cap: The cap id
|
||||
- parameter version: The version of the image to get
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
func saveImage(at url: URL, for cap: Int, version: Int = 0) -> UIImage? {
|
||||
let targetUrl = localImageUrl(for: cap, version: version)
|
||||
do {
|
||||
if fm.fileExists(atPath: targetUrl.path) {
|
||||
try fm.removeItem(at: targetUrl)
|
||||
}
|
||||
try fm.moveItem(at: url, to: targetUrl)
|
||||
return UIImage(contentsOfFile: targetUrl.path)
|
||||
} catch {
|
||||
log("Failed to delete or move image \(version) for cap \(cap)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Save an image to disk
|
||||
- parameter image: The image
|
||||
- parameter cap: The cap id
|
||||
- parameter version: The version of the image
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
func save(image: UIImage, for cap: Int, version: Int = 0) -> Bool {
|
||||
guard let data = image.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||
return false
|
||||
}
|
||||
return save(imageData: data, for: cap, version: version)
|
||||
}
|
||||
|
||||
/**
|
||||
Save image data to disk
|
||||
- parameter image: The data of the image
|
||||
- parameter cap: The cap id
|
||||
- parameter version: The version of the image
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
func save(imageData: Data, for cap: Int, version: Int = 0) -> Bool {
|
||||
write(imageData, to: localImageUrl(for: cap, version: version))
|
||||
}
|
||||
|
||||
/**
|
||||
Save a thumbnail.
|
||||
- parameter thumbnail: The image
|
||||
- parameter cap: The cap id
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
func save(thumbnail: UIImage, for cap: Int) -> Bool {
|
||||
guard let data = thumbnail.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||
return false
|
||||
}
|
||||
return save(thumbnailData: data, for: cap)
|
||||
}
|
||||
|
||||
/**
|
||||
Save a thumbnail to the download folder
|
||||
- parameter thumbnailData: The data of the image
|
||||
- parameter cap: The cap id
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
private func save(thumbnailData: Data, for cap: Int) -> Bool {
|
||||
write(thumbnailData, to: thumbnailUrl(for: cap))
|
||||
}
|
||||
|
||||
/**
|
||||
Save the downloaded and compiled recognition model.
|
||||
- Parameter url: The temporary location to which the model was compiled.
|
||||
- Returns: `true`, if the model was moved.
|
||||
*/
|
||||
func save(recognitionModelAt url: URL) -> Bool {
|
||||
move(url, to: modelUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
Save the downloaded and database.
|
||||
- Parameter url: The temporary location to which the database was downloaded.
|
||||
- Returns: `true`, if the database was moved.
|
||||
*/
|
||||
func save(databaseAt url: URL) -> Bool {
|
||||
move(url, to: dbUrl)
|
||||
}
|
||||
|
||||
private func move(_ url: URL, to destination: URL) -> Bool {
|
||||
if fm.fileExists(atPath: destination.path) {
|
||||
do {
|
||||
try fm.removeItem(at: destination)
|
||||
} catch {
|
||||
log("Failed to remove file \(destination.lastPathComponent) before writing new version: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
do {
|
||||
try fm.moveItem(at: url, to: destination)
|
||||
return true
|
||||
} catch {
|
||||
self.error("Failed to move file \(destination.lastPathComponent) to permanent location: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func write(_ data: Data, to url: URL) -> Bool {
|
||||
do {
|
||||
try data.write(to: url)
|
||||
} catch {
|
||||
self.error("Could not write data to \(url): \(error)")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: High-level functions
|
||||
|
||||
func switchMainImage(to version: Int, for cap: Int) -> Bool {
|
||||
guard deleteThumbnail(for: cap) else {
|
||||
return false
|
||||
}
|
||||
let newImagePath = localImageUrl(for: cap, version: version)
|
||||
guard fm.fileExists(atPath: newImagePath.path) else {
|
||||
return deleteImage(for: cap, version: version)
|
||||
}
|
||||
let oldImagePath = localImageUrl(for: cap, version: 0)
|
||||
return move(newImagePath, to: oldImagePath)
|
||||
}
|
||||
|
||||
// MARK: Status
|
||||
|
||||
/**
|
||||
Check if an image exists for a cap
|
||||
- parameter cap: The id of the cap
|
||||
- returns: True, if an image exists
|
||||
*/
|
||||
func hasImage(for cap: Int) -> Bool {
|
||||
fm.fileExists(atPath: localImageUrl(for: cap, version: 0).path)
|
||||
}
|
||||
|
||||
/**
|
||||
Check if a thumbnail exists for a cap
|
||||
- parameter cap: The id of the cap
|
||||
- returns: True, if a thumbnail exists
|
||||
*/
|
||||
func hasThumbnail(for cap: Int) -> Bool {
|
||||
fm.fileExists(atPath: thumbnailUrl(for: cap).path)
|
||||
}
|
||||
|
||||
func existingImageUrl(for cap: Int, version: Int = 0) -> URL? {
|
||||
let url = localImageUrl(for: cap, version: version)
|
||||
return fm.fileExists(atPath: url.path) ? url : nil
|
||||
}
|
||||
|
||||
// MARK: Retrieval
|
||||
|
||||
/**
|
||||
Get the image data for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
If no image exists locally, then this function returns nil.
|
||||
- parameter cap: The id of the cap
|
||||
- parameter version: The image version
|
||||
- returns: The image data, or `nil`
|
||||
*/
|
||||
func imageData(for cap: Int, version: Int = 0) -> Data? {
|
||||
readData(from: localImageUrl(for: cap, version: version))
|
||||
}
|
||||
|
||||
/**
|
||||
Get the image for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
If no image exists locally, then this function returns nil.
|
||||
- parameter cap: The id of the cap
|
||||
- parameter version: The image version
|
||||
- returns: The image, or `nil`
|
||||
- note: Removes invalid image data on disk, if the data is not a valid image
|
||||
- note: Must be called on the main thread
|
||||
*/
|
||||
func image(for cap: Int, version: Int) -> UIImage? {
|
||||
guard let data = imageData(for: cap, version: version) else {
|
||||
return nil
|
||||
}
|
||||
guard let image = UIImage(data: data) else {
|
||||
log("Removing invalid image \(version) of cap \(cap) from disk")
|
||||
deleteImage(for: cap, version: version)
|
||||
return nil
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
func image(for cap: Int) -> UIImage? {
|
||||
image(for: cap, version: 0)
|
||||
}
|
||||
|
||||
/**
|
||||
Get the thumbnail data for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
If no image exists locally, then this function returns nil.
|
||||
- parameter cap: The id of the cap
|
||||
- returns: The image data, or `nil`
|
||||
*/
|
||||
func thumbnailData(for cap: Int) -> Data? {
|
||||
readData(from: thumbnailUrl(for: cap))
|
||||
}
|
||||
|
||||
/**
|
||||
Get the thumbnail for a cap.
|
||||
If the image exists on disk, it is returned.
|
||||
If no image exists locally, then this function returns nil.
|
||||
- parameter cap: The id of the cap
|
||||
- returns: The image, or `nil`
|
||||
*/
|
||||
func thumbnail(for cap: Int) -> UIImage? {
|
||||
guard let data = thumbnailData(for: cap) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(data: data)
|
||||
}
|
||||
|
||||
/// The compiled recognition model on disk
|
||||
var recognitionModel: VNCoreMLModel? {
|
||||
guard fm.fileExists(atPath: modelUrl.path) else {
|
||||
log("No recognition model to load")
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
let model = try MLModel(contentsOf: modelUrl)
|
||||
|
||||
return try VNCoreMLModel(for: model)
|
||||
} catch {
|
||||
self.error("Failed to load recognition model: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func ciImage(for cap: Int) -> CIImage? {
|
||||
let url = thumbnailUrl(for: cap)
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
guard let image = CIImage(contentsOf: url) else {
|
||||
error("Failed to read CIImage for main image of cap \(cap)")
|
||||
return nil
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
private func readData(from url: URL) -> Data? {
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
return nil
|
||||
}
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch {
|
||||
self.error("Could not read data from \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Deleting data
|
||||
|
||||
@discardableResult
|
||||
func deleteDatabase() -> Bool {
|
||||
do {
|
||||
try fm.removeItem(at: dbUrl)
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to delete database: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func deleteImage(for cap: Int, version: Int) -> Bool {
|
||||
let url = localImageUrl(for: cap, version: version)
|
||||
return delete(at: url)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func deleteThumbnail(for cap: Int) -> Bool {
|
||||
let url = thumbnailUrl(for: cap)
|
||||
return delete(at: url)
|
||||
}
|
||||
|
||||
private func delete(at url: URL) -> Bool {
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
return true
|
||||
}
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to delete file \(url.lastPathComponent): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Storage: Logger { }
|
||||
|
@@ -1,134 +0,0 @@
|
||||
//
|
||||
// TileImage.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 20.05.20.
|
||||
// Copyright © 2020 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
extension Database {
|
||||
|
||||
struct TileImage {
|
||||
|
||||
let name: String
|
||||
|
||||
let width: Int
|
||||
|
||||
/// The tiles of each cap, with the index being the tile, and the value being the cap id.
|
||||
let caps: [Int]
|
||||
|
||||
var encodedCaps: Data {
|
||||
caps.map(UInt16.init).withUnsafeBytes { (p) in
|
||||
Data(buffer: p.bindMemory(to: UInt8.self))
|
||||
}
|
||||
}
|
||||
|
||||
init(name: String, width: Int, caps: [Int]) {
|
||||
self.name = name
|
||||
self.width = width
|
||||
self.caps = caps
|
||||
}
|
||||
|
||||
init(row: Row) {
|
||||
self.name = row[TileImage.columnName]
|
||||
self.width = row[TileImage.columnWidth]
|
||||
self.caps = row[TileImage.columnCaps].withUnsafeBytes { p in
|
||||
p.bindMemory(to: UInt16.self).map(Int.init)
|
||||
}
|
||||
}
|
||||
|
||||
var insertQuery: Insert {
|
||||
TileImage.table.insert(
|
||||
TileImage.columnName <- name,
|
||||
TileImage.columnWidth <- width,
|
||||
TileImage.columnCaps <- encodedCaps)
|
||||
}
|
||||
|
||||
var updateQuery: Update {
|
||||
TileImage.table.update(
|
||||
TileImage.columnWidth <- width,
|
||||
TileImage.columnCaps <- encodedCaps)
|
||||
}
|
||||
|
||||
static let columnName = Expression<String>("name")
|
||||
|
||||
static let columnWidth = Expression<Int>("width")
|
||||
|
||||
static let columnCaps = Expression<Data>("caps")
|
||||
|
||||
static let table = Table("images")
|
||||
|
||||
|
||||
static func named(_ name: String) -> Table {
|
||||
table.filter(columnName == name)
|
||||
}
|
||||
static func exists(_ name: String) -> Table {
|
||||
named(name).select(columnName)
|
||||
}
|
||||
|
||||
static var createQuery: String {
|
||||
table.create(ifNotExists: true) { t in
|
||||
t.column(Cap.columnId, primaryKey: true)
|
||||
t.column(columnName)
|
||||
t.column(columnWidth)
|
||||
t.column(columnCaps)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func save(tileImage: TileImage) -> Bool {
|
||||
guard exists(tileImage.name) else {
|
||||
return insert(tileImage)
|
||||
}
|
||||
return update(tileImage)
|
||||
}
|
||||
|
||||
var tileImages: [TileImage] {
|
||||
(try? db.prepare(TileImage.table).map(TileImage.init)) ?? []
|
||||
}
|
||||
|
||||
func tileImage(named name: String) -> TileImage? {
|
||||
do {
|
||||
guard let row = try db.pluck(TileImage.named(name)) else {
|
||||
return nil
|
||||
}
|
||||
return TileImage(row: row)
|
||||
} catch {
|
||||
log("Failed to get tile image \(name): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func exists(_ tileImage: String) -> Bool {
|
||||
do {
|
||||
return try db.pluck(TileImage.exists(tileImage)) != nil
|
||||
} catch {
|
||||
log("Failed to check tile image \(tileImage): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func insert(_ tileImage: TileImage) -> Bool {
|
||||
do {
|
||||
try db.run(tileImage.insertQuery)
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to insert tile image \(tileImage): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func update(_ tileImage: TileImage) -> Bool {
|
||||
do {
|
||||
try db.run(tileImage.updateQuery)
|
||||
return true
|
||||
} catch {
|
||||
log("Failed to update tile image \(tileImage): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,212 +0,0 @@
|
||||
//
|
||||
// Upload.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 26.04.20.
|
||||
// Copyright © 2020 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SQLite
|
||||
|
||||
struct Upload {
|
||||
|
||||
static let offlineKey = "offline"
|
||||
|
||||
let serverUrl: URL
|
||||
|
||||
let table = Table("uploads")
|
||||
|
||||
let rowCapId = Expression<Int>("cap")
|
||||
|
||||
let rowCapVersion = Expression<Int>("version")
|
||||
|
||||
init(server: URL) {
|
||||
self.serverUrl = server
|
||||
}
|
||||
|
||||
// MARK: Paths
|
||||
|
||||
var serverImageUrl: URL {
|
||||
serverUrl.appendingPathComponent("images")
|
||||
}
|
||||
|
||||
private func serverImageUrl(for cap: Int, version: Int = 0) -> URL {
|
||||
serverImageUrl.appendingPathComponent("\(cap)/\(cap)-\(version).jpg")
|
||||
}
|
||||
|
||||
private func serverImageUploadUrl(for cap: Int) -> URL {
|
||||
serverImageUrl.appendingPathComponent("\(cap)")
|
||||
}
|
||||
|
||||
private func serverNameUploadUrl(for cap: Int) -> URL {
|
||||
serverUrl.appendingPathComponent("name/\(cap)")
|
||||
}
|
||||
|
||||
private func serverChangeMainImageUrl(for cap: Int, to newValue: Int) -> URL {
|
||||
serverUrl.appendingPathComponent("switch/\(cap)/\(newValue)")
|
||||
}
|
||||
|
||||
// MARK: SQLite
|
||||
|
||||
var createQuery: String {
|
||||
table.create(ifNotExists: true) { t in
|
||||
t.column(rowCapId)
|
||||
t.column(rowCapVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func existsQuery(for cap: Int, version: Int) -> ScalarQuery<Int> {
|
||||
table.filter(rowCapId == cap && rowCapVersion == version).count
|
||||
}
|
||||
|
||||
func insertQuery(for cap: Int, version: Int) -> Insert {
|
||||
table.insert(rowCapId <- cap, rowCapVersion <- version)
|
||||
}
|
||||
|
||||
func deleteQuery(for cap: Int, version: Int) -> Delete {
|
||||
table.filter(rowCapId == cap && rowCapVersion == version).delete()
|
||||
}
|
||||
|
||||
// MARK: Uploading data
|
||||
|
||||
func upload(name: String, for cap: Int, completion: @escaping (_ success: Bool) -> Void) {
|
||||
var request = URLRequest(url: serverNameUploadUrl(for: cap))
|
||||
request.httpMethod = "POST"
|
||||
request.httpBody = name.data(using: .utf8)
|
||||
let task = URLSession.shared.dataTask(with: request) { data, response, error in
|
||||
if let error = error {
|
||||
self.log("Failed to upload name of cap \(cap): \(error)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let response = response else {
|
||||
self.log("Failed to upload name of cap \(cap): No response")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let urlResponse = response as? HTTPURLResponse else {
|
||||
self.log("Failed to upload name of cap \(cap): \(response)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard urlResponse.statusCode == 200 else {
|
||||
self.log("Failed to upload name of cap \(cap): Response \(urlResponse.statusCode)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
completion(true)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload(_ cap: Cap, timeout: TimeInterval = 30) -> Bool {
|
||||
upload(name: cap.name, for: cap.id, timeout: timeout)
|
||||
}
|
||||
|
||||
func upload(name: String, for cap: Int, timeout: TimeInterval = 30) -> Bool {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var result = true
|
||||
upload(name: name, for: cap) { success in
|
||||
if success {
|
||||
self.log("Uploaded cap \(cap)")
|
||||
} else {
|
||||
result = false
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
||||
log("Timed out uploading cap \(cap)")
|
||||
return false
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func upload(imageAt url: URL, for cap: Int, completion: @escaping (_ count: Int?) -> Void) {
|
||||
var request = URLRequest(url: serverImageUploadUrl(for: cap))
|
||||
request.httpMethod = "POST"
|
||||
let task = URLSession.shared.uploadTask(with: request, fromFile: url) { data, response, error in
|
||||
if let error = error {
|
||||
self.log("Failed to upload image of cap \(cap): \(error)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let response = response else {
|
||||
self.log("Failed to upload image of cap \(cap): No response")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let urlResponse = response as? HTTPURLResponse else {
|
||||
self.log("Failed to upload image of cap \(cap): \(response)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard urlResponse.statusCode == 200 else {
|
||||
self.log("Failed to upload image of cap \(cap): Response \(urlResponse.statusCode)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let d = data, let string = String(data: d, encoding: .utf8), let int = Int(string) else {
|
||||
self.log("Failed to upload image of cap \(cap): Invalid response")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(int)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
|
||||
func upload(imageAt url: URL, of cap: Int, timeout: TimeInterval = 30) -> Int? {
|
||||
let group = DispatchGroup()
|
||||
group.enter()
|
||||
var result: Int? = nil
|
||||
upload(imageAt: url, for: cap) { count in
|
||||
result = count
|
||||
group.leave()
|
||||
}
|
||||
guard group.wait(timeout: .now() + timeout) == .success else {
|
||||
log("Timed out uploading image of \(cap)")
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
Sets the main image for a cap to a different version.
|
||||
|
||||
- Parameter cap: The id of the cap
|
||||
- Parameter version: The version to set as the main version.
|
||||
- Parameter completion: A callback with the result on completion.
|
||||
*/
|
||||
func setMainImage(for cap: Int, to version: Int, completion: @escaping (_ success: Bool) -> Void) {
|
||||
let url = serverChangeMainImageUrl(for: cap, to: version)
|
||||
let task = URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
if let error = error {
|
||||
self.log("Failed to set main image of cap \(cap) to \(version): \(error)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let response = response else {
|
||||
self.log("Failed to set main image of cap \(cap) to \(version): No response")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard let urlResponse = response as? HTTPURLResponse else {
|
||||
self.log("Failed to set main image of cap \(cap) to \(version): \(response)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
guard urlResponse.statusCode == 200 else {
|
||||
self.log("Failed to set main image of cap \(cap) to \(version): Response \(urlResponse.statusCode)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
completion(true)
|
||||
}
|
||||
task.resume()
|
||||
}
|
||||
}
|
||||
|
||||
extension Upload: Logger { }
|
Reference in New Issue
Block a user