Select to view content in your preferred language

Applying a Definition Query/Filter to a Web Map in a Web Experience

224
1
Jump to solution
3 weeks ago
TristanJonas
New Contributor

Hi all 🙂

I want to make a set of user friendly custom tools for Web Maps in a Web Experience that each correspond to one layer and one field, and the user selects a value from a dropdown menu of unique values for that field to filter it. So for example, I'd make a tool called "Filter by State" with a dropdown box that says "Ohio", "Pennsylvania" and "Kentucky". The user would make their selection and click "Run" and the layer I hardcode would be filtered with a Definition Query for that state and display accordingly.

However, what was pretty trivial to do in ArcGIS Pro has proved to be impossible for me in the context of a Web Experience. I can't get it to apply it as a filter that the user can see, the same way if they applied a filter.

I am in an organization so the login information reflects that, if I'm doing it wrong please let me know.

    # ---------- 3. Execute ----------
    def execute(self, params, messages):
        p_layer, p_field, p_op, p_val = params
        layer_name = (getattr(p_layer.value, "name", None)
                      or p_layer.valueAsText
                      or str(p_layer.value))
        lyr_obj = p_layer.value
        fld_real = _base_field(p_field.valueAsText or "")

        # Validate we have all required inputs
        if not (lyr_obj and fld_real and p_op.value and p_val.value):
            raise RuntimeError("Required parameters missing.")
    
        # Build the SQL clause
        ftype = next((f.type for f in arcpy.ListFields(lyr_obj, fld_real)),
                     "String")
        sql = f"{fld_real} {p_op.value} {self._val_sql(p_val.value, ftype)}"

        # Get the simple layer name (without path)
        # This handles cases where layer name has path format: "Group\LayerName"
        simple_layer_name = layer_name.split("\\")[-1] if "\\" in layer_name else layer_name
        messages.addMessage(f"Layer name: {layer_name}")
        messages.addMessage(f"Simple layer name: {simple_layer_name}")

        # First try the local approach (ArcGIS Pro)
        applied = False
        try:
            aprx = arcpy.mp.ArcGISProject("CURRENT")
            for m in aprx.listMaps():
                for lyr in m.listLayers():
                    # Try both full name and simple name
                    if (lyr.name == layer_name or lyr.name == simple_layer_name) and lyr.supports("DEFINITIONQUERY"):
                        lyr.definitionQuery = sql
                        applied = True
                        messages.addMessage(f"Applied filter to layer '{lyr.name}' in ArcGIS Pro")
                        break
                if applied:
                    break
        except Exception as e:
            messages.addWarningMessage(f"Local application failed: {str(e)}")

        # If local application failed, try to apply to web map
        if not applied:
            try:
                messages.addMessage("Attempting to apply filter to web map...")
            
                # Request webmap title from user
                webmap_title = arcpy.GetParameterAsText(4) or None
            
                if not webmap_title:
                    messages.addWarningMessage("No web map title provided - skipping web map update.")
                else:
                    messages.addMessage(f"Attempting to update web map: {webmap_title}")
            
                    # Import the GIS modules required for web map updates
                    import copy
                    from json import dumps
                    from arcgis.gis import GIS
            
                    # Connect to the GIS using the current Pro connection
                    messages.addMessage("Connecting to GIS using active ArcGIS Pro connection...")
                    gis = GIS("pro")
                    messages.addMessage(f"Connected as: {gis.properties.user.username}")
            
                    # Search for the web map by title
                    messages.addMessage(f"Searching for web map: {webmap_title}")
                    webmap_search = gis.content.search(
                        query=f"title:\"{webmap_title}\"", item_type="Web Map"
                    )
            
                    if not webmap_search:
                        messages.addWarningMessage(f"No web maps found with title '{webmap_title}'")
                    else:
                        # Find exact match for the web map
                        webmap_item = None
                        for item in webmap_search:
                            if item.title == webmap_title:
                                webmap_item = item
                                break
                
                        if not webmap_item:
                            messages.addWarningMessage(f"No exact match for web map title '{webmap_title}'")
                        else:
                            messages.addMessage(f"Found web map: {webmap_item.title} (ID: {webmap_item.id})")
                    
                            # Get the web map JSON
                            webmap_data = webmap_item.get_data()
                    
                            if not webmap_data or "operationalLayers" not in webmap_data:
                                messages.addWarningMessage("Web map data is invalid or has no operational layers")
                            else:
                                # Make a copy for modification
                                updated_map_json = copy.deepcopy(webmap_data)
                            
                                # Print all operational layer titles for debugging
                                messages.addMessage("Available layers in web map:")
                                for idx, lyr in enumerate(updated_map_json["operationalLayers"]):
                                    messages.addMessage(f"  {idx}: {lyr.get('title', 'Unnamed')}")
                            
                                # Try multiple approaches to find the target layer
                                target_layer = None
                            
                                # 1. Try exact match on full name
                                target_layers = [
                                    lyr for lyr in updated_map_json["operationalLayers"]
                                    if lyr.get("title") == layer_name
                                ]
                            
                                # 2. If not found, try with simple name (no path)
                                if not target_layers:
                                    target_layers = [
                                        lyr for lyr in updated_map_json["operationalLayers"]
                                        if lyr.get("title") == simple_layer_name
                                    ]
                                
                                # 3. If still not found, try case-insensitive match
                                if not target_layers:
                                    target_layers = [
                                        lyr for lyr in updated_map_json["operationalLayers"]
                                        if lyr.get("title", "").lower() == simple_layer_name.lower()
                                    ]
                                
                                # 4. If still not found, try partial match (layer name is contained in title)
                                if not target_layers:
                                    target_layers = [
                                        lyr for lyr in updated_map_json["operationalLayers"]
                                        if simple_layer_name.lower() in lyr.get("title", "").lower()
                                    ]
                        
                                if not target_layers:
                                    messages.addWarningMessage(f"Layer '{layer_name}' not found in web map using any matching method")
                                else:
                                    # Get the first matching layer
                                    target_layer = target_layers[0]
                                    messages.addMessage(f"Found matching layer: {target_layer.get('title')}")
                            
                                    # Make sure layerDefinition exists
                                    if "layerDefinition" not in target_layer:
                                        target_layer["layerDefinition"] = {}
                            
                                    # Set the definition expression
                                    target_layer["layerDefinition"]["definitionExpression"] = sql
                                    messages.addMessage(f"Setting definition expression: {sql}")
                            
                                    # Update the web map
                                    webmap_item.update(item_properties={"text": dumps(updated_map_json)})
                                    messages.addMessage(" Filter applied to web map layer successfully")
                                    applied = True
    
            except Exception as e:
                messages.addWarningMessage(f"Failed to update web map: {str(e)}")
        
                # Try applying directly to the layer object as a last resort
                try:
                    if hasattr(lyr_obj, "definitionQuery"):
                        lyr_obj.definitionQuery = sql
                        messages.addMessage("Applied filter directly to layer object")
                        applied = True
                except Exception as ex:
                    messages.addWarningMessage(f"Failed to apply to layer object: {str(ex)}")

        if not applied:
            raise RuntimeError(f"Layer '{layer_name}' not found or unsupported.")

        messages.addMessage(f" SQL applied:\n    {sql}")
0 Kudos
1 Solution

Accepted Solutions
TristanJonas
New Contributor

I'm going to mark this as solved because I got it working, but anyone else who stumbles across this I'd like to take a few moments to explain some of my misconceptions that are probably obvious to most but they still made this take longer for me to figure out than it otherwise would have, maybe I can save someone else some time here.

1. You don't have to publish the tool on the portal to test the API. Even though I was making commands that were designed to act on the currently open Web Experience/Web App, I could still run tests at least. When I wanted to test a tool, I put it through the whole rigamarole of making a pyt and adding it to arcgis pro, then testing it (which threw me off, since how can you test a ArcGIS Web Definition Query command in ArcGIS Pro that was designed to work with ArcGIS Web? If you want to filter a layer, wouldn't it just say "Layer not found" or something since you're not testing it in the Web environment, and a successful test is the prerequisite to being able to upload it to the Portal? The whole thing made my head swim but it turns out the answer is "do it anyway, it won't do anything in ArcGIS Pro but it'll still execute as a success, you can upload it to the Web from there and it should work.) then uploading it to the web, then readding it in the web experience and testing it out in the test play. It was a lot that I did every time that I didn't have to do, but that's my mistake.

2. One of the important tests was getting all the content info of a specific ArcGIS Web object. Sometimes there were unexpected intermediary objects (Like the Map name you need to go through to get to the layer you need in a Web Map, I spent a good while wondering why my script couldn't find the Layer I was telling it to and it turned out it was because some other objects were in the way.

Here's the basic code I used to get the list of layers/objects/whatever I needed to pick from:

from arcgis.gis import GIS
import json

# Connect to your active Enterprise or AGOL session
gis = GIS("home")

# Load a fake Web Experience app (realistic demo ID)
app_id = "a1b2c3d4e5f6478899abcdef12345678"
app_item = gis.content.get(app_id)

print(f":mobile_phone: App: {app_item.title}")
print("🔍 Parsing embedded data sources...\n")

# Load the JSON config from the Web Experience item
data_json = app_item.get_data()

# Helper: recursively extract all 'itemId' references
def extract_datasources(data):
    seen = set()
    if not isinstance(data, dict):
        return seen

    def extract_ids(obj):
        if isinstance(obj, dict):
            for k, v in obj.items():
                if k == "itemId" and isinstance(v, str):
                    seen.add(v)
                else:
                    extract_ids(v)
        elif isinstance(obj, list):
            for entry in obj:
                extract_ids(entry)

    extract_ids(data)
    return seen

# Get all referenced item IDs
referenced_ids = extract_datasources(data_json)

print("🧾 Referenced Item IDs:\n")
for rid in referenced_ids:
    try:
        item = gis.content.get(rid)
        print(f"- {item.title} ({item.type}) | ID: {item.id}")
    except:
        print(f"- (Unknown or inaccessible item) | ID: {rid}")


3. I basically just chipped away at each object, seeing what was inside it until I got to where I needed to be, and from there I added the SQL and webupdate method and it appeared to do the trick.

4. One other thing I didn't understand was that you need to be careful using Authorization commands, all the examples I saw of people with similar problems used a url/username/password auth approach, but if you're part of an organization then you want to just use the "home" approach, shown below

Here's the final code (edited for privacy)

import arcpy
from arcgis.gis import GIS
from arcgis.mapping import WebMap

# ----------------------------------------------------------------------
class Toolbox(object):
    def __init__(self):
        self.label = "Web Map Filter Tools"
        self.alias = "webmapfilters"
        self.tools = [ApplyStateFilter]

# ----------------------------------------------------------------------
class ApplyStateFilter(object):
    def __init__(self):
        self.label = "Apply State Filter to Web Map"
        self.description = "Applies a definition filter to a nested layer in a web map."

    def getParameterInfo(self):
        return []

    def isLicensed(self):
        return True

    def execute(self, params, messages):
        # Connect using current session
        gis = GIS("home")

        # Example web map and layer configuration (demo values)
        webmap_id = "abc123def4567890ghi123jkl456mnop"  # fake but realistic Web Map ID
        group_title = "Public Data Layers"              # fake group layer name
        layer_title = "Census Blocks - Race"            # fake sublayer title

        messages.addMessage(f"🔎 Connecting to Web Map: {webmap_id}")
        webmap_item = gis.content.get(webmap_id)
        webmap = WebMap(webmap_item)

        # Find the group layer
        group_layer = None
        for lyr in webmap.definition["operationalLayers"]:
            if lyr.get("title") == group_title:
                group_layer = lyr
                break

        if not group_layer:
            raise arcpy.ExecuteError(f" Group layer '{group_title}' not found.")

        # Find the target sublayer
        target_layer = None
        for sublayer in group_layer.get("layers", []):
            if sublayer.get("title") == layer_title:
                target_layer = sublayer
                break

        if not target_layer:
            raise arcpy.ExecuteError(f" Sublayer '{layer_title}' not found in group '{group_title}'.")

        # Apply the definition expression
        target_layer["layerDefinition"] = target_layer.get("layerDefinition", {})
        target_layer["layerDefinition"]["definitionExpression"] = "StateName = 'Ohio'"

        webmap.update()
        messages.addMessage(f" Applied filter: StateName = 'Ohio' on '{layer_title}' in group '{group_title}'")

 

View solution in original post

0 Kudos
1 Reply
TristanJonas
New Contributor

I'm going to mark this as solved because I got it working, but anyone else who stumbles across this I'd like to take a few moments to explain some of my misconceptions that are probably obvious to most but they still made this take longer for me to figure out than it otherwise would have, maybe I can save someone else some time here.

1. You don't have to publish the tool on the portal to test the API. Even though I was making commands that were designed to act on the currently open Web Experience/Web App, I could still run tests at least. When I wanted to test a tool, I put it through the whole rigamarole of making a pyt and adding it to arcgis pro, then testing it (which threw me off, since how can you test a ArcGIS Web Definition Query command in ArcGIS Pro that was designed to work with ArcGIS Web? If you want to filter a layer, wouldn't it just say "Layer not found" or something since you're not testing it in the Web environment, and a successful test is the prerequisite to being able to upload it to the Portal? The whole thing made my head swim but it turns out the answer is "do it anyway, it won't do anything in ArcGIS Pro but it'll still execute as a success, you can upload it to the Web from there and it should work.) then uploading it to the web, then readding it in the web experience and testing it out in the test play. It was a lot that I did every time that I didn't have to do, but that's my mistake.

2. One of the important tests was getting all the content info of a specific ArcGIS Web object. Sometimes there were unexpected intermediary objects (Like the Map name you need to go through to get to the layer you need in a Web Map, I spent a good while wondering why my script couldn't find the Layer I was telling it to and it turned out it was because some other objects were in the way.

Here's the basic code I used to get the list of layers/objects/whatever I needed to pick from:

from arcgis.gis import GIS
import json

# Connect to your active Enterprise or AGOL session
gis = GIS("home")

# Load a fake Web Experience app (realistic demo ID)
app_id = "a1b2c3d4e5f6478899abcdef12345678"
app_item = gis.content.get(app_id)

print(f":mobile_phone: App: {app_item.title}")
print("🔍 Parsing embedded data sources...\n")

# Load the JSON config from the Web Experience item
data_json = app_item.get_data()

# Helper: recursively extract all 'itemId' references
def extract_datasources(data):
    seen = set()
    if not isinstance(data, dict):
        return seen

    def extract_ids(obj):
        if isinstance(obj, dict):
            for k, v in obj.items():
                if k == "itemId" and isinstance(v, str):
                    seen.add(v)
                else:
                    extract_ids(v)
        elif isinstance(obj, list):
            for entry in obj:
                extract_ids(entry)

    extract_ids(data)
    return seen

# Get all referenced item IDs
referenced_ids = extract_datasources(data_json)

print("🧾 Referenced Item IDs:\n")
for rid in referenced_ids:
    try:
        item = gis.content.get(rid)
        print(f"- {item.title} ({item.type}) | ID: {item.id}")
    except:
        print(f"- (Unknown or inaccessible item) | ID: {rid}")


3. I basically just chipped away at each object, seeing what was inside it until I got to where I needed to be, and from there I added the SQL and webupdate method and it appeared to do the trick.

4. One other thing I didn't understand was that you need to be careful using Authorization commands, all the examples I saw of people with similar problems used a url/username/password auth approach, but if you're part of an organization then you want to just use the "home" approach, shown below

Here's the final code (edited for privacy)

import arcpy
from arcgis.gis import GIS
from arcgis.mapping import WebMap

# ----------------------------------------------------------------------
class Toolbox(object):
    def __init__(self):
        self.label = "Web Map Filter Tools"
        self.alias = "webmapfilters"
        self.tools = [ApplyStateFilter]

# ----------------------------------------------------------------------
class ApplyStateFilter(object):
    def __init__(self):
        self.label = "Apply State Filter to Web Map"
        self.description = "Applies a definition filter to a nested layer in a web map."

    def getParameterInfo(self):
        return []

    def isLicensed(self):
        return True

    def execute(self, params, messages):
        # Connect using current session
        gis = GIS("home")

        # Example web map and layer configuration (demo values)
        webmap_id = "abc123def4567890ghi123jkl456mnop"  # fake but realistic Web Map ID
        group_title = "Public Data Layers"              # fake group layer name
        layer_title = "Census Blocks - Race"            # fake sublayer title

        messages.addMessage(f"🔎 Connecting to Web Map: {webmap_id}")
        webmap_item = gis.content.get(webmap_id)
        webmap = WebMap(webmap_item)

        # Find the group layer
        group_layer = None
        for lyr in webmap.definition["operationalLayers"]:
            if lyr.get("title") == group_title:
                group_layer = lyr
                break

        if not group_layer:
            raise arcpy.ExecuteError(f" Group layer '{group_title}' not found.")

        # Find the target sublayer
        target_layer = None
        for sublayer in group_layer.get("layers", []):
            if sublayer.get("title") == layer_title:
                target_layer = sublayer
                break

        if not target_layer:
            raise arcpy.ExecuteError(f" Sublayer '{layer_title}' not found in group '{group_title}'.")

        # Apply the definition expression
        target_layer["layerDefinition"] = target_layer.get("layerDefinition", {})
        target_layer["layerDefinition"]["definitionExpression"] = "StateName = 'Ohio'"

        webmap.update()
        messages.addMessage(f" Applied filter: StateName = 'Ohio' on '{layer_title}' in group '{group_title}'")

 

0 Kudos