In this post I am going to explore methods to modify Map’s and Layers in ArcGIS Pro. By using the arcpy.mp library we can turn layers on and off in a mapframe, change text in a layout and save a layout to a PDF using code. ArcGIS Pro switched from ArcMap Document Files (.mxd) to Project Files (.aprx). Project files allow you to have multiple maps and multiple layouts in the same project. This is one of the bigger changes to python scripting going from ArcMap for Desktop to ArcGIS Pro, see here for some details on what has changed. The changes were needed to accommodate both multiple maps and layouts and as a result arcpy.mapping is now arcpy.mp in ArcGIS Pro. I think it is great that we now have the ability to put as many maps and layouts as we want into a single project. I find it rare that I am creating just one output from a set of data, so this should make it a lot simpler.
I spent a bit of time playing with the examples using arcpy.mp from the Esri ArcGIS Pro help and this post will be a step by step guide through a few of the features. I will cover topics including retrieving maps and layouts from a project, modifying layers within a map, copying all of the layers from one map to another, modifying objects within a layout and printing layouts using arcpy.mp. By utilizing arcpy.mp you will be able to automate producing map and save a lot of time if you are making static maps for use in a report or print out.
Getting Started With arcpy.mp
For the coding in this tutorial I will be using the Python Window in ArcGIS Pro. To load the python console go to the View tab and press Python. This loads the interactive python interpreter so we can play with our map and see what is changing as we go. Here is a screenshot of the map window I am working with so you can see the names of my layers. I am using a dataset I have containing hospitals, nursing stations and banks in Halifax, Nova Scotia. You can use any dataset you want, I just wanted to have a few layers present so that I could turn them on and off using arpy.mp.
In this map you can see that I have three point layers (all prefixed with ‘NS_’), a line layer and a topographic basemap. I have three maps including ‘HalifaxMap’, ‘NewHalifaxMap’ and ‘SuperTestMap’. The layers in my project are ‘Layout’ and ‘OutputMap’.
The first step is to create a object that references the current project. This is done using arcpy.mp.ArcGISProject(), since we are going to be using arcpy through the console we can reference the “CURRENT” keyword to get the project we currently have open. If we were trying to make a standalone python script the reference would have to be to an actual project .aprx file. Once the project object has been created we can now create a list of the maps within the project using .listMaps()
project = arcpy.mp.ArcGISProject("CURRENT") # get the current project maps = project.listMaps() # get a list of maps matchign the filter for m in maps: print(m.name) >HalifaxMap >NewHalifaxMap >SuperTestMap
The listMaps method lets you send in a wildcard so I could use “HalifaxMap” to get HalifaxMap, “*Halifax*” to get HalifaxMap and NewHalifaxMap or “Halifax*” to get HalifaxMap (and any others that started with Halifax). This is really useful to retrieve a set of maps that follow a specific naming structure. I use the .name property of the map class to return the name of the map. To learn more about the map class go here.
Next I am going to load a specific Map and see what layers it contains. To do this I will create a maps object for just the HalifaxMap. Regardless if you are returning one value or many, this class always returns a list. To get at the actual HalifaxMap map object I will use the index of .
halifaxMap = project.listMaps("HalifaxMap") layers = halifaxMap.listLayers() # get a list of the layers in this map for l in layers: print(l.name) >NS_Hospitals >NS_Nurse >NS_Banks >Halifax_Streets >Topographic
We can also use wildcards in .listLayers so if I wanted to return all of the layers that start with ‘NS’ I would have used layers = halifaxMap.listLayers(‘NS*’), this is very similar to listMaps. Here is a link to see all of the properties of the Layer class. There are also properties to determine the type of layer including is3DLayer, isFeatureLayer, isGroupLayer, isRasterLayer, and isWebLayer.
for l in layers: print(l.isFeatureLayer) >True >True >True >True >False for l in layers: print(l.isWebLayer) >False >False >False >False >True
You can also use the .supports(layer_property) method to see if the layer supports a property.
layers.supports("BRIGHTNESS") >False layers.supports("TRANSPARENCY") >True
Another important layer property that you might want to manage is layer visibility. If we wanted to turn off all of the layers that started with “NS” it is simple using a wildcard filter and a loop.
layers = halifaxMap.listLayers("NS*") # Now we want to turn layers starting with "NS" off for layer in l_filter: layer.visible = False
We can do a very similar process with layouts. Retrieve all of the layouts in the project using .listLayouts. Again it has the same set of wildcard operations if you want to filter down to a specific layout or a set of layouts.
layouts = project.listLayouts() for l in layouts: print(l.name) >Layout >OutputMap
Printing a PDF of Each Layer in a Map Window
Looping Through Layers
Next I am going to use a loop to produce a PDF map of each layer in a map window. To do this you need to have a layout already set up because are writing the layout (you can also use a Map if you wanted to, but layouts let you do more formatting). In this code snippet I load the map named ‘HalifaxMap’, load the layers starting with ‘NS’ in that map and load the layout ‘Layout’. Then using a for loop to loop through each of the layers and turn them off (we only want to print one at a time and do not know their visibility to start). Then looping through the layers again turning each on and using the layout .exportToPDF method to write a pdf of the layer. The layer visibility is then turned off again and we move on to the next layer. There are also functions to exportToJPEG, exportToTIFF, exportToPNG etc..
project = arcpy.mp.ArcGISProject("CURRENT") # get the current project m = project.listMaps("HalifaxMap") # get a list of maps matchign the filter outMap = m outLayers = outMap.listLayers("NS*") layouts = project.listLayouts("Layout") outLayout = layouts for layer in outLayers: # first make sure all of the layers are off layer.visible = False for layer in l_filter: # now print each layer one at a time layer.visible = True print(r"C:\Users\ryan\Documents\Ryan\GIS\ArcGIS_Pro_MappingPython\pdf" + "\\" + layer.name + ".pdf") outLayout.exportToPDF(r"C:\Users\ryan\Documents\Ryan\GIS\ArcGIS_Pro_MappingPython\pdf" + "\\" + layer.name + ".pdf") layer.visible = False
Looping Through Bookmarks
Another neat trick I saw on the ArcGIS Pro help site was looping through bookmarks to output maps zoomed in to different extents to PDF. I created three bookmarks in my map frame to test out this functionality. This is a common workflow I have to do in order to make maps showing different locations in a map in detail. The code modified to work with my example is:
layout = project.listLayouts("Layout") mapFrame = layout.listElements("MAPFRAME_ELEMENT", "HalifaxMapFrame") bookmarks = mapFrame.map.listBookmarks() for bookmark in bookmarks: mapFrame.zoomToBookmark(bookmark) layout.exportToPDF(r"C:\Users\ryan\Documents\Ryan\GIS\ArcGIS_Pro_MappingPython\pdf" + "\\" + bookmark.name + ".pdf")
You first load the layout that you want to be printing. The next step is to load the map frames contained in the layout. This is a little tricky, what you are doing is asking the layout ‘what map frames do you have?’. One thing to note is that the map frame name is not necessarily the same as the Map name. My map is named HalifaxMap but I ended up naming my map frame within the layout HalifaxMapFrame (which then contains HalifaxMap). So be careful setting up the wild card you using to search. Once you have the map frame you can ask it for all of its bookmarks using listBookmarks(). Then you loop through the bookmarks, zoom to each of them (zoomToBookmark()) and save a PDF using the bookmark name. That is a super smooth way to produce a lot of different maps with a few lines of code.
Modifying Text Elements in a Layout
So you have probably noticed that in my maps they have the text “Main Title” and “Sub Title” at the top. I know you have been thinking gee Ryan, that is not a very informative title. Its ok! I have just been waiting to get to that part. We can use arcpy.mp to modify text within a layout. That sounds fantastic doesn’t it?
Once again we are going to create a layout object referencing the layout we want to change. Then we use the listElements method with the “TEXT_ELEMENT” element type to get all of the text elements in the layout. Other options for element_type include GRAPHIC_ELEMENT, LEGEND_ELEMENT, MAPFRAME_ELEMENT, MAPSURROUND_ELEMENT and PICTURE_ELEMENT. You can also use a wild card to pick a specific element, but I am going to be searching by the actual text in the text element, rather then its name.
I created a for loop going through each text element and look at the text contained within it. Since we have two text elements containing “Main Title” and “Sub Title”. Using an if statement to match the text we want to change and then grabing a reference to the text element object. Then I use the text element .text method to change the text. Once you type in the command the text changes in our layout. The last line returns the text to the original values so that the script can find these text objects again the next time it is run. If we left it as ‘Super Fun Test Title’ it would not match in the if statement when we run the code again.
layouts = project.listLayouts("Layout") outLayout = layouts layoutTextElements = outLayout.listElements("TEXT_ELEMENT") for txt in layoutTextElements: if txt.text == "Main Title": mainTitleText = txt elif txt.text == "Sub Title": subTitleText = txt mainTitleText.text = "Super Fun Test Title" subTitleText.text = "Super fun Sub Title" # return them to the original mainTitleText.text = "Main Title" subTitleText.text = "Sub Title"
Pulling it Together – Creating a Series of Maps with Changing Titles
Now lets try making a lot of maps. What if we wanted to make a map for each bookmark showing each of the three different layers. Now we have to produce nine maps (and if you had more than 3 bookmarks and three layers it would get out of control quickly). Zooming to different locations and saving each map can be very laborious and boring, so clearly automating this process is crucial. In addition we can even change text on the map to better describe what is being displayed. I don’t know about you, but I can’t count the number of times I have finalized a map layout, produced all the PDFs only to notice a problem after all the work is done. Switching around views, turning layers on and off and producing PDFs is not the most exciting task, so a way to speed this up / automate the process is great. Now you can use python and arcpy.mp to change the elements in your map frame and output all the maps in one go.
This next script pulls it all together, it will loop through each of the bookmarks, then loop through each of the layers, change the titles of the maps and save them to PDFs or JPEGs
### Bringing Bookmark example and layer example into one project = arcpy.mp.ArcGISProject("CURRENT") # get the current project layout = project.listLayouts("Layout") mapFrame = layout.listElements("MAPFRAME_ELEMENT", "HalifaxMapFrame") # get the map frame in the layout maps = project.listMaps("HalifaxMap") # get the HalifaxMap layers = halifaxMap.listLayers("NS*") # get a list of the layers in map we want to show for layer in outLayers: # Turn off all the layers we are going to loop through layer.visible = False # get the text elements we want to change layoutTextElements = layout.listElements("TEXT_ELEMENT") for txt in layoutTextElements: if txt.text == "Main Title": mainTitleText = txt elif txt.text == "Sub Title": subTitleText = txt bookmarks = mapFrame.map.listBookmarks() # get the bookmarks in the map frame for bookmark in bookmarks: mapFrame.zoomToBookmark(bookmark) for layer in layers: layer.visible = True mainTitleText.text = bookmark.name subTitleText.text = layer.name[3:] layout.exportToJPEG(r"C:\Users\ryan\Documents\Ryan\GIS\ArcGIS_Pro_MappingPython\pdf\HalifaxMap_" + bookmark.name + "_" + layer.name + ".pdf", 300) layer.visible = False # Return the text objects to their original values mainTitleText.text = "Main Title" subTitleText.text = "Sub Title"
Here are all the maps in one display! That was so fast.
So there we have it. By using ArcGIS Pro, the arcpy.mp library and some basic scripting we were able to produce a slew of maps based off of the same dataset. We used the text items present in a layout to change their values to reflect the layers that were currently displayed. I think this is a very powerful workflow and can really simplify your life if your producing lots of maps based off of the same dataset.