Select to view content in your preferred language

Programmatically switch metadata source for a given layer

153
4
Jump to solution
4 weeks ago
GIS_Weasel
Regular Contributor

I have a simple question - I'd like to change the value of this dropdown box programmatically (in Python) for a large number of layers in a large number of layer files.

GIS_Weasel_0-1736866915379.png

The default setting for loading .lyr files into pro sets this to 'Layer has its own metadata'. This is also the case when loading programmatically via LayerFile("uri-to-layer.lyr") I want to run a Python script to reset this to 'Show metadata from data source (read-only)', as is possible in the Pro UI.

All of the methods and mechanisms in the Metadata class seem to be based around the assumption I want to do something with metadata for this layer. I do not! I want to make it read-only and show users the metadata stored in our spatial database. But, Metadata.isReadOnly is itself read only!

I then intend to re-save them as .lyrx files.

Any suggestions welcome.

0 Kudos
2 Solutions

Accepted Solutions
HaydenWelch
MVP

I believe you can kinda get around this by directly modifying the CIM definition of the layer once you import it:

 

 

from pathlib import Path

from arcpy.mp import LayerFile, ArcGISProject
from arcpy._mp import Map, Layer # Hidden mp types

from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property

def get_map(project: ArcGISProject, map_name: str) -> Map:
    return project.listMaps(map_name)[0]

def set_metadata_readonly(project: ArcGISProject, layerfiles: list[str], output_map: str | None=None):
    """Adds layerfiles to a map then sets the useSourceMetadata property to True.
    
    Parameters:
        project (ArcGISProject): The project to add the layers to.
        layerfiles (list[str]): The list of layerfiles to add to the map.
        output_map (Optional[str]): The name of the map to add the layers to. (default is first map in project)
    """
    for layer_path in layerfiles:
        # Set Up LayerFile object
        lyrpath =  Path(layer_path)
        lyr = LayerFile(str(lyrpath))
        
        # Add Layer to top of map
        target_map = get_map(project, output_map)
        target_map.addLayer(lyr, add_position="TOP")
        
        # Get the new layer
        new_layer: Layer = target_map.listLayers()[0]
        
        # Get CIM definition for new layer
        new_layer_cim: CIMDefinition = new_layer.getDefinition("V3")
        
        # Set CIM useSourceMetadata to True
        new_layer_cim.useSourceMetadata = True
        
        # Update CIM definition
        new_layer.setDefinition(new_layer_cim)
            
def main():
    ## Implement your script here
    return

if __name__ == "__main__":
    main()

 

 

This won't set that property in the layerfile itself, but it will make sure that once the layer is added to a target map/project that the useSourceMetadata CIM property is set to True.

 

NOTE:

You will need to .save() the ArcGISProject in your implementation, or the edits will be dropped.

 

This is also untested, so if you still can't get it to work by directly modifying the CIM definition of the layer, creating an Ideas post as per @MErikReedAugusta 's suggestion is the best bet.

View solution in original post

GIS_Weasel
Regular Contributor

Thanks lads. CIM was the solution.

I just need one and done on this, but digging through group layers in several hundred files wasn't especially appealing!

Included a functional example below (no, it's not pretty, but it works).

# Script to resave lyr files as lyrx
# Plan:
# Send a folder location
# Walk through each folder and open lyr file (into new map? Or reuse a single map?)
# Modify to use metadata from source
# Save lyrx with same name in same folder structrure

# https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/walk.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/python-cim-access.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/layer-class.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/metadata/metadata-class.htm

import arcpy
import logging
import os
from arcpy._mp import Layer # Hidden mp types
from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property

workspace = "H:/Data/ESRI"
layerfiles = []
projectLocation = r'C:\Users\user\Documents\ArcGIS\Projects\MyProject\MyProject.aprx'
mapName = "Map"
outputLocation = r'C:\Users\user\Desktop\Layerfiles'

# Configure the logging system
logging.basicConfig(
    filename = os.path.join(outputLocation, "updateLyrsToLyrx.log"),
    level = logging.DEBUG,
    format = "%(asctime)s - %(levelname)s - %(message)s"
)

# Recursive function to step through group layers
def updateMetadataForLayer(layer: Layer):

   # dump to log
   logging.info("updateMetadataForLayer: " + layer.name)

   # Recurse through groups
   if layer.isGroupLayer:
      for subLayer in layer.listLayers():
         updateMetadataForLayer(subLayer)
   
   # Get CIM definition for new layer
   new_layer_cim: CIMDefinition = layer.getDefinition("V3")
   
   # Set CIM useSourceMetadata to True
   new_layer_cim.useSourceMetadata = True
   
   # Update CIM definition
   layer.setDefinition(new_layer_cim)

# Main entry point, uses global variables
def updateLyrsToLyrx():

   ## Walk the directory to find all layer files
   walk = arcpy.da.Walk(workspace, topdown=True)

   # Load a project
   p = arcpy.mp.ArcGISProject(projectLocation)

   # Use the default "Map"
   map = p.listMaps(mapName)[0]

   for dirpath, dirnames, filenames in walk:

      for filename in filenames:

         if (filename.find(".lyr") != -1) and (filename.find(".lyrx") == -1): # Only operate on layer files, excluding lyrx

            # full filename
            layerFilePath = os.path.join(dirpath, filename)
            
            # dump to log
            logging.info("Found: " + layerFilePath)

            # Add this layer to the map
            lyrFile = arcpy.mp.LayerFile(layerFilePath)
            
            # Retain the list
            list_of_added_maplayers = map.addLayer(lyrFile)
            
            # Loop through every added layers, catch errors and log failures
            for added_layer in list_of_added_maplayers:
               try:
                  updateMetadataForLayer(added_layer)
               except:
                  # Log & fall through - save layer as lyrx anyway - check errored files later
                  logging.error('Unable to update metadata for: ' + added_layer.name)

            # Grab the first, or root, layer
            firstLayer = list_of_added_maplayers[0]

            # subfolder path - trim the workspace
            folderPath = os.path.dirname(layerFilePath[len(workspace):])

            # find the filename
            filename = os.path.splitext(os.path.basename(layerFilePath))[0]

            # new output location
            outputFolderPath = os.path.join(outputLocation + folderPath)

            # create output folders if necessary
            os.makedirs(outputFolderPath, exist_ok=True)

            # Now we've updated the layer, flaky save a copy as lyrx (lol)
            firstLayer.saveACopy(os.path.join(outputFolderPath, filename + ".lyrx"))

def main():
   ## Implement your script here
   updateLyrsToLyrx()
   return

if __name__ == "__main__":
   main()

 

Again, thanks for taking the time to reply. Much appreciated 🙂

View solution in original post

4 Replies

From my reading, this doesn't look to be currently possible.  You could import Metadata for a fossilized copy of the Metadata at the time you created the LYRX files, but that's the closest I think you can get.  Normally, this level of advanced editing is hiding in the CIM, but that seems just to point back to the regular Metadata class object, as you've found.

This probably warrants a post on the Python Ideas Board.

 

There's also the larger question, though, of the use-case here.  Is this something you'll have to regularly do and update, hence the desire for automation?  Or is it a one-and-done operation for most/all of the sources?  If it's the latter, you're probably better off setting aside time for the obnoxious labor of doing it manually.

------------------------------
M Reed
"The pessimist may be right oftener than the optimist, but the optimist has more fun, and neither can stop the march of events anyhow." — Robert A. Heinlein, in Time Enough for Love
0 Kudos
HaydenWelch
MVP

I believe you can kinda get around this by directly modifying the CIM definition of the layer once you import it:

 

 

from pathlib import Path

from arcpy.mp import LayerFile, ArcGISProject
from arcpy._mp import Map, Layer # Hidden mp types

from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property

def get_map(project: ArcGISProject, map_name: str) -> Map:
    return project.listMaps(map_name)[0]

def set_metadata_readonly(project: ArcGISProject, layerfiles: list[str], output_map: str | None=None):
    """Adds layerfiles to a map then sets the useSourceMetadata property to True.
    
    Parameters:
        project (ArcGISProject): The project to add the layers to.
        layerfiles (list[str]): The list of layerfiles to add to the map.
        output_map (Optional[str]): The name of the map to add the layers to. (default is first map in project)
    """
    for layer_path in layerfiles:
        # Set Up LayerFile object
        lyrpath =  Path(layer_path)
        lyr = LayerFile(str(lyrpath))
        
        # Add Layer to top of map
        target_map = get_map(project, output_map)
        target_map.addLayer(lyr, add_position="TOP")
        
        # Get the new layer
        new_layer: Layer = target_map.listLayers()[0]
        
        # Get CIM definition for new layer
        new_layer_cim: CIMDefinition = new_layer.getDefinition("V3")
        
        # Set CIM useSourceMetadata to True
        new_layer_cim.useSourceMetadata = True
        
        # Update CIM definition
        new_layer.setDefinition(new_layer_cim)
            
def main():
    ## Implement your script here
    return

if __name__ == "__main__":
    main()

 

 

This won't set that property in the layerfile itself, but it will make sure that once the layer is added to a target map/project that the useSourceMetadata CIM property is set to True.

 

NOTE:

You will need to .save() the ArcGISProject in your implementation, or the edits will be dropped.

 

This is also untested, so if you still can't get it to work by directly modifying the CIM definition of the layer, creating an Ideas post as per @MErikReedAugusta 's suggestion is the best bet.

Actually, speaking of untested options and building on this:

I missed that useSourceMetadata property.  If you can set that on an active layer, then theoretically you should then be able to export that layer from the map as a LYRX file, right?

If the untested assumptions above are correct (and if the useSourceMetadata property persists in an LYRX file), then theoretically you could run the code to import the data into a dummy map file, then re-export it back out as a LYRX file.

Try running Hayden's code, followed by calling "Save Layer to File (Data Management)"  and passing it the resulting map layer.

 

If we're both right, and a with a little luck, the end result would hopefully be a LYRX file that points to the source metadata.  I likely won't have time to test this for a few days, at least, but if either you is able before then, definitely report back with what you found.

------------------------------
M Reed
"The pessimist may be right oftener than the optimist, but the optimist has more fun, and neither can stop the march of events anyhow." — Robert A. Heinlein, in Time Enough for Love
GIS_Weasel
Regular Contributor

Thanks lads. CIM was the solution.

I just need one and done on this, but digging through group layers in several hundred files wasn't especially appealing!

Included a functional example below (no, it's not pretty, but it works).

# Script to resave lyr files as lyrx
# Plan:
# Send a folder location
# Walk through each folder and open lyr file (into new map? Or reuse a single map?)
# Modify to use metadata from source
# Save lyrx with same name in same folder structrure

# https://pro.arcgis.com/en/pro-app/latest/arcpy/data-access/walk.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/python-cim-access.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/mapping/layer-class.htm
# https://pro.arcgis.com/en/pro-app/latest/arcpy/metadata/metadata-class.htm

import arcpy
import logging
import os
from arcpy._mp import Layer # Hidden mp types
from arcpy.cim import CIMDefinition # All CIM types support the useSourceMetadata property

workspace = "H:/Data/ESRI"
layerfiles = []
projectLocation = r'C:\Users\user\Documents\ArcGIS\Projects\MyProject\MyProject.aprx'
mapName = "Map"
outputLocation = r'C:\Users\user\Desktop\Layerfiles'

# Configure the logging system
logging.basicConfig(
    filename = os.path.join(outputLocation, "updateLyrsToLyrx.log"),
    level = logging.DEBUG,
    format = "%(asctime)s - %(levelname)s - %(message)s"
)

# Recursive function to step through group layers
def updateMetadataForLayer(layer: Layer):

   # dump to log
   logging.info("updateMetadataForLayer: " + layer.name)

   # Recurse through groups
   if layer.isGroupLayer:
      for subLayer in layer.listLayers():
         updateMetadataForLayer(subLayer)
   
   # Get CIM definition for new layer
   new_layer_cim: CIMDefinition = layer.getDefinition("V3")
   
   # Set CIM useSourceMetadata to True
   new_layer_cim.useSourceMetadata = True
   
   # Update CIM definition
   layer.setDefinition(new_layer_cim)

# Main entry point, uses global variables
def updateLyrsToLyrx():

   ## Walk the directory to find all layer files
   walk = arcpy.da.Walk(workspace, topdown=True)

   # Load a project
   p = arcpy.mp.ArcGISProject(projectLocation)

   # Use the default "Map"
   map = p.listMaps(mapName)[0]

   for dirpath, dirnames, filenames in walk:

      for filename in filenames:

         if (filename.find(".lyr") != -1) and (filename.find(".lyrx") == -1): # Only operate on layer files, excluding lyrx

            # full filename
            layerFilePath = os.path.join(dirpath, filename)
            
            # dump to log
            logging.info("Found: " + layerFilePath)

            # Add this layer to the map
            lyrFile = arcpy.mp.LayerFile(layerFilePath)
            
            # Retain the list
            list_of_added_maplayers = map.addLayer(lyrFile)
            
            # Loop through every added layers, catch errors and log failures
            for added_layer in list_of_added_maplayers:
               try:
                  updateMetadataForLayer(added_layer)
               except:
                  # Log & fall through - save layer as lyrx anyway - check errored files later
                  logging.error('Unable to update metadata for: ' + added_layer.name)

            # Grab the first, or root, layer
            firstLayer = list_of_added_maplayers[0]

            # subfolder path - trim the workspace
            folderPath = os.path.dirname(layerFilePath[len(workspace):])

            # find the filename
            filename = os.path.splitext(os.path.basename(layerFilePath))[0]

            # new output location
            outputFolderPath = os.path.join(outputLocation + folderPath)

            # create output folders if necessary
            os.makedirs(outputFolderPath, exist_ok=True)

            # Now we've updated the layer, flaky save a copy as lyrx (lol)
            firstLayer.saveACopy(os.path.join(outputFolderPath, filename + ".lyrx"))

def main():
   ## Implement your script here
   updateLyrsToLyrx()
   return

if __name__ == "__main__":
   main()

 

Again, thanks for taking the time to reply. Much appreciated 🙂