Version 1

This commit is contained in:
Christoph Hagen
2019-03-15 13:19:19 +01:00
parent bd63eb38e2
commit 2806733b71
42 changed files with 3561 additions and 259 deletions

View File

@ -0,0 +1,46 @@
//
// CapCell.swift
// CapFinder
//
// Created by User on 22.04.18.
// Copyright © 2018 User. All rights reserved.
//
import UIKit
class CapCell: UITableViewCell {
@IBOutlet private weak var capImage: RoundedImageView!
@IBOutlet private weak var matchLabel: UILabel!
@IBOutlet private weak var nameLabel: UILabel!
@IBOutlet weak var countLabel: UILabel!
var id = 0
var cap: Cap! {
didSet {
updateCell()
}
}
func updateCell() {
capImage.image = cap.image
//capImage.borderColor = AppDelegate.tintColor
matchLabel.text = text(for: cap.match)
nameLabel.text = cap.name
countLabel.text = "\(cap.id) (\(cap.count) image" + (cap.count > 1 ? "s)" : ")")
}
private func text(for value: Float?) -> String? {
guard let nr = value else {
return nil
}
let percent = Int((nr * 100).rounded())
return String(format: "%d %%", arguments: [percent])
}
}

View File

@ -0,0 +1,273 @@
//
// GridViewController.swift
// CapCollector
//
// Created by Christoph on 07.01.19.
// Copyright © 2019 CH. All rights reserved.
//
import UIKit
class GridViewController: UIViewController {
private let columns = 40
static let len: CGFloat = 60
private lazy var rowHeight = GridViewController.len * 0.866
private lazy var margin = GridViewController.len - rowHeight
private var myView: UIView!
private var canvasSize: CGSize = .zero
@IBOutlet weak var scrollView: UIScrollView!
private var selectedTile: Int? = nil
private weak var selectionView: RoundedButton!
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return true
}
override func viewDidLoad() {
super.viewDidLoad()
let width = CGFloat(columns) * GridViewController.len + GridViewController.len / 2
let height = (CGFloat(Cap.totalCapCount) / CGFloat(columns)).rounded(.up) * rowHeight + margin
canvasSize = CGSize(width: width, height: height)
myView = UIView(frame: CGRect(origin: .zero, size: canvasSize))
scrollView.addSubview(myView)
scrollView.contentSize = canvasSize
scrollView.delegate = self
scrollView.zoomScale = 0.5
scrollView.maximumZoomScale = 1
setZoomRange()
/*
guard let image = Cap.mosaic else {
error("No mosaic")
return
}
imageView.image = image
imageHeight.constant = image.size.height
imageWidth.constant = image.size.width
scrollView.contentSize = image.size
let button = RoundedButton(frame: CGRect(origin: .zero, size: CGSize(width: Cap.mosaicCellSize, height: Cap.mosaicCellSize)))
imageView.addSubview(button)
selectionView = button
button.borderColor = AppDelegate.tintColor
button.borderWidth = 3
button.isHidden = true
scrollView.delegate = self
scrollView.zoomScale = 1
scrollView.maximumZoomScale = 1
setZoomRange()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
scrollView.addGestureRecognizer(tapRecognizer)
event("did load")
*/
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
updateTiles()
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap))
myView.addGestureRecognizer(tapRecognizer)
}
override func viewDidLayoutSubviews() {
setZoomRange()
updateTiles()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
Cap.save()
}
private func setZoomRange() {
let size = scrollView.frame.size
let a = size.width / canvasSize.width
let b = size.height / canvasSize.height
let scale = min(a,b)
scrollView.minimumZoomScale = min(a,b)
if scrollView.zoomScale < scale {
scrollView.setZoomScale(scale, animated: true)
}
}
@objc func handleTap(_ sender: UITapGestureRecognizer) {
let loc = sender.location(in: myView)
let y = loc.y
let s = y.truncatingRemainder(dividingBy: rowHeight)
let row = Int(y / rowHeight)
guard s > margin else {
return
}
let column: CGFloat
if row.isEven {
column = loc.x / GridViewController.len
} else {
column = (loc.x - GridViewController.len / 2) / GridViewController.len
}
handleTileTapped(tile: row * columns + Int(column))
/*
event("Tapped")
let loc = sender.location(in: imageView)
guard let tile = Cap.tile(for: loc) else {
event("No tile for location \(loc)")
return
}
handleTileTapped(tile: tile)
*/
}
private func handleTileTapped(tile: Int) {
if let selected = selectedTile {
switchTiles(oldTile: selected, newTile: tile)
} else {
showSelection(tile: tile)
}
}
/*
private func showSelection(tile: Int) {
event("Selecting tile \(tile)")
let point = Cap.origin(for: tile)
selectionView.frame = CGRect(origin: point, size: selectionView.frame.size)
selectionView.isHidden = false
selectedTile = tile
}
private func switchTiles(oldTile: Int, newTile: Int) {
event("Switching tiles \(oldTile) and \(newTile)")
selectionView.isHidden = true
selectedTile = nil
guard oldTile != newTile else {
return
}
Cap.switchTiles(oldTile, newTile)
Cap.switchTilesInMosaic(imageView, tile1: oldTile, tile2: newTile)
}
*/
private var installedTiles = [Int : RoundedImageView]()
private func showSelection(tile: Int) {
clearTileSelection()
if let view = installedTiles[tile] {
view.borderWidth = 3
view.borderColor = AppDelegate.tintColor
selectedTile = tile
} else {
selectedTile = nil
}
}
private func tileIsVisible(tile: Int, in rect: CGRect) -> Bool {
return rect.intersects(frame(for: tile))
}
private func makeTile(_ tile: Int) {
let view = RoundedImageView(frame: frame(for: tile))
myView.addSubview(view)
view.image = Cap.tileImage(tile: tile)
installedTiles[tile] = view
}
private func frame(for tile: Int) -> CGRect {
let row = tile / columns
let column = tile - row * columns
let x = CGFloat(column) * GridViewController.len + (row.isEven ? 0 : GridViewController.len / 2)
let y = CGFloat(row) * rowHeight
return CGRect(x: x, y: y, width: GridViewController.len, height: GridViewController.len)
}
private func switchTiles(oldTile: Int, newTile: Int) {
if oldTile != newTile {
Cap.switchTiles(oldTile, newTile)
installedTiles[oldTile]?.image = Cap.tileImage(tile: oldTile)
installedTiles[newTile]?.image = Cap.tileImage(tile: newTile)
}
clearTileSelection()
}
private func clearTileSelection() {
guard let tile = selectedTile else {
return
}
installedTiles[tile]?.borderWidth = 0
selectedTile = nil
}
private func showTiles(in rect: CGRect) {
for i in 0..<Cap.totalCapCount {
if tileIsVisible(tile: i, in: rect) {
if installedTiles[i] != nil {
continue
}
makeTile(i)
} else if let tile = installedTiles[i] {
tile.removeFromSuperview()
installedTiles[i] = nil
}
}
}
private func updateTiles() {
guard #available(iOS 12.0, *) else {
return
}
let scale = scrollView.zoomScale
let offset = scrollView.contentOffset
let size = scrollView.visibleSize
let scaledOrigin = CGPoint(x: offset.x / scale, y: offset.y / scale)
let scaledSize = CGSize(width: size.width / scale, height: size.height / scale)
let rect = CGRect(origin: scaledOrigin, size: scaledSize)
showTiles(in: rect)
}
}
extension GridViewController: UIScrollViewDelegate {
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return myView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
updateTiles()
}
}
private extension Int {
var isEven: Bool {
return self % 2 == 0
}
}
extension GridViewController: Logger {
static let logToken: String = "[Grid]"
}

View File

@ -0,0 +1,15 @@
//
// ImageCell.swift
// CapFinder
//
// Created by User on 07.02.18.
// Copyright © 2018 User. All rights reserved.
//
import UIKit
class ImageCell: UICollectionViewCell {
@IBOutlet weak var capView: UIImageView!
}

View File

@ -0,0 +1,166 @@
//
// ListViewController.swift
// CapFinder
//
// Created by User on 22.02.18.
// Copyright © 2018 User. All rights reserved.
//
import UIKit
import SwiftyDropbox
class ImageSelector: UIViewController {
// MARK: - Constants
/// The number of items per row
private let itemsPerRow: CGFloat = 3
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return false
}
// MARK: - CollectionView
private var images = [UIImage?]()
var cap: Cap!
@IBOutlet weak var collection: UICollectionView!
// MARK: - Life cycle
override func viewDidLoad() {
super.viewDidLoad()
collection.dataSource = self
collection.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
downloadImages()
}
// MARK: Image download
private func downloadImages() {
images = [UIImage?](repeating: nil, count: cap.count)
event("\(cap.count) images for cap \(cap.id)")
for number in 0..<cap.count {
cap.downloadImage(number) { image in
self.images[number] = image
self.collection.reloadItems(at: [IndexPath(row: number, section: 0)])
}
}
}
// MARK: Select
private func selectedImage(nr: Int) {
guard images[nr] != nil else {
return
}
let tempId = cap.count
let tempFile = "/Images/\(cap.id)/\(cap.id)-\(tempId).jpg"
let oldFile = "/Images/\(cap.id)/\(cap.id)-0.jpg"
let newFile = "/Images/\(cap.id)/\(cap.id)-\(nr).jpg"
guard oldFile != newFile else {
return
}
DropboxController.shared.move(file: oldFile, to: tempFile) { success in
guard success else {
self.error("Could not move \(oldFile) to \(tempFile)")
return
}
DropboxController.shared.move(file: newFile, to: oldFile) { success in
guard success else {
self.error("Could not move \(newFile) to \(oldFile)")
return
}
DropboxController.shared.move(file: tempFile, to: newFile) { success in
if !success {
self.error("Could not move \(tempFile) to \(newFile)")
}
self.finish(with: nr)
}
}
}
}
private func finish(with nr: Int) {
let image = images[nr]!
guard cap.save(mainImage: image) else {
return
}
event("Successfully switched image")
}
}
// MARK: - UICollectionViewDataSource
extension ImageSelector: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: "Image",
for: indexPath) as! ImageCell
cell.capView.image = images[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedImage(nr: indexPath.row)
navigationController?.popViewController(animated: true)
}
}
// MARK: - UICollectionViewDelegateFlowLayout
extension ImageSelector : UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
sizeForItemAt indexPath: IndexPath) -> CGSize {
let widthPerItem = collectionView.frame.width / itemsPerRow
return CGSize(width: widthPerItem, height: widthPerItem)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
func collectionView(_ collectionView: UICollectionView,
layout collectionViewLayout: UICollectionViewLayout,
minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
}
extension ImageSelector: Logger {
static let logToken = "[ImageSelector]"
}

View File

@ -0,0 +1,33 @@
//
// NavigationController.swift
// CapCollector
//
// Created by Christoph on 08.01.19.
// Copyright © 2019 CH. All rights reserved.
//
import UIKit
class NavigationController: UINavigationController {
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return allowLandscape ? .allButUpsideDown : .portrait
}
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
return .portrait
}
override var shouldAutorotate: Bool {
return allowLandscape
}
var allowLandscape: Bool = false
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}

View File

@ -0,0 +1,239 @@
//
// SettingsController.swift
// CapCollector
//
// Created by Christoph on 15.10.18.
// Copyright © 2018 CH. All rights reserved.
//
import UIKit
class SettingsController: UITableViewController {
@IBOutlet weak var totalCapsLabel: UILabel!
@IBOutlet weak var totalImagesLabel: UILabel!
@IBOutlet weak var recognizedCapsLabel: UILabel!
@IBOutlet weak var imagesStatsLabel: UILabel!
@IBOutlet weak var databaseUpdatesLabel: UILabel!
@IBOutlet weak var dropboxAccountLabel: UILabel!
@IBOutlet weak var countsLabel: UILabel!
private var nameFileChanges = false
private var isUpdatingCounts = false
private var isUpdatingThumbnails = false
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .portrait
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool) {
updateDropboxStatus()
updateNameFileStats()
updateDatabaseStats()
(navigationController as! NavigationController).allowLandscape = false
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
setAccessories()
}
private func updateThumbnails() {
isUpdatingThumbnails = true
for cap in Cap.all.values {
cap.makeThumbnail()
}
isUpdatingThumbnails = false
}
private func updateDatabaseStats() {
let totalCaps = Cap.totalCapCount
totalCapsLabel.text = "\(totalCaps) caps"
totalImagesLabel.text = "\(Cap.imageCount) images"
let recognizedCaps = Persistence.recognizedCapCount
let newCapCount = totalCaps - recognizedCaps
recognizedCapsLabel.text = "\(newCapCount) new caps"
let ratio = Float(Cap.imageCount)/Float(Cap.totalCapCount)
let (images, count) = Cap.getCapStatistics().enumerated().first(where: { $0.element != 0 })!
imagesStatsLabel.text = String(format: "%.2f images per cap, lowest count: %d (%dx)", ratio, images, count)
}
private func updateNameFileStats() {
let capCount = Cap.totalCapCount - Persistence.lastUploadedCapCount
let imageCount = Cap.imageCount - Persistence.lastUploadedImageCount
nameFileChanges = capCount > 0 || imageCount > 0
databaseUpdatesLabel.text = "\(capCount) new caps and \(imageCount) new images"
}
private func updateDropboxStatus() {
dropboxAccountLabel.text = DropboxController.shared.isEnabled ? "Sign out" : "Sign in"
}
private func setAccessories() {
tableView.cellForRow(at: IndexPath(row: 0, section: 2))?.accessoryType = Persistence.squeezenet ? .checkmark : .none
tableView.cellForRow(at: IndexPath(row: 1, section: 2))?.accessoryType = Persistence.resnet ? .checkmark : .none
tableView.cellForRow(at: IndexPath(row: 2, section: 2))?.accessoryType = Persistence.xcode ? .checkmark : .none
}
private func toggleClassifier(index: Int) {
switch index {
case 0: Persistence.squeezenet = !Persistence.squeezenet
case 1: Persistence.resnet = !Persistence.resnet
case 2: Persistence.xcode = !Persistence.xcode
default:
return
}
setAccessories()
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
switch indexPath.section {
case 0: // Mosaic
return true
case 1: // Database
return indexPath.row == 2 && nameFileChanges
case 2: // Choose models
return true
case 3: // Refresh
if indexPath.row == 0 {
return !isUpdatingCounts
} else {
return !isUpdatingThumbnails
}
case 4: // Dropbox account
return true
default: return false
}
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
switch indexPath.section {
case 0: // Mosaic
return indexPath
case 1: // Database
return (indexPath.row == 2 && nameFileChanges) ? indexPath : nil
case 2: // Choose models
return indexPath
case 3: // Refresh count
if indexPath.row == 0 {
return isUpdatingCounts ? nil : indexPath
} else {
return isUpdatingThumbnails ? nil : indexPath
}
case 4: // Dropbox account
return indexPath
default: return nil
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 1: // Upload
if indexPath.row == 2 && nameFileChanges {
uploadNameFile()
}
case 2: // Choose models
toggleClassifier(index: indexPath.row)
case 3: // Refresh count
if indexPath.row == 0 {
updateCounts()
} else {
updateThumbnails()
}
default:
break
}
}
private func updateCounts() {
isUpdatingCounts = true
Cap.shouldSave = false
// TODO: Don't make all requests at the same time
DispatchQueue.global(qos: .userInitiated).async {
let list = Cap.all.keys.sorted()
let total = list.count
var finished = 0
let chunks = list.chunked(into: 10)
for chunk in chunks {
self.updateCounts(ids: chunk)
finished += 10
DispatchQueue.main.async {
self.countsLabel.text = "\(finished) / \(total) finished"
}
}
self.isUpdatingCounts = false
Cap.shouldSave = true
DispatchQueue.main.async {
self.countsLabel.text = "Refresh image counts"
self.updateDatabaseStats()
}
}
}
private func updateCounts(ids: [Int]) {
var count = ids.count
let s = DispatchSemaphore(value: 0)
for cap in ids {
Cap.all[cap]!.updateCount { _ in
count -= 1
if count == 0 {
s.signal()
}
}
}
_ = s.wait(timeout: .now() + .seconds(30))
event("Finished updating ids \(ids.first!) to \(ids.last!)")
}
private func uploadNameFile() {
Cap.saveAndUpload() { _ in
self.updateNameFileStats()
}
}
private func toggleDropbox() {
guard !DropboxController.shared.isEnabled else {
DropboxController.shared.signOut()
updateDropboxStatus()
return
}
DropboxController.shared.setup(in: self)
updateDropboxStatus()
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let id = segue.identifier, id == "showMosaic" else {
return
}
(navigationController as! NavigationController).allowLandscape = true
}
}
extension SettingsController: Logger {
static let logToken = "[Settings]"
}
private extension Array {
func chunked(into size: Int) -> [[Element]] {
return stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}

View File

@ -0,0 +1,92 @@
//
// SortController.swift
// CapCollector
//
// Created by Christoph on 12.11.18.
// Copyright © 2018 CH. All rights reserved.
//
import UIKit
enum SortCriteria: Int {
case id = 0
case name = 1
case count = 2
case match = 3
}
protocol SortControllerDelegate {
func sortController(didSelect sortType: SortCriteria, ascending: Bool)
}
class SortController: UITableViewController {
var selected: SortCriteria = .count
var ascending: Bool = true
var delegate: SortControllerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
let height = Cap.hasMatches ? 310 : 270
preferredContentSize = CGSize(width: 200, height: height)
}
private func setCell() {
for i in 0..<4 {
let index = IndexPath(row: i, section: 1)
let cell = tableView.cellForRow(at: index)
cell?.accessoryType = i == selected.rawValue ? .checkmark : .none
}
let index = IndexPath(row: 0, section: 0)
let cell = tableView.cellForRow(at: index)
cell?.accessoryType = ascending ? .checkmark : .none
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
setCell()
}
private func giveFeedback(_ style: UIImpactFeedbackGenerator.FeedbackStyle) {
let generator = UIImpactFeedbackGenerator(style: style)
generator.impactOccurred()
}
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard indexPath.section == 1 else {
ascending = !ascending
setCell()
delegate?.sortController(didSelect: selected, ascending: ascending)
giveFeedback(.light)
return
}
giveFeedback(.medium)
selected = SortCriteria(rawValue: indexPath.row)!
delegate?.sortController(didSelect: selected, ascending: ascending)
self.dismiss(animated: true)
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard indexPath.row == 3 else { return indexPath }
return Cap.hasMatches ? indexPath : nil
}
/*
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller using segue.destination.
// Pass the selected object to the new view controller.
}
*/
}