Mueva la leyenda si superpone entidades dentro del marco de datos usando ArcPy

8

Tratando de encontrar una manera programática (arcpy) de mover la leyenda si intercepta características dentro de un marco de datos, en el escenario a continuación, si la leyenda oscurece la vista del AOI, entonces quiero que se mueva a una esquina diferente hasta que no sea un problema. Esto tiene que estar encima del marco de datos en lugar de hacer que el marco de datos sea más pequeño y simplemente ponerlo a un lado.

ingrese la descripción de la imagen aquí

Slevy
fuente
1
Si está utilizando páginas controladas por datos, puede encontrar ayuda en esto: gis.stackexchange.com/questions/167975/… . En términos más generales, buscaría en Google algo como "mover leyenda en páginas controladas por datos" para obtener más sugerencias. Con leyendas fijas, las convertí en una imagen y utilicé lo siguiente para moverlas: support.esri.com/en/technical-article/000011951 Ninguna de estas son respuestas, solo soluciones.
John
Sí, actualmente estoy usando páginas controladas por datos, gracias por el enlace Johns
Slevy el

Respuestas:

7

Entradas: ingrese la descripción de la imagen aquí 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()

SALIDA: ingrese la descripción de la imagen aquí

NOTAS: Para cada página en las páginas controladas por datos, el script intenta encontrar suficiente espacio en las esquinas del marco de datos para colocar la Leyenda (llamada myLegend) sin cubrir ninguna capa de entidad visible. El script usa mallas para identificar celdas de esquina. La dimensión de la celda es ligeramente mayor que la dimensión Leyenda en las unidades de vista de datos. La celda de la esquina es la que comparte un límite con 3 vecinos. Si no se encuentran esquinas o sala, Legend se colocó fuera del marco de datos en la página de diseño.

Lamentablemente, no sé cómo administrar la consulta de definición de página. Los puntos mostrados estaban originalmente dispersos por toda la extensión RECTANGLE, y algunos de ellos no tenían asociación con las páginas. Arcpy todavía ve la capa completa, aunque apliqué la consulta de definición (coincidencia) a los puntos.

FelixIP
fuente
Gracias por la excelente redacción de este Felix, aunque tengo problemas para implementar esta solución para que funcione de manera tan fluida como su ejemplo, tan detallado como esto, ¿hay algo que deba tener en cuenta al crear el documento de mapa, los puntos de anclaje de la leyenda, etc. ?
Slevy
1
Los puntos de ancla son inferiores a la izquierda tanto para la leyenda como para el marco de datos. ¿Cómo olvidé esto?
FelixIP
Sí, definitivamente hizo una diferencia en la prueba aquí. Si quisiera cambiar el punto de anclaje al medio (para el marco de datos), ¿supongo que toda la lógica está fuera de control? qué parte necesitaría configurar. ¿Solo líneas 33 a 44?
Slevy
1
Calcule xmin y xmax a través del ancho y la posición x. Similar con el eje y. No estoy seguro de por qué lo necesitas ...
FelixIP
Parte de otro flujo de trabajo, gracias Felix, gran paso adelante aquí
Slevy
3

La forma en que haría esto sería crear una clase de entidad "elemento de leyenda" que represente su elemento de leyenda en el mismo sistema de coordenadas que esas entidades.

De esa manera, podría usar Seleccionar capa por ubicación para probar si su elemento de leyenda se superpone con alguna característica y moverlo si lo hace.

No es trivial pero es eminentemente factible y hay un Q&A en este sitio (¿ Convertir el punto XY en unidades de página XY usando arcpy? ) Que podría usarse para resolver la parte más difícil de la conversión entre las coordenadas de la página y el mapa.

PolyGeo
fuente
1
La parte más difícil es encontrar una brecha lo suficientemente grande como para caber en el cuadro de leyenda.
FelixIP
1
@FelixIP ¿Por qué es así? Parece que el autor de la pregunta se limita a probar solo cuatro esquinas del marco de datos. Supongo que tienen una regla de lo que sucede si no hay esquina adecuada.
PolyGeo
Creo que este es el camino a seguir, aunque la brecha en la leyenda probablemente será el menor de mis problemas. Idealmente, la escala del mapa continuará cambiando hasta que la leyenda no intercepte el polígono de interés. ¡Aunque me gustaría escuchar o ver algunos ejemplos prácticos que la gente ha intentado!
Slevy
2

A continuación se muestra el código que he usado para mover leyendas y mapas insertados para no ocultar datos. Usted preguntó acerca de la función de verificación de intersección en otro hilo. Esta es mi implementación del código de otra persona. No recuerdo exactamente de dónde es. Creo que fue un guión para mover un mapa insertado de un estado en Nueva Inglaterra.

El recuadro es el identificador de la leyenda o elemento del mapa insertado.

#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

Entonces, el siguiente código de esta publicación ( Páginas controladas por datos con leyenda móvil / mapa de inserción ) debería tener más sentido.

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
CSB
fuente
1
debería mencionar: en este ejemplo, el elemento está anclado en la parte inferior izquierda.
CSB
Gracias CSB, sí, para mi caso, necesito que el marco de datos esté anclado en el medio, así que solo estoy en el proceso de personalizar su fórmula de extensiones de origen de página, publicaré el ejemplo una vez que llegue allí. De lo contrario, parece muy prometedor en las pruebas iniciales. Además, hay una referencia a "unobscured_lyr", suponiendo que se haga referencia fuera de la secuencia de comandos como la capa que se debe evitar.
Slevy
correcto, "unobscured_lyr" es el que estamos tratando de no cubrir. por supuesto, también puedes hacer que funcione con múltiples capas.
CSB