0

I am trying to calculate the area of a polygon in Swift and it seems that it cannot calculate the area of it correctly. So i tried it with this code to show you that it calculate the same area - even if one of this two areas should only have 75% of the other one:


    let tmp_size = 100
    var path = CGMutablePath()
    path.move(to: CGPoint(x: 0, y: 0))
    path.addLine(to: CGPoint(x: 0, y: tmp_size / 2  ))
    path.addLine(to: CGPoint(x: tmp_size / 2, y: tmp_size / 2))
    path.addLine(to: CGPoint(x: tmp_size / 2, y: tmp_size   ))
    path.addLine(to: CGPoint(x: tmp_size    , y: tmp_size   ))
    path.addLine(to: CGPoint(x: tmp_size    , y: 0      ))
    path.closeSubpath()
    var pb = SKPhysicsBody(polygonFrom: path)
    print("AREA: \(pb.area * pow(150, 2))")

    path = CGMutablePath()
    path.move(to: CGPoint(x: 0, y: 0))
    path.addLine(to: CGPoint(x: 0, y: tmp_size))
    path.addLine(to: CGPoint(x: tmp_size, y: tmp_size))
    path.addLine(to: CGPoint(x: tmp_size, y: 0))
    path.closeSubpath()
    pb = SKPhysicsBody(polygonFrom: path)
    print("AREA2: \(pb.area * pow(150, 2))")

Output:

AREA: 10000.000409781933
AREA2: 10000.00074505806

Is this a known bug?

Anyway, i need a function (ideally in swift, but all other languages are welcome) of how to calculate an area of a polygon. Any help would be appreciated!

if this makes things easier: my polygon has only 90-degree-angles, so i could split it into rectangles (if i would know how to do it)

Many thanks for your help!

ma2412
  • 135
  • 7

2 Answers2

2

Solution for iOS12.4 / XCode 10.3 / Swift 5:

First, add this extension into you code, to extract the CGPoints of the path into an array:

extension CGPath {
    func forEach( body: @escaping @convention(block) (CGPathElement) -> Void) {
        typealias Body = @convention(block) (CGPathElement) -> Void
        let callback: @convention(c) (UnsafeMutableRawPointer, UnsafePointer<CGPathElement>) -> Void = { (info, element) in
            let body = unsafeBitCast(info, to: Body.self)
            body(element.pointee)
        }
        //print(MemoryLayout.size(ofValue: body))
        let unsafeBody = unsafeBitCast(body, to: UnsafeMutableRawPointer.self)
        self.apply(info: unsafeBody, function: unsafeBitCast(callback, to: CGPathApplierFunction.self))
    }
    func getPathElementsPoints() -> [CGPoint] {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
            default: break
            }
        }
        return arrayPoints
    }
    func getPathElementsPointsAndTypes() -> ([CGPoint],[CGPathElementType]) {
        var arrayPoints : [CGPoint]! = [CGPoint]()
        var arrayTypes : [CGPathElementType]! = [CGPathElementType]()
        self.forEach { element in
            switch (element.type) {
            case CGPathElementType.moveToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addLineToPoint:
                arrayPoints.append(element.points[0])
                arrayTypes.append(element.type)
            case .addQuadCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            case .addCurveToPoint:
                arrayPoints.append(element.points[0])
                arrayPoints.append(element.points[1])
                arrayPoints.append(element.points[2])
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
                arrayTypes.append(element.type)
            default: break
            }
        }
        return (arrayPoints,arrayTypes)
    }
}

source: How to get the CGPoint(s) of a CGPath (answer from user Alessandro Ornano; swift 5.x Version)

Second, my example code is:

let tmp_size = 100

    var path = CGMutablePath()
    path.move(to: CGPoint(x: 0, y: 0))
    path.addLine(to: CGPoint(x: 0, y: tmp_size / 2  ))
    path.addLine(to: CGPoint(x: tmp_size / 2, y: tmp_size / 2))
    path.addLine(to: CGPoint(x: tmp_size / 2, y: tmp_size   ))
    path.addLine(to: CGPoint(x: tmp_size    , y: tmp_size   ))
    path.addLine(to: CGPoint(x: tmp_size    , y: 0      ))
    path.closeSubpath()
    var pb = SKPhysicsBody(polygonFrom: path)

    print("area1: \(getAreaFromPolygon(path: path))")


    path = CGMutablePath()
    path.move(to: CGPoint(x: 0, y: 0))
    path.addLine(to: CGPoint(x: 0, y: tmp_size))
    path.addLine(to: CGPoint(x: tmp_size, y: tmp_size))
    path.addLine(to: CGPoint(x: tmp_size, y: 0))
    path.closeSubpath()
    pb = SKPhysicsBody(polygonFrom: path)

    print("area2: \(getAreaFromPolygon(path: path))")

And last, my function to calculate the area of the polygon (in CGPoints):

func getAreaFromPolygon (path: CGMutablePath) -> Double {
    // get an array of the CGPoints of this path
    let PathArray = path.getPathElementsPoints()

    // store all CGPoint.x into an array        
    var xArray = [Int]()
    for PosX in PathArray {
        xArray.append(Int(PosX.x))
    }

    // store all CGPoints.y into an array
    var yArray = [Int]()
    for PosY in PathArray {
        yArray.append(Int(PosY.y))
    }

    // calculate the area
    var area = 0.0
    for i in 0..<PathArray.count {
        let firstPart = Double((yArray[i] + yArray[(i+1) % PathArray.count]))
        let secondPart = (xArray[i] - xArray[(i+1) % PathArray.count])
        area += Double(firstPart) * Double(secondPart)
    }
    area = abs(area) / 2.0
    return area
}

source: https://de.wikipedia.org/wiki/Gaußsche_Trapezformel#Programmcode

(was in Java, but i translated it into Swift 5)

note: i divided the formula into two sections (fristPart and secondPart) since it was too complex for Xcode to check

btw, my output is:

area1: 7500.0
area2: 10000.0

which is correct, since area1 ist 75% from the area2.

P.S.: i only have polygons of rectangles, but it should also work with any polygons (except maybe polygons with intersecting lines)

ma2412
  • 135
  • 7
2

This is a supplement to ma2412's answer for efficiency.

He is using what is called the shoelace formula.

The code has been modified to remove needless looping and the modulus

func getAreaFromPolygon (path: CGMutablePath) -> Double {
    // get an array of the CGPoints of this path
    var Array = path.getPathElementsPoints()
    Array.append(Array[0])
    // calculate the area
    var area = 0.0
    for i in 0..<Array.count - 1 {
        let firstPart = Double(Array[i].y + Array[i+1].y)
        let secondPart = Double(Array[i].x - Array[i+1].x)
        area += firstPart * secondPart
    }
    return (abs(area) / 2.0)
}
Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44
  • shouldn't it be "for i in 0.. – ma2412 Sep 16 '19 at 19:22
  • No, if you notice the array, I inject element zero to the end of the chain to avoid the mod – Knight0fDragon Sep 16 '19 at 19:23
  • Mod is generally “slow”, because it has to do division to get the remainder (there are exceptions with constants). It is usually a good idea to try and avoid it if you are in a situation when you are looping. – Knight0fDragon Sep 16 '19 at 19:32
  • avoiding "slow" is always good! func getAreaFromPolygon (path: CGMutablePath) -> Double { // get an array of the CGPoints of this path var Array = path.getPathElementsPoints() Array.append(Array[0]) // calculate the area var area = 0.0 for i in 0.. – ma2412 Sep 16 '19 at 19:38
  • I would leave firstPart and secondPart as variables. The optimizer should be smart enough to remove the unnecessary allocation. Do not sacrifice code readability for trivial optimization – Knight0fDragon Sep 16 '19 at 20:00