Dr. Dobb's Journal February, 2005
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:
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 workingsyou 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 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:
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 macrosyou may need to adjust your security settings in Word before this macro executes. Select Tools->Macros->Security... on the toolbar and enable macros.
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 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.
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
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