Select to view content in your preferred language

Creating an Addin Extension that Responds to a Python Script

4342
19
08-23-2012 04:27 PM
RichardFairhurst
MVP Alum
I want to know if it is possible to create an Add-in extension that listens for map refresh events to specifically respond to the actions of a python script?

I envision adding a hidden text object into a specific kind of map that will at first indicate to the Add-in Extension that nothing needs to be done.  When the Python script runs it will write to that text object to set a flag telling the Extension that it is processing a change to the map, so ignore any map refresh actions during the script processing.  Just prior to completion of the Python script, the Python script will update the Text Object to say that it just finished and needs the Extension to do its thing.  Then the Python script will refresh the map.  I assume the Add-in Extension can detect the map refresh event triggered by the Python script, read the text object to detect that python wants the extension to run and then the extension can fire its action on the map.  When the extension finishes it will reset the text object back to the state indicating that nothing needs to be done by the extension.

Does this sound like it will work?

I want the Add-in to detect when arcpy.mapping finishes updating everything that it can in the map, and then have the extension change the data frame clipping geometry to match a feature's shape according to the current definition query on a layer that the python script set.  I do not want the user to have to push another button after the Python script finishes to do things that Python seemingly cannot do, and I do not want the Add-in to do the Python script actions.  I do not see any other way to have Python trigger an ArcObjects action within the currently open map, since the ctypes functionality says it is limited to basic C++ types and I do not see a way to pass a hook for the current map to an ArcObjects DLL method from Python.

Please correct me if I am wrong about any of this or if there is a better approach to do what I want.
0 Kudos
19 Replies
RichardFairhurst
MVP Alum
In .NET maybe it is hard to capture every possible user "oops".

In Java, it's straight-forward.

DecimalFormat.. DateFormat..  fun stuff.

If you enter junk into this text field, it will default to 0.00

        // Create an instance of DecimalFormat to limit the number of integers the user can type into certain fields.
        DecimalFormat temperatureFormat = new DecimalFormat("##0.00");

        ftfTemperature = new JFormattedTextField(new DefaultFormatterFactory(new NumberFormatter(temperatureFormat),new NumberFormatter(temperatureFormat),new NumberFormatter(temperatureFormat)));
        ftfTemperature.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                SwingUtilities.invokeLater(new Runnable(){
                    @Override
                    public void run() {
                        ftfTemperature.selectAll();
                    }
                });
            }
        });
        ftfTemperature.setBounds(120, 8, 85, 20);
        panel.add(ftfTemperature);


Lines of Python code to restrict a script tool field to a long = None: Define the input as a Long in the parameter dialog.
Lines of Python code to restrict a script tool field to a date = None: Define the input as a Date in the parameter dialog.

Hard to get any simpler than that.

Since I have no background in using Java I can't say the code above gives me confidence that I could easily convert what I have done to a Java add-in, but I will start looking into that as another option.
0 Kudos
LeoDonahue
Deactivated User
This prints the title of the only mxd open on my machine, index = 0.  You would probably have to make sure you have the right mxd application name by iterating over the count of open mxds and getting the title.

public class Console {

    public static void main(String[] args) {
        EngineInitializer.initializeEngine();
        initializeArcGISLicenses();
        
        try {
            IAppROT aprot = new AppROT();
            
            IApplication application = aprot.getItem(0);
            
            System.out.println(application.getDocument().getTitle());
            
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static void initializeArcGISLicenses() {
        try {
            com.esri.arcgis.system.AoInitialize ao = new com.esri.arcgis.system.AoInitialize();
            if (ao.isProductCodeAvailable(com.esri.arcgis.system.esriLicenseProductCode.esriLicenseProductCodeArcView) == com.esri.arcgis.system.esriLicenseStatus.esriLicenseAvailable)
                ao.initialize(com.esri.arcgis.system.esriLicenseProductCode.esriLicenseProductCodeArcView);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
0 Kudos
LeoDonahue
Deactivated User
Lines of Python code to restrict a script tool field to a long: Define the input as a Long.
Lines of Python code to restrict a script tool field to a date: Define the input as a Date.

Hard to get any simpler than that.

True.  Got me there.
0 Kudos
RichardFairhurst
MVP Alum
This prints the title of the only mxd open on my machine, index = 0.  You would probably have to make sure you have the right mxd application name by iterating over the count of open mxds and getting the title.

public class Console {

    public static void main(String[] args) {
        EngineInitializer.initializeEngine();
        initializeArcGISLicenses();
        
        try {
            IAppROT aprot = new AppROT();
            
            IApplication application = aprot.getItem(0);
            
            System.out.println(application.getDocument().getTitle());
            
        } catch (UnknownHostException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    static void initializeArcGISLicenses() {
        try {
            com.esri.arcgis.system.AoInitialize ao = new com.esri.arcgis.system.AoInitialize();
            if (ao.isProductCodeAvailable(com.esri.arcgis.system.esriLicenseProductCode.esriLicenseProductCodeArcView) == com.esri.arcgis.system.esriLicenseStatus.esriLicenseAvailable)
                ao.initialize(com.esri.arcgis.system.esriLicenseProductCode.esriLicenseProductCodeArcView);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


Do you know how to call this console application from the Python side and have Python provide a parameter?  Would the Python ctypes interface be the way to go?  If so then this seems to have everything I would need to chain the two together and from there could decide what I wanted Python to do and what I wanted the console application to do.
0 Kudos
LeoDonahue
Deactivated User
Doesn't python have an OS module that you can call?

os.system(your application, your parameter)
0 Kudos
RichardFairhurst
MVP Alum
Doesn't python have an OS module that you can call?

os.system(your application, your parameter)


My question is now answered and the solution is much better than my original idea.

Using the method below I can now create a Python script that calls an ArcMap Console Application and that passes an argument with a name of the map that Python wants the Console Application to update.  The Console Application is able use the IAppROT interface to get an application hook to the Map that Python wants updated and can then do things using ArcObjects code that arcpy mapping cannot do.  Python can pass as many additonal standard C type (char, int, date, etc.) arguments that the Console Application may need to coordinate its actions with the Python script.  I am fairly certain that the Console application can pass back a result to Python using the method outlined below as well.

Therefore, there is no need to wait on the user to do anything to coordinate the triggering of the code, or to use an Add-in extension event listener, or store notification text objects in the map to handle communications between Python and ArcObjects.

The Python subprocess.check_call method works for calling an ArcMap Desktop Console Application with arguments in this format:

from subprocess import check_call
check_call(["DesktopConsoleApplication1.exe", "Collision_Segment_Diagram2.mxd"])


The following ArcMap Desktop Console Application code worked to notify me whether or not a map is open and whether or not it matches the title python passed as an agrument to the console application:

using System;
using System.Collections.Generic;
using System.Text;
using ESRI.ArcGIS.esriSystem;
using ESRI.ArcGIS.Framework;

namespace DesktopConsoleApplication1
{
    class Program
    {
        private static LicenseInitializer m_AOLicenseInitializer = new DesktopConsoleApplication1.LicenseInitializer();

        [STAThread()]
        static void Main(string[] args)
        {
            //ESRI License Initializer generated code.
            m_AOLicenseInitializer.InitializeApplication(new esriLicenseProductCode[] { esriLicenseProductCode.esriLicenseProductCodeArcView, esriLicenseProductCode.esriLicenseProductCodeArcEditor, esriLicenseProductCode.esriLicenseProductCodeArcInfo },
            new esriLicenseExtensionCode[] { });
            //ESRI License Initializer generated code.
        try
        {
            IAppROT aprot = new AppROT();
            IApplication application = null;
            for(int a = 0; a < aprot.Count; a++)
            {
                application = aprot.get_Item(a);
                System.Console.WriteLine("Application Title = " + application.Document.Title);

                foreach (string curItem in args)
                {
                    System.Console.WriteLine("Args: " + curItem);
                    if (application.Document.Title == curItem)
                    {
                         System.Console.WriteLine("The map was found!  Do something!");
                    }
                    else
                    {
                        System.Console.WriteLine("The open map does not match that title.");
                    }
                }
            }
            if (aprot.Count == 0)
            {
                System.Console.WriteLine("No ArcMap application is open");
            }
            //System.Console.ReadLine();  //Uncomment this line if you want to have the console pause before closing.
        }
        catch (Exception e)
        {
            Console.WriteLine("{0} Exception caught.", e);
        }
            //Do not make any call to ArcObjects after ShutDownApplication()
            m_AOLicenseInitializer.ShutdownApplication();
        }
    }
}
0 Kudos
RichardFairhurst
MVP Alum
I spoke too soon. While the script and console application work perfectly when I run the Python script from Idle, it hangs when I run the same code from within a script tool. The Console application window opens, but the parameters never get read when the script tool runs the code.

Anyway, I know how to get the Data Frame Clip Geometry updated from the ArcObjects side, but I still need to find a way to connect it automatically to the Python code at runtime.

If the parameters are all that is hanging it up, I may be able to eliminate the need for any parameters on the Console application by using the original technique I suggested of using an intermediate text object for passing parameter information between Python and ArcObjects.  That would be acceptable for my needs.  However, I will have to do additional testing to be certain that the Console application will run from a script tool and not hang if parameters are eliminated.

Edit:

It hangs without parameters as well from a script tool.  Now I am looking at the subprocess options to see if it can create a shell that works within the script tool, but I am not optimistic.

Edit2:

The shell option causes an error and using call instead of check_call hangs also.  So it looks like a script tool does not work with a Console application at all.  Bummer.
0 Kudos
RichardFairhurst
MVP Alum
In .NET maybe it is hard to capture every possible user "oops".

In Java, it's straight-forward.

DecimalFormat.. DateFormat..  fun stuff.

If you enter junk into this text field, it will default to 0.00

        // Create an instance of DecimalFormat to limit the number of integers the user can type into certain fields.
        DecimalFormat temperatureFormat = new DecimalFormat("##0.00");

        ftfTemperature = new JFormattedTextField(new DefaultFormatterFactory(new NumberFormatter(temperatureFormat),new NumberFormatter(temperatureFormat),new NumberFormatter(temperatureFormat)));
        ftfTemperature.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                SwingUtilities.invokeLater(new Runnable(){
                    @Override
                    public void run() {
                        ftfTemperature.selectAll();
                    }
                });
            }
        });
        ftfTemperature.setBounds(120, 8, 85, 20);
        panel.add(ftfTemperature);


Leo:

Could you screen shot what this code actually produces as far as an interface that the user would see?  Searching help on Java Add-ins has left me completely in the dark on what this code would end up looking like or how it launches or anything.  Is the Java IDE environment text based only for defining the form or does it have visual drag and drop tools?  I need the real basics to get oriented to decide if I want to even attempt to go down this path.  How is building a Java add-in any different from building a .Net add-in?
0 Kudos
RichardFairhurst
MVP Alum
I have decided to give my original idea a try and set up a listener for the AfterDraw event in an Add-in extension. 

Even though Python's refresh of an ActiveView does not seem to trigger the extension code, it appears that this approach will work because the mere act of closing the geoprocessing results window when the script is done triggers the extension code to detect an AfterDraw event if that dialog covered any portion of the map.  This behavior should be nearly unavoidable.  Even if closing that tool dialog does not work, if anything covers a portion of the map for even a moment, like a messagebox or a dockable window expansion, the code is triggered and should be able to evaluate the map configuration to determine if it needs to execute its custom code.  And failing all of that if any other user action triggers a refresh the code will fire.

At this point I mainly need to identify the optimum code sequence to ensure that the AfterDraw event always fires in connection with the proper ActiveView (the LayoutView) and that it quickly determines when it does or does not need to implement its custom behavior.  The map must be in Layout view because the Python code I have written ensures this is the view when it finishes running, and if that is the case it should then determine if the Text object exists in the map, and if that is the case it should detect if the Text object has the proper information for executing the code.  If there are any suggestions on how to best optimize such code, I would appreciate them.  I will continue experimenting in the meantime.
0 Kudos
LeoDonahue
Deactivated User
Leo:
Could you screen shot what this code actually produces as far as an interface that the user would see?

Weather screenshot is attached.


Searching help on Java Add-ins has  left me completely in the dark on what this code would end up looking  like or how it launches or anything.

The code is not Java "Add-in" code.  It is Java Swing.

Is the Java IDE environment text  based only for defining the form or does it have visual drag and drop  tools?  I need the real basics to get oriented to decide if I want to  even attempt to go down this path.

It has visual drag and drop.  I'm running Eclipse 3.7.1  See ide screenshot.

How is building a Java add-in any  different from building a .Net add-in?

I was about to dabble in building a .NET add-in over the weekend to answer that question for myself.  Aside from the obvious IDE configuration differences and language patterns, add-ins should be relatively the same.  I think the ArcObjects API still favors COM when it comes to GUI development, and that is based on my experience with displaying Java Dialogs to the end user of Add-ins within ArcMap, specifically JOptionPane.  I will say that my specific issue with JOptionPane is partly due to the JRE living on top of a COM operating system.  I've seen the C#.NET Visual Studio Express IDE enough to know that switching from Java to C# using Visual Studio Express would be a pain getting used to the way you work with the add-in config.xml file and the way you refactor, and then there is all of the fun Windows issues still looming about in .NET when it comes to registering dlls.  But anything you learn for the first time sticks with you as the "easy way".
0 Kudos