Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

GIS Web Services and Microsoft Word


February, 2005: GIS Web Services and Microsoft Word

VBA and macros carry the load

Keith is a software developer for Staff IT in Oak Ridge, TN. He can be contacted at [email protected].


Geographical Information Systems (GIS) are making inroads into applications ranging from utilities and facility management, to city planning and security, among others. In this article, I present a GIS application that lets you embed a map into a Microsoft Word document simply by highlighting an address in the text. As such, this article is a convergence of several distinct technologies: GIS, Visual Basic for Applications (VBA), web services, and word processing.

To build the sample program (actually, a Word macro), you need to have installed on your PC Microsoft Word 2002 (or higher), Internet Explorer 5.x (or higher), and the Microsoft SOAP Toolkit 3.0. However, to use the code I present here, you need to set up a web service on a protected intranet. You can easily replace my web service with your own or use a third-party subscription web service such as Microsoft's MapPoint (http://www .mappoint.com/).

Listing One is the code for the Word macro. The Word document that accompanies this article is available electronically; see "Resource Center," page 5. To view and open the Word document, press ALT+F11. The execution of the sample program is a two-part operation:

  1. Highlight a street address for Knoxville, Tennessee.
  2. Right click your mouse and choose "Validate address..." from the context menu.

If an invalid address is highlighted, a message box appears informing you that the address is invalid; otherwise, you are presented with a dialog box that lets you select various map "themes" (an aerial photo, zoning restrictions, and so on) as well as specifying a map size and scale.

Nearly all the heavy lifting in this program is performed by a web service that lives on the Internet (or an intranet) and performs discrete operations; in this example, the web service first validates an address, then returns a map at the request of users. The beauty of web services is that you don't have to know anything about their inner workings—you simply use them. Web services expose their methods, which makes it easy to know what data you have to pass in (in this example, an address as a string), and what you get back (again, a string representing the URL of a map). The other nice thing about web services is that they can be written in one language, but called from any other. My web service is written in Visual Basic; however, you could call it from a C# or C++ application. In short, web services go a long way toward delivering on the promise of seamless, heterogeneous, distributed computing.

The KGIS Address Web Service

The web service used by my Word macro is a product of Knoxville Geographic Information Systems (KGIS) of Knoxville, Tennessee. KGIS is responsible for maintaining all the geographical data for Knox County, and is a consortium of the city of Knoxville, Knox County, and the Knoxville Utilities Board (KUB). KGIS is thus responsible for supporting a wide user base with diverse data needs. The primary GIS tool is ArcIMS from ESRI (http://www.esri.com/). ArcIMS generates the map that gets inserted into a Word document when an address is validated. The web service I use in this article does not actually return a map image, but rather, the URL of the map image. The main point here is that a web service exists that validates an address (for Knox County, Tennessee) and returns you a reference to a map of that address.

The web service itself is integrated in a Microsoft Word document as a macro. This entire problem of inserting a map into Word can be broken down into three steps:

  1. Create the macro that calls the web service.
  2. Provide a way for the user to call the web service.
  3. Do something with the result.

To create a Word macro, select Tools-> Macro->Macros... from the toolbar. This launches a dialog that lists all current macros, plus, displays a Create button. Enter the name of your macro and click this button. This launches the VBA editor, which defaults to editing a subroutine with the same name as your macro from the previous screen. The code for my macro (available electronically; see Validate_Address()) is responsible for reading the address that users select (highlight), and configuring the structures needed by the web service. This is done by using the Selection object; see Example 1.

Saving the selected string is necessary for two reasons. First, you need to read the string containing the address. Second, if the address turns out to be valid and users want to see a map, I insert a blank line immediately below the highlighted address for inserting the map. I then rehighlight the address. Saving the starting/ending locations of the address string lets me do this. It also gives users visual feedback as to which address they selected.

One final caveat regarding macros—you may need to adjust your security settings in Word before this macro executes. Select Tools->Macros->Security... on the toolbar and enable macros.

Calling a Web Service

The next step is to provide a way for users to call the web service from inside Word. For my sample program, I modified the context menu that appears when you right-click in Word; see Figure 1. Begin by clicking Customize under Tools on the main menubar of Word. Select the Toolbars tab and check the box next to Shortcut Menus. That opens the shortcut menu bar editor; see Figure 2.

Next, select Text from the Shortcut Menus toolbar, and add the text for the context menu (Validate Address...). Then, on the Customize dialog, select Commands and then Macros. Add the name of the macro here; see Figure 3.

The Code

The code exists in a macro called Test(). In the sample Word document (available electronically), you can access the code by pressing ALT+F11 to open the macro editor. The code declares many different variables; some of these are related to calling my map service. The generic variables are concerned with getting the text that users have highlighted, namely, sSelection. The contents of this variable must be checked for nonvisible ASCII characters. The only thing you want to pass to the web service is a valid street address. Example 2 does just that.

Once you have the address, you have to validate it against the KGIS database (again, your project would use your database). If valid, the dialog box in Figure 4 appears, letting users select the properties of the map for the given address.

This dialog is called UserForm1 in the project. The Theme combobox is populated with the values Topographic, Aerial Photo, Case Parcel Map, and Zoning. The web service uses this information to return the type of map. Scale and Size are self explanatory. The Owner Data combobox is an additional criterion peculiar to the KGIS web service. Its values are Map ONLY, Owner Info., and Owner Info./Parcel ID. Normally just a map would be returned, but the other options allow information about the owner of the address and the parcel to be returned as well.

After users have made their selections on this dialog and clicked the OK button, the real fun starts. The structures used by the web service are completed and the web service is called using this line:

sMapURL =
myMap.wsm_GetMapByAddressMSLink
(CLng(Val (myAddrInfo.AddrMSLINK)),
myMapProps)

The web service returns the URL of a map that lives on the server. The next programming chore is to get this image into the Word document. This is done by first creating a temporary file in the user's TEMP directory, then copying the JPEG map image to this file. Once on the local machine, it's a snap to insert it into Word; see Example 3. The line in bold is a little-known API call that does all the work. According to the documentation, its function is to "Download bits from the Internet and save them to a file"—exactly what you want. Finally, a Selection object is created for an InLineShape, and the map/JPEG is assigned to the AddPicture method. You now have a map in your Word document.

Conclusion

There are a number of Internet sites that provides maps identifying stores, points of interests, and the like. With the merging of GIS with the Office tools, you can greatly extend and enhance the utility of your GIS-related applications.

DDJ



Listing One

Private Declare Function URLDownloadToFile Lib "urlmon" Alias _
"URLDownloadToFileA" (ByVal pCaller As Long, ByVal szURL As String, _
ByVal szFileName As String, ByVal dwReserved As Long, _
ByVal lpfnCB As Long) As Long

' This is the prototype for calling a Web service from inside a Word doc
Sub Test()
    Dim sSelection As String
    Dim sValidAddr, sTheme, sScale, sSize As String
    Dim iStart, iEnd, iResult, iLastChar, k As Integer
    Dim selOrig As Selection
    Dim sMapURL As String
    Dim bLoop As Boolean
    Dim myMap As clsws_GetMap
    Dim myMapProps As struct_MapSettings
    Dim localFileName, szTempDir, szTempFileName As String
    Dim fso As New FileSystemObject
    Dim err As Long
    '
    Set selOrig = Selection
    sSelection = Selection
    iStart = Selection.Range.Start
    iEnd = Selection.Range.End
    iLastChar = Asc(Mid(sSelection, (iEnd - iStart), 1))
    ' Code to strip off a trailing <cr>, comma, space, or period
    bLoop = True
    k = 0
    Do While bLoop    '
        iLastChar = Asc(Mid(sSelection, ((iEnd - k) - iStart), 1))
        If ((iLastChar >= 65 And iLastChar <= 90) Or 
                                (iLastChar >= 97 And iLastChar <= 122)) Then
            bLoop = False
        Else
            k = k + 1
        End If
    Loop
    sSelection = Left(sSelection, ((iEnd - k) - iStart))

    Dim myGetAddress As clsws_GetAddress
    Dim myAddrInfo As struct_AddressInfo
    Dim mySearchInfo As struct_SearchProperties
    
    Set mySearchInfo = New struct_SearchProperties
    Set myGetAddress = New clsws_GetAddress
    
    mySearchInfo.AddressStyle = "FullAddress"
    mySearchInfo.SearchMethod = "Exact"
    mySearchInfo.SpatialSearch = "None"
    mySearchInfo.FullAddress = sSelection
   
    Set myAddrInfo = myGetAddress.wsm_GetAddressInfo(mySearchInfo)
    sValidAddr = myAddrInfo.MatchStatus
    If sValidAddr = "Exact" Then
        '
        UserForm1.Show (vbModal) 'show UserForm1
        iResult = UserForm1.sResult
        sTheme = UserForm1.sTheme
        sScale = UserForm1.sScale
        sSize = UserForm1.sSize
        
        Unload UserForm1 ' transfered values, so safe to unload the Form
        ' does user want a map or an owner card?
        Select Case iResult
            Case 1
                '   *************************
                '   *   User wants a map    *
                '   *************************
                Selection.Paragraphs(1).Range.InsertParagraphAfter
                Selection.MoveDown ' this is required!
                'now get the map
                Set myMap = New clsws_GetMap
                Set myMapProps = New struct_MapSettings
                myMapProps.MapScale = sScale    ' set scale user picked
                myMapProps.MapType = sTheme     ' set theme user picked
                Select Case sSize
                    Case "Small"
                        myMapProps.MapHeight = 300
                        myMapProps.MapWidth = 400
                        '
                    Case "Medium"
                        myMapProps.MapHeight = 400
                        myMapProps.MapWidth = 500
                        '
                    Case "Large"
                        myMapProps.MapHeight = 500
                        myMapProps.MapWidth = 600
                End Select
                'myMapProps.UseExtents = False
                 myMapProps.UseScale = True
                 
                 sMapURL = myMap.wsm_GetMapByAddressMSLink(CLng(Val
                                     (myAddrInfo.AddrMSLINK)), myMapProps)
                 Dim sMapParams() As String
                 sMapParams = Split(sMapURL, "|")
                 Dim obj As InlineShape
                 Dim sNewImage As String
                 '
                 szTempDir = Environ("TEMP")        ' get temp. folder
                 szTempFileName = fso.GetTempName() ' get temp file name
                 ' remove the .tmp extension and change to .jpg
                 szTempFileName = Left(szTempFileName, 9)
                 szTempFileName = szTempDir & "\" & szTempFileName & "jpg"
                      sNewImage = sMapParams(0) '
                      err = URLDownloadToFile(0,sNewImage,szTempFileName,0,0)
                       Selection.InlineShapes.AddPicture 
                             FileName:=szTempFileName, SaveWithDocument:=True
                    Selection.MoveLeft Unit:=wdCharacter, Count:=1, 
                                                          Extend:=wdExtend
    With Selection.InlineShapes(1)
        With .Borders(wdBorderLeft)
            .LineStyle = wdLineStyleSingle
            .LineWidth = wdLineWidth050pt
            .Color = wdColorAutomatic
        End With
        With .Borders(wdBorderRight)
            .LineStyle = wdLineStyleSingle
            .LineWidth = wdLineWidth050pt
            .Color = wdColorAutomatic
        End With
        With .Borders(wdBorderTop)
            .LineStyle = wdLineStyleSingle
            .LineWidth = wdLineWidth050pt
            .Color = wdColorAutomatic
        End With
        With .Borders(wdBorderBottom)
            .LineStyle = wdLineStyleSingle
            .LineWidth = wdLineWidth050pt
            .Color = wdColorAutomatic
        End With
        .Borders.Shadow = False
    End With
    With Options
        .DefaultBorderLineStyle = wdLineStyleSingle
        .DefaultBorderLineWidth = wdLineWidth050pt
        .DefaultBorderColor = wdColorAutomatic
    End With
                     ' restore the highlight to orig. addr string
                     ActiveDocument.Range(iStart, iEnd).Select
                ' now delete the file in the TEMP directory
                fso.DeleteFile szTempFileName, False
                 
            Case 2  ' user wants OWNER CARD info, no Parcel ID
                Selection.Paragraphs(1).Range.InsertParagraphAfter
                Selection.MoveDown ' this is required!
                Set myMap = New clsws_GetMap
                Set myMapProps = New struct_MapSettings
                

                
                myMapProps.MapScale = sScale    ' set scale user picked
                myMapProps.MapType = sTheme     ' set theme user picked
    
                myMapProps.MapHeight = 500
                myMapProps.MapWidth = 600
                myMapProps.UseScale = True
                
                sMapURL = myMap.wsm_GetMapByAddressMSLink(CLng(
                                    Val(myAddrInfo.AddrMSLINK)), myMapProps)
                Selection.InsertAfter Text:=vbCrLf
                Selection.InsertAfter Text:=CStr(myAddrInfo.Owner) & vbCrLf
                Selection.InsertAfter Text:=
                                 CStr(myAddrInfo.OwnerMailingAddr_1) & vbCrLf
                Selection.InsertAfter Text:=
                                 CStr(myAddrInfo.OwnerMailingAddr_2) & vbCrLf
                '
                ActiveDocument.Range(iStart, iEnd).Select   ' restore original selection
        Case 3  ' user wants OWNER CARD info, and Parcel ID
                Selection.Paragraphs(1).Range.InsertParagraphAfter
                Selection.MoveDown ' this is required!
                Set myMap = New clsws_GetMap
                Set myMapProps = New struct_MapSettings
                
                myMapProps.MapScale = sScale    ' set scale user picked
                myMapProps.MapType = sTheme     ' set theme user picked
                myMapProps.MapHeight = 500
                        myMapProps.MapWidth = 600
                'myMapProps.UseExtents = False
                myMapProps.UseScale = True
                
                sMapURL = myMap.wsm_GetMapByAddressMSLink(CLng(
                                 Val(myAddrInfo.AddrMSLINK)), myMapProps)
                Selection.InsertAfter Text:=vbCrLf
                Selection.InsertAfter Text:=CStr(myAddrInfo.Owner) & vbCrLf
                Selection.InsertAfter Text:=
                                CStr(myAddrInfo.OwnerMailingAddr_1) & vbCrLf
                Selection.InsertAfter Text:=
                                 CStr(myAddrInfo.OwnerMailingAddr_2) & vbCrLf
                Selection.InsertAfter Text:=CStr(myAddrInfo.ParcelID)
                Selection.InsertAfter Text:=vbCrLf
                '
                ActiveDocument.Range(iStart, iEnd).Select   
                                              ' restore original selection
        End Select
    Else
        MsgBox "That address is NOT VALID!", vbCritical Or vbOKOnly
    End If
End Sub
Back to article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.