10

I have an arbitrary CGPath and I'd like to find it's geographic center. I can get the path bounding box with CGPathGetPathBoundingBox and then find the center of that box. But is there a better way to find the center of a path?

Update for those who like to see code: here is code for using the average-of-points method suggested by Adam in the answers (don't miss the even better technique in the answers below)...

    BOOL moved = NO; // the first coord should be a move, the rest add lines
    CGPoint total = CGPointZero;
    for (NSDictionary *coord in [polygon objectForKey:@"coordinates"]) {
        CGPoint point = CGPointMake([(NSNumber *)[coord objectForKey:@"x"] floatValue], 
                                    [(NSNumber *)[coord objectForKey:@"y"] floatValue]);
        if (moved) {
            CGContextAddLineToPoint(context, point.x, point.y);
            // calculate totals of x and y to help find the center later
            // skip the first "move" point since it is repeated at the end in this data
            total.x = total.x + point.x;
            total.y = total.y + point.y;
        } else {
            CGContextMoveToPoint(context, point.x, point.y);
            moved = YES; // we only move once, then we add lines
        }
    }

    // the center is the average of the total points
    CGPoint center = CGPointMake(total.x / ([[polygon objectForKey:@"coordinates"] count]-1), total.y / ([[polygon objectForKey:@"coordinates"] count]-1));

If you have a better idea, please share!

EFC
  • 1,890
  • 18
  • 39

6 Answers6

9

The technique works, but the code you put in the question doesn't. AFAICS, that only works for the few situations where you are doing straight-line polygons ONLY, and you have a list of points, and you haven't made the CGPath object yet.

I needed to do it for arbitrary CGPath objects. Using Adam's (other Adam) suggestion, and Apple's CGPathApply, I came up with this, which works very well:

{
            float dataArray[3] = { 0, 0, 0 };
            CGPathApply( (CGPathRef) YOUR_PATH, dataArray, pathApplierSumCoordinatesOfAllPoints);

            float averageX = dataArray[0] / dataArray[2];
            float averageY = dataArray[1]  / dataArray[2];
            CGPoint centerOfPath = CGPointMake(averageX, averageY);
}

static void pathApplierSumCoordinatesOfAllPoints(void* info, const CGPathElement* element)
{
    float* dataArray = (float*) info;
    float xTotal = dataArray[0];
    float yTotal = dataArray[1];
    float numPoints = dataArray[2];


    switch (element->type)
    {
        case kCGPathElementMoveToPoint:
        {
            /** for a move to, add the single target point only */

            CGPoint p = element->points[0];
            xTotal += p.x;
            yTotal += p.y;
            numPoints += 1.0;

        }
            break;
        case kCGPathElementAddLineToPoint:
        {
            /** for a line to, add the single target point only */

            CGPoint p = element->points[0];
            xTotal += p.x;
            yTotal += p.y;
            numPoints += 1.0;

        }
            break;
        case kCGPathElementAddQuadCurveToPoint:
            for( int i=0; i<2; i++ ) // note: quad has TWO not THREE
            {
                /** for a curve, we add all ppints, including the control poitns */
                CGPoint p = element->points[i];
                xTotal += p.x;
                yTotal += p.y;
                numPoints += 1.0;
            }
            break;
        case kCGPathElementAddCurveToPoint:         
            for( int i=0; i<3; i++ ) // note: cubic has THREE not TWO
            {
                /** for a curve, we add all ppints, including the control poitns */
                CGPoint p = element->points[i];
                xTotal += p.x;
                yTotal += p.y;
                numPoints += 1.0;
            }
            break;
        case kCGPathElementCloseSubpath:
            /** for a close path, do nothing */
            break;
    }

    //NSLog(@"new x=%2.2f, new y=%2.2f, new num=%2.2f", xTotal, yTotal, numPoints);
    dataArray[0] = xTotal;
    dataArray[1] = yTotal;
    dataArray[2] = numPoints;
}
Adam
  • 32,900
  • 16
  • 126
  • 153
  • Adam, thanks for giving this another look. Yes, I was dealing with only straight line polygons where I had all the coordinates. Your solution is more generally useful. – EFC Dec 27 '11 at 02:50
  • This is great! Saved me hours of head banging. – PruitIgoe Jul 01 '13 at 11:26
  • 1
    Its great, but has serious error, kCGPathElementAddCurveToPoint element has only 3 points, not 4 points. – sky1224 Aug 15 '15 at 09:47
  • Good point, fixed. I must have been thinking of "cubic curves have 4 points" which they do - Apple only takes 3 of course because they take your current position as point 1. In 4 years, no-one else seems to have noticed it - cubic curves seem to be rare in CGPath! – Adam Aug 18 '15 at 00:29
6

For me, the simple average of all points in the path did not suffice for some of the polygons I was dealing with.

I implemented it using the area (see wikipedia, Centroid of polygon and Paul Bourke's page). It might not be the most efficient implementation but it works for me.

Note that it only works for closed, non-intersecting polygons. The vertices are assumed to be numbered in order of their occurrence along the polygon's perimeter, and the last point is assumed to be the same as the first point.

CGPoint GetCenterPointOfCGPath (CGPathRef aPath)
{
    // Convert path to an array
    NSMutableArray* a = [NSMutableArray new];
    CGPathApply(aPath, (__bridge void *)(a), convertToListOfPoints);
    return centroid(a);
}

static void convertToListOfPoints(void* info, const CGPathElement* element)
{
    NSMutableArray* a = (__bridge NSMutableArray*) info;

    switch (element->type)
    {
        case kCGPathElementMoveToPoint:
        {
            [a addObject:[NSValue valueWithCGPoint:element->points[0]]];
        }
        break;
        case kCGPathElementAddLineToPoint:
        {
            [a addObject:[NSValue valueWithCGPoint:element->points[0]]];
        }
        break;
        case kCGPathElementAddQuadCurveToPoint:
        {
            for (int i=0; i<2; i++)
                [a addObject:[NSValue valueWithCGPoint:element->points[i]]];
        }
        break;
        case kCGPathElementAddCurveToPoint:
        {
            for (int i=0; i<3; i++)
                [a addObject:[NSValue valueWithCGPoint:element->points[i]]];
        }
        break;
        case kCGPathElementCloseSubpath:
        break;
    }
}

double polygonArea(NSMutableArray* points) {
    int i,j;
    double area = 0;
    int N = [points count];

    for (i=0;i<N;i++) {
        j = (i + 1) % N;
        CGPoint pi =  [(NSValue*)[points objectAtIndex:i] CGPointValue];
        CGPoint pj =  [(NSValue*)[points objectAtIndex:j] CGPointValue];
        area += pi.x * pj.y;
        area -= pi.y * pj.x;
    }

    area /= 2;
    return area;
}

CGPoint centroid(NSMutableArray* points) {
    double cx = 0, cy = 0;
    double area = polygonArea(points);

    int i, j, n = [points count];

    double factor = 0;
    for (i = 0; i < n; i++) {
        j = (i + 1) % n;
        CGPoint pi =  [(NSValue*)[points objectAtIndex:i] CGPointValue];
        CGPoint pj =  [(NSValue*)[points objectAtIndex:j] CGPointValue];
        factor = (pi.x * pj.y - pj.x * pi.y);
        cx += (pi.x + pj.x) * factor;
        cy += (pi.y + pj.y) * factor;
    }

    cx *= 1 / (6.0f * area);
    cy *= 1 / (6.0f * area);

    return CGPointMake(cx, cy);
}
masam
  • 2,268
  • 1
  • 22
  • 24
  • I copied and pasted this code and it seems to return the wrong centroid – etayluz Jul 01 '15 at 05:42
  • @etayluz Could you be a bit more specific? Are you sure that you are using a closed polygon? Also note that the above code is not a simple average of all points in the CGPath as I also indicated in the answer. – masam Jul 06 '15 at 08:14
  • There's a bug here, `kCGPathElementAddCurveToPoint` has 3 points, not 4. – yonilevy Feb 28 '19 at 21:15
  • Thanks @yonilevy - that was indeed a typo. – masam Mar 11 '19 at 12:43
2

Does the simple average of all x and all y for the points in the path give the point you want? Calculate one value for x and one for y. I made a quick sketch and this method gave a believable answer.

See wikipedia, finding the centroid of a finite set of points.

If not you may need to first find the area - see Paul Bourke's page.

Adam Eberbach
  • 12,309
  • 6
  • 62
  • 114
  • Very nice! Getting the average point was much better than the center of the bounds and very easy to pull off. Thanks! – EFC Aug 25 '11 at 14:29
1

Updated Adam's answer to swift4 version:

extension CGPath {
    func findCenter() -> CGPoint {
        class Context {
            var sumX: CGFloat = 0
            var sumY: CGFloat = 0
            var points = 0
        }

        var context = Context()

        apply(info: &context) { (context, element) in
            guard let context = context?.assumingMemoryBound(to: Context.self).pointee else {
                return
            }
            switch element.pointee.type {
            case .moveToPoint, .addLineToPoint:
                let point = element.pointee.points[0]
                context.sumX += point.x
                context.sumY += point.y
                context.points += 1
            case .addQuadCurveToPoint:
                let controlPoint = element.pointee.points[0]
                let point = element.pointee.points[1]
                context.sumX += point.x + controlPoint.x
                context.sumY += point.y + controlPoint.y
                context.points += 2
            case .addCurveToPoint:
                let controlPoint1 = element.pointee.points[0]
                let controlPoint2 = element.pointee.points[1]
                let point = element.pointee.points[2]
                context.sumX += point.x + controlPoint1.x + controlPoint2.x
                context.sumY += point.y + controlPoint1.y + controlPoint2.y
                context.points += 3
            case .closeSubpath:
                break
            }
        }

        return CGPoint(x: context.sumX / CGFloat(context.points),
                y: context.sumY / CGFloat(context.points))
    }
}

But be careful, CGPath may have extra move commands that will break this logic because of point count

Kirow
  • 1,077
  • 12
  • 25
-1

Here is the centroid, come get it:

-(CLLocationCoordinate2D)getCentroidFor:(GMSMutablePath *)rect
{
  CLLocationCoordinate2D coord = [rect coordinateAtIndex:0];
  double minX = coord.longitude;
  double maxX = coord.longitude;
  double minY = coord.latitude;
  double maxY = coord.latitude;
  for (int i = 1; i < rect.count; i++)
  {
    CLLocationCoordinate2D coord = [rect coordinateAtIndex:i];
    if (minX > coord.longitude)
      minX = coord.longitude;
    if (maxX < coord.longitude)
      maxX = coord.longitude;
    if (minY > coord.latitude)
      minY = coord.latitude;
    if (maxY < coord.latitude)
      maxY = coord.latitude;
  }

  CLLocationDegrees centerX = minX + ((maxX - minX) / 2);
  CLLocationDegrees centerY = minY + ((maxY - minY) / 2); 
    return CLLocationCoordinate2DMake(centerY, centerX);
}
etayluz
  • 15,920
  • 23
  • 106
  • 151
-3

Find the bounding box of the CGPath and take the center of it.

CGRect boundingBox = CGPathGetBoundingBox(my_path); my_center_point = ccp(boundingBox.origin.x+boundingBox.size.width/2, boundingBox.origin.y+boundingBox.size.height/2);

jjpp
  • 1,298
  • 1
  • 15
  • 31
  • As I said in the statement of the problem, finding the center of the bounding box does NOT provide a good solution in this case. – EFC Oct 01 '13 at 21:52