Search code examples
iosarraysswiftcgpoint

How to get a centerpoint from an array of cgpoints?


I have an array of CGPoints:

let points = [(1234.0, 1053.0), (1241.0, 1111.0), (1152.0, 1043.0)]

what I'm trying to do is find the center of the CGPoints. So I can place an object at the center of all the points. If this was an array of lets say integers I would reduce the array like this:

points.reduce(0, +)

and then divide by the total array count to get the average. But since its CGPoints this does not work. Any ideas on how to achieve this?


Solution

  • Farhad has answered the question about how to average all of these data points. (+1)

    But is that really what you want? Consider:

    this image

    This convex shape is defined by the blue points, but the mean of all of those points is the red point. But that’s not really in the center of the shape. It is skewed up because there are five data points near the top, and only two down below. This convex shape illustrates the problem well, but it doesn’t need to be convex to manifest this issue. Any situation where the data points are not relatively evenly distributed can manifest this behavior.

    The green point is the centroid of the polygon. You can see that it falls below the center of the bounding box (the crosshairs in the above image), like you’d expect. For simple shapes, that might be a better place to place your label. That can be calculated as follows:

    extension Array where Element == CGPoint {
        /// Calculate signed area.
        ///
        /// See https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
        ///
        /// - Returns: The signed area
    
        func signedArea() -> CGFloat {
            if isEmpty { return .zero }
    
            var sum: CGFloat = 0
            for (index, point) in enumerated() {
                let nextPoint: CGPoint
                if index < count-1 {
                    nextPoint = self[index+1]
                } else {
                    nextPoint = self[0]
                }
    
                sum += point.x * nextPoint.y - nextPoint.x * point.y
            }
    
            return sum / 2
        }
    
        /// Calculate centroid
        ///
        /// See https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
        ///
        /// - Note: If the area of the polygon is zero (e.g. the points are collinear), this returns `nil`.
        ///
        /// - Parameter points: Unclosed points of polygon.
        /// - Returns: Centroid point.
    
        func centroid() -> CGPoint? {
            if isEmpty { return nil }
    
            let area = signedArea()
            if area == 0 { return nil }
    
            var sumPoint: CGPoint = .zero
    
            for (index, point) in enumerated() {
                let nextPoint: CGPoint
                if index < count-1 {
                    nextPoint = self[index+1]
                } else {
                    nextPoint = self[0]
                }
    
                let factor = point.x * nextPoint.y - nextPoint.x * point.y
                sumPoint.x += (point.x + nextPoint.x) * factor
                sumPoint.y += (point.y + nextPoint.y) * factor
            }
    
            return sumPoint / 6 / area
        }
    
        func mean() -> CGPoint? {
            if isEmpty { return nil }
    
            return reduce(.zero, +) / CGFloat(count)
        }
    }
    
    extension CGPoint {
        static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
            CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
        }
    
        static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
            CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
        }
    
        static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
            CGPoint(x: lhs.x / rhs, y: lhs.y / rhs)
        }
    
        static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
            CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
        }
    }
    

    And you’d calculate the centroid like so:

    let points = [(1234.0, 1053.0), (1241.0, 1111.0), (1152.0, 1043.0)]
        .map(CGPoint.init)
    
    guard let point = points.centroid() else { return }
    

    FWIW, with complex, concave shapes, even the centroid is not optimal. See What is the fastest way to find the "visual" center of an irregularly shaped polygon?