mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
242 lines
7.7 KiB
Swift
242 lines
7.7 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
private let pointsCount: Int = 64
|
|
private let squareSize: Double = 250.0
|
|
private let diagonal = sqrt(squareSize * squareSize + squareSize * squareSize)
|
|
private let halfDiagonal = diagonal * 0.5
|
|
private let angleRange: Double = .pi / 4.0
|
|
private let anglePrecision: Double = .pi / 90.0
|
|
|
|
class Unistroke {
|
|
let points: [CGPoint]
|
|
|
|
init(points: [CGPoint]) {
|
|
var points = resample(points: points, totalPoints: pointsCount)
|
|
let radians = indicativeAngle(points: points)
|
|
points = rotate(points: points, byRadians: -radians)
|
|
points = scale(points: points, toSize: squareSize)
|
|
points = translate(points: points, to: .zero)
|
|
self.points = points
|
|
}
|
|
|
|
func match(templates: [UnistrokeTemplate], minThreshold: Double = 0.8) -> String? {
|
|
var bestDistance = Double.infinity
|
|
var bestTemplate: UnistrokeTemplate?
|
|
for template in templates {
|
|
let templateDistance = distanceAtBestAngle(points: self.points, strokeTemplate: template.points, fromAngle: -angleRange, toAngle: angleRange, threshold: anglePrecision)
|
|
if templateDistance < bestDistance {
|
|
bestDistance = templateDistance
|
|
bestTemplate = template
|
|
}
|
|
}
|
|
|
|
if let bestTemplate = bestTemplate {
|
|
bestDistance = 1.0 - bestDistance / halfDiagonal
|
|
if bestDistance < minThreshold {
|
|
return nil
|
|
}
|
|
return bestTemplate.name
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
class UnistrokeTemplate : Unistroke {
|
|
var name: String
|
|
|
|
init(name: String, points: [CGPoint]) {
|
|
self.name = name
|
|
super.init(points: points)
|
|
}
|
|
}
|
|
|
|
private struct Edge {
|
|
var minX: Double
|
|
var minY: Double
|
|
var maxX: Double
|
|
var maxY: Double
|
|
|
|
init(minX: Double, maxX: Double, minY: Double, maxY: Double) {
|
|
self.minX = minX
|
|
self.minY = minY
|
|
self.maxX = maxX
|
|
self.maxY = maxY
|
|
}
|
|
|
|
mutating func addPoint(value: CGPoint) {
|
|
self.minX = min(self.minX,value.x)
|
|
self.maxX = max(self.maxX,value.x)
|
|
self.minY = min(self.minY,value.y)
|
|
self.maxY = max(self.maxY,value.y)
|
|
}
|
|
|
|
}
|
|
|
|
private extension Double {
|
|
func toRadians() -> Double {
|
|
let res = self * .pi / 180.0
|
|
return res
|
|
}
|
|
}
|
|
|
|
private func resample(points: [CGPoint], totalPoints: Int) -> [CGPoint] {
|
|
var initialPoints = points
|
|
let interval = pathLength(points: initialPoints) / Double(totalPoints - 1)
|
|
var totalLength: Double = 0.0
|
|
var newPoints: [CGPoint] = [points[0]]
|
|
for i in 1 ..< initialPoints.count {
|
|
let currentLength = initialPoints[i - 1].distance(to: initialPoints[i])
|
|
if totalLength + currentLength >= interval {
|
|
let newPoint = CGPoint(
|
|
x: initialPoints[i - 1].x + ((interval - totalLength) / currentLength) * (initialPoints[i].x - initialPoints[i - 1].x),
|
|
y: initialPoints[i - 1].y + ((interval - totalLength) / currentLength) * (initialPoints[i].y - initialPoints[i - 1].y)
|
|
)
|
|
newPoints.append(newPoint)
|
|
initialPoints.insert(newPoint, at: i)
|
|
totalLength = 0.0
|
|
} else {
|
|
totalLength += currentLength
|
|
}
|
|
}
|
|
if newPoints.count == totalPoints - 1 {
|
|
newPoints.append(points.last!)
|
|
}
|
|
return newPoints
|
|
}
|
|
|
|
private func pathLength(points: [CGPoint]) -> Double {
|
|
var distance: Double = 0.0
|
|
for index in 1 ..< points.count {
|
|
distance += points[index - 1].distance(to: points[index])
|
|
}
|
|
return distance
|
|
}
|
|
|
|
private func pathDistance(path1: [CGPoint], path2: [CGPoint]) -> Double {
|
|
var d: Double = 0.0
|
|
for idx in 0 ..< min(path1.count, path2.count) {
|
|
d += path1[idx].distance(to: path2[idx])
|
|
}
|
|
return d / Double(path1.count)
|
|
}
|
|
|
|
private func centroid(points: [CGPoint]) -> CGPoint {
|
|
var centroidPoint: CGPoint = .zero
|
|
for point in points {
|
|
centroidPoint.x = centroidPoint.x + point.x
|
|
centroidPoint.y = centroidPoint.y + point.y
|
|
}
|
|
centroidPoint.x = (centroidPoint.x / Double(points.count))
|
|
centroidPoint.y = (centroidPoint.y / Double(points.count))
|
|
return centroidPoint
|
|
}
|
|
|
|
private func boundingBox(points: [CGPoint]) -> CGRect {
|
|
var edge = Edge(minX: +Double.infinity, maxX: -Double.infinity, minY: +Double.infinity, maxY: -Double.infinity)
|
|
for point in points {
|
|
edge.addPoint(value: point)
|
|
}
|
|
return CGRect(x: edge.minX, y: edge.minY, width: (edge.maxX - edge.minX), height: (edge.maxY - edge.minY))
|
|
}
|
|
|
|
private func rotate(points: [CGPoint], byRadians radians: Double) -> [CGPoint] {
|
|
let centroid = centroid(points: points)
|
|
let cosinus = cos(radians)
|
|
let sinus = sin(radians)
|
|
var result: [CGPoint] = []
|
|
for point in points {
|
|
result.append(
|
|
CGPoint(
|
|
x: (point.x - centroid.x) * cosinus - (point.y - centroid.y) * sinus + centroid.x,
|
|
y: (point.x - centroid.x) * sinus + (point.y - centroid.y) * cosinus + centroid.y
|
|
)
|
|
)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private func scale(points: [CGPoint], toSize size: Double) -> [CGPoint] {
|
|
let boundingBox = boundingBox(points: points)
|
|
var result: [CGPoint] = []
|
|
for point in points {
|
|
result.append(
|
|
CGPoint(
|
|
x: point.x * (size / boundingBox.width),
|
|
y: point.y * (size / boundingBox.height)
|
|
)
|
|
)
|
|
}
|
|
return result
|
|
}
|
|
|
|
private func translate(points: [CGPoint], to pt: CGPoint) -> [CGPoint] {
|
|
let centroidPoint = centroid(points: points)
|
|
var newPoints: [CGPoint] = []
|
|
for point in points {
|
|
newPoints.append(
|
|
CGPoint(
|
|
x: point.x + pt.x - centroidPoint.x,
|
|
y: point.y + pt.y - centroidPoint.y
|
|
)
|
|
)
|
|
}
|
|
return newPoints
|
|
}
|
|
|
|
private func vectorize(points: [CGPoint]) -> [Double] {
|
|
var sum: Double = 0.0
|
|
var vector: [Double] = []
|
|
for point in points {
|
|
vector.append(point.x)
|
|
vector.append(point.y)
|
|
sum += (point.x * point.x) + (point.y * point.y)
|
|
}
|
|
let magnitude = sqrt(sum)
|
|
for i in 0 ..< vector.count {
|
|
vector[i] = vector[i] / magnitude
|
|
}
|
|
return vector
|
|
}
|
|
|
|
private func indicativeAngle(points: [CGPoint]) -> Double {
|
|
let centroid = centroid(points: points)
|
|
return atan2(centroid.y - points[0].y, centroid.x - points[0].x)
|
|
}
|
|
|
|
private func distanceAtBestAngle(points: [CGPoint], strokeTemplate: [CGPoint], fromAngle: Double, toAngle: Double, threshold: Double) -> Double {
|
|
func distanceAtAngle(points: [CGPoint], strokeTemplate: [CGPoint], radians: Double) -> Double {
|
|
let rotatedPoints = rotate(points: points, byRadians: radians)
|
|
return pathDistance(path1: rotatedPoints, path2: strokeTemplate)
|
|
}
|
|
|
|
let phi: Double = (0.5 * (-1.0 + sqrt(5.0)))
|
|
|
|
var fromAngle = fromAngle
|
|
var toAngle = toAngle
|
|
|
|
var x1 = phi * fromAngle + (1.0 - phi) * toAngle
|
|
var f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
|
|
|
|
var x2 = (1.0 - phi) * fromAngle + phi * toAngle
|
|
var f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
|
|
|
|
while abs(toAngle - fromAngle) > threshold {
|
|
if f1 < f2 {
|
|
toAngle = x2
|
|
x2 = x1
|
|
f2 = f1
|
|
x1 = phi * fromAngle + (1.0 - phi) * toAngle
|
|
f1 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x1)
|
|
} else {
|
|
fromAngle = x1
|
|
x1 = x2
|
|
f1 = f2
|
|
x2 = (1.0 - phi) * fromAngle + phi * toAngle
|
|
f2 = distanceAtAngle(points: points, strokeTemplate: strokeTemplate, radians: x2)
|
|
}
|
|
}
|
|
return min(f1, f2)
|
|
}
|