Select to view content in your preferred language

Inset Maps using ArcPy

363
1
05-29-2024 10:10 AM
YahyaMasri
Emerging Contributor

Hello I'm working on automating the process of making maps using ArcPy and an issue I'm running into is trying to make inset maps. I've successfully used ArcPy to find clusters of locations using DBSCAN, but I want to make an inset map, along with an extent indicator using ArcPy. Is this possible?

0 Kudos
1 Reply
Brian_Wilson
Honored Contributor

I just did a project with 3 inset maps. The project has four maps, "Main Map", "Overview 1".. etc

There is one layout, and it has frames for the main map and each overview map.

There is a script tool in the projects toolbox that I run to (1) set up the layout. I am also using a map series, so the script tool tries to pull in the current map series page as the default but you can enter the name for a page. When I run it, it changes the contents of the overviews to highlight and set the extent.

Some of the maps don't need all three overviews so on maps that don't need all of them the script makes those frame elements invisible.

Execution code

 

"""
    This is the code that you see in the ArcGIS Pro toolbox, in the Properties->Execution box.
    This file gets reloaded into the script tool in ArcGIS Pro every time you save it.
"""
import sys
from importlib import reload
import arcpy
sys.path.append("K:/Taxmaps/Toolbox")
import cc_taxmaps 
reload(cc_taxmaps)

__version__ = "1.0"

if __name__ == "__main__":
    arcpy.AddMessage(f"ExportTaxmapTool {__version__}")

# Get the parameter values from ArcGIS Pro 
    layoutName = arcpy.GetParameterAsText(0)
    pageName = arcpy.GetParameterAsText(1)
    can_table = arcpy.GetParameterAsText(2)
    outputFolder = arcpy.GetParameterAsText(3)

    projectFile = 'CURRENT'
    tm = cc_taxmaps.BuildTaxMap(projectFile, layoutName, can_table)
    try:
        tm.ConfigureLayout(pageName)
        tm.ExportPdf(outputFolder)
    except Exception as e:
        arcpy.AddError(f"Export failed. {e}")

# That's all!

 

 

 

Validation code

 

import os
import arcpy

# I struggled and failed to get these constants to load from a separate file.
#
# Map names we need to have
MAIN_MAP = "Main Map"
#
# Name of the layer that should appear in the MAIN_MAP
CANCELLED_NUMBERS_TABLE = "cancelled_numbers"
#
# Use these settings if none were provided
DEFAULT_LAYOUT = "Taxmap_18x24"
DEFAULT_CANCELLED_NUMBERS_TABLE = 'W:\\ALL\\GIS\CONNECTIONS\\cc-sqlservers_WINAUTH.sde\\cancelled_numbers'
DEFAULT_PDF_FOLDER="C:\\Temp\\pdflib"
DEFAULT_MAP_INDEX = 'W:\\ALL\\GIS\CONNECTIONS\\cc-sqlservers_WINAUTH.sde\\taxlots_fd\\mapindex'

class ToolValidator:
  # Class to add custom behavior and properties to the tool and tool parameters.

    def __init__(self):
        # Set self.params for use in other validation methods.
        self.params = arcpy.GetParameterInfo()

    def initializeParameters(self):
        # Customize parameter properties. This method gets called when the tool is opened.
        """
            0 layout
            1 page name
            2 cancel table
            3 output
        """
        project = arcpy.mp.ArcGISProject('CURRENT')
       
        layout = None
        layoutName = self.params[0].value # This is always just a string
        if not layoutName:
            # If no layout is selected, pick the first one in this project
            layout = project.listLayouts(wildcard='*')[0]
            self.params[0].value = layout.name
       
        ms = None
        try:
            layout = project.listLayouts(wildcard=layoutName)[0]
            ms = layout.mapSeries
        except Exception as e:
            self.params[0].setErrorMessage("Can't find layout or map series.")    
       
        # In the layout, figure out which page is selected, if any.
        self.params[1].clearMessage()
        self.params[1].value = ''
        try:
            self.params[1].value = ms.pageRow.PageName
        except Exception as e:
            self.params[1].setErrorMessage("Can't find pageName.")

        # Cancelled numbers table
        self.params[2].clearMessage()
        self.params[2].value = ''
        try:
            # Find the cancelled numbers table in the map.
            map = project.listMaps(wildcard=MAIN_MAP)
            self.params[2].value = map.listTables(wildcard=CANCELLED_NUMBERS_TABLE)[0]
        except Exception as e:
            self.params[2].setErrorMessage("Can't find table.")
     
        self.params[3].clearMessage()
        self.params[3].value = DEFAULT_PDF_FOLDER
        return

    def updateParameters(self):
        # Modify the values and properties of parameters before internal
        # validation is performed.
       
        # Make sure can_table exists
        if not self.params[2].valueAsText:
            self.params[2].value = DEFAULT_CANCELLED_NUMBERS_TABLE
               
        return

    def updateMessages(self):
        # Modify the messages created by internal validation for each tool
        # parameter. This method is called after internal validation.

        # Make sure the selected page name exists.
        layoutName = self.params[0].valueAsText
        pageName = self.params[1].valueAsText
       
        try:
            project = arcpy.mp.ArcGISProject('CURRENT')
            layout = project.listLayouts(wildcard=layoutName)[0]        
        except Exception as e:
            self.params[1].setErrorMessage("Blimey, can't find layout")
            layout = None
           
        if layout:
            ms = None
            try:
                ms = layout.mapSeries
            except Exception as e:
                self.params[1].setErrorMessage("Blimey, can't find map series.")
       
            if pageName:
                try:
                    pageNum = ms.getPageNumberFromName(pageName)
                except Exception as e:
                    pageNum = None
                if not pageNum:
                    self.params[1].setErrorMessage("Blimey, I don't see that page name!")

        if not arcpy.Exists(self.params[2].valueAsText):
            self.params[2].setWarningMessage("Blimey, fix this or I won't be able to find any cancelled numbers!")
       
        # Make sure the output folder exists
        if not os.path.exists(self.params[3].valueAsText):
            self.params[3].setErrorMessage("Blimey, I can't do anything without a folder!")

        return

    def isLicensed(self):
         # Set whether the tool is licensed to execute.
         return True

    # def postExecute(self):
    #     # This method takes place after outputs are processed and
    #     # added to the display.
    #     return
​

"Business logic" from cc_taxmaps.py

 

 

# Search for polygons
# Use them to set extent
# Generate a PDF for each extent, named after the polygon and with a numeric prefix. e.g. "01_shively.pdf"
# 
# Set title to name from polygon
# Set extent polygon in key map
import sys
import os
import re
from importlib import reload
import datetime
from pypdf import PdfWriter
import arcpy
sys.path.append("K:/Taxmaps/Toolbox")
import ormapnum
reload(ormapnum)

__version__ = '1.0'

# Other scalebars have to have prefix followed by scale, eg "Scalebar 24000" for 1:24000 maps

MAXCANCELLEDROWS = 15 # Go to 8 point font if # of rows exceeds this

def make_table(cancellations:list, columns:int) -> tuple:
    """Break the list of cancellations into 'n' columns and return them.

    Args:
        cancellations (list): list of cancellations
        columns (int): number of columns

    Returns:
        (int, list): int is the height of the tallest column and list contains a list for each column. 
    """    

    col = "" # Each column is a string with line breaks in it.
    maxy = int((len(cancellations)+columns-1)/columns) # max items to put in one column
    i = 0
    columns = []
    for item in cancellations:
        col += str(item).ljust(6) + "\n"
        i += 1
        if not i % maxy:
            columns.append(col)
            col = "" # we're done with this
    if col: 
        # some left overs
        columns.append(col)

    return maxy,columns

def load_cancellations(mapnumber: str, tablename: str) -> list:
    """Load the cancelled numbers table and return it as a list.
    """
    cancellations = []
    try:
        with arcpy.da.SearchCursor(in_table=tablename, 
                                    field_names=["MapNumber","Taxlot"],
                                    where_clause=f"MapNumber='{mapnumber}'") as rows:
            cancellations = [row[1] for row in rows]
    except Exception as e:
        arcpy.AddError(f"Could not load cancelled numbers table. {e}")
    return cancellations
    
def sort_taxlots(mylist:list) -> list:
    """Sorts a list of taxlots. Deals with the weird suffixes like "M1"
    and sorts numerically (e.g. 100 is greater than 20)

    Args:
        mylist (list): list to be sorted

    Returns:
        list: sorted list
    """
    d = dict()
    for orig in mylist:

        # Taxlots can be a simple integer like "4900" or a string with something on the end like "400M1"
        # and they should sort into a proper list as if they were all just integers.

        mo = re.search(r'^(\d+).*$', orig)
        s = orig
        try:
            # Add some leading zeros if the integer part is less than 8 digits
            # so, "100M1" will become "00000100M1"
            n = mo.group(1)
            s = '0' * (8 - len(n)) + orig 
        except Exception as e:
            print(e)
        d[s] = orig # This will de-duplicate too.

    # Sort the dictionary. 
    # sorted() returns a list of keys.
    # Make the dict into a list of values
    # return the new list
    return [d[k] for k in sorted(d)]


class BuildTaxMap(object):

    # map the scale of the current map to the scalebar element to use
    scales = {
        120 : 'Scalebar 240',  
        240 : 'Scalebar 240',  
        360 : 'Scalebar 240',     
        480 : 'Scalebar 240',    
        600 : 'Scalebar 600',     
        720 : 'Scalebar 600',    
        1200 : 'Scalebar 1200', 
        2400 : 'Scalebar 600', 
        4800 : 'Scalebar 600',    
        9600 : 'Scalebar 24000', # no examples; untested
        12000 : 'Scalebar 24000', # no examples; untested
        24000 : 'Scalebar 24000',
    }

    def __init__(self, projectFile, layoutName, can_table) -> None:

        self.project = arcpy.mp.ArcGISProject(projectFile)
        self.cancelled_taxlots_table = can_table
        self.layout = self.project.listLayouts(wildcard=layoutName)[0]
        self.ms = self.layout.mapSeries
        self.orm = None
        self.pageNames = None
        #arcpy.AddMessage(f"Layout name={self.layout.name}")
    
        # Make a dictionary of all the scalebar elements in this layout.
        self.scalebar_elements = {}
        for elem in self.layout.listElements(element_type="MAPSURROUND_ELEMENT", wildcard="Scalebar*"):
            name = elem.name
            self.scalebar_elements[name] = elem
        
        return
    
    
    def findPageNum(self, pageName: str) -> int:
        """Find the page number for a given pageName. Map series has to be set.

        Args:
            pageName (str): A page name from the map series.

        Returns:
            pageNum (int) or None
        """
        pageNum = None
        if pageName:
            try:
                pageNum = self.ms.getPageNumberFromName(pageName)
            except Exception as e:
                pageNum = None
        return pageNum


    def range(self, firstPageName=None, lastPageName=None) -> list:
        """Return a list of page names

        Be careful using first and last, because the sort order is not
        what you expect. Leave them empty to get a complete list.

        Args:
            firstPageName (str): start of range, use first page if None
            lastPageName (str): end of range, use last page if None

        Returns:
            list: page names found
        """
        pageNames = list()
        layer = self.ms.indexLayer

        # Cartographers are not maintaining PageNumber
        # which is why we can't use it to sort the table.
        #fields = ['PageNumber','PageName']
        fields = ['MapNumber','PageName'] # IGNORE unmaintained PageNumber

        started = False
        if not firstPageName:
            started = True # Begin at the first page

        # The table is not automatically sorted out,
        # we have to read it and sort it before finding our fields

        if not self.pageNames: # Only hit the database once
            # Create a dictionary with the pageNumber as the key
            # so that when we sort it, the items end up in the correct order.
            # Then generate a sorted list of pageNames from that and cache it.
            d = {}
            i = 0
            with arcpy.da.SearchCursor(layer,fields) as rows:
                for row in rows:
                    if row[0] in d:
                        arcpy.AddMessage(f"Duplicate pageNumber {row[0]}, {row[1]}!!")
                    d[row[0]] = row[1]
                    
                    i += 1
                self.pageNames = [d[i] for i in sorted(d)]
            print(len(d), len(self.pageNames)) # sanity check, should be 1082 (or more)

        for pageName in self.pageNames:
            if pageName == firstPageName:
                started = True
            if started:
                pageNames.append(pageName)
            if pageName == lastPageName:
                break

        return pageNames
    
    
    def find_scalebar(self, mapscale: int) -> arcpy._mp.MapSurroundElement:
        """Given a mapscale like 24000, find the scalebar to use with this map.

        Args:
            mapscale (int): Scale from a known set; 1200, 4800, etc; 
            if there is no match, returns the default which is the scalebar on the layout
        Returns:
            arcpy._mp.MapSurroundElement: An element from the layout we're working with.
        """

        sb = self.scalebar_elements["Scalebar"] # The scalebar on the layout MUST be named "Scalebar"!!
        try:
            sb = self.scalebar_elements[self.scales[mapscale]]
        except KeyError:
            print("scalebar key error")
            pass
        return sb
    

    def setPlotDate(self) -> None:
        # Set plot date (because the dateExported property fails!)
        d = datetime.datetime.now()
        plotDate = d.strftime("PLOT DATE: %x")
        text_element = self.layout.listElements(element_type='Text_Element', wildcard="Plot Date")[0]
        text_element.text = plotDate
        #print(plotDate)
        return


    def setCountyOverview(self) -> None: 
        # note, extent of the county map never needs to change, 
        # the whole county always in view
        county_overview_map = self.project.listMaps(wildcard="County Overview Map")[0]

        # Highlight the township
        highlight_layer = county_overview_map.listLayers(wildcard="Highlight")[0]
        highlight_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.tr, 'isActive': True}])
        #arcpy.AddMessage(f"County overview Highlight query={highlight_layer.listDefinitionQueries()}")

        # Set def query to turn off the township so highlight label shows (instead of both showing)
        township_layer = county_overview_map.listLayers(wildcard="Townships")[0]
        township_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': f"NOT({self.tr})", 'isActive': True}])
        #arcpy.AddMessage(f"County overview Township query={township_layer.listDefinitionQueries()}")

        return


    def setTownshipOverview(self, frame) -> None:
        # Note that pagename queries don't work here, we have to use definition queries.
        township_overview_map = self.project.listMaps(wildcard="Township Overview Map")[0]

        # Highlight the section
        highlight_layer = township_overview_map.listLayers(wildcard="Highlight")[0]
        highlight_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.trs, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Highlight query={highlight_layer.listDefinitionQueries()}")

        # Outline the township
        township_layer = township_overview_map.listLayers(wildcard="Township")[0]        
        township_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.tr, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Township query={township_layer.listDefinitionQueries()}")

        # Turn off the sections outside the current section.
        # Turn off the selected section in "Sections" (highlight layer will BOLD it the label.)
        sections_layer = township_overview_map.listLayers(wildcard="Sections")[0]
        sections_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.trsonly, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Sections query={sections_layer.listDefinitionQueries()}")

        background_layer = township_overview_map.listLayers(wildcard="Background")[0]
        background_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.notsection, 'isActive': True}])
        #arcpy.AddMessage(f"Township Overview Background query={background_layer.listDefinitionQueries()}")

        # Adjust extent to center on the township (not the section or taxmap)
        try:
            rows = arcpy.da.SearchCursor(township_layer, ['SHAPE@', 'tr'])
            row = rows.next() # There should only be one feature if the definition query actually worked?
            extent = row[0].extent
            arcpy.AddMessage(f"In township map, using extent of township {row[1]}")
            padding = (extent.XMax - extent.XMin) / 10
            new_extent = arcpy.Extent(extent.XMin - padding, extent.YMin - padding,
                extent.XMax + padding, extent.YMax + padding,
                None, None, None, None,
                extent.spatialReference)
            frame.camera.setExtent(new_extent)
            frame.visible = True
        except Exception as e:
            arcpy.AddMessage(f"Can't show township overview. {e}")
            pass
        finally:
            del rows # release the lock
            
        return


    def setSectionOverview(self, frame) -> None:
        # Page Query on Current Taxmap selects the correct taxmap shape
        section_overview_map = self.project.listMaps(wildcard="Section Overview Map")[0]

        highlight_layer = section_overview_map.listLayers(wildcard="Highlighted Section")[0]
        highlight_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': self.trs, 'isActive': True}])
        #arcpy.AddMessage(f"Highlight query={highlight_layer.listDefinitionQueries()}")

        # Turn off the selected section in "Sections" (highlight layer will BOLD it the label.)
        sections_layer = section_overview_map.listLayers(wildcard="Sections")[0]
        sections_layer.updateDefinitionQueries(definitionQueries=[{'name': 'TaxmapQuery', 'sql': f"NOT({self.trs})", 'isActive': True}])
        #arcpy.AddMessage(f"Section Overview Sections query={sections_layer.listDefinitionQueries()}")

        # Adjust extent to center on the section (not the Current Taxmap) 
        try:
            rows = arcpy.da.SearchCursor(highlight_layer, ['SHAPE@', 'SECTION'])
            row = rows.next() # There should only be one feature if the definition query actually worked?
            extent = row[0].extent
            padding = (extent.XMax - extent.XMin) / 10
            new_extent = arcpy.Extent(extent.XMin - padding, extent.YMin - padding,
                extent.XMax + padding, extent.YMax + padding,
                None, None, None, None,
                extent.spatialReference)
            arcpy.AddMessage(f"In section map, using extent of section {row[1]}")
            frame.camera.setExtent(new_extent)
            frame.visible = True
        except Exception as e:
            arcpy.AddMessage(f"Can't show section overview. {e}")
            pass
        finally: 
            del rows # release the lock
        
        return


    def adjust_scalebar_element(self):
        try:
            scale = self.ms.pageRow.MapScale
            settings = self.find_scalebar(scale) # These are the settings we want
            sb_element = self.scalebar_elements["Scalebar"] # This is the one we see
            #print(scale, settings.name, sb_element.elementWidth)
            
            if settings != sb_element:
                # Copy settings from desired scalebar
                #   Some things simply are ignored if you set them on the element;
                #   you have to set them in the CIM definition.

                new_cim = settings.getDefinition("V3")
                
                sb_cim = sb_element.getDefinition("V3")
                sb_cim.unitLabel = new_cim.unitLabel
                sb_cim.units = new_cim.units
                
                sb_element.setDefinition(sb_cim)
                sb_element.elementWidth = settings.elementWidth

                #print(sb_element.elementWidth)

        except Exception as e:
            arcpy.AddError(f"No scale set for this map. {e}")
            pass
        return


    def populate_cancel_table(self, cancellations:list) -> None:

        # Find the cancel table elements and put them in order
        elements = self.layout.listElements(element_type='Text_Element', wildcard="can_col*")
        ncols = len(elements)
        column_elements = [None] * ncols # Make a list of the right size
        for e in elements:
            mo = re.search('^can_col(\d+)$', e.name)
            if mo:
                n = int(mo.group(1))-1
                column_elements[n] = e
                e.text = " " # This element has some text in it (event just a single space) so ArcMap does not "lose" it.
    
        # Fill in the table in an aesthetically pleasing fashion.
        maxy, table = make_table(cancellations, ncols)
        
        # Adjust the font size of the table according to the number of rows
        fontsize = 10
        if maxy > MAXCANCELLEDROWS:
            fontsize = 8

        x = 0
        for column in table:
            column_elements[x].text = column
            column_elements[x].fontSize = fontsize
            x += 1
        
        return


    def ConfigureLayout(self, pageName) -> None:
        """ Change the current layout to set it up for the named page. """
        
        self.pageName = pageName
        pageNum = self.ms.getPageNumberFromName(pageName)

        # Fix sundry metadata (titles etc)
        self.setPlotDate()
        self.ms.currentPageNumber = pageNum
        pageRow = self.ms.pageRow

        # Parse the ormapnum from the current page to break out all its fields
        self.orm = ormapnum.ORMapNum()
        self.orm.unpack(pageRow.ORMapNum)
        
        cancellations = load_cancellations(self.orm.dotted, self.cancelled_taxlots_table)

        self.tr = f"TR = '{self.orm.township}{self.orm.township_dir}{self.orm.range}{self.orm.range_dir}'"
        self.trs = f"TOWN = '{self.orm.township}' AND RANGE = '{self.orm.range}' AND SECTION = '{self.orm.section}'"
        self.trsonly = f"(TOWN = '{self.orm.township}' AND RANGE = '{self.orm.range}') AND NOT({self.trs})"
        self.notsection = f"NOT(TOWN = '{self.orm.township}' AND RANGE = '{self.orm.range}')"

        top_title = self.layout.listElements(element_type='Text_Element', wildcard="Top Title")[0]
        bottom_title = self.layout.listElements(element_type='Text_Element', wildcard="Bottom Title")[0]
        plss_description = self.layout.listElements(element_type='Text_Element', wildcard="PLSS Description")[0]

        #print(f"short title=\"{self.orm.shortmaptitle}\"" long title=\"{self.orm.longmaptitle}\"")
        top_title.text = bottom_title.text = self.orm.shortmaptitle
        plss_description.text = self.orm.longmaptitle

        self.setCountyOverview()

        township_overview_frame = self.layout.listElements(element_type="MAPFRAME_ELEMENT", wildcard="Township Overview Frame")[0]
        township_overview_frame.visible = False
        section_overview_frame = self.layout.listElements(element_type="MAPFRAME_ELEMENT", wildcard="Section Overview Frame")[0]
        section_overview_frame.visible = False
        if self.orm.section:
            self.setTownshipOverview(township_overview_frame)
            self.setSectionOverview(section_overview_frame)

        self.adjust_scalebar_element()

        # Fix up the cancelled numbers table, or hide it if it's empty.
        can_table = self.layout.listElements(element_type='GROUP_ELEMENT', wildcard="Cancelled Numbers Table")[0]
        if len(cancellations):

            cancellations = sort_taxlots(cancellations)
            self.populate_cancel_table(cancellations)
            can_table.visible = True

            # Position the table based on which overview maps are visible
            if not township_overview_frame.visible:
                can_table.elementPositionY = township_overview_frame.elementPositionY
            elif not section_overview_frame.visible:
                can_table.elementPositionY = section_overview_frame.elementPositionY
            else:
                can_table.elementPositionY = section_overview_frame.elementPositionY - section_overview_frame.elementHeight - 0.20

            #arcpy.AddMessage(f"can_table y-pos set to {can_table.elementPositionY}")
            
        else:
            can_table.visible = False

        return


    def show_elements(self) -> None:
        """ This is just to help in development """
        # Find all the layout elements
        main_elements = self.layout.listElements(element_type="MAPFRAME_ELEMENT", wildcard="*")
        print("Map Frames:")
        for e in main_elements:
            print(e.name)
        print()
        print("Text Elements:")
        text_elements = self.layout.listElements(element_type='Text_Element', wildcard="*")
        for e in text_elements:
            print(e.name)
        print()
        print("Scale bars")
        scalebar_elements = self.layout.listElements(element_type='MAPSURROUND_ELEMENT', wildcard="scale*")
        for e in scalebar_elements:
            print(e.name)
        print()
        return
    
    
    def ExportPdf(self, outputFolder: str) -> bool:
        """Generate a Taxmap PDF file.

        Args:
            outputFolder (_type_): An existing folder to hold output files.

        Returns:
            bool: True if PDF was successfully written.
        """

        # Generate a PDF without metadata.
        try:
            # Export the map to the scratch folder
            scratch_output_pathname = os.path.join(arcpy.env.scratchFolder, self.orm.filename + "_scratch.pdf")
            self.layout.exportToPDF(scratch_output_pathname)
        except Exception as e:
            if not self.orm:
                arcpy.AddError(f"Did you call ConfigureLayout? {e}")
            else:
                arcpy.AddError(f"Error in export; is file open (in Acrobat maybe)? {e}")
            return False
    
        projfile = os.path.join(self.project.homeFolder, self.project.filePath)
        person = os.environ['USERNAME']

        # Update the metadata on the PDF file. Then delete the scratch file.
        try:
            final_output_pathname = os.path.join(outputFolder, self.orm.filename + ".pdf")
            writer=PdfWriter(clone_from=scratch_output_pathname)
            metadata = {
                '/Title': f"Taxmap {self.pageName}",
                '/Subject': self.orm.longmaptitle + " Clatsop County, Oregon",
                '/Author':  f"{person} {self.layout.name}",
                '/Creator': f'{projfile}', #  Shows as "Application" in Acrobat
                '/Producer': f"{__file__} {__version__}", # Shows as "PDF Producer"
                '/Keywords': ','.join(['property tax', 'map', 'gis']),
            }
            writer.add_metadata(metadata)
            writer.write(final_output_pathname)
            arcpy.AddMessage(f"Exported to {final_output_pathname}")
        except Exception as e:
            arcpy.AddError(f"Error in export. {e}")
            return
        finally:
            # Delete the file without the metadata
            os.unlink(scratch_output_pathname)

        return True
    

# =====================================================================================
if __name__ == "__main__":

    DEFAULT_PROJECT_FILE = "taxmaps.aprx"
    DEFAULT_CAN_TABLE = "W:\\ALL\\GIS\\CONNECTIONS\\cc-sqlservers_WINAUTH.sde\\cancelled_numbers"

    assert(os.path.exists(DEFAULT_PROJECT_FILE))
    assert(arcpy.Exists(DEFAULT_CAN_TABLE))

    tm = BuildTaxMap(DEFAULT_PROJECT_FILE, "Taxmap_18x24", can_table=DEFAULT_CAN_TABLE)
    pageNames = (tm.range(None, None))
    print(f"There are {len(pageNames)} pages in the index.")

#    pageNames = (tm.range('4 06', '4 09'))
#    print(f"{len(pageNames)} pages will be exported.")

    # This list of page names tests every aspect of the process.
    sample_list = [   
        # scales
        "4 06",         # 1:24000 "2000 Scale"
        "8 09",         # 1:24000 "2000 Scale"
        "5 10 19AD D2", # 1:120     "10 Scale"
        "5 10 30AA D1", # 1:240     "20 Scale"
        "6 10 3 D2",    # 1:360     "30 Scale"
        "6 10 16DD D3", # 1:480     "40 Scale"
        "8 09 18BA D2", # 1:600     "50 Scale"
        "6 10 4DA D4",  # 1:720     "60 Scale"
        "4 07 3BC",     # 1:1200   "100 Scale"
        "5 07 29A",     # 1:2400   "200 Scale"  SCALEBAR shows 0.1 Mile
        "4 09 22",      # 1:4800   "400 Scale"  SCALEBAR shows 0.2 Mile
        #"NONE",        # 1:9600   "800 Scale"
        #"NONE",        # 1:12000 "1000 Scale"
    ]
    print(f"{len(sample_list)} sample pages will be exported.")

    for pageName in pageNames:

        tm = BuildTaxMap(DEFAULT_PROJECT_FILE, "Taxmap_18x24", can_table=DEFAULT_CAN_TABLE)
        print(f"Page {pageName}")

        #tm.show_elements()

        tm.ConfigureLayout(pageName)

        scale = tm.ms.pageRow.MapScale
        sbe = tm.find_scalebar(scale)
        print(f"1:{scale} {sbe.longName}")

        outputFolder = "C:\\Temp\\pdflib" # testing
        tm.ExportPdf(outputFolder)

        del tm # Attempt to release the project so APRX can be updated

# That's all!

 

 

 
 
0 Kudos