mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
309 lines
9.6 KiB
Swift
309 lines
9.6 KiB
Swift
import Foundation
|
|
|
|
private func intersect(seg1: [CGPoint], seg2: [CGPoint]) -> Bool {
|
|
func ccw(_ seg1: CGPoint, _ seg2: CGPoint, _ seg3: CGPoint) -> Bool {
|
|
let ccw = ((seg3.y - seg1.y) * (seg2.x - seg1.x)) - ((seg2.y - seg1.y) * (seg3.x - seg1.x))
|
|
return ccw > 0 ? true : ccw < 0 ? false : true
|
|
}
|
|
let segment1 = seg1[0]
|
|
let segment2 = seg1[1]
|
|
let segment3 = seg2[0]
|
|
let segment4 = seg2[1]
|
|
return ccw(segment1, segment3, segment4) != ccw(segment2, segment3, segment4)
|
|
&& ccw(segment1, segment2, segment3) != ccw(segment1, segment2, segment4)
|
|
}
|
|
|
|
private func convex(points: [CGPoint]) -> [CGPoint] {
|
|
func cross(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
|
|
return (a.x - o.x) * (b.y - o.y) - (a.y - o.y) * (b.x - o.x)
|
|
}
|
|
|
|
func upperTangent(_ points: [CGPoint]) -> [CGPoint] {
|
|
var lower: [CGPoint] = []
|
|
for point in points {
|
|
while lower.count >= 2 && (cross(lower[lower.count - 2], lower[lower.count - 1], point) <= 0) {
|
|
_ = lower.popLast()
|
|
}
|
|
lower.append(point)
|
|
}
|
|
_ = lower.popLast()
|
|
return lower
|
|
}
|
|
|
|
func lowerTangent(_ points: [CGPoint]) -> [CGPoint] {
|
|
let reversed = points.reversed()
|
|
var upper: [CGPoint] = []
|
|
for point in reversed {
|
|
while upper.count >= 2 && (cross(upper[upper.count - 2], upper[upper.count - 1], point) <= 0) {
|
|
_ = upper.popLast()
|
|
}
|
|
upper.append(point)
|
|
}
|
|
_ = upper.popLast()
|
|
return upper
|
|
}
|
|
|
|
var convex: [CGPoint] = []
|
|
convex.append(contentsOf: upperTangent(points))
|
|
convex.append(contentsOf: lowerTangent(points))
|
|
return convex
|
|
}
|
|
|
|
private class Grid {
|
|
var cells = [Int: [Int: [CGPoint]]]()
|
|
var cellSize: Double = 0
|
|
|
|
init(_ points: [CGPoint], _ cellSize: Double) {
|
|
self.cellSize = cellSize
|
|
for point in points {
|
|
let cellXY = point2CellXY(point)
|
|
let x = cellXY[0]
|
|
let y = cellXY[1]
|
|
if self.cells[x] == nil {
|
|
self.cells[x] = [Int: [CGPoint]]()
|
|
}
|
|
if self.cells[x]![y] == nil {
|
|
self.cells[x]![y] = [CGPoint]()
|
|
}
|
|
self.cells[x]![y]!.append(point)
|
|
}
|
|
}
|
|
|
|
func point2CellXY(_ point: CGPoint) -> [Int] {
|
|
let x = Int(point.x / self.cellSize)
|
|
let y = Int(point.y / self.cellSize)
|
|
return [x, y]
|
|
}
|
|
|
|
func extendBbox(_ bbox: [Double], _ scaleFactor: Double) -> [Double] {
|
|
return [
|
|
bbox[0] - (scaleFactor * self.cellSize),
|
|
bbox[1] - (scaleFactor * self.cellSize),
|
|
bbox[2] + (scaleFactor * self.cellSize),
|
|
bbox[3] + (scaleFactor * self.cellSize)
|
|
]
|
|
}
|
|
|
|
func removePoint(_ point: CGPoint) {
|
|
let cellXY = point2CellXY(point)
|
|
let cell = self.cells[cellXY[0]]![cellXY[1]]!
|
|
var pointIdxInCell = 0
|
|
for idx in 0 ..< cell.count {
|
|
if cell[idx].x == point.x && cell[idx].y == point.y {
|
|
pointIdxInCell = idx
|
|
break
|
|
}
|
|
}
|
|
self.cells[cellXY[0]]![cellXY[1]]!.remove(at: pointIdxInCell)
|
|
}
|
|
|
|
func rangePoints(_ bbox: [Double]) -> [CGPoint] {
|
|
let tlCellXY = point2CellXY(CGPoint(x: bbox[0], y: bbox[1]))
|
|
let brCellXY = point2CellXY(CGPoint(x: bbox[2], y: bbox[3]))
|
|
var points: [CGPoint] = []
|
|
for x in tlCellXY[0]..<brCellXY[0]+1 {
|
|
for y in tlCellXY[1]..<brCellXY[1]+1 {
|
|
points += cellPoints(x, y)
|
|
}
|
|
}
|
|
return points
|
|
}
|
|
|
|
func cellPoints(_ xAbs: Int, _ yOrd: Int) -> [CGPoint] {
|
|
if let x = self.cells[xAbs] {
|
|
if let y = x[yOrd] {
|
|
return y
|
|
}
|
|
}
|
|
return []
|
|
}
|
|
|
|
}
|
|
|
|
private let maxConcaveAngleCos = cos(90.0 / (180.0 / Double.pi))
|
|
|
|
private func filterDuplicates(_ pointSet: [CGPoint]) -> [CGPoint] {
|
|
let sortedSet = sortByX(pointSet)
|
|
return sortedSet.filter { (point: CGPoint) -> Bool in
|
|
let index = pointSet.firstIndex(where: {(idx: CGPoint) -> Bool in
|
|
return idx.x == point.x && idx.y == point.y
|
|
})
|
|
if index == 0 {
|
|
return true
|
|
} else {
|
|
let prevEl = pointSet[index! - 1]
|
|
if prevEl.x != point.x || prevEl.y != point.y {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private func sortByX(_ pointSet: [CGPoint]) -> [CGPoint] {
|
|
return pointSet.sorted(by: { (lhs, rhs) -> Bool in
|
|
if lhs.x == rhs.x {
|
|
return lhs.y < rhs.y
|
|
} else {
|
|
return lhs.x < rhs.x
|
|
}
|
|
})
|
|
}
|
|
|
|
private func squaredLength(_ a: CGPoint, _ b: CGPoint) -> Double {
|
|
return pow(b.x - a.x, 2) + pow(b.y - a.y, 2)
|
|
}
|
|
|
|
private func cosFunc(_ o: CGPoint, _ a: CGPoint, _ b: CGPoint) -> Double {
|
|
let aShifted = [a.x - o.x, a.y - o.y]
|
|
let bShifted = [b.x - o.x, b.y - o.y]
|
|
let sqALen = squaredLength(o, a)
|
|
let sqBLen = squaredLength(o, b)
|
|
let dot = aShifted[0] * bShifted[0] + aShifted[1] * bShifted[1]
|
|
return dot / sqrt(sqALen * sqBLen)
|
|
}
|
|
|
|
private func intersectFunc(_ segment: [CGPoint], _ pointSet: [CGPoint]) -> Bool {
|
|
for idx in 0..<pointSet.count - 1 {
|
|
let seg = [pointSet[idx], pointSet[idx + 1]]
|
|
if segment[0].x == seg[0].x && segment[0].y == seg[0].y ||
|
|
segment[0].x == seg[1].x && segment[0].y == seg[1].y {
|
|
continue
|
|
}
|
|
if intersect(seg1: segment, seg2: seg) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private func occupiedAreaFunc(_ points: [CGPoint]) -> CGPoint {
|
|
var minX = Double.infinity
|
|
var minY = Double.infinity
|
|
var maxX = -Double.infinity
|
|
var maxY = -Double.infinity
|
|
for idx in 0 ..< points.reversed().count {
|
|
if points[idx].x < minX {
|
|
minX = points[idx].x
|
|
}
|
|
if points[idx].y < minY {
|
|
minY = points[idx].y
|
|
}
|
|
if points[idx].x > maxX {
|
|
maxX = points[idx].x
|
|
}
|
|
if points[idx].y > maxY {
|
|
maxY = points[idx].y
|
|
}
|
|
}
|
|
return CGPoint(x: maxX - minX, y: maxY - minY)
|
|
}
|
|
|
|
private func bBoxAroundFunc(_ edge: [CGPoint]) -> [Double] {
|
|
return [min(edge[0].x, edge[1].x),
|
|
min(edge[0].y, edge[1].y),
|
|
max(edge[0].x, edge[1].x),
|
|
max(edge[0].y, edge[1].y)]
|
|
}
|
|
|
|
private func midPointFunc(_ edge: [CGPoint], _ innerPoints: [CGPoint], _ convex: [CGPoint]) -> CGPoint? {
|
|
var point: CGPoint?
|
|
var angle1Cos = maxConcaveAngleCos
|
|
var angle2Cos = maxConcaveAngleCos
|
|
var a1Cos: Double = 0
|
|
var a2Cos: Double = 0
|
|
for innerPoint in innerPoints {
|
|
a1Cos = cosFunc(edge[0], edge[1], innerPoint)
|
|
a2Cos = cosFunc(edge[1], edge[0], innerPoint)
|
|
if a1Cos > angle1Cos &&
|
|
a2Cos > angle2Cos &&
|
|
!intersectFunc([edge[0], innerPoint], convex) &&
|
|
!intersectFunc([edge[1], innerPoint], convex) {
|
|
angle1Cos = a1Cos
|
|
angle2Cos = a2Cos
|
|
point = innerPoint
|
|
}
|
|
}
|
|
return point
|
|
}
|
|
|
|
private func concaveFunc(_ convex: inout [CGPoint], _ maxSqEdgeLen: Double, _ maxSearchArea: [Double], _ grid: Grid, _ edgeSkipList: inout [String: Bool]) -> [CGPoint] {
|
|
var edge: [CGPoint]
|
|
var keyInSkipList: String = ""
|
|
var scaleFactor: Double
|
|
var midPoint: CGPoint?
|
|
var bBoxAround: [Double]
|
|
var bBoxWidth: Double = 0
|
|
var bBoxHeight: Double = 0
|
|
var midPointInserted: Bool = false
|
|
|
|
for idx in 0..<convex.count - 1 {
|
|
edge = [convex[idx], convex[idx+1]]
|
|
keyInSkipList = edge[0].key.appending(", ").appending(edge[1].key)
|
|
|
|
scaleFactor = 0
|
|
bBoxAround = bBoxAroundFunc(edge)
|
|
|
|
if squaredLength(edge[0], edge[1]) < maxSqEdgeLen || edgeSkipList[keyInSkipList] == true {
|
|
continue
|
|
}
|
|
|
|
repeat {
|
|
bBoxAround = grid.extendBbox(bBoxAround, scaleFactor)
|
|
bBoxWidth = bBoxAround[2] - bBoxAround[0]
|
|
bBoxHeight = bBoxAround[3] - bBoxAround[1]
|
|
midPoint = midPointFunc(edge, grid.rangePoints(bBoxAround), convex)
|
|
scaleFactor += 1
|
|
} while midPoint == nil && (maxSearchArea[0] > bBoxWidth || maxSearchArea[1] > bBoxHeight)
|
|
|
|
if bBoxWidth >= maxSearchArea[0] && bBoxHeight >= maxSearchArea[1] {
|
|
edgeSkipList[keyInSkipList] = true
|
|
}
|
|
if let midPoint = midPoint {
|
|
convex.insert(midPoint, at: idx + 1)
|
|
grid.removePoint(midPoint)
|
|
midPointInserted = true
|
|
}
|
|
}
|
|
|
|
if midPointInserted {
|
|
return concaveFunc(&convex, maxSqEdgeLen, maxSearchArea, grid, &edgeSkipList)
|
|
}
|
|
|
|
return convex
|
|
}
|
|
|
|
private extension CGPoint {
|
|
var key: String {
|
|
return "\(self.x),\(self.y)"
|
|
}
|
|
}
|
|
|
|
func getHull(_ points: [CGPoint], concavity: Double) -> [CGPoint] {
|
|
let points = filterDuplicates(points)
|
|
let occupiedArea = occupiedAreaFunc(points)
|
|
let maxSearchArea: [Double] = [
|
|
occupiedArea.x * 0.6,
|
|
occupiedArea.y * 0.6
|
|
]
|
|
|
|
var convex = convex(points: points)
|
|
|
|
var innerPoints = points.filter { (point: CGPoint) -> Bool in
|
|
let idx = convex.firstIndex(where: { (idx: CGPoint) -> Bool in
|
|
return idx.x == point.x && idx.y == point.y
|
|
})
|
|
return idx == nil
|
|
}
|
|
|
|
innerPoints.sort(by: { (lhs: CGPoint, rhs: CGPoint) -> Bool in
|
|
return lhs.x == rhs.x ? lhs.y > rhs.y : lhs.x > rhs.x
|
|
})
|
|
|
|
let cellSize = ceil(occupiedArea.x * occupiedArea.y / Double(points.count))
|
|
let grid = Grid(innerPoints, cellSize)
|
|
|
|
var skipList: [String: Bool] = [String: Bool]()
|
|
return concaveFunc(&convex, pow(concavity, 2), maxSearchArea, grid, &skipList)
|
|
}
|