Mover legenda se ela sobrepuser recursos no dataframe usando o ArcPy

8

Tentando encontrar uma maneira programaticamente (arcpy) de mover a legenda se ela interceptar recursos dentro de um quadro de dados, no cenário abaixo, se a legenda obscurecer a visão da AOI, quero que ela se mova para um canto diferente até que não seja um questão. Isso deve estar no topo do quadro de dados, em vez de torná-lo menor e colocá-lo de lado.

insira a descrição da imagem aqui

Slevy
fonte
1
Se você estiver usando Páginas Orientadas a Dados, poderá encontrar alguma ajuda nisto: gis.stackexchange.com/questions/167975/… . Em geral, eu pesquisaria no Google algo como "mover legenda nas páginas orientadas a dados" para obter mais sugestões. Com legendas fixas, converti-as em uma imagem e usei o seguinte para movê-las: support.esri.com/en/technical-article/000011951 Nada disso é resposta, apenas solução alternativa.
johns
Sim, atualmente estou usando páginas controladas por dados, obrigado pelo link Johns
Slevy 13/17

Respostas:

7

Entradas: insira a descrição da imagem aqui 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()

RESULTADO: insira a descrição da imagem aqui

OBSERVAÇÕES: Para cada página nas páginas controladas por dados, o script tenta encontrar espaço suficiente nos cantos do quadro de dados para colocar o Legend (chamado myLegend) sem cobrir nenhuma camada de recurso visível. Script usa arrastão para identificar células de canto. A dimensão da célula é um pouco maior que a dimensão da legenda nas unidades de exibição de dados. A célula de canto é a que compartilha um limite com 3 vizinhos. Se nenhum canto ou sala for encontrado, o Legend é colocado fora do quadro de dados na página de layout.

Infelizmente, não sei como gerenciar a consulta de definição de página. Os pontos mostrados foram originalmente espalhados por toda a extensão RECTANGLE, com alguns deles sem associação com as páginas. O Arcpy ainda vê a camada inteira, embora eu tenha aplicado a consulta de definição (correspondência) aos pontos.

FelixIP
fonte
Obrigado pela excelente redação deste Felix, embora eu esteja tendo problemas para implementar esta solução para funcionar com a fluidez de seu exemplo, por mais detalhado que seja, existe alguma coisa que eu deva estar ciente ao criar o documento do mapa, pontos de ancoragem da legenda etc. ?
Slevy 18/09/19
1
Os pontos de ancoragem estão no canto inferior esquerdo da legenda e do quadro de dados. Como eu esqueci isso?
FelixIP # 18/17
Sim, definitivamente fez a diferença no teste aqui. Se eu quisesse mudar o ponto de ancoragem para o meio (para o quadro de dados), estou assumindo que toda a lógica está fora de sintonia? qual parte eu precisaria configurar. Apenas as linhas 33 a 44?
Slevy 18/09/19
1
Calcule xmin e xmax através da largura e posição x. Semelhante ao eixo y. Não sei por que você precisa dele ...
FelixIP
Parte de um outro fluxo de trabalho, graças Felix, grande passo em frente aqui
Slevy
3

A maneira que eu faria isso seria criar uma classe de recurso "elemento de legenda" que represente seu elemento de legenda no mesmo sistema de coordenadas que esses recursos.

Dessa forma, você pode usar Selecionar camada por local para testar se o seu elemento de legenda se sobrepõe a algum recurso e movê-lo, se houver.

É não trivial, mas eminentemente realizável e há uma sessão de perguntas e respostas neste site ( converter o ponto XY em unidades de página XY usando arcpy? ) Que poderia ser usado para resolver a parte mais difícil da conversão entre as coordenadas da página e do mapa.

PolyGeo
fonte
1
A parte mais difícil é encontrar uma lacuna grande o suficiente para caber na caixa da legenda.
FelixIP
1
@FelixIP Por quê? Parece que o autor da pergunta está se restringindo apenas a testar quatro cantos do quadro de dados. Presumo que eles tenham uma regra para o que acontece se nenhum canto for adequado.
PolyGeo
Acho que esse é o caminho a seguir, embora a lacuna na lenda provavelmente seja o menor dos meus problemas. Idealmente, a escala do mapa continuará a mudar até que a legenda não intercepte o polígono de interesse. Embora gostaria de ouvir ou ver alguns exemplos práticos que as pessoas tentaram!
Slevy 13/09/17
2

Abaixo está o código que eu usei para mover legendas e inserir mapas para não obscurecer os dados. Você perguntou sobre a função de interseção de verificação em outro segmento. Esta é a minha implementação do código de outra pessoa. Não me lembro exatamente de onde é. Foi um roteiro para mover um mapa inserido para um estado na Nova Inglaterra, eu acho.

inset é a alça do elemento de legenda ou mapa de inserção.

#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

Em seguida, o código abaixo desta postagem ( Páginas orientadas a dados com legenda móvel / mapa de inserção ) deve fazer mais 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
fonte
1
deve mencionar: neste exemplo, o elemento está ancorado no canto inferior esquerdo.
CSB 20/09
Obrigado CSB, Sim, para o meu caso, preciso que o quadro de dados seja ancorado no meio, por isso estou apenas no processo de personalizar sua fórmula de extensões de Origem da página, postarei o exemplo assim que chegar lá. Caso contrário, parecerá muito promissor nos testes iniciais. Além disso, há referência ao "unobscured_lyr", assumindo que isso seja referenciado fora do script como a camada a ser evitada?
Slevy 21/09
correto, o "unobscured_lyr" é o que estamos tentando não cobrir. é claro, você também pode fazê-lo funcionar com várias camadas.
CSB 21/09