Move legend if it overlaps features within dataframe using ArcPy
Inputs: Script:
import arcpy, traceback, os, sys, time
from arcpy import env
import numpy as np
env.overwriteOutput = True
outFolder=arcpy.GetParameterAsText(0)
env.workspace = outFolder
dpi=2000
tempf=r'in_memory\many'
sj=r'in_memory\sj'
## ERROR HANDLING
def showPyMessage():
arcpy.AddMessage(str(time.ctime()) + " - " + message)
try:
mxd = arcpy.mapping.MapDocument("CURRENT")
allLayers=arcpy.mapping.ListLayers(mxd,"*")
ddp = mxd.dataDrivenPages
df = arcpy.mapping.ListDataFrames(mxd)[0]
SR = df.spatialReference
## GET LEGEND ELEMENT
legendElm = arcpy.mapping.ListLayoutElements(mxd, "LEGEND_ELEMENT", "myLegend")[0]
# GET PAGES INFO
thePagesLayer = arcpy.mapping.ListLayers(mxd,ddp.indexLayer.name)[0]
fld = ddp.pageNameField.name
# SHUFFLE THROUGH PAGES
for pageID in range(1, ddp.pageCount+1):
ddp.currentPageID = pageID
aPage=ddp.pageRow.getValue(fld)
arcpy.RefreshActiveView()
## DEFINE WIDTH OF legend IN MAP UNITS..
E=df.extent
xmin=df.elementPositionX;xmax=xmin+df.elementWidth
x=[xmin,xmax];y=[E.XMin,E.XMax]
aX,bX=np.polyfit(x, y, 1)
w=aX*legendElm.elementWidth
## and COMPUTE NUMBER OF ROWS FOR FISHNET
nRows=(E.XMax-E.XMin)//w
## DEFINE HEIGHT OF legend IN MAP UNITS
ymin=df.elementPositionY;ymax=ymin+df.elementHeight
x=[ymin,ymax];y=[E.YMin,E.YMax]
aY,bY=np.polyfit(x, y, 1)
h=aY*legendElm.elementHeight
## and COMPUTE NUMBER OF COLUMNS FOR FISHNET
nCols=(E.YMax-E.YMin)//h
## CREATE FISHNET WITH SLIGHTLY BIGGER CELLS (due to different aspect ratio between legend and dataframe)
origPoint='%s %s' %(E.XMin,E.YMin)
yPoint='%s %s' %(E.XMin,E.YMax)
endPoint='%s %s' %(E.XMax,E.YMax)
arcpy.CreateFishnet_management(tempf, origPoint,yPoint,
"0", "0", nCols, nRows,endPoint,
"NO_LABELS", "", "POLYGON")
arcpy.DefineProjection_management(tempf, SR)
## CHECK CORNER CELLS ONLY
arcpy.SpatialJoin_analysis(tempf, tempf, sj, "JOIN_ONE_TO_ONE",
match_option="SHARE_A_LINE_SEGMENT_WITH")
nCorners=0
with arcpy.da.SearchCursor(sj, ("Shape@","Join_Count")) as cursor:
for shp, neighbours in cursor:
if neighbours!=3:continue
nCorners+=1; N=0
for lyr in allLayers:
if not lyr.visible:continue
if lyr.isGroupLayer:continue
if not lyr.isFeatureLayer:continue
## CHECK IF THERE ARE FEATURES INSIDE CORNER CELL
arcpy.Clip_analysis(lyr, shp, tempf)
result=arcpy.GetCount_management(tempf)
n=int(result.getOutput(0))
N+=n
if n>0: break
## IF NONE, CELL FOUND; COMPUTE PAGE COORDINATES FOR LEGEND AND BREAK
if N==0:
tempRaster=outFolder+os.sep+aPage+".png"
e=shp.extent;X=e.XMin;Y=e.YMin
x=(X-bX)/aX;y=(Y-bY)/aY
break
if nCorners==0: N=1
## IF NO CELL FOUND PLACE LEGEND OUTSIDE DATAFRAME
if N>0:
x=df.elementPositionX+df.elementWidth
y=df.elementPositionY
legendElm.elementPositionY=y
legendElm.elementPositionX=x
outFile=outFolder+os.sep+aPage+".png"
arcpy.AddMessage(outFile)
arcpy.mapping.ExportToPNG(mxd,outFile)
except:
message = "\n*** PYTHON ERRORS *** "; showPyMessage()
message = "Python Traceback Info: " + traceback.format_tb(sys.exc_info()[2])[0]; showPyMessage()
message = "Python Error Info: " + str(sys.exc_type)+ ": " + str(sys.exc_value) + "\n"; showPyMessage()
OUTPUT:
NOTES: For each page in data driven pages script attempts to find enough room in dataframe corners to place Legend (called myLegend) without covering any visible feature layer. Script uses fishnet to identify corner cells. Cell dimension is slightly greater than Legend dimension in data view units. Corner cell is the one that shares a boundary with 3 neighbours. If no corners or room found, Legend placed outside dataframe on layout page.
Unfortunately I don't know how manage page definition query. Points shown were originally scattered all around RECTANGLE extent, with some of them having no association with pages. Arcpy still sees entire layer, although I applied definition query (match) to the points.
The way that I would do this would be to create a "legend element" feature class that represents your legend element in the same coordinate system as those features.
That way you could use Select Layer By Location to test whether your legend element overlaps with any features, and move it if it does.
Its non-trivial but eminently doable and there is a Q&A on this site (Convert point XY to page units XY using arcpy?) that could be used to work out the hardest part of converting between page and map coordinates.
Below is code I've used to move legends and inset maps so as not to obscure data. You asked about the check intersect function on another thread. This is my implementation of someone else's code. I don't recall exactly where it's from. It was a script to move an inset map for a state in New England I think.
inset is the handle for the legend or inset map element.
#check intersect function
def checkIntersect(MovableObject):
#get absolute x and y disatnce of MovableObject in page units
PageOriginDistX = (inset.elementPositionX + inset.elementWidth) - DataFrame.elementPositionX #Xmax in page units
PageOriginDistY = (inset.elementPositionY + inset.elementHeight) - DataFrame.elementPositionY #absolute y disatnce of element
#Generate x/y pairs for new tempfile used to test intersection of original MovableObject placement
Xmax = DataFrame.extent.XMin + ((DataFrame.extent.XMax - DataFrame.extent.XMin) *
(PageOriginDistX / DataFrame.elementWidth))
Xmin = DataFrame.extent.XMin + ((DataFrame.extent.XMax - DataFrame.extent.XMin) *
((inset.elementPositionX - DataFrame.elementPositionX) / DataFrame.elementWidth))
Ymax = DataFrame.extent.YMin + ((DataFrame.extent.YMax - DataFrame.extent.YMin) *
(PageOriginDistY / DataFrame.elementHeight))
Ymin = DataFrame.extent.YMin + ((DataFrame.extent.YMax - DataFrame.extent.YMin) *
((inset.elementPositionY - DataFrame.elementPositionY) / DataFrame.elementHeight))
#list of coords for temp polygon
coordList = [[[Xmax,Ymax], [Xmax,Ymin], [Xmin,Ymin], [Xmin,Ymax]]]
#create empty temp poly as tempShape, give it a spatial ref, make it into a featureclass so it works
#with intersect
tempShape = os.path.join(sys.path[0], "temp.shp")
arcpy.CreateFeatureclass_management(sys.path[0], "temp.shp","POLYGON")
array = arcpy.Array()
point = arcpy.Point()
featureList = []
arcpy.env.overwriteOutput = True
for feature in coordList:
for coordPair in feature:
point.X = coordPair[0]
point.Y = coordPair[1]
array.add(point)
array.add(array.getObject(0))
polygon = arcpy.Polygon(array)
array.removeAll()
featureList.append(polygon)
arcpy.CopyFeatures_management(featureList, tempShape)
arcpy.MakeFeatureLayer_management(tempShape, "tempShape_lyr")
#check for intersect
arcpy.SelectLayerByLocation_management("unobscured_lyr", "INTERSECT", "tempShape_lyr", "", "NEW_SELECTION")
#initiate search and count
polyCursor = arcpy.SearchCursor("unobscured_lyr")
polyRow = polyCursor.next()
count = 0
#Clear Selection
arcpy.SelectLayerByAttribute_management("unobscured_lyr","CLEAR_SELECTION")
#Delete the temporary shapefile.
arcpy.Delete_management(tempShape)
#count
while polyRow:
count = count + 1
polyRow = polyCursor.next()
#Clear Selection
arcpy.SelectLayerByAttribute_management("unobscured_lyr","CLEAR_SELECTION")
#Delete the temporary shapefile.
arcpy.Delete_management(tempShape)
#Return the count value to main part of script to determine placement of locator map.
return count
Then, the code below from this post (Data Driven Pages with Movable Legend/Inset Map) should make more sense.
for pageNum in range(1, mxd.dataDrivenPages.pageCount + 1):
#setup naming and path for output maps
path = mxd.filePath
bn = os.path.basename(path)[:-4]
mxd.dataDrivenPages.currentPageID = pageNum
insetDefaultX = inset.elementPositionX
insetDefaultY = inset.elementPositionY
#check defualt position for intersect
intersect = checkIntersect(inset)
if intersect == 0: #if it doesn't intersect, print the map
arcpy.mapping.ExportToEPS(mxd, exportFolder + "\\" + bn + "_"+ str(pageNum) + ".eps", "Page_Layout",640,480,300,"BETTER","RGB",3,"ADAPTIVE","RASTERIZE_BITMAP",True,False)
else: #intersect != 0: #move inset to SE corner
inset.elementPositionX = (DataFrame.elementPositionX + DataFrame.elementWidth) - inset.elementWidth
inset.elementPositionY = DataFrame.elementPositionY