The Google Static Maps API is a simplified way of getting maps to include in your application. I am writing an amateur radio application that would benefit from a map showing the location of a remote station, or a map showing the path between home and the remote station. I found a posting by JustinJake that showed the way of accessing Google Static Maps and built that into my own solution. Thank you Justin.
The map display is now at the stage where it is working well enough to merit it's inclusion in the project, but still needs some tidying up, which I am putting off while I focus on other aspects of the overall task.
However, the current code may be of interest to someone, so here it is. The core of the code is written as a thread and would be easy to add to any application. If anyone cares to design / find some nice compass rose and Zoom up/down icons I could blend them in... a gentle hint!
Download the two source files into the same directory with the thread as "MyGoogleMap.pbi" and the mini app as "MyGoogleMap.pb".
Bug reports, corrections and suggestions welcomed.
Cheers,
RichardL
This is a simple Application to show how to call the main thread...
Code: Select all
; ====================================================================
; Simple 'Main Application' to exercise GoogleMapper() Thread
; Author : RichardL
; Date : 16th July 2012
; Usage : Free to use, no warranties, your responsibility!
; Original application was for Amateur Radio purposes that involved
; locations of a 'Home' radio station and a 'Remote' station.
; Thanks to JustinJack http://www.purebasic.fr/english/viewtopic.php?f=12&t=46428
; ====================================================================
; Required resources.
InitNetwork()
UsePNGImageDecoder()
; Include files
XIncludeFile "MyGoogleMap.pbi"
; Display
OpenWindow(99,10,10,170,150, "Main application")
ButtonGadget(10,10,10,150,45,"Get Google Map")
TextGadget(#PB_Any,10,70,60,20,"Latitude" ,#PB_Text_Center)
TextGadget(#PB_Any,90,70,60,20,"Longitude",#PB_Text_Center)
SpinGadget(12,10,90,70,20,-180,180,#PB_Spin_Numeric) : SetGadgetState(12,52)
SpinGadget(13,90,90,70,20,-180,180,#PB_Spin_Numeric) : SetGadgetState(13,00)
; Dispatch
Repeat
Select WaitWindowEvent()
Case #PB_Event_CloseWindow ; Window closing
Break
Case #PB_Event_Gadget ; Gadget events
Select EventGadget()
Case 10 ;{- Specify Google map and launch thread
With *MyGMap ; Structure defined in <MyGoogleMap.pbi>
; Specify Map Lat and Long etc...
\Google_MapAddress.s = ""
\Google_Lat.f = ValF(GetGadgetText(12)); ValF()? So I can type in special values.
\Google_Long.f = ValF(GetGadgetText(13))
\Google_Zoom.i = 14 ; 1 to 21
\Google_Comment.s = "A helpful comment" ; Normally bearing and range... no code in this demo.
\Google_GetMap.i = #MapNew ; Trigger a map download
; If first time thern launch thread...
If \Google_ThreadActive = 0 ; If thread is not running:
\Google_HomeLat.f = 51 ; Home location Lat and...
\Google_HomeLong.f = 0 ; Long.
\Google_MapWinX = 200 ; Initial screen position of map display, X...
\Google_MapWinY = 10 ; and Y.
\Google_ImageW.i = 500 ; Image width in pixels and...
\Google_ImageH.i = 500 ; height.
\Google_MapType.i = #HYBRID ; Initial Map type (0-3)
\Google_ThreadActive = CreateThread(@GoogleMapper(),*MyGMap); Start the Google map thread.
EndIf
EndWith
;}
Case 12,13 ;{- Request revised Google map
; (NOTE: If SpinGadgets() 12 and 13 are included the map will update each time a SpinGadget() is changed.
; This counts against Google useage and involves un-necessary network activity. Your choice!)
; If *MyGMap\Google_ThreadActive ; If running...
; With *MyGMap
; \Google_MapAddress.s = ""
; \Google_Lat.f = ValF(GetGadgetText(12))
; \Google_Long.f = ValF(GetGadgetText(13))
; \Google_Zoom.i = 14 ; 1 to 21
; \Google_GetMap = #MapNew ; Trigger a download
; EndWith
; Else
; MessageRequester("Sorry....","Google Static Map thread is not running")
; EndIf
;}
EndSelect
EndSelect
ForEver
Code: Select all
; ====================================================================
; Simple : GoogleMapper() Thread
; Author : RichardL
; Date : 16th July 2012
; Usage : Free to use, no warranties, your responsibility!
====================================================================
; Still to do: Good looking control Icons, Navigation rollover 0,90,+-180 etc, EnableExplicit (tidily)
#CRLF$ = Chr(13)+Chr(10)
; Structure to hold data for drawing Google map image.
Structure GoogleMaps
Google_ThreadActive.i ; Flag to indicate the mapping thread is active.
Google_GetMap.i ; Set this flag #True to download a new map.
Google_ImageW.i ; Image pixel width requested from Google...
Google_ImageH.i ; and Image Height.
Google_MapAddress.s ; Location for centre of map. Typically "nnn.nnn,nnn.nnn" =Lat,Long or ZIP code
Google_Lat.f ; Remote Lat and Long in degrees.
Google_Long.f
Google_HomeLat.f ; Home Lat and Long in degrees
Google_HomeLong.f
Google_Zoom.i ; 0 to 21. Each step represents a 2:1 change in scale.
Google_MapType.i ; Map type. See enumeration
Google_Markers.s ; Typically "" or "markers=color:blue|label:Q|Lat.nnn,Long.nnn|". <NOT USED>
Google_MapWinX.i ; Initial pixel position of map window.
Google_MapWinY.i
Google_Comment.s ; Information to go in window.
EndStructure
Global *MyGMap.GoogleMaps = AllocateMemory(SizeOf(GoogleMaps))
Enumeration ; Google map types
#ROADMAP
#SATELLITE
#TERRAIN
#HYBRID
EndEnumeration
Enumeration ; Map request flags (For: *myGmap\Google_GetMap)
#MapNone ; No map requeset outstanding.
#MapNew ; Passed by calling program, the 'R' label definition is generated and kept in the thread.
#MapUpdate ; Used internally by thread to indicate a revised map is wanted, using original 'R' label.
EndEnumeration
Procedure GoogleMapper(*MyGMap.GoogleMaps) ;- Thread to download and display a map from Google.
; Reference : http://code.google.com/apis/maps/documentation/staticmaps
; Communication between the main application and this thread is via structure <*MyGMap.GoogleMap>
Enumeration 2000 ; Windows and controls
#Win_GoogleMap
#Gad_GoogleClickLocate
#Gad_GoogleNSEW
#Gad_GoogleZoom
#Gad_GoogleMapType
#Gad_GoogleInfo
#Gad_GoogleFullPath
#Gad_GoogleMessage
EndEnumeration
;{- Make display window and controls
OpenWindow(#Win_GoogleMap,*MyGMap\Google_MapWinX,*MyGMap\Google_MapWinY,*MyGMap\Google_ImageW,*MyGMap\Google_ImageH+90,"Map Display (Rev:0,8,1)",#PB_Window_SystemMenu)
StickyWindow(#Win_GoogleMap,#True)
ImageGadget(#Gad_GoogleClickLocate,0,0,*MyGMap\Google_ImageW,*MyGMap\Google_ImageH,0) ; For Map image
py = *MyGMap\Google_ImageH + 5
TextGadget(#PB_Any,5, py,60,20, "N-S-E-W" ,#PB_Text_Center)
TextGadget(#PB_Any,75, py,60,20, "Zoom", #PB_Text_Center)
TextGadget(#PB_Any,145,py,70,20, "Map type",#PB_Text_Center)
TextGadget(#PB_Any,225,py,140,20,"Co-Ords" ,#PB_Text_Center)
TextGadget(#PB_Any,375,py,80,20,"Full path",#PB_Text_Center)
py + 20
ButtonImageGadget(#Gad_GoogleNSEW,5, py,60,60,0) ; Move map centre North,South,East,West
ButtonImageGadget(#Gad_GoogleZoom,90, py,30,30,0) ; Zoom up/down
ComboBoxGadget(#Gad_GoogleMapType,145,py,70,20) ; Select map type
StringGadget(#Gad_GoogleInfo, 225,py,140,20,"",#PB_String_ReadOnly) ; For showing centre of display
For n = 0 To 3
AddGadgetItem(#Gad_GoogleMapType,-1,StringField("Roadmap,Satellite,Terrain,Hybrid",n+1,","))
Next
SetGadgetState(#Gad_GoogleMapType,*MyGMap\Google_MapType) ; Initial display type
CheckBoxGadget(#Gad_GoogleFullPath,415,py,20,20,"")
; Image for NSEW control (Crude... needs some loving care and attention!)
TempFont = LoadFont(#PB_Any,"Courier New",10)
NSEW_Image = CreateImage(#PB_Any,58,58)
StartDrawing(ImageOutput(NSEW_Image))
Box(0,0,58,58,GetSysColor_(#COLOR_BTNFACE))
DrawingFont(FontID(TempFont))
H = (TextHeight(">")/2)
For n = 0 To 360 Step 45
f.f = Radian(n)
DrawRotatedText(29-(H*Sin(f.f)),29-(H*Cos(f.f))," >",n,#Black)
Next
StopDrawing()
SetGadgetAttribute(#Gad_GoogleNSEW,#PB_Button_Image,ImageID(NSEW_Image))
; Image for Zoom control (Crude... needs some loving care and attention!)
Zoom_Image = CreateImage(#PB_Any,28,28)
StartDrawing(ImageOutput(Zoom_Image))
Box(0,0,28,28,GetSysColor_(#COLOR_BTNFACE))
DrawingMode(#PB_2DDrawing_Transparent )
DrawText(10,0 ,"+",#Black)
DrawText(12,13,"-",#Black)
StopDrawing()
SetGadgetAttribute(#Gad_GoogleZoom,#PB_Button_Image,ImageID(Zoom_Image))
py + 30
StringGadget(#Gad_GoogleMessage, 225,py,240,20,"",#PB_String_ReadOnly) ; Message from user
FreeFont(TempFont)
;}
;- Main dispatch
Repeat
Select WaitWindowEvent(200)
Case #PB_Event_Repaint ;{- Rewrite image display
If RetVal
SetGadgetState(#Gad_GoogleClickLocate,ImageID(RetVal))
EndIf
;}
Case #PB_Event_CloseWindow ;{- Close window and exit the thread
If EventWindow() = #Win_GoogleMap
*MyGMap\Google_ThreadActive = #False
Break
EndIf
;}
Case #PB_Event_Gadget ;{- Buttons and gadgets
EGad = EventGadget()
Select EGad
Case #Gad_GoogleNSEW ;{- Adjust Google map centre N-S and E-W *** TO DO : Manage value transitions through 0,180 And 360 etc ***
If ClickLocate ; True if Lat and Long are valid.
dx = (WindowMouseX(#Win_GoogleMap)-GadgetX(EGad))/(GadgetWidth (EGad)/3) ; 0,1,2
dy = (WindowMouseY(#Win_GoogleMap)-GadgetY(EGad))/(GadgetHeight(EGad)/3) ; 0,1,2
With *MyGMap
f.f = Abs(Cos(Radian(*MyGMap\Google_Lat)))
Select dy
Case 0 ; Move North
\Google_Lat + (90/Pow(2,*MyGMap\Google_Zoom-1))
If \Google_Lat > 90 : \Google_Lat = 90 : EndIf
Case 2 ; Move South
\Google_Lat - (90/Pow(2,*MyGMap\Google_Zoom-1))
If \Google_Lat < -90 : \Google_Lat = -90 : EndIf
EndSelect
Select dx
Case 0 ; Move West
\Google_Long - (90/Pow(2,*MyGMap\Google_Zoom-1) / f.f)
Case 2 ; Move East
\Google_Long + (90/Pow(2,*MyGMap\Google_Zoom-1) / f.f)
EndSelect
\Google_GetMap = #MapUpdate ; Trigger re-load of map
EndWith
Else
MessageRequester("Google Static Maps","N,S,E,W navigation not available without"+Chr(10)+"map centre Latitude And Longitude")
EndIf
;}
Case #Gad_GoogleClickLocate ;{- Centralise map at mouse pointer
If ClickLocate ; True if Lat and Long are valid.
With *MyGMap
; f.f = Abs(Cos(Radian(\Google_Lat)))
dx = WindowMouseX(#Win_GoogleMap) - GadgetX(EGad) - (GadgetWidth (EGad)/2) ; dx in pixels relative to centre of display
dy = -WindowMouseY(#Win_GoogleMap) + GadgetY(EGad) + (GadgetHeight(EGad)/2) ; dy...
\Google_Long + ((90 / Pow(2,\Google_Zoom -2)) * (dx /(GadgetWidth (EGad)/2)))
\Google_Lat + ((90 / Pow(2,\Google_Zoom -2)) * (dy /(GadgetHeight(EGad)/2)))
\Google_GetMap = #MapUpdate ; Trigger re-load of map
EndWith
Else
MessageRequester("Google Static Maps","N,S,E,W navigation not available without"+Chr(10)+"map centre Latitude And Longitude")
EndIf
;}
Case #Gad_GoogleZoom ;{- Adjust Google Map zoom factor
With *MyGMap
If (WindowMouseY(#Win_GoogleMap)-GadgetY(#Gad_GoogleZoom)) < GadgetHeight(#Gad_GoogleZoom)/2 ; Top or bottom of gadget?
\Google_Zoom + 1
If \Google_Zoom > 20 : \Google_Zoom = 20 : EndIf
Else
\Google_Zoom - 1
If \Google_Zoom < 2 : \Google_Zoom = 2 : EndIf
EndIf
\Google_GetMap = #MapUpdate ; Trigger re-load of map
EndWith
;}
Case #Gad_GoogleMapType ;{- Set map type
*MyGMap\Google_GetMap = #MapUpdate ; Trigger re-load of map
;}
Case #Gad_GoogleFullPath ;{-
*MyGMap\Google_GetMap = #MapUpdate ; Option was changed so re-request map
;}
EndSelect
;}
EndSelect
If *MyGMap\Google_GetMap ; Flagged to download a map?
;{- Google Map download manager
;{- Decide the control options to be available, based on user map information provided
; NOTES: The Google Static Maps API does NOT return the map center co-ords or zoom factor
; Position 0,0 is safely out at sea!
; Rules (for the guidance of wise men):
; - If map centre Lat and Long are available the N-S-E-W and Map-Click options are available.
; - If Home (H) and Remote (R) Markers are BOTH available Google will auto scale / zoom. (Leave Lat, Long and Zoom out of request).
; - 'Show Path' switches between showing Remote region or Home and Remote on same map.
; - If no user Lat and Long provided, but Address (eg: ZIP/Postcode) is available, N-S-E-W and Map-Click will have no centre value to work with.
; Google makes a best guess regarding map that is wanted.
; If no Lat, Long or Address then skip. 'Address' alone is OK, but restricts control options to 'zoom'
If (*MyGMap\Google_Lat = 0) And (*MyGMap\Google_Long = 0) And(*MyGMap\Google_MapAddress = "")
Continue
EndIf
; If Lat or Long are provided the 'NSEW', 'Map-Click' controls are enabled
If (*MyGMap\Google_Lat <> 0) Or (*MyGMap\Google_Long <> 0)
DisableGadget(#Gad_GoogleNSEW,#False)
ClickLocate = #True
Else
DisableGadget(#Gad_GoogleNSEW,#True)
ClickLocate = #False
EndIf
; To show compass bearing and distance from Home station.
SetGadgetText(#Gad_GoogleMessage,*MyGMap\Google_Comment)
;}
;{- Build map request string
myRequestTwo.s = "/maps/api/staticmap?"
With *MyGMap
; If map area is to be defined by the Markers then skip specifying Lat, Long and Zoom
If GetGadgetState(#Gad_GoogleFullPath) = #False
; Add 'Center' address from Lat and Long
\Google_MapAddress = StrF(\Google_Lat,6)+","+StrF(\Google_Long,6)
myRequestTwo + "center=" + URLEncoder(\Google_MapAddress)
SetGadgetText(#Gad_GoogleInfo,\Google_MapAddress) ; Show to user
; Add 'Zoom' parameter, if defined by user...
If \Google_Zoom
myRequestTwo.s + "&zoom=" + Str(\Google_Zoom)
EndIf
Else
SetGadgetText(#Gad_GoogleInfo,"")
EndIf
; If new map then make Remote station label
If \Google_GetMap = #MapNew
HomeLabel.s = "&" + URLEncoder("markers=color:blue|label:R|" + StrF(\Google_Lat,6) + "," + StrF(\Google_Long,6))
EndIf
If HomeLabel.s
myRequestTwo.s + HomeLabel.s
EndIf
; Add Home station label
myRequestTwo.s + "&" + URLEncoder("markers=color:green|label:H|" + StrF(\Google_HomeLat,6) + "," + StrF(\Google_HomeLong,6))
myRequestTwo.s + "|"
; Add map 'Size' in pixels, Image Width and Height...
myRequestTwo.s + "&size=" + Str(*MyGMap\Google_ImageW) + "x" + Str(*MyGMap\Google_ImageH)
; Specify the type of map wanted...
Select GetGadgetState(#Gad_GoogleMapType)
Case #SATELLITE
myRequestTwo.s + "&maptype=satellite"
Case #TERRAIN
myRequestTwo.s + "&maptype=terrain"
Case #HYBRID
myRequestTwo.s + "&maptype=hybrid"
Default
myRequestTwo.s + "&maptype=roadmap"
EndSelect
; Indicate the request is NOT from a mobile device
myRequestTwo.s + "&sensor=false"
EndWith
;}
;{- Open network connection to Google, send request and receive map data
myConnection = OpenNetworkConnection("maps.google.com", 80, #PB_Network_TCP)
If myConnection
; Send request for map and receive response into buffer
myRequest.s = "GET "+ myRequestTwo + " HTTP/1.0" + #CRLF$
myRequest.s + "Content-Type: application/x-www-form-urlencoded; charset=iso-8859-1" + #CRLF$
myRequest.s + "Host: maps.google.com" + #CRLF$ + #CRLF$
mSize = 65536 ; Initial buffer size
*bufferTwo = AllocateMemory(mSize) ; Buffer for header+image (Expanded as download progresses)
*Buffer = AllocateMemory(65536) ; Work buffer to receive packets of download.
DataLength = 0 ; PNG Image size
found = 0
Debug myRequest
If SendNetworkString(myConnection, myRequest.s) = Len(myRequest.s)
; Receive map data into buffer
bfTwoPtr = 0
dwBytes = 1
While dwBytes > 0
dwBytes = ReceiveNetworkData(myConnection, *Buffer, 65536) ; Recover data from network
If dwBytes > 0 ; If data received:
If (*bufferTwo + dwBytes) >= mSize ; If adding data will overfill the output buffer,
mSize + 65536 ; re-specify the output buffer size,
*bufferTwo = ReAllocateMemory(*bufferTwo, mSize) ; and increase the buffer size. (Preserves existing content.)
EndIf
CopyMemory(*Buffer, *bufferTwo+bfTwoPtr, dwBytes) ; Move received data into output buffer,
bfTwoPtr + dwBytes ; maintain offset of end of data.
EndIf
Wend
; Find end of header / image start address in buffer'
For k = 0 To bfTwoPtr
If PeekL(*bufferTwo+k) = $0A0D0A0D ; ='CRLFCRLF'
found = k ; Keep header length
headerInfo.s = PeekS(*bufferTwo, k)
Debug headerInfo.s ; An interesting read, but no map centre or zoom info :-(
dataBeginSpot = (k + 4) ; Offset after CRLFCRLF
*dataBegin = (*bufferTwo + dataBeginSpot) ; Image address in memory
DataLength = (bfTwoPtr - dataBeginSpot) ; Size of image
Break ; Done...
EndIf
Next
EndIf
CloseNetworkConnection(myConnection)
FreeMemory(*Buffer) ; Finished with work buffer
EndIf
;}
;{- Make image from contents of buffer, and show it.
If found
If RetVal : FreeImage(RetVal) : EndIf ; Dispose of a prior image
If (DataLength > 5000) And (found > 0) ; If received data big enough...
RetVal = CatchImage(#PB_Any, *dataBegin, DataLength) ; Make image from received data
Else ; Make "Error" didplay...
; The DataLength test above is used to check if there is map data (c.150KB), or a Google Error (c.4KB)
; Although the error messages are the right size, they are are 32 bit and this causes a problem with
; rendering and other things. Until I can fix this I use this cludge...
RetVal = CreateImage(#PB_Any,*MyGMap\Google_ImageW,*MyGMap\Google_ImageH,24)
StartDrawing(ImageOutput(RetVal))
DrawingMode(#PB_2DDrawing_Transparent)
Box(0,0,*MyGMap\Google_ImageW,*MyGMap\Google_ImageH,GetSysColor_(#COLOR_BTNFACE))
DrawText(10,*MyGMap\Google_ImageH/2,"*** No image data received ***",#Black)
StopDrawing()
EndIf
SetGadgetState(#Gad_GoogleClickLocate,ImageID(RetVal)) ; Update the display
EndIf
;}
; Finished with download output buffer
If *bufferTwo
FreeMemory(*bufferTwo)
*bufferTwo = 0
EndIf
;}
*MyGMap\Google_GetMap = #MapNone ; Clear the map download flag.
EndIf
ForEver
EndProcedure