Last active
July 31, 2019 03:52
-
-
Save sam-w/e3258027b1b41a266f6ddb91c2465591 to your computer and use it in GitHub Desktop.
2D Search Pattern
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SearchPattern2D.swift | |
// MapScan | |
// | |
// Created by Sam Warner on 31/7/19. | |
// Copyright © 2019 LoungeBuddy. All rights reserved. | |
// | |
import UIKit | |
precedencegroup XorPrecedence { | |
lowerThan: LogicalDisjunctionPrecedence | |
higherThan: DefaultPrecedence | |
associativity: left | |
assignment: false | |
} | |
infix operator ^^: XorPrecedence | |
private func ^^ (left: Bool, right: Bool) -> Bool { | |
return left != right | |
} | |
private extension CGRect { | |
init(centeredAt center: CGPoint, width: CGFloat, height: CGFloat) { | |
self.init(x: center.x - width / 2, y: center.y - width / 2, width: width, height: height) | |
} | |
} | |
struct SearchPattern2D<T: SignedNumeric & Strideable> { | |
struct Coordinates { | |
let x: T | |
let y: T | |
} | |
let path: [Coordinates] | |
private static func wind(from path: [Coordinates], from depth: T = 0, to finalDepth: T) -> [Coordinates] { | |
let origin = path.last! | |
let translate = (depth * 2) | |
guard depth < finalDepth else { | |
return path + walk(from: origin, x: 0, y: translate) | |
} | |
let down = walk(from: origin, x: 0, y: translate + 1) | |
let right = walk(from: down.last!, x: translate + 1, y: 0) | |
let up = walk(from: right.last!, x: 0, y: -(translate + 2)) | |
let left = walk(from: up.last!, x: -(translate + 2), y: 0) | |
return wind(from: path + down + right + up + left, from: depth + 1, to: finalDepth) | |
} | |
private static func walk(from origin: Coordinates, x: T, y: T) -> [Coordinates] { | |
precondition(x == 0 ^^ y == 0) | |
return walk(from: origin, to: Coordinates(x: origin.x + x, y: origin.y + y)) | |
} | |
private static func walk(from: Coordinates, to: Coordinates) -> [Coordinates] { | |
precondition(from.x == to.x ^^ from.y == to.y) | |
let walk = stride(from: from.x, through: to.x, by: to.x > from.x ? 1 : -1).flatMap { x in | |
stride(from: from.y, through: to.y, by: to.y > from.y ? 1 : -1).map { y in | |
Coordinates(x: x, y: y) | |
} | |
} | |
return Array(walk.dropFirst()) | |
} | |
static func unwinding(from center: (x: T, y: T), maxDisplacement: T, stepSize: T) -> SearchPattern2D { | |
let path = wind(from: [Coordinates(x: center.x, y: center.y)], to: maxDisplacement) | |
.map { Coordinates(x: $0.x * stepSize, y: $0.y * stepSize) } | |
return .init(path: path) | |
} | |
static func horizontalScan(from center: (x: T, y: T), maxDisplacement: T, stepSize: T.Stride) -> SearchPattern2D { | |
let path = stride(from: center.y - maxDisplacement, through: center.y + maxDisplacement, by: stepSize).flatMap { y in | |
stride(from: center.x - maxDisplacement, through: center.x + maxDisplacement, by: stepSize).map { x in | |
Coordinates(x: x, y: y) | |
} | |
} | |
return .init(path: path) | |
} | |
static func verticalScan(from center: (x: T, y: T), maxDisplacement: T, stepSize: T.Stride) -> SearchPattern2D { | |
let path = stride(from: center.x - maxDisplacement, through: center.x + maxDisplacement, by: stepSize).flatMap { x in | |
stride(from: center.y - maxDisplacement, through: center.y + maxDisplacement, by: stepSize).map { y in | |
Coordinates(x: x, y: y) | |
} | |
} | |
return .init(path: path) | |
} | |
} | |
extension SearchPattern2D where T == CGFloat { | |
enum RenderError: Swift.Error { | |
case emptyPath | |
} | |
func render() throws -> UIImage { | |
let targetScale: CGFloat = 60 | |
let padding: CGFloat = 20 | |
let convertedPath = path.map { CGPoint(x: $0.x, y: $0.y) } | |
let xs = Set(convertedPath.map { $0.x }) | |
let ys = Set(convertedPath.map { $0.y }) | |
guard let minX = xs.min(), let maxX = xs.max(), let minY = ys.min(), let maxY = ys.max() else { | |
throw RenderError.emptyPath | |
} | |
let xStep: CGFloat = (maxX - minX) / (CGFloat(xs.count) - 1) | |
let xScale = targetScale / xStep | |
let yStep: CGFloat = (maxY - minY) / (CGFloat(ys.count) - 1) | |
let yScale = targetScale / yStep | |
let bounds = CGRect( | |
x: 0, | |
y: 0, | |
width: ((maxX - minX) * xScale) + (padding * 2), | |
height: ((maxY - minY) * yScale) + (padding * 2) | |
) | |
let scaledPath = convertedPath.map { CGPoint(x: $0.x * xScale, y: $0.y * yScale) } | |
guard let start = scaledPath.first, let end = scaledPath.last else { | |
throw RenderError.emptyPath | |
} | |
let renderer = UIGraphicsImageRenderer(bounds: bounds) | |
return renderer.image { ctx in | |
ctx.cgContext.translateBy( | |
x: (-minX * xScale) + padding, | |
y: (-minY * yScale) + padding | |
) | |
ctx.cgContext.setLineWidth(3) | |
ctx.cgContext.setLineCap(.round) | |
ctx.cgContext.setLineJoin(.round) | |
ctx.cgContext.setStrokeColor(UIColor.lightGray.cgColor) | |
ctx.cgContext.addLines(between: scaledPath) | |
ctx.cgContext.drawPath(using: .stroke) | |
ctx.cgContext.setFillColor(UIColor.green.cgColor) | |
ctx.cgContext.addEllipse(in: | |
CGRect(centeredAt: start, width: 8, height: 8) | |
) | |
ctx.cgContext.fillPath() | |
ctx.cgContext.setFillColor(UIColor.red.cgColor) | |
ctx.cgContext.addEllipse(in: | |
CGRect(centeredAt: end, width: 8, height: 8) | |
) | |
ctx.cgContext.fillPath() | |
ctx.cgContext.setFillColor(UIColor.blue.cgColor) | |
scaledPath.dropFirst().dropLast().forEach { | |
ctx.cgContext.addEllipse(in: | |
CGRect(centeredAt: $0, width: 8, height: 8) | |
) | |
} | |
ctx.cgContext.fillPath() | |
} | |
} | |
} | |
extension SearchPattern2D where T: BinaryInteger { | |
func render() throws -> UIImage { | |
return try SearchPattern2D<CGFloat>(path: | |
self.path.map { .init(x: CGFloat($0.x), y: CGFloat($0.y)) } | |
).render() | |
} | |
} | |
extension SearchPattern2D where T == Float { | |
func render() throws -> UIImage { | |
return try SearchPattern2D<CGFloat>(path: | |
self.path.map { .init(x: CGFloat($0.x), y: CGFloat($0.y)) } | |
).render() | |
} | |
} | |
extension SearchPattern2D where T == Double { | |
func render() throws -> UIImage { | |
return try SearchPattern2D<CGFloat>(path: | |
self.path.map { .init(x: CGFloat($0.x), y: CGFloat($0.y)) } | |
).render() | |
} | |
} | |
let pattern = SearchPattern2D<Double>.unwinding(from: .init(x: 500, y: 500), maxDisplacement: 2, stepSize: 5) | |
try! pattern.render() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment