More With Google Maps
Had a great time creating the Out-LastEarthquake.ps1 script, but let’s face it, other than it’s cool factor it doesn’t have much use. But what we do have is the framework for the ability to place markers on a Google Map, and there are definitely some uses for that. I created a proof of concept script that shows just what you could do.
Longitude and Latitude
The great thing about the Last Earthquake script is the data feed we had already had the longitude and latitude data in it, but to have any useful application the more likely data we’re going to have is address. So we need to figure out a way of translating address information to longitude and latitude. I really wanted to use Google for this, because one thing I’ve learned over the years is scripts often outlast the time you expect them to. That means if you are using an outside source you want it to be around for a long time. There are a lot of great websites out there, but so many of them just don’t last and I wanted to make sure this one does. Besides, we’re using the Google Maps API so it just makes sense to keep it in the house.
A quick Google search and I locate inside the Google Maps API a simple feed that will do this translation for you without too much trouble! It’s called Geocoding (translating an Address to coordinates) and is pretty easy to use. First, let’s test with this address:
4 Yawkey Way
Boston, MA
02215
Now, no peeking! To find out what this is you have to use your own script (or the one below)! First thing is we need to remove the comma’s and replace the space’s with the “+” symbol. For this proof of concept I just omitted the comma from the data, and I get to use a new method of inputting data into a script that I ran across a couple of weeks ago. But first, let’s do some Geocoding and see what we get!
In this example I pulled the data into $Geocode and then went exploring. Everything’s under the Results property, including a very useful property called formatted_address but chasing down the geometry property I eventually found the longitude and latitude data we’re looking for! And that’s really all there is, isn’t it? The rest is just window dressing, such as wrapping this in a loop, putting the actual HTML code (taken from the Last Earthquake script) and we’re all set. We now have the ability to create web pages that will have a map on it and points of our choosing all plotted out.
Data Input
So let’s get some test data and start playing. There will come a time that you want to use some test data, but you may not want to create a separate CSV file and input it every time. It’s just a pain and your test folder ends up cluttered with tons of miscellaneous files. Admittedly, there are worse problems in the world but here’s a technique for getting data into your script without that extra file.
It starts with a here-string that you format like a CSV (including the header row) and then use the ConvertFrom-CSV to convert that here-string into a collection of objects. Here’s the example I used for this script:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$Data = @" | |
Place,Address,Item,Qty,Tot | |
Gillette Stadium,1 Patriot Pl Foxborough MA 02035,Green Frogs,550,1100 | |
Fenway Park,4 Yawkey Way Boston MA 02215,Yellow Toads,100,5000 | |
Boston Garden,100 Legends Way Boston MA 02114,Blue Dragons,5,10000 | |
"@ | |
$Locations = ForEach ($Addr in ($Data | ConvertFrom-Csv )) |
As a proof of concept I added some labels and some other data just to show how things can be done. In most scripts you’ll see a line like $MyData = $Data | ConvertFrom-CSV and that’s fine, as $MyData will now be a collection of objects with our data in it but I like to cut out any variables I don’t really need. I only need to run through this data once so why keep a variable with the data in it for the entire lifetime of the script?!
Now we need to loop through that data and create our PSCustomObject with what we need in it:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$Locations = ForEach ($Addr in ($Data | ConvertFrom-Csv )) | |
{ $Addr.Address = $Addr.Address -replace " ","+" | |
$Location = Invoke-RestMethod –uri "http://maps.googleapis.com/maps/api/geocode/json?address=$($Addr.Address)&sensor=false" | |
[PSCustomObject]@{ | |
Place = $Addr.Place | |
Location = $Location.Results.Formatted_Address | |
Lat = $Location.Results.Geometry.Location.Lat | |
Lng = $Location.Results.Geometry.Location.Lng | |
Item = $Addr.Item | |
Qty = $Addr.Qty | |
Tot = $Addr.Tot | |
} | |
} |
After that we’ll need to create the text for the JavaScript array, just like in the Last Earthquake script (in fact, it’s almost exactly the same so I won’t show it again). But I have more data this time, and I wanted to have it displayed all the time. I’ve mentioned it before, but my HTML-fu is pretty basic, so for those HTML guru’s out there, please don’t laugh at me! But the first thing I do is create a table, with 2 cells in it. The left cell is for our Google Map and the right will display our data.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<table> | |
<td id="map-canvas"></td> | |
<td><b><ol>$TableInfo</ol></b></td> | |
</table> | |
</body> |
I also put the second set of data in an ordered list so it will be numbered (you’ll see why soon). So now I just need to create the $TableInfo string to have the data in it we need:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ForEach ($Location in $Locations) | |
{ $TableInfo += "<li><p>$($Location.Place)<br>Item: $($Location.Item)<br>Qty: $($Location.Qty)<br>Tot: `$$($Location.Tot)</p></li>" | |
} |
Pretty simple really, each <li> in the ordered list represents a record in our data. I discovered that if I wrap all of my data in a <p></p> and then use <br> to do new lines within it it will give me new lines without 1.5 line spacing that HTML always uses! May not sound like much but I’ve been trying to figure that out for along time.
Numbered Markers
So we have our map with all of the standard markers on it, and a numbered list to the right with our data on it. Wouldn’t it be nice if we could create markers with numbers on them so you could correlate the data with the placement on the map? Turns out, Google has that built into the API as well, in fact you can insert just about any data into this!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var image = 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=' + (i + 1) + '|FF0000|000000'; | |
marker = new google.maps.Marker({ | |
position: position, | |
map: map, | |
title: markers[i][0] + '\n' + markers[i][1], | |
icon: image | |
}); |
The image variable (line 1) simply points to the place where the image can be found, which is a dynamic image created by Google based on the data you pass to it. So I take advantage of the loop I’m already using to place all the markers to feed the variable into the image creation API. Easy peesy!
The final output is a simple map with coordinates to the left, data to the right. Imagine a display screen in your Customer Service area or warehouse showing the last 10 customers shipped to? Or maybe you have drivers that you want to show people where they’re going today? Maybe you’re a service provider and want to show a map of all the clients being visited today with the times on the right? For these kinds of visualizations this script would work very well. Of course, you’d have to work out where the data is coming from and what format it’s in but you can see where we’re going with this one.
Here’s the full proof of concept for those who are interested:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$Data = @" | |
Place,Address,Item,Qty,Tot | |
Gillette Stadium,1 Patriot Pl Foxborough MA 02035,Green Frogs,550,1100 | |
Fenway Park,4 Yawkey Way Boston MA 02215,Yellow Toads,100,5000 | |
Boston Garden,100 Legends Way Boston MA 02114,Blue Dragons,5,10000 | |
"@ | |
$Path = Split-path $MyInvocation.MyCommand.Definition | |
$Locations = ForEach ($Addr in ($Data | ConvertFrom-Csv )) | |
{ $Addr.Address = $Addr.Address -replace " ","+" | |
$Location = Invoke-RestMethod –uri "http://maps.googleapis.com/maps/api/geocode/json?address=$($Addr.Address)&sensor=false" | |
[PSCustomObject]@{ | |
Place = $Addr.Place | |
Location = $Location.Results.Formatted_Address | |
Lat = $Location.Results.Geometry.Location.Lat | |
Lng = $Location.Results.Geometry.Location.Lng | |
Item = $Addr.Item | |
Qty = $Addr.Qty | |
Tot = $Addr.Tot | |
} | |
} | |
$Markers = "var markers = [`n" | |
ForEach ($Num in (0..($Locations.Count – 1))) | |
{ $Location = $Locations[$Num].Location.Replace("`'","\'") | |
$Markers += " ['$($Locations[$Num].Place)','$Location',$($Locations[$Num].Lat),$($Locations[$Num].Lng)]" | |
If ($Num -lt ($Locations.Count – 1)) | |
{ $Markers += ",`n" | |
} | |
} | |
$Markers += "`n ];`n" | |
ForEach ($Location in $Locations) | |
{ $TableInfo += "<li><p>$($Location.Place)<br>Item: $($Location.Item)<br>Qty: $($Location.Qty)<br>Tot: `$$($Location.Tot)</p></li>" | |
} | |
$HTML = @" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"> | |
<meta charset="utf-8"> | |
<title>Address Mapper</title> | |
<style> | |
html, body { | |
margin: 0px; | |
padding: 0px | |
} | |
#map-canvas { | |
height: 500px; | |
width: 800px; | |
margin: 0px; | |
padding: 0px | |
} | |
td { | |
vertical-align: top; | |
} | |
</style> | |
<script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script> | |
<script> | |
function initialize() { | |
$Markers | |
var bounds = new google.maps.LatLngBounds(); | |
var mapOptions = { | |
mapTypeId: google.maps.MapTypeId.HYBRID, | |
} | |
var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions); | |
for( i = 0; i < markers.length; i++ ) { | |
var position = new google.maps.LatLng(markers[i][2], markers[i][3]); | |
bounds.extend(position); | |
var image = 'http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=' + (i + 1) + '|FF0000|000000'; | |
marker = new google.maps.Marker({ | |
position: position, | |
map: map, | |
title: markers[i][0] + '\n' + markers[i][1], | |
icon: image | |
}); | |
} | |
if (markers.length > 1) { | |
map.fitBounds(bounds); | |
} | |
else { | |
map.setCenter(new google.maps.LatLng(markers[0][1],markers[0][2])); | |
map.setZoom(4); | |
} | |
} | |
google.maps.event.addDomListener(window, 'load', initialize); | |
</script> | |
</head> | |
<body> | |
<table> | |
<td id="map-canvas"></td> | |
<td><b><ol>$TableInfo</ol></b></td> | |
</table> | |
</body> | |
</html> | |
"@ | |
Write-Verbose "$(Get-Date): Saving file…" | |
Try { | |
$HTML | Out-File $Path\AddrMap.html –Encoding ASCII –ErrorAction Stop | |
} | |
Catch { | |
Write-Warning "Unable to save HTML at $Path\AddrMap.html because $($Error[0])" | |
Exit | |
} | |
& $Path\AddrMap.html |
[…] post, I’m going to talk about how this project morphed into something else! A type of project I’ve been wanting to do before but never really got around to it but I […]
[…] I created a simple CSV type here-string and used ConvertFrom-CSV to make objects out of it (a handy way of quickly creating a large array of objects). Added the Get-SavedCredential function and had a working script. Then it […]
Martin, this looks awesome, in my case I only get it to work in IE8 Compatibility mode, what can be done to get the code to generate an HTML file that will support IE 9+
Typical IE! Have you tried 10? I did all my testing on Chrome and since it was really just a demonstration I didn’t check for browser compatibility. For being such a “common” language there is an amazing level of incompatibility between browsers! I feel for our web developers!
This is a great script and tutorial. Thank you very much for taking the time to post this!
I write only this
$Data = @”
Place,Address
MyHome, 175 carrer de Provenza Barcelona 08036
“@
and I cannot display the map even when in the html page, I can see my place coordinates that agree with my GPS
Remember that the script is just a proof of concept. I added the other 3 fields that this script needs to your data and it worked great for me.
$Data = @”
Place,Address,Item,Qty,Tot
MyHome, 175 carrer de Provenza Barcelona 08036,Red Dinosaur’s,1000,80
Gillette Stadium,1 Patriot Pl Foxborough MA 02035,Green Frogs,550,1100
Fenway Park,4 Yawkey Way Boston MA 02215,Yellow Toads,100,5000
Boston Garden,100 Legends Way Boston MA 02114,Blue Dragons,5,10000
“@
You forgot to put in the Item, Qty, and Tot numbers. I just threw some dummy numbers in there and ran the script with this data and can see you just off of Carrer d’Aribau 🙂
Thanks a lot Martin. It works for me too!!. I like this kind of programs. As a former Captain of the Spanish Merchand F. and Air Traffic Controller and Pilot, I like all this kind of programs. I even programmed with Visual Basis and Map Objects how to move by car inside the airport taxiways with fog and worked perfectly. Now retired, I just start practicing PowerShell, just to have fun but following to you and Bob McCoy it is a pleasure. The trick here was how to write my address correctly so as to be plotted on map as we always write Provenza 175 and 08036 Barcelona. Maybe it works too. I will check. Kind regards Emilio
Martin, I change this and also works very well. I wanted to experiment and I got it. Regards
$Data = @”
Place,Address
OtraCasa, 242 carrer de Provenza Barcelona 08036
MiCasa, 167 carrer de Provenza Barcelona 08036
“@
[PSCustomObject]@{
Place = $Addr.Place
Location = $Location.Results.Formatted_Address
Lat = $Location.Results.Geometry.Location.Lat
Lng = $Location.Results.Geometry.Location.Lng
#Item = $Addr.Item
#Qty = $Addr.Qty
#Tot = $Addr.Tot
ForEach ($Location in $Locations)
{ $TableInfo += “$($Location.Place)Latitud__:$($Location.Lat)Longitud :$($Location.Lng)”
}
One of the ways that may interrupt this code is when google maps returns Multiple addresses for the one you have provided(Plug in your address to google maps manually and you will see this), when this happens the map does not show correctly. One solution, though not always ideal takes the first result from google instead of leaving an array of items
Location = $Location.Results.Formatted_Address|Select -first 1
Lat = $Location.Results.Geometry.Location.Lat|Select -first 1
Lng = $Location.Results.Geometry.Location.Lng|Select -first 1
Adding the |Select -first 1 solves this nicely
Before the code we see we have 2 locations (one is with a West, one isnt)
Place : Site 8
Location : {110 Souris Avenue, Weyburn, SK S4H 2Z9, Canada, 110 Souris Avenue, Weyburn, SK S4H 2Z8, Canada}
Lat : {49.6622956, 49.6613164}
Lng : {-103.8560219, -103.8558935}
After the code, Powershell just grabs the first entry
Place : Site 8
Location : 110 Souris Avenue, Weyburn, SK S4H 2Z9, Canada
Lat : 49.6622956
Lng : -103.8560219
This made my mapping and pulling in data work VERY nicely
Great point. I’ve always assumed that I’d be giving Google enough information (address, city state zip, country, etc) that that wouldn’t happen.