Skip to content

Instantly share code, notes, and snippets.

@sam-w
Last active July 31, 2019 03:52
Show Gist options
  • Save sam-w/e3258027b1b41a266f6ddb91c2465591 to your computer and use it in GitHub Desktop.
Save sam-w/e3258027b1b41a266f6ddb91c2465591 to your computer and use it in GitHub Desktop.
2D Search Pattern
//
// 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