Version 1
This commit is contained in:
746
CapCollector/Data/Cap.swift
Normal file
746
CapCollector/Data/Cap.swift
Normal file
@ -0,0 +1,746 @@
|
||||
//
|
||||
// Cap.swift
|
||||
// CapCollector
|
||||
//
|
||||
// Created by Christoph on 19.11.18.
|
||||
// Copyright © 2018 CH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftyDropbox
|
||||
|
||||
protocol CapsDelegate: class {
|
||||
|
||||
func capHasUpdates(_ cap: Cap)
|
||||
|
||||
func capsLoaded()
|
||||
}
|
||||
|
||||
final class Cap {
|
||||
|
||||
// MARK: - Static variables
|
||||
|
||||
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
|
||||
|
||||
static var delegate: CapsDelegate?
|
||||
|
||||
static var shouldSave = true {
|
||||
didSet {
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
||||
static var hasMatches = false {
|
||||
didSet {
|
||||
guard !hasMatches else { return }
|
||||
all.forEach { _, cap in
|
||||
cap.match = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var nextUnusedId: Int {
|
||||
return (all.keys.max() ?? 0) + 1
|
||||
}
|
||||
|
||||
/// The number of caps currently in the database
|
||||
static var totalCapCount: Int {
|
||||
return all.count
|
||||
}
|
||||
|
||||
/// The total number of images for all caps
|
||||
static var imageCount: Int {
|
||||
return all.reduce(0) { sum, cap in
|
||||
return sum + cap.value.count
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Match all cap names against the given string and return matches.
|
||||
- note: Each space-separated part of the string is matched individually
|
||||
*/
|
||||
static func caps(matching text: String) -> [Cap] {
|
||||
let cleaned = text.clean
|
||||
let found = all.compactMap { (_,cap) -> Cap? in
|
||||
// For each part of text, check if name contains it
|
||||
for textItem in cleaned.components(separatedBy: " ") {
|
||||
if textItem != "" && !cap.name.contains(textItem) { return nil }
|
||||
}
|
||||
return cap
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
// MARK: - Variables
|
||||
|
||||
/// The unique number of the cap
|
||||
let id: Int
|
||||
|
||||
/// The tile position of the cap
|
||||
var tile: Int
|
||||
|
||||
/// The name of the cap
|
||||
var name: String {
|
||||
didSet {
|
||||
cleanName = name.clean
|
||||
Cap.save()
|
||||
event("Updated name for cap \(id) to \(name)")
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of the cap wothout special characters
|
||||
private(set) var cleanName: String
|
||||
|
||||
/// The number of images existing for the cap
|
||||
private(set) var count: Int {
|
||||
didSet {
|
||||
Cap.save()
|
||||
event("Updated count for cap \(id) to \(count)")
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// The similarity of the cap to the currently processed image
|
||||
var match: Float? = nil
|
||||
|
||||
// MARK: - All caps
|
||||
|
||||
/// A dictionary of all known caps
|
||||
static var all = [Int : Cap]()
|
||||
|
||||
// MARK: - Tile information
|
||||
|
||||
/// A dictionary of the caps for the tiles
|
||||
static var tiles = [Int : Cap]()
|
||||
|
||||
/**
|
||||
Get the cap image for a tile.
|
||||
*/
|
||||
static func tileImage(tile: Int) -> UIImage? {
|
||||
return tiles[tile]?.thumbnail
|
||||
}
|
||||
|
||||
/**
|
||||
Switch two tiles.
|
||||
*/
|
||||
static func switchTiles(_ lhs: Int, _ rhs: Int) {
|
||||
let l = tiles[lhs]!
|
||||
let r = tiles[rhs]!
|
||||
l.tile = rhs
|
||||
r.tile = lhs
|
||||
tiles[rhs] = l
|
||||
tiles[lhs] = r
|
||||
event("Switched tiles \(lhs) and \(rhs)")
|
||||
}
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/**
|
||||
Create a new cap with an image
|
||||
- parameter image: The main image of the cap
|
||||
- parameter name: The name of the cap
|
||||
*/
|
||||
init?(image: UIImage, name: String) {
|
||||
self.id = Cap.nextUnusedId
|
||||
self.tile = id - 1
|
||||
self.name = name
|
||||
self.count = 1
|
||||
self.cleanName = name.clean
|
||||
guard save(mainImage: image) else {
|
||||
return nil
|
||||
}
|
||||
upload(mainImage: image) { success in
|
||||
guard success else { return }
|
||||
Cap.all[self.id] = self
|
||||
Cap.tiles[self.id] = self
|
||||
Cap.save()
|
||||
Cap.updateMosaicWithNewCap(id: self.id, image)
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Create a cap from a line in the cap list file
|
||||
*/
|
||||
init?(line: String) {
|
||||
guard line != "" else {
|
||||
return nil
|
||||
}
|
||||
let parts = line.components(separatedBy: ";")
|
||||
guard parts.count == 4 else {
|
||||
Cap.error("Cap names: Invalid line \(line)")
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let nr = Int(parts[0]) else {
|
||||
Cap.error("Invalid id in line \(line)")
|
||||
return nil
|
||||
}
|
||||
guard let count = Int(parts[2]) else {
|
||||
Cap.error("Invalid count in line \(line)")
|
||||
return nil
|
||||
}
|
||||
guard let tile = Int(parts[3]) else {
|
||||
Cap.error("Invalid tile in line \(line)")
|
||||
return nil
|
||||
}
|
||||
self.id = nr
|
||||
self.name = parts[1]
|
||||
self.count = count
|
||||
self.cleanName = name.clean
|
||||
self.tile = tile
|
||||
Cap.tiles[tile] = self
|
||||
Cap.all[id] = self
|
||||
}
|
||||
|
||||
// MARK: - Images
|
||||
|
||||
/// The main image of the cap
|
||||
var image: UIImage? {
|
||||
guard let data = DiskManager.image(for: id) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(data: data)
|
||||
}
|
||||
|
||||
/// The main image of the cap
|
||||
var thumbnail: UIImage? {
|
||||
if let data = DiskManager.thumbnail(for: id) {
|
||||
return UIImage(data: data)
|
||||
}
|
||||
return makeThumbnail()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func makeThumbnail() -> UIImage? {
|
||||
guard let img = image else {
|
||||
return nil
|
||||
}
|
||||
let len = GridViewController.len * 2
|
||||
let thumb = img.resize(to: CGSize.init(width: len, height: len))
|
||||
guard let data = thumb.pngData() else {
|
||||
error("Failed to get PNG data from thumbnail for cap \(id)")
|
||||
return nil
|
||||
}
|
||||
_ = DiskManager.save(thumbnailData: data, for: id)
|
||||
event("Created thumbnail for cap \(id)")
|
||||
return thumb
|
||||
}
|
||||
|
||||
/**
|
||||
Download a specified image of the cap.
|
||||
- Note: If the downloaded image is the main image, it is automatically saved to disk
|
||||
- Note: If the main image is requested and already downloaded, it is returned directly
|
||||
- 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
|
||||
*/
|
||||
func downloadImage(_ number: Int = 0, completion: @escaping (_ image: UIImage?) -> Void) {
|
||||
if number == 0, let image = self.image {
|
||||
event("Main image for cap \(id) already downloaded")
|
||||
completion(image)
|
||||
return
|
||||
}
|
||||
let path = "/Images/\(id)/\(id)-\(number).jpg"
|
||||
DropboxController.client.files.download(path: path).response { data, dbError in
|
||||
if let error = dbError {
|
||||
self.error("Failed to download image data (\(number)) for cap \(self.id): \(error)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
guard let d = data?.1 else {
|
||||
self.error("Failed to download image data (\(number)) for cap \(self.id)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let image = UIImage(data: d) else {
|
||||
self.error("Corrupted image data (\(number)) for cap \(self.id)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
if number == 0 {
|
||||
guard self.save(mainImage: image) else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
self.event("Downloaded image data (\(number)) for cap \(self.id)")
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
|
||||
func save(mainImage: UIImage) -> Bool {
|
||||
guard let data = mainImage.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||
error("Failed to convert image to data")
|
||||
return false
|
||||
}
|
||||
guard DiskManager.save(imageData: data, for: id) else {
|
||||
error("Failed to save main image for cap \(id)")
|
||||
return false
|
||||
}
|
||||
event("Saved main image for cap \(id) to disk")
|
||||
guard let _ = makeThumbnail() else {
|
||||
return true
|
||||
}
|
||||
|
||||
Cap.delegate?.capHasUpdates(self)
|
||||
return true
|
||||
}
|
||||
|
||||
func add(image: UIImage, completion: @escaping (Bool) -> Void) {
|
||||
upload(image: image) { saved in
|
||||
guard saved else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
// Increment cap count
|
||||
self.count += 1
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image upload
|
||||
|
||||
private func upload(mainImage: UIImage, completion: @escaping (_ success: Bool) -> Void) {
|
||||
self.createFolder { created in
|
||||
guard created else { return }
|
||||
self.upload(image: mainImage, number: 0, savedCallback: completion)
|
||||
}
|
||||
}
|
||||
|
||||
private func folderExists(completion: @escaping (_ exists: Bool?) -> Void) {
|
||||
let path = "/Images"
|
||||
DropboxController.client.files.listFolder(path: path).response { response, error in
|
||||
if let e = error {
|
||||
self.error("Failed to get image folder list: \(e)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let result = response else {
|
||||
self.error("Failed to get image folder list")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let exists = result.entries.contains { $0.name == "\(self.id)" }
|
||||
completion(exists)
|
||||
}
|
||||
}
|
||||
|
||||
private func createFolder(completion: @escaping (_ success: Bool) -> Void) {
|
||||
// Create folder for cap
|
||||
let path = "/Images/\(id)"
|
||||
DropboxController.client.files.createFolderV2(path: path).response { _, error in
|
||||
if let err = error {
|
||||
self.event("Could not create folder for new cap \(self.id): \(err)")
|
||||
completion(false)
|
||||
} else {
|
||||
self.event("Created folder for new cap \(self.id)")
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func upload(image: UIImage, savedCallback: @escaping (Bool) -> Void) {
|
||||
upload(image: image, number: count, savedCallback: savedCallback)
|
||||
}
|
||||
|
||||
private func upload(image: UIImage, number: Int, savedCallback: @escaping (Bool) -> Void) {
|
||||
// Convert to data
|
||||
guard let data = image.jpegData(compressionQuality: Cap.jpgQuality) else {
|
||||
error("Failed to convert image to data")
|
||||
return
|
||||
}
|
||||
let fileName = "\(id)-\(number).jpg"
|
||||
// Save image to upload folder
|
||||
guard let url = DiskManager.saveForUpload(imageData: data, name: fileName) else {
|
||||
error("Could not save image for cap \(id) to upload folder")
|
||||
savedCallback(false)
|
||||
return
|
||||
}
|
||||
event("Saved image \(number) for cap \(id) for upload")
|
||||
savedCallback(true)
|
||||
|
||||
Cap.upload(url: url) { success in
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private static func upload(url: URL, completion: @escaping (Bool) -> Void) {
|
||||
let cap = Int(url.lastPathComponent.components(separatedBy: "-").first!)!
|
||||
let path = "/Images/\(cap)/" + url.lastPathComponent
|
||||
|
||||
let data: Data
|
||||
do {
|
||||
data = try Data(contentsOf: url)
|
||||
} catch {
|
||||
self.error("Could not read data from url \(url.path): \(error)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
DropboxController.client.files.upload(path: path, input: data).response { response, error in
|
||||
if let err = error {
|
||||
self.error("Failed to upload file at url: \(url): \(err)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
Cap.event("Uploaded image \(path)")
|
||||
guard DiskManager.removeFromUpload(url: url) else {
|
||||
self.error("Could not delete uploaded image for cap \(cap) at url \(url)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
static func uploadRemainingImages() {
|
||||
guard let list = DiskManager.pendingUploads else {
|
||||
return
|
||||
}
|
||||
guard list.count != 0 else {
|
||||
event("No pending uploads")
|
||||
return
|
||||
}
|
||||
event("\(list.count) image uploads pending")
|
||||
|
||||
for url in list {
|
||||
upload(url: url) { didUpload in
|
||||
// Delete image from disk if uploaded
|
||||
guard didUpload else {
|
||||
self.error("Could not upload image at url \(url)")
|
||||
return
|
||||
}
|
||||
guard DiskManager.removeFromUpload(url: url) else {
|
||||
self.error("Could not delete uploaded image at url \(url)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Counts
|
||||
|
||||
func updateCount(completion: @escaping (Bool) -> Void) {
|
||||
getImageCount { response in
|
||||
guard let count = response else {
|
||||
self.error("Could not update count for cap \(self.id)")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
self.count = count
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
private func getImageCount(completion: @escaping (Int?) -> Void) {
|
||||
DropboxController.client.files.listFolder(path: "/Images/\(id)").response { response, error in
|
||||
if let err = error {
|
||||
self.error("Error getting folder content of cap \(self.id): \(err)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
guard let files = response?.entries else {
|
||||
self.error("No content for folder of cap \(self.id)")
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
completion(files.count)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Sorted caps
|
||||
|
||||
static var unsortedCaps: Set<Cap> {
|
||||
return Set(all.values)
|
||||
}
|
||||
|
||||
static func capList(sortedBy criteria: SortCriteria, ascending: Bool) -> [Cap] {
|
||||
if ascending {
|
||||
return sorted([Cap](all.values), ascendingBy: criteria)
|
||||
} else {
|
||||
return sorted([Cap](all.values), descendingBy: criteria)
|
||||
}
|
||||
}
|
||||
|
||||
private static func sorted(_ list: [Cap], ascendingBy parameter: SortCriteria) -> [Cap] {
|
||||
switch parameter {
|
||||
case .id: return list.sorted { $0.id < $1.id }
|
||||
case .count: return list.sorted { $0.count < $1.count }
|
||||
case .name: return list.sorted { $0.name < $1.name }
|
||||
case .match: return list.sorted { $0.match ?? 0 < $1.match ?? 0 }
|
||||
}
|
||||
}
|
||||
|
||||
private static func sorted(_ list: [Cap], descendingBy parameter: SortCriteria) -> [Cap] {
|
||||
switch parameter {
|
||||
case .id: return list.sorted { $0.id > $1.id }
|
||||
case .count: return list.sorted { $0.count > $1.count }
|
||||
case .name: return list.sorted { $0.name > $1.name }
|
||||
case .match: return list.sorted { $0.match ?? 0 > $1.match ?? 0 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Loading, Saving & Uploading cap list
|
||||
|
||||
/**
|
||||
Either load the names from disk or download them from dropbox.
|
||||
- parameter completion: The handler that is called with true on success, false on failure
|
||||
*/
|
||||
static func load() {
|
||||
NameFile.makeAvailable { content in
|
||||
guard let lines = content else {
|
||||
return
|
||||
}
|
||||
self.readNames(from: lines)
|
||||
DispatchQueue.main.async {
|
||||
createAndSaveMosaic()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read all caps from the content of a file
|
||||
private static func readNames(from fileContent: String) {
|
||||
let parts = fileContent.components(separatedBy: "\n")
|
||||
for line in parts {
|
||||
_ = Cap(line: line)
|
||||
}
|
||||
event("Loaded \(totalCapCount) caps from file")
|
||||
delegate?.capsLoaded()
|
||||
}
|
||||
|
||||
static func getCapStatistics() -> [Int] {
|
||||
let counts = all.values.map { $0.count }
|
||||
var c = [Int](repeating: 0, count: counts.max()! + 1)
|
||||
counts.forEach { c[$0] += 1 }
|
||||
return c
|
||||
}
|
||||
|
||||
static func save() {
|
||||
guard shouldSave else { return }
|
||||
let content = namesAsString()
|
||||
NameFile.save(names: content)
|
||||
}
|
||||
|
||||
static func saveAndUpload(completion: @escaping (Bool) -> Void) {
|
||||
let content = namesAsString()
|
||||
NameFile.saveAndUpload(names: content) { success in
|
||||
guard success else {
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
Persistence.lastUploadedCapCount = totalCapCount
|
||||
Persistence.lastUploadedImageCount = imageCount
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
private static func namesAsString() -> String {
|
||||
return capList(sortedBy: .id, ascending: true).reduce("") { $0 + $1.description }
|
||||
}
|
||||
|
||||
// MARK: - GridView
|
||||
|
||||
private static func size(for tiles: Int) -> CGSize {
|
||||
let columns = CGFloat(mosaicColumns)
|
||||
// Add half of a cell due to row shift
|
||||
let width = (columns + 0.5) * mosaicCellSize
|
||||
let rows = (CGFloat(tiles) / columns).rounded(.up)
|
||||
// Add margin because the last row does not overlap
|
||||
let height = rows * mosaicRowHeight + mosaicMargin
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
static func origin(for tile: Int) -> CGPoint {
|
||||
let row = tile / mosaicColumns
|
||||
let column = tile - row * mosaicColumns
|
||||
let x = ( CGFloat(column) + (row.isEven ? 0 : 0.5) ) * mosaicCellSize
|
||||
let y = CGFloat(row) * mosaicRowHeight
|
||||
return CGPoint(x: x, y: y)
|
||||
}
|
||||
|
||||
private static func makeTile(_ tile: Int, image: UIImage) -> RoundedImageView {
|
||||
let point = origin(for: tile)
|
||||
let frame = CGRect(origin: point, size: CGSize(width: mosaicCellSize, height: mosaicCellSize))
|
||||
let view = RoundedImageView(frame: frame)
|
||||
view.image = image
|
||||
return view
|
||||
}
|
||||
|
||||
private static func makeMosaicCanvas() -> UIView {
|
||||
let view = UIView(frame: CGRect(origin: .zero, size: size(for: Cap.totalCapCount)))
|
||||
view.backgroundColor = UIColor(red: 36/255, green: 36/255, blue: 36/255, alpha: 1)
|
||||
return view
|
||||
}
|
||||
|
||||
private static func makeMosaic() -> UIImage? {
|
||||
let canvas = makeMosaicCanvas()
|
||||
|
||||
for cap in Cap.all.values {
|
||||
if let img = cap.image {
|
||||
let view = makeTile(cap.id - 1, image: img)
|
||||
canvas.addSubview(view)
|
||||
} else {
|
||||
error("No image for cap \(cap.id)")
|
||||
}
|
||||
}
|
||||
return render(view: canvas)
|
||||
}
|
||||
|
||||
private static func render(view: UIView) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0)
|
||||
defer { UIGraphicsEndImageContext() }
|
||||
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
|
||||
return UIGraphicsGetImageFromCurrentImageContext()
|
||||
}
|
||||
|
||||
private static func createAndSaveMosaic() {
|
||||
guard !DiskManager.mosaicExists else {
|
||||
event("Mosaic already created")
|
||||
return
|
||||
}
|
||||
updateMosaic()
|
||||
}
|
||||
|
||||
static var mosaic: UIImage? {
|
||||
guard let data = DiskManager.mosaicData else {
|
||||
error("No mosaic data on disk")
|
||||
return nil
|
||||
}
|
||||
guard let image = UIImage(data: data, scale: UIScreen.main.scale) else {
|
||||
error("Failed to create image from mosaic data")
|
||||
return nil
|
||||
}
|
||||
return image
|
||||
}
|
||||
|
||||
private static func updateMosaicWithNewCap(id: Int, _ image: UIImage) {
|
||||
guard let old = mosaic else { return }
|
||||
|
||||
let view = UIImageView(image: old)
|
||||
let canvas = makeMosaicCanvas()
|
||||
let tile = makeTile(id - 1, image: image)
|
||||
canvas.addSubview(view)
|
||||
canvas.addSubview(tile)
|
||||
guard let img = render(view: canvas) else {
|
||||
error("Failed to update mosaic for cap \(id)")
|
||||
return
|
||||
}
|
||||
saveMosaic(img)
|
||||
}
|
||||
|
||||
static func tile(for point: CGPoint) -> Int? {
|
||||
let s = point.y.truncatingRemainder(dividingBy: mosaicRowHeight)
|
||||
let row = Int(point.y / mosaicRowHeight)
|
||||
guard s > mosaicMargin else {
|
||||
return nil
|
||||
}
|
||||
let column: CGFloat
|
||||
if row.isEven {
|
||||
column = point.x / mosaicCellSize
|
||||
} else {
|
||||
column = (point.x - mosaicCellSize / 2) / mosaicCellSize
|
||||
}
|
||||
return row * mosaicColumns + Int(column)
|
||||
}
|
||||
|
||||
static func switchTilesInMosaic(_ mosaic: UIImageView, tile1: Int, tile2: Int) {
|
||||
let tileView1 = makeTile(tile1, image: Cap.tileImage(tile: tile1)!)
|
||||
let tileView2 = makeTile(tile2, image: Cap.tileImage(tile: tile2)!)
|
||||
|
||||
|
||||
mosaic.addSubview(tileView1)
|
||||
mosaic.addSubview(tileView2)
|
||||
defer {
|
||||
tileView1.removeFromSuperview()
|
||||
tileView2.removeFromSuperview()
|
||||
}
|
||||
|
||||
guard let img = render(view: mosaic) else {
|
||||
error("Failed to switch \(tile1) and \(tile2) in mosaic")
|
||||
return
|
||||
}
|
||||
|
||||
mosaic.image = img
|
||||
saveMosaic(img)
|
||||
}
|
||||
|
||||
static func updateMosaic() {
|
||||
guard let image = makeMosaic() else {
|
||||
error("No mosaik image created")
|
||||
return
|
||||
}
|
||||
saveMosaic(image)
|
||||
}
|
||||
|
||||
static func saveMosaic(_ image: UIImage) {
|
||||
guard let data = image.pngData() else {
|
||||
error("Failed to convert mosaic to data")
|
||||
return
|
||||
}
|
||||
guard DiskManager.saveMosaicData(data) else {
|
||||
error("Failed to write mosaic to disk")
|
||||
return
|
||||
}
|
||||
event("Mosaic saved")
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return "\(id);\(name);\(count);\(tile)\n"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protocol Logger
|
||||
|
||||
extension Cap: Logger {
|
||||
|
||||
static let logToken = "[CAP]"
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
214
CapCollector/Data/DiskManager.swift
Normal file
214
CapCollector/Data/DiskManager.swift
Normal file
@ -0,0 +1,214 @@
|
||||
//
|
||||
// DiskManager.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 23.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class DiskManager {
|
||||
|
||||
enum LocalDirectory: String {
|
||||
/// Folder for new images to upload
|
||||
case upload = "Upload"
|
||||
|
||||
/// Folder for downloaded images
|
||||
case images = "Images"
|
||||
|
||||
/// Folder for downloaded images
|
||||
case thumbnails = "Thumbnails"
|
||||
|
||||
/// Directory for name file
|
||||
case files = "Files"
|
||||
|
||||
private static let fm = FileManager.default
|
||||
|
||||
/// The url to the file sstem
|
||||
var url: URL {
|
||||
return URL(fileURLWithPath: self.rawValue, isDirectory: true, relativeTo: DiskManager.documentsDirectory)
|
||||
}
|
||||
|
||||
fileprivate func create() -> Bool {
|
||||
return DiskManager.create(directory: url)
|
||||
}
|
||||
|
||||
var content: [URL]? {
|
||||
do {
|
||||
return try LocalDirectory.fm.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
|
||||
} catch {
|
||||
print("[LocalDirectory] Could not read directory \(self.rawValue): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The folder where images and name list are stored
|
||||
static let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
|
||||
private static let fm = FileManager.default
|
||||
|
||||
// MARK: - First launch
|
||||
|
||||
@discardableResult static func setupOnFirstLaunch() -> Bool {
|
||||
return LocalDirectory.files.create() &&
|
||||
LocalDirectory.images.create() &&
|
||||
LocalDirectory.thumbnails.create() &&
|
||||
LocalDirectory.upload.create()
|
||||
}
|
||||
|
||||
private static func create(directory: URL) -> Bool {
|
||||
do {
|
||||
if !fm.fileExists(atPath: directory.path) {
|
||||
try fm.createDirectory(at: directory, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
return true
|
||||
} catch {
|
||||
event("Could not create \(directory): \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Image retrieval
|
||||
|
||||
/**
|
||||
Check if an image exists for a cap
|
||||
- parameter cap: The id of the cap
|
||||
- returns: True, if an image exists
|
||||
*/
|
||||
static func hasImage(for cap: Int) -> Bool {
|
||||
let url = localUrl(for: cap)
|
||||
return fm.fileExists(atPath: url.path)
|
||||
}
|
||||
|
||||
private static func localUrl(for cap: Int) -> URL {
|
||||
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.images.url)
|
||||
}
|
||||
|
||||
private static func thumbnailUrl(for cap: Int) -> URL {
|
||||
return URL(fileURLWithPath: "\(cap).jpg", isDirectory: true, relativeTo: LocalDirectory.thumbnails.url)
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
- returns: The image data, or `nil`
|
||||
*/
|
||||
static func image(for cap: Int) -> Data? {
|
||||
// If the image exists on disk, get it
|
||||
let url = localUrl(for: cap)
|
||||
return readData(from: url)
|
||||
}
|
||||
|
||||
private static let mosaicURL: URL = documentsDirectory.appendingPathComponent("mosaic.png")
|
||||
|
||||
|
||||
static var mosaicExists: Bool {
|
||||
return fm.fileExists(atPath: mosaicURL.path)
|
||||
}
|
||||
|
||||
static func saveMosaicData(_ data: Data) -> Bool {
|
||||
return write(data, to: mosaicURL)
|
||||
}
|
||||
|
||||
static var mosaicData: Data? {
|
||||
return readData(from: mosaicURL)
|
||||
}
|
||||
|
||||
/**
|
||||
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 data, or `nil`
|
||||
*/
|
||||
static func thumbnail(for cap: Int) -> Data? {
|
||||
// If the image exists on disk, get it
|
||||
let url = thumbnailUrl(for: cap)
|
||||
return readData(from: url)
|
||||
}
|
||||
|
||||
private static 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Save an image to the download folder
|
||||
- parameter imageData: The data of the image
|
||||
- parameter cap: The cap id
|
||||
- returns: True, if the image was saved
|
||||
*/
|
||||
static func save(imageData: Data, for cap: Int) -> Bool {
|
||||
let url = localUrl(for: cap)
|
||||
return write(imageData, to: url)
|
||||
}
|
||||
|
||||
/**
|
||||
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
|
||||
*/
|
||||
static func save(thumbnailData: Data, for cap: Int) -> Bool {
|
||||
let url = thumbnailUrl(for: cap)
|
||||
return write(thumbnailData, to: url)
|
||||
}
|
||||
|
||||
private static 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
|
||||
}
|
||||
|
||||
static func removeFromUpload(url: URL) -> Bool {
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
return true
|
||||
}
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
return true
|
||||
} catch {
|
||||
self.error("Could not delete file \(url.path)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
static var pendingUploads: [URL]? {
|
||||
return LocalDirectory.upload.content
|
||||
}
|
||||
|
||||
static var availableImages: [Int]? {
|
||||
return LocalDirectory.images.content?.compactMap {
|
||||
Int($0.lastPathComponent.components(separatedBy: ".").first ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Save an image to the uploads folder for later
|
||||
*/
|
||||
static func saveForUpload(imageData: Data, name: String) -> URL? {
|
||||
let url = LocalDirectory.upload.url.appendingPathComponent(name)
|
||||
return write(imageData, to: url) ? url : nil
|
||||
}
|
||||
}
|
||||
|
||||
extension DiskManager: Logger {
|
||||
|
||||
static let logToken = "[DiskManager]"
|
||||
}
|
145
CapCollector/Data/NameFile.swift
Normal file
145
CapCollector/Data/NameFile.swift
Normal file
@ -0,0 +1,145 @@
|
||||
//
|
||||
// NameFile.swift
|
||||
// CapFinder
|
||||
//
|
||||
// Created by User on 23.04.18.
|
||||
// Copyright © 2018 User. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyDropbox
|
||||
|
||||
final class NameFile: Logger {
|
||||
|
||||
static let logToken = "[NameFile]"
|
||||
|
||||
/// The name of the file
|
||||
private static let fileName = "names.txt"
|
||||
|
||||
private static let path = "/" + fileName
|
||||
|
||||
/// The url of the file on disk
|
||||
private static let url = DiskManager.documentsDirectory.appendingPathComponent(fileName)
|
||||
|
||||
private static let fm = FileManager.default
|
||||
|
||||
// MARK: - Reading from disk
|
||||
|
||||
/// Indicates if the name list was written to disk
|
||||
private static var nameFileExistsOnDisk: Bool {
|
||||
return fm.fileExists(atPath: url.path)
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
@discardableResult static func save(names: String) -> Bool {
|
||||
let data = names.data(using: .utf8)!
|
||||
return save(names: data)
|
||||
}
|
||||
|
||||
static func saveAndUpload(names: String, completion: @escaping (Bool) -> Void) {
|
||||
let data = names.data(using: .utf8)!
|
||||
guard save(names: data) else {
|
||||
return
|
||||
}
|
||||
|
||||
let client = DropboxController.client
|
||||
client.files.upload(path: path, mode: .overwrite, input: data).response { _ , error in
|
||||
if let error = error {
|
||||
self.error("Error uploading name list: \(error)")
|
||||
completion(false)
|
||||
} else {
|
||||
self.event("Uploaded name list")
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// The content of the name file as a String
|
||||
private static var content: String? {
|
||||
do {
|
||||
return try String(contentsOf: url, encoding: .utf8)
|
||||
} catch {
|
||||
self.error("Error reading \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Save the name file to disk
|
||||
- parameter names: The new name file content
|
||||
- returns: True, if the data was written to disk
|
||||
*/
|
||||
@discardableResult private static func save(names: Data) -> Bool {
|
||||
do {
|
||||
try names.write(to: url, options: .atomic)
|
||||
event("Name file saved to disk")
|
||||
return true
|
||||
} catch {
|
||||
self.error("Could not save names to file: \(error)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static func makeAvailable(completion: ((String?) -> Void)? = nil) {
|
||||
if nameFileExistsOnDisk {
|
||||
completion?(self.content)
|
||||
} else {
|
||||
download() { success in
|
||||
guard success else {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
completion?(self.content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The data of the name list
|
||||
private static var data: Data? {
|
||||
guard nameFileExistsOnDisk else { return nil }
|
||||
do {
|
||||
return try Data(contentsOf: url)
|
||||
} catch {
|
||||
self.error("Could not read data from \(url): \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Delete the file on disk
|
||||
- returns: True, if the file no longer exists on disk
|
||||
*/
|
||||
@discardableResult private static func delete() -> Bool {
|
||||
guard nameFileExistsOnDisk else {
|
||||
event("No name file to delete")
|
||||
return true
|
||||
}
|
||||
|
||||
do {
|
||||
try fm.removeItem(at: url)
|
||||
} catch {
|
||||
self.error("Could not delete name file: \(error)")
|
||||
return false
|
||||
}
|
||||
event("Deleted name file on disk")
|
||||
return true
|
||||
}
|
||||
|
||||
private static func download(completion: ((Bool) -> Void)? = nil) {
|
||||
let client = DropboxController.client
|
||||
event("Downloading names from Dropbox")
|
||||
client.files.download(path: path).response { response, error in
|
||||
guard let data = response?.1 else {
|
||||
self.error("Error downloading file: \(error!)")
|
||||
completion?(false)
|
||||
return
|
||||
}
|
||||
self.event("Downloaded name file")
|
||||
completion?(NameFile.save(names: data))
|
||||
}
|
||||
}
|
||||
}
|
@ -28,21 +28,51 @@ final class Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
static var notUploadedCapCount: Int {
|
||||
static var lastUploadedCapCount: Int {
|
||||
get {
|
||||
return UserDefaults.standard.integer(forKey: "notUploadedCaps")
|
||||
return UserDefaults.standard.integer(forKey: "lastUploadedCaps")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "notUploadedCaps")
|
||||
UserDefaults.standard.set(newValue, forKey: "lastUploadedCaps")
|
||||
}
|
||||
}
|
||||
|
||||
static var notUploadedImageCount: Int {
|
||||
static var lastUploadedImageCount: Int {
|
||||
get {
|
||||
return UserDefaults.standard.integer(forKey: "notUploadedImages")
|
||||
return UserDefaults.standard.integer(forKey: "lastUploadedImages")
|
||||
}
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "notUploadedImages")
|
||||
UserDefaults.standard.set(newValue, forKey: "lastUploadedImages")
|
||||
}
|
||||
}
|
||||
|
||||
static var squeezenet: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "squeezenet")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "squeezenet")
|
||||
}
|
||||
}
|
||||
|
||||
static var resnet: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "resnet")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "resnet")
|
||||
}
|
||||
}
|
||||
|
||||
static var xcode: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: "xcode")
|
||||
}
|
||||
|
||||
set {
|
||||
UserDefaults.standard.set(newValue, forKey: "xcode")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user