Wednesday, October 22, 2008

Batch Printing ArcMap MXD Documents

It seems like right clicking on an MXD file in Windows Explorer and trying to print never works right. Here's a simple utility that lets a user select a whole bunch of MXD files and sends them all to the printer without having to open ArcMap. Fast and easy.



This was written in Visual Basic using Visual Studio 2008 and requires that ArcMap 9.3 or later is already installed on the machine.








Here's the full Visual Studio Project:



And here's an installer if you just want the executable:



Here's some sample code that shows how the actual printing function works:


    Private Function PrintMXD(ByVal FullFileName As String) As Integer

        'Function PrintMXD
        '
        'This opens an MXD file specified by FullFileName and sends it to the printer
        'The map will be sent to whatever printer is specified in the MXD document's settings
        '
        'Function returns value 1 for success, 0 for failure

'-------------------------------------------------------------------------------------------------------------

        'Verify that the file exists
        Dim aFileInfo As FileInfo
        Dim DocName As String
        aFileInfo = New FileInfo(FullFileName)
        If aFileInfo.Exists Then
            'Create a short string to send to the printer later
            DocName = aFileInfo.Name
        Else
            MessageBox.Show(FullFileName & " does not exist", "File not found", MessageBoxButtons.OK, MessageBoxIcon.Error)
            PrintMXD = 0
            Exit Function
        End If

 
        '-------------------------------------------------------------------------------------------------------------
        'Get this Application's hWnd

        Dim aHWnd As Long
        Dim m As System.Reflection.Module
        m = Me.GetType.Module
        aHWnd = System.Runtime.InteropServices.Marshal.GetHINSTANCE(m)

        '-------------------------------------------------------------------------------------------------------------
        'Make a MapDocument and open the specified file
        'If this fails, exit the function with error code

        Dim aDoc As IMapDocument = Nothing
        Try
            While aDoc Is Nothing
                aDoc = New MapDocument
            End While
            aDoc.Open(FullFileName)
        Catch ex As Exception
            MessageBox.Show("Unable to start ArcMap..." & vbNewLine & ex.Message, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error)
            PrintMXD = 0
            Exit Function
        End Try
 
        '-------------------------------------------------------------------------------------------------------------
        'When using IMapDocument, it it necessary to Activate the Page Layout and each Map to update the displays
        Dim i As Integer
        Dim pActiveView As IActiveView
 
        'Activate each map
        For i = 0 To aDoc.MapCount - 1
            pActiveView = aDoc.Map(i)
            pActiveView.Activate(aHWnd)
        Next
        'Activate the Page Layout and keep a reference to it
        pActiveView = aDoc.PageLayout
        pActiveView.Activate(aHWnd)
 
        '-------------------------------------------------------------------------------------------------------------
        ' Get the printer's hDC, so we can use the Win32 GetDeviceCaps fuction to
        ' get Printer's Physical Printable Area x and y margins
        ' If this fails for some reason, batch printing won't work
        Dim pPrinter As IPrinter
        Dim hInfoDC As Integer
        Dim dpi As Double
        Try
            pPrinter = aDoc.Printer
            hInfoDC = CreateDC(pPrinter.DriverName, pPrinter.Paper.PrinterName, "", IntPtr.Zero)
            dpi = pPrinter.Resolution
        Catch ex As Exception
            MessageBox.Show("There was a problem with the printer settings for " & FullFileName & vbNewLine & _
                            "Please manually open and print this document.", "Print Settings Error", MessageBoxButtons.OK, MessageBoxIcon.Hand)
            PrintMXD = 0
            Exit Function
        End Try
 
        '-------------------------------------------------------------------------------------------------------------
        Dim docPrinterBounds As ESRI.ArcGIS.Geometry.IEnvelope
        Dim VisibleBounds As ESRI.ArcGIS.Geometry.IEnvelope
        docPrinterBounds = New EnvelopeClass()
        VisibleBounds = New EnvelopeClass()
 
        'Put the printer's physical bounds into docPrinterBounds
        aDoc.PageLayout.Page.GetDeviceBounds(pPrinter, 0, 0, dpi, docPrinterBounds)
 
        'userRect is used to specify the area on the page layout that is to be printed
        Dim userRECT As tagRECT
        userRECT.left = CType((docPrinterBounds.XMin - GetDeviceCaps(hInfoDC, 112)), Integer)
        userRECT.right = CType((docPrinterBounds.XMax - GetDeviceCaps(hInfoDC, 112)), Integer)
        userRECT.bottom = CType((docPrinterBounds.YMax - GetDeviceCaps(hInfoDC, 113)), Integer)
        userRECT.top = CType((docPrinterBounds.YMin - GetDeviceCaps(hInfoDC, 113)), Integer)
 
        ' Transfer offsetted PrinterBounds envelope back to the userRECT
        docPrinterBounds.PutCoords(0, 0, userRECT.right - userRECT.left, userRECT.bottom - userRECT.top)
        aDoc.PageLayout.Page.GetPageBounds(pPrinter, 0, 0, VisibleBounds)
 
        '-------------------------------------------------------------------------------------------------------------
        'Send the Page Layout to the printer
        Dim hDc As Long
        Try
            hDc = pPrinter.StartPrinting(docPrinterBounds, 0)
            pPrinter.SpoolFileName = DocName
            pActiveView.Output(hDc, Nothing, userRECT, VisibleBounds, Nothing)
            pPrinter.FinishPrinting()
        Catch ex As Exception
            MessageBox.Show(FullFileName & vbNewLine & ex.Message, "Print Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
            PrintMXD = 0
            Exit Function
        End Try
 
        PrintMXD = 1
 
    End Function

Wednesday, May 23, 2007

Automating ArcMap

At some point between the release of ArcMap 8.0 and now, automated MXD creation/editing became much easier with ArcMap. Previously, a stand-alone application had to open up a session of ArcMap and then control it remotely through a poorly documented and buggy automation-like process. This wasn't true automation in the MS Office sense, but it was very similar. Unfortunately, it was very complicated to do properly and suffered from inexplicable speed problems - drawing a simple 8 point polygon on a map could take up to 20 minutes!

But now thanks to the IMapDocument interface, it's very easy to create a standalone application that can create a new MXD file or alter an existing one. Pretty much any object an ArcMap document can be accessed - data layers, page layout, graphic elements - you name it. It's still not true automation since you're not actually opening an ArcMap session, but if you really needed to, you could launch your MXD document once the stand-alone program does its job.

How easy is it? Here's a very simple sample that draws a big square on an existing MXD document and then saves it with a different name. From this starting point, you could also add layers, text and whatever crazy stuff you'd want on a map.


SimpleMXDChanger.zip Visual Studio Project


Imports ESRI.ArcGIS.Carto
Imports ESRI.ArcGIS.Geometry

Public Class Form1
Private m_pDoc As IMapDocument

Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click

CreateMXD()
AddRectangle()
SaveDoc()
MessageBox.Show("Done")
m_pDoc = Nothing
Me.Close()
End Sub

Private Sub
CreateMXD()
Dim aDoc As IMapDocument = Nothing
'Create a new MXD Document
Try
While aDoc Is Nothing
aDoc = New MapDocument
End While
Catch ex As Exception
MessageBox.Show(ex.Message, "Error")
End Try
aDoc.Open(Me.tbxOpen.Text)
m_pDoc = aDoc
End Sub

Private Sub
AddRectangle()
'Adds a graphic element to the layout
Dim pPageLayout As IPageLayout
Dim pContainer As IGraphicsContainer
Dim pGraphicElement As IElement
Dim pEnvelope As IEnvelope
'Specify where to put the rectangle (on the page layout)
pPageLayout = m_pDoc.PageLayout
pContainer = pPageLayout
'Create a rectangle

'If you wanted to get fancy, you could change the symbology here
pGraphicElement = New RectangleElement
pEnvelope = New Envelope
pEnvelope.XMax = 8
pEnvelope.XMin = 0.5
pEnvelope.YMax = 8
pEnvelope.YMin = 0.5
pGraphicElement.Geometry = pEnvelope

'Add the rectangle to the page layout
pContainer.AddElement(pGraphicElement, 0)
End Sub

Private Sub
SaveDoc()
'Save the MXD document
m_pDoc.SaveAs(Me.tbxSaveAs.Text)
End Sub

End Class

Monday, May 21, 2007

A better raster clipping and mosaic tool for ArcMap



I've been frustrated with processing raster imagery in ArcMap ever since 8.0 came out way back when. I finally got around to making my own tool that makes clipping and mosaicking much easier and more intuitive. All the relevant info (mosaic type, extent, etc) is right there on the dialog, and most importantly, this tool works seamlessly with raster catalogs. Another nicety is that the output is a plain old TIF file with a TFW file, so that AutoCad puts the image into the right place (for some reason I could never get ArcMap to make a GeoTIFF file that AutoCad Map could place properly).
The only thing I wasn't able to work out satisfactorily is setting the cell size. I wasn't able to have ArcMap do this while performing the mosaic, it would have to be a totally seperate step, which ended up being too time consuming.


Here's the full Visual Studio solution, including an installer:
WSSI_ArcMapTools1.zip

Here's some of the code for anyone that's interested in the inner workings. This is the section that performs the actual mosaic:



private IDataset Mosaic(IRasterCollection pRasterCollection, string strPath, string strName, IEnvelope pEnvelope, double CellSize, string MosaicType)
{
IMosaicRaster pMosaicRaster;
ISaveAs2 pSaveAs;
IDataset pResultData;
IWorkspaceFactory pWkspFact;
IWorkspace pWorkspace;
IRasterProps pRasterProps;

//Set mosaic type
pMosaicRaster = (IMosaicRaster)pRasterCollection;
switch (MosaicType)
{
case "Maximum":
pMosaicRaster.MosaicOperatorType = rstMosaicOperatorType.MT_MAX;
break;
case "Minimum":
pMosaicRaster.MosaicOperatorType = rstMosaicOperatorType.MT_MIN;
break;
case "Mean":
pMosaicRaster.MosaicOperatorType = rstMosaicOperatorType.MT_MEAN;
break;
case "Blend":
pMosaicRaster.MosaicOperatorType = rstMosaicOperatorType.MT_BLEND;
break;
case "First":
pMosaicRaster.MosaicOperatorType = rstMosaicOperatorType.MT_FIRST;
break;
case "Last":
pMosaicRaster.MosaicOperatorType = rstMosaicOperatorType.MT_LAST;
break;
default:
pMosaicRaster.MosaicOperatorType = rstMosaicOperatorType.MT_MAX;
break;
}

//Set the extent
pRasterProps = (IRasterProps)pMosaicRaster;
pRasterProps.Extent = pEnvelope;
pRasterProps.SpatialReference = m_MxDocument.FocusMap.SpatialReference;

//Set Cell Size...
// ... nevermind, we can't :(

//Set the save as
pWkspFact = new RasterWorkspaceFactoryClass();
pWorkspace = pWkspFact.OpenFromFile(strPath, 0);
pSaveAs = (ISaveAs2)pMosaicRaster;
IRasterStorageDef pRasterDef = new RasterStorageDefClass();
IPnt pPnt = new PntClass();
pPnt.X = CellSize;
pPnt.Y = CellSize;
pRasterDef.CellSize = pPnt; //This actually only works when storing an SDE Raster

pResultData = (IDataset)pSaveAs.SaveAsRasterDataset(strName, pWorkspace, "TIFF", pRasterDef);
return pResultData;
}

Monday, February 13, 2006

Open Source GIS on Linux

So I'm halfway into installing a desktop GIS on my home Linux machine. I started using PCLinuxOS as my main operating system at home a couple months ago, and am really enjoying it. It's the first Linux distribution I've tried that worked with all my hardware on the first try. There's quite a bit of software available on the repositories for free - almost anything you'd want for everyday home use, as well as a lot business apps.

My goal for now is to get Quantum GIS running and reading data from PostGIS. Since neither one is available in the PCLinuxOS repositories, I'll be installing both from source. My other option would be to find a linux distro that offers GIS functionality right out of the box, but most of those distros don't seem very up to date. Anyways, I'm looking at this as a learning expererience.

PostGIS installed easily enough. All the dependecies I needed were in the repositories, so it was just a matter of using Synaptic to install PostgreSQL and then following the instructions on the PostGIS website to install from source.

Quantum GIS has been a different story. One of its dependencies was not in the repositories, so I had to first install that from source. I had a little trouble with that, but got some quick help from one of the developers via the mailing list. Unfortunately, I had further troubles when it came time to install the actual QGIS package. In short, I can do ./configure, but when I try to make, I get a segmentation error. I've posted a question on the support forum, but so far no responses.

In related news, Mike at MJR42 is blogging about his own attempts to set up a similar GIS on Red Hat Linux. The main difference with his is that it will be GRASS and QGIS rather than PostGIS and QGIS. I'll keep watching his blog to see what happens.

Tuesday, January 17, 2006

US Survey Feet vs. International Feet

I get asked this now and then and I can never remember. This is not an issue for most people. If you live in the US, you are probably using US Survey Feet. If you're not in the US, you're most likely using meters. The only place I've ever seen International Feet used was Prince William County, who used that unit until about 2002, when they joined the rest of the country and started using US Survey Feet. Since that time, I've now and then run across a dataset that didn't get converted, or found a survey that's still in International Feet. In Virginia State Plane NAD83, you'll notice your data being offset by about 20 feet if you mix up the two units.

Here's a brief summary of how to convert from one unit to another:

1 International Foot = 0.3048 meters exactly
39.37 Inches (US Survey) = 1 meter exactly

To go from X US Survey Feet to Y International Feet:

Take X
Multiply times 12
Divide by 39.37
Divide by 0.3048 = Y
Overall conversion factor = 1.000002000004000008000016000032 (approximately)

To go from Y International Feet to X US Survey Feet:

Take Y
Multiply times 0.3048
Multiply times 39.37
Divide by 12 = X
Overall conversion factor = 0.999998 (exactly)
If you're in Autocad, you can scale all your features from 0,0 using the overall conversion factor. This assumes that whoever produced the drawing was using a coordinate system at all - a pretty big assumption at many firms.

If you're in ArcGIS, you can alter the projection properties so that the units are the correct one, and ArcGIS will do all the coordinate conversions on the fly for you.

Monday, January 16, 2006

Introductions

Welcome to my blog. I'm Ben Slater, GIS Data Manager at Wetland Studies and Solutions, Inc. This blog is intended as a place for me to record various tricks and hacks I have learned while working as a GIS dude for the past 10 years and discuss whatever GIS related stuff I happen to be thinking about. My main interests are ESRI software (especially customizations), .NET programming, and open source GIS. I work in the environmental and archeology fields, though most of the stuff I do could probably be applied to any fields.

Some of the things I hope to be discussing are:

Fully automated map production in ESRI ArcGIS
Setting up a GIS in Linux
Methodologies for producing archeology maps
ArcGIS troubleshooting
General .NET programming and database programming

If anyone reads this and has some specific questions, feel free to leave comments or email me.