Rotate Data Frame and Zoom to Minimum Bounded Feature

Discussion created by rfairhur24 on Aug 23, 2012
I wanted to volunteer some code that may help some of those doing python coding.  I have created a python script using arcpy and the portions of the Minimum Bounding Rectangle code by Dan Patterson that can take a selected feature (or a portion of a road as a feature) and Rotate and Zoom the Data Frame so that the feature is at its optimum angle and centered completely within its minimum rectangle extent.  I found that the order of operations involved:

1. Get the minimum bounding rectangle and it centeriod coordinate.

2. Get and set the rotation angle of Data Frame, using the rectangle angle.  The angle needs to be adjusted to ensure that North never drops below the horizontal plain of the map when the Data Frame is rotated.

3. Determine the optimum scale in the longer plain.  For a Data Frame that is wider by 2.5 over its height, I found a scale that is equal to 88% of the length of the long axis of the rectangle resulted in a nice margin on the sides of the map.  When a minimum bounding rectangle has a width that exceeds that adjusted length I use that distance as the scale.

4. Set the Extent coordinates centered on the rectangle centroid and position the coordinates so that X and Y spread equal the length and width of the minimum bounding rectangle.

5 Repeat the steps 3 and 4 six more times.  The Extent refuses to set correctly the first time except for rectanlges perfectly compass oriented in their native postions so that they match the Data Frame's longest plane.  The repetition results in a successive approximation that gradually moves the Data Frame center to the centroid of the feature.  (At about the 6th time through it seems fine, but I went to 7 to be perfect).

The end result is like the screen shot attached.

At the moment the code is overly long, since I have not optimized it (banged my head for quite some time to figure out that the repetition was essential to the process.)

A main portion of the code is below:

import sys, os, math
import arcpy
import arcpy_helper
reload(arcpy_helper)    #uncomment if editing arcpy_helper  Got it from Dan Patterson's code.
    import convexHull_helper, geom_helper, smallestCircle_helper, groupPnts_helper
    arcpy.AddMessage("\n  The helper scripts:" +
                "\n  convexHull_helper, geom_helper, arcpy_helper, smallestCircle_helper " +
                "\n  groupPnts_helper are not located in the same folder as the main" +
                "\n  script and the toolbox OR one of the scripts is being edited." +
                "\n" + arcpy.GetMessages())
# get a feature with a cursor.  Code not shown.
desc = arcpy.Describe
SR = desc(studySegmentLyr).spatialReference
# Check the shape type and get the geometry field
theType = desc(studySegmentLyr).shapeType
shape_field = desc(studySegmentLyr).shapeFieldName
minN = 3
dx = None
dy = None
angle = None
rotation = 90
rows = arcpy.SearchCursor(studySegmentLyr)
for row in rows:
    aShape = row.getValue(shape_field)    #get the shape from the shape field
    pnts = arcpy_helper.shapeToPoints(aShape, theType, arcpy)  
    if len(pnts) < minN:
        arcpy.AddMessage("Ignoring this set.  You need at least " + str(minN) + " valid points")
        returnedList = convexHull_helper.convexHull(pnts)
        if len(returnedList)!=3:
            arcpy.AddMessage("No hull returned")
            hull = returnedList[0]
            theReturned = geom_helper.minRect(list(hull))
            aRect = theReturned[0]      #rectangle
            arcpy.AddMessage("Here is the rect = " + str(aRect))
            rectAngle = theReturned[1]  #angle
            arcpy.AddMessage("Here is the rectAngle = " + str(rectAngle))
            dx = min(theReturned[2], theReturned[3])  #width and axis length
            arcpy.AddMessage("Here is dx = " + str(dx))
            dy = max(theReturned[2], theReturned[3])
            arcpy.AddMessage("Here is dy = " + str(dy))
            p12dist = geom_helper.dxdyHypot(aRect[0], aRect[1])
            p23dist = geom_helper.dxdyHypot(aRect[1], aRect[2])
            if p12dist >= p23dist:
                pnts = ([aRect[0], aRect[1]])
                pnts = ([aRect[1], aRect[2]])
            Xs = ([aRect[0][0], aRect[1][0], aRect[2][0], aRect[3][0]])
            rectXCenter = Xs[3] - ((Xs[3] - Xs[0])/2)
            arcpy.AddMessage("Here is Xmin,Xmax,XCenter = " + str(Xs[0]) + "; " + str(Xs[3]) + "; " + str(rectXCenter))
            Ys = ([aRect[0][1], aRect[1][1], aRect[2][1], aRect[3][1]])
            rectYCenter = Ys[3] - ((Ys[3] - Ys[0])/2)
            arcpy.AddMessage("Here is Ymin,Ymax,YCenter = " + str(Ys[0]) + "; " + str(Ys[3]) + "; " + str(rectYCenter))
            arcpy.AddMessage("Try to set extent")
            newExtent = colldf.extent
            shapeextent = aShape.extent
            angle = geom_helper.p1p2Azimuth(pnts[0], pnts[1])
            if angle < 0:
                rotation = angle + 360
            elif angle < 90:
                rotation = angle + 270
            elif angle < 180:
                rotation = angle - 90
            elif angle < 270:
                rotation = angle + 90
            elif angle < 360:
                rotation = angle - 270
            elif angle >= 360:
                rotation = angle - 360
            for df in arcpy.mapping.ListDataFrames(mxd):
                df.rotation = rotation
                arcpy.AddMessage("Rotation = " + str(rotation))

The code below is then repeated 7 times:

             if dy * 0.88 >= dx * 1.78:
                 colldf.scale = dy * .88 # 0.88 factor worked. Adjust to taste
                 colldf.scale = dx * 1.78 # 1.78 factor worked. Adjust to taste
             newExtent = colldf.extent
             dfwidth = newExtent.XMax - newExtent.XMin
             dfheight = newExtent.YMax - newExtent.YMin
             newExtent.XMin, newExtent.YMin, newExtent.XMax, newExtent.YMax = (rectXCenter - dfwidth/2), (rectYCenter - dfheight/2), (rectXCenter + dfwidth/2), (rectYCenter + dfheight/2)
             colldf.extent = newExtent