Tuesday, April 28, 2015

HSPEXP+ Released!

I was recently notified that the new version of HSPEXP, HSPEXP+, has been released by AQUA TERRA!  They retained a lot of the work I did coding up the old advice (and adding some new advice) and designing the user interface so listed me as an author.  I'm excited to see all of their efforts come to fruition, and for the modeling world to finally have a modern version of my favorite calibration system!  Go check it out!

Thursday, May 8, 2014

ArcGIS 10.1 - Geoprocessing Quirks and Solutions

Well, this one stumped me for a good week, so I figured time to return to the blog!


At long last, my colleagues at the City all upgraded to ArcGIS 10.1 SP1.  So I figured it was high time I follow suit.


Unbeknownst to me at the time, the 10.1 upgrade apparently did some small tweaks to the geoprocessor's background processing.  As a result, certain tools no longer worked when run synchronously - most notably for me, the Dissolve and Erase tools.  I had to call ESRI support before we figured out this one - see my StackExchange post on the subject.  Long story short, this was a known bug (presumably fixed at 10.2, which we won't be upgrading to for a LOOOONG time), and the support technician suggested running the tools Asynchronously rather than synchronously - and poof! problem solved.  Kind of.


So I went about my merry way completely and totally restructuring my code to support the asynchronous calls to the geoprocessor.  I did the minimal restructuring, creating the geoprocessing tools as I went along.  When I finally finished this rather monumental task, I discovered that I could no longer establish a schema lock on my feature classes, and once I just did away with the schema locks, that I could no longer call insert and update cursors, and finally once I switched those to select cursors, that I could no longer create a feature on a new feature class.  It was this last one that presented no workarounds that I could find and finally broke me down into a full day of testing and attempting to isolate the cause of my woes.


After extensive testing, it appears that my nemesis the Dissolve tool was somewhat to blame.  I'm still not sure entirely why...but if the Dissolve tool ran, all the processes I just listed would not.  If I used an existing file and did not run Dissolve, all the processes would run just fine.
After hours and hours of experimentation and googling, I finally ran across this one ever-so-important comment in the code listing here


To run multiple GP tools asynchronously, all tool inputs must exist before ExecuteAsync is called.




As I mentioned before, I was just creating the tools as I needed them as the code progressed.  I had read this line so many times and it didn't trigger anything in my brain...but finally, after everything else had been exhausted, I paid attention.  I moved all my tool declarations (Clip, Dissolve, and Intersect) to my main method and set them up immediately after I declared the geoprocessor.  As I created them, I stored them in a Queue as suggested by the code sample.  Then to run the tools, I called the geoprocessor with the dequeued processes as I needed them.


Poof!  Problem solved.  I was able to re-enable my schema locks, insert cursors, update cursors, and ... most importantly... my feature creation. 


And the moral of the story is....read all the comments in ESRI's code samples, no matter how minor... 



Monday, October 14, 2013

OpenXML: Get Column.Width Property for a Spreadsheet

I searched high and low today looking for the answer to what I thought was a simple question: if I know the width of a column in pixels, how do I convert that to a Double for use with the Column.Width property of a column in OpenXML?

I found this article by Vincent that I thought would be useful.  The problem, though, is that the Graphics.MeasureString method he mentioned returns a length that includes padding (I kept on getting about 12.5px for Calibri 11, which I know from the Column description should be 7px).  I couldn't get the Graphics.MeasureCharacterRanges to give me anything at all useful (as recommended at this link by one of the commentator's on Vincent's post).

Finally I stumbled upon a useful comment by AtmaWeapon.  His comment to use a different overload of Graphics.MeasureString did not work for me; however, using TextRenderer.MeasureText was a brilliant suggestion!  Finally something that works! 

Here's my final code (including the conversion I simplified from the Column description):

Public Function ConvertPxToXLSWidth(ByVal dblPxWidth As Double, ByVal strFontName As String, ByVal dblFontSize As Double) As Double
        'thanks to
http://polymathprogrammer.com/2010/01/18/calculating-column-widths-in-excel-open-xml/ for some initial ideas
        Dim dblWidthOfZero As Double = 0.0
        Dim dblTruncWidth As Double = 0.0
        Try
            Dim drawfont As System.Drawing.Font = New System.Drawing.Font(strFontName, dblFontSize)
            'need a graphics object to measure the font size
            Dim g As Graphics = Graphics.FromImage(New Bitmap(200, 200))
            g.PageUnit = GraphicsUnit.Pixel
            dblWidthOfZero = TextRenderer.MeasureText(g, "0", drawfont, New Size(Integer.MaxValue, Integer.MaxValue), TextFormatFlags.NoPadding).Width
            g.Dispose()
            ' Excel help says: width = Truncate([{Number of Characters} * {Maximum Digit Width} + {5 pixel padding}]/{Maximum Digit Width}*256)/256
            'the numerator is just to get the total number of pixels
            'so, since I know the total number of pixels, my calculation becomes:
            'Truncate({pixel width}/{Maximum Digit Width}/256) * 256
            dblTruncWidth = Math.Truncate(dblPxWidth / dblWidthOfZero * 256) / 256
        Catch ex As Exception
            dblTruncWidth = 8.43 'default width for most Excel installations
        End Try
        Return dblTruncWidth
    End Function

Tuesday, October 8, 2013

ArcPad 10.2 Fun

I know I haven't posted in a LONG while, but I've encountered a problem with ArcPad 10.2 that I haven't found ANYTHING on while Googling, and since this version is so new, I thought it would be good to get this information out ASAP.

First, let me set up how this problem manifested.  I have an ArcPad 'application' (read: AXF file paired with Applet) that handles new and existing feature creation to display some pre-filled information on an EDITFORM.  This information involves retrieving the actual new/existing feature shape.

Previously (i.e., ArcPad 10.0), I identified the selected feature through the Map.SelectionBookmark property, and applied that bookmark to the Map.SelectionLayer.Records (the SelectionLayer is my layer of interest).  (On a side note...I use the SelectionLayer instead of the EditLayer because if the user has two layers active for editing - e.g., a point and a line layer - ArcPad doesn't seem to be able to tell that the current open form is for one or the other.)  This worked just fine regardless of whether this was a new feature or an existing feature.  A new feature of course does not have a record yet; however, in the interests of minimizing subroutines, I dealt with that case right before retrieving the value I needed from the feature (pulling it from the EDITFORM instead), and the code execution still passed through the record selection.  Everything worked just fine.

Upgrade to 10.2. 

All of the sudden my EDITFORM was cleared of some previously filled fields when I created a new feature.  I had populated the current date and user in a DateTime control and a ComboBox control.  These were wiped as soon as I attempted to set the bookmark of the line layer's recordset.  Mind you, I hadn't DONE anything with that information yet - it was just the act of setting the recordset's bookmark to an invalid number (-1, which is what Map.SelectionBookmark is for a new feature).  My theory is that this somehow disconnected the EDITFORM from the new feature (because the bookmark of the recordset associated with the EDITFORM was set to something invalid), but I don't have the time to test it.  The odd thing is that I can test the value of the controls at any point and it always comes out correctly - it just won't actually display.

SO, the SOLUTION!

To fix this, before setting the recordset bookmark in my function designed for that purpose, I test to see whether Map.SelectionBookmark is -1 (the value for a new feature).  If it is, I do not attempt to retrieve the recordset and simply pass a value of Nothing back for my recordset.  My main code then handles the Nothing value for the recordset by pulling from the EDITFORM's shape field instead.

Sunday, November 11, 2012

Excel VBA and Reference Styles


I encountered something new with Excel reference styles (i.e., A1 vs. R1C1) the other day.  I've mostly used the reference styles when using the Cells.Formula or Cells.FormulaR1C1 properties...for these, you use A1 for the former and R1C1 for the latter, and most importantly, it doesn't matter what reference style you've actually specified in the workbook - Excel just figures it out.

The other day, I tried to use the Range.Validation.Modify function in VBA for the first time.  As is my custom (for ease of programming), I had set the reference style of the workbook to R1C1 before starting.
  
However, not thinking much about it, in the formula of the Modify command, I used A1 style - mostly because Excel had never cared which way I went with the Formula properties, also because I figure this is the style the end user will have.

BIG problem, evil error...I got a nondescript '400' error message.  Debugging showed that the error description was "Application-defined or object-defined error"...big help there.  Hours and hours of Googling were no help.
 
At long last, I thought to check the reference style.  Voila!  When I changed the reference style to R1C1, the code worked!  So, unlike the Formula/FormulaR1C1 property of Cells, apparently Excel will NOT convert your reference style on the fly for the Formula1 property of the Range.Validation.Modify method.
 
So, this wasn't working:
    Dim userValidation As Validation
    Set userValidation = ActiveSheet.Range("C1").Validation
    userValidation.Modify xlValidateList, xlValidAlertStop, _
        xlBetween, "=Lists!$G$2:$G$20"
 
BUT this does:
    Dim userValidation As Validation
    Set userValidation = ActiveSheet.Range("C1").Validation
    userValidation.Modify xlValidateList, xlValidAlertStop, _
        xlBetween, "=Lists!R2C7:R20C7"
 
(when the workbook is set to R1C1 notation)
 
Of course, not knowing what the end user might have, it's necessary to do a check:
Dim userValidation As Validation
Set userValidation = ActiveSheet.Range("C1").Validation
If Application.ReferenceStyle = xlA1 Then
userValidation.Modify xlValidateList, xlValidAlertStop, _
xlBetween, "=Lists!$G$2:$G$20"
Else 'ReferenceStyle = xlR1C1
    userValidation.Modify xlValidateList, xlValidAlertStop, _
xlBetween, "=Lists!R2C7:R20C7"
End If
 
I hope this saves some other poor person from hours of Googling!  It is really such a simple thing, but it didn't occur to me for so long!

Sunday, July 29, 2012

Labeling Supporting Layers in ArcPad

Googling didn't help me much on this topic so I thought it might be worth sharing...regarding labeling background layers for data exported from ArcMap to ArcPad using the ArcPad Data Manager extension ("Get Data for ArcPad" tool) for ArcGIS 10.

The scenario that prompted my discovery was this: I had an ArcMap document that contained the geodatabase feature classes that I needed to check out as well as several supporting layers - parcels, streets, streams, etc.  I wanted the streets to be labeled, but did not need to check them out for editing.  I labeled the streets within the ArcMap file.

Initially I tried exporting the streets as a background shapefile.  However, the labels did not transfer when I opened the exported map file in ArcPad.  After a bit of experimenting I discovered that exporting the streets as a background AXF file kept the labels intact. I suppose in retrospect this does make sense, as the AXF files are supposed to be these wonderful all-encompassing databases.

So there you go, if you want to label your background layers in an export for ArcPad, make sure to export them to an AXF file!

Friday, June 29, 2012

Capturing Button Clicks in ArcObjects

It's been quite a while since I've posted...I've been busily moving myself to North Carolina for a contract position as a GIS Developer with the City of Charlotte.  I'm working through a company called Systemtec.  Everything is going great and Charlotte is a lot of fun!  But with all the commotion involved in moving to a new city, this blog fell to the wayside!

This week, I found something fun in ArcObjects that I thought I should share, as it took a bit of Googling and then some guessing to figure out, so clearly there need to be more posts on the topic!  In particular I found it difficult to find an example of this for VB.net in ArcGIS 10 (rather than 8.3 or VBA) - the syntax is a little different.

My goal in doing this was to determine when the user clicked the Merge button on the Editor menu in ArcMap (for context, I am developing an Editor Extension Add-In for ArcMap 10 using VB.net).  Because Merge works differently from many of the other tools - a bit of experimentation showed that it didn't trigger the OnCurrentTaskChanged event in the Editor - this proved to be a bit problematic.

post by Kirk Kuykendall set me in the right direction - use the ICustomizationFilter interface.  This allows you to listen for ANY button click (and other things - see the ArcGIS help) and react appropriately.  (Note that the button names you'll need are available here.)  It's designed to prevent the user from doing things you don't want him to do (e.g., accessing VBA, clicking buttons, using tools, etc.).  This is done by setting the result of the function to TRUE - this is one of those tidbits that isn't explicitly stated anywhere that I could see, but a true result from the OnCustomizationEvent function prevents the user from doing whatever it was he was trying to do.

In my case, I didn't want to prevent the user from doing anything, I just wanted to KNOW if he did something.  To do this, just return false after executing whatever code you're interested in, and you can detect the button clicks without affecting the usability of the program.  Only trick is that apparently only one customization filter can be active at a time, so if you have a bunch of different add-ins/dlls running that each has its own filter, that could be a problem.

So enough of that, here's the code I wrote to capture the user clicking the button:

First, create a NEW class module and put in code like this:


Public Class clsCustomizationFilter
  Implements ESRI.ArcGIS.Framework.ICustomizationFilter


  Public Function OnCustomizationEvent(custEventType As _
   ESRI.ArcGIS.Framework.esriCustomizationEvent, eventCtx As Object) As Boolean _
   Implements ESRI.ArcGIS.Framework.ICustomizationFilter.OnCustomizationEvent
   
   If custEventType = ESRI.ArcGIS.Framework.esriCustomizationEvent.esriCEInvokeCommand Then
      Dim cmd As ESRI.ArcGIS.Framework.ICommandItem
      cmd = TryCast(eventCtx, ESRI.ArcGIS.Framework.ICommandItem)
      If cmd.Name = "Editor_Merge" Then
 ImperviousEditorExtension.g_blnDeleteOK = True
      End If
   End If


   Return False


  End Function


End Class

The line ImperviousEditorExtension.g_blnDeleteOK = True is where you would put whatever code you want to have run as a result of the button click.  The "Editor_Merge" name can be replaced with the name (from the link above) of any button you want to monitor.  Note the key "Return False" at the end - including this means that you don't stop the user from completing the merge or whatever else he wants to do.  (If for whatever reason you wanted to stop him from doing something, write a condition to check and within that condition Return True.)

To activate this class, take two steps.  First, assuming you're doing this in an editor extension, in the editor extension class create a global variable:
Public Shared m_Filter As ESRI.ArcGIS.Framework.ICustomizationFilter

(if you're not using an editor extension, put this in whatever root module you have)

Second, put this code somewhere that makes sense:

m_Filter = New clsCustomizationFilter
My.ArcMap.Application.LockCustomization("password", m_Filter)
For example, I included it in my OnStartEditing event handler.

Now, to keep things neat and tidy, I unlocked the customization once I was done, so I included the following line in my OnStopEditing event handler:
My.ArcMap.Application.UnlockCustomization("password")

There you go!  A method to detect the user's button click on any button in the interface.  Hope it helps you as much as it did me!!