Creating Google Maps with PowerShell
You know, it’s funny how projects sometimes grow from something very small and become something quite different. This is a story about exactly that.
Earthquake Data
It all started with the most simple little email from PowerShell.com. It was an easy little one-liner to show the latest earthquakes from a site call SEISMI.org. It was a great example for how to use Invoke-RestMethod to pull data from a JSON feed. This is a growing standard for retrieving data and Invoke-RestMethod will take that data stream and create PowerShell objects for you, and everything from there is simple object manipulation–just like every other PowerShell script!
And while playing with it, it occurred to me that the data I’m getting has longitude and latitude, wouldn’t it be cool if you could use a Google Visualization to plot those points on a map and see where they are? I’ve done this kind of thing with other projects so I wasn’t too worried that I could get this working. Cut and pasted the Google example into a here-string, but my data into it and bang, it didn’t work at all. WTH? Invalid token? Compare, compare and compare to the example the webpage and it’s essentially identical. Google the hell out of the issue and the best I could get was some corruption in the actual file. But it’s a here-string going to Out-File? How can it be corrupted? Finally it occurred to me to try a different encoding value when using Out-File and yep, it’s working now. So, note to self, if using Out-File to output some JavaScript to a file make sure to set -Encoding ASCII!
But there’s another problem. The zoom level on the map is all the way zoomed in. You can’t see a thing. Zoom it out and it all looks wonderful! Shouldn’t be a big deal right? Just set the zoom and walk away? Turns out there’s a map option called zoomLevel, so I put it in the code and absolutely nothing happens. More Googling and I discover that there’s a bug in the Map Visualization that zoomLevel doesn’t work. Since Google is focusing all of their energy on the Google Map API who knows when this will get fixed so it’s game over. I’ll have to switch gears.
You may have noticed that the vast majority of my time was spent getting the JavaScript to work, not on the PowerShell side? Well, it gets worse. Now, I won’t go into every little problem I had getting the Google Map API working, but there were a fair amount of them. It’s easy enough to get a single marker on map, but a bit harder to get multiple ones in there. But a lot of Googling and trial and error eventually came up with a working script.
What I wanted was a script where you could specify how big the map was, where you wanted to save the HTML and how many quakes you wanted to be displayed. So first we need to specify the parameters and set the path up. I decided I wanted the script to default to the same path that the script is run from, or what you specify.
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
[CmdletBinding()] | |
Param ( | |
[string]$Path, | |
[Alias("q")] | |
[int]$Quakes = 5, | |
[Alias("h")] | |
[int]$Height = 500, | |
[Alias("w")] | |
[int]$Width = 800 | |
) | |
If ($Path) | |
{ If (-not (Test-Path $Path –ErrorAction SilentlyContinue)) | |
{ Write-Warning "$Path does not exist, please correct and rerun script" | |
Exit | |
} | |
} | |
Else | |
{ $Path = Split-path $MyInvocation.MyCommand.Definition | |
} |
You may be asking why I don’t use the validate capabilities for Parameters? The simple answer is user friendliness. The errors that come up when a validation fails are very cryptic and I like to control my message to the user. So I do the validation myself. With the preliminary stuff done, now I need to call the data from the SEISMI.org JSON feed. From there, I needed to get that data into the JavaScript so it can be used with the Google Map API. Since we’re constructing a text file (for the HTML) it’s just a matter of creating a variable with the correct text in it. Here it is:
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 = @(Invoke-RestMethod –Uri "http://www.seismi.org/api/eqs" –ErrorAction Stop | Select –ExpandProperty Earthquakes | Select –First $Quakes) | |
$Markers = "var markers = [`n" | |
ForEach ($Num in (0..($Locations.Count – 1))) | |
{ $Region = $Locations[$Num].Region.Replace("`'","\'") | |
$Markers += " ['$Region',$($Locations[$Num].Lat),$($Locations[$Num].Lon),'$($Locations[$Num].timedate)','$($Locations[$Num].magnitude)']" | |
If ($Num -lt ($Locations.Count – 1)) | |
{ $Markers += ",`n" | |
} | |
} | |
$Markers += "`n ];`n" |
Notice line #5. What I found was some of the data coming out of the data feed already had single apostrophe’s in it, which borks up the JavaScript array, so in line #5 I just escape the apostrophe (in JavaScript the escape character is a backslash, where it’s a back tick in PowerShell). All the square brackets and so on is just how you define a multi-dimensional array in JavaScript. The fields I’m using 0: Title, 1: Latitude, 2: Longitude, 3:time and 4: magnitude.
Next comes the here-string with the HTML and the rest of the JavaScript in it. Using the Height and Width parameters I also change the CSS properties to control the size of map.
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
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta name="viewport" content="initial-scale=1.0, user-scalable=no"> | |
<meta charset="utf-8"> | |
<title>Earthquakes Map</title> | |
<style> | |
html, body, #map-canvas { | |
height: $($Height)px; | |
width: $($Width)px; | |
margin: 0px; | |
padding: 0px | |
} | |
</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][1], markers[i][2]); | |
bounds.extend(position); | |
marker = new google.maps.Marker({ | |
position: position, | |
map: map, | |
title: markers[i][0] + "\n" + markers[i][3] + "\nMagnitude: " + markers[i][4] | |
}); | |
} | |
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> | |
<div id="map-canvas"></div> | |
</body> | |
</html> |
On line 18 I insert the Markers variable which has all of our data in it. Then on lines 25-33 we loop through the multi-dimensional array and insert the markers into the map. I’d love to say this part all went smoothly, but it didn’t. It took me a lot of Google searching and trial and error before I finally got everything finalized and working. I was quite proud of how it came out. But during testing I ran into another big problem. If there was more than one data element everything worked fine, but if there was only 1 data element it was zoomed way out. It didn’t make sense to be so far out when there’s only 1 element, you want to be closer so you can see more information about the area the quake was in. I already knew about the zoomLevel so I knew it had to be something like that, and indeed mapZoom(x) was the way to do it. Only it didn’t work. At all. Sheesh, here we go again. A bit more Googling discovered that when using the fitBound method (which will center and zoom the map to multiple selections) you can’t then turn around and use setZoom right after it. A bit more Googling and I was able to find lines 35-41 which essentially tests for more than one marker, if there are then use fitBound and if there isn’t use setZoom.
Finally, after several hours fully a working Last Earthquake mapping script! How cool is that?! After that I wrapped some Verbose output, some error trapping for common problems, that kind of thing and published it on Spiceworks as a goofy script to play with.
Not Done Yet
Of course, it wasn’t long before someone at Spiceworks noticed a problem. What Amazilia noticed was the data on SEISMI.org was actually quite a bit out of date. And when I actually looked at it closer he some completely right. Damn. Luckily he provided a link to a site where I might be able to find what I need. A little digging around found the data feeds and sure enough they have a JSON feed! Found the right feed, and then had to figure out how to use it.
First thing I noticed when I used Invoke-RestMethod on the website though was that I only got one record back. Turns out that’s the format. It has only a couple of properties, one is a very high level metadata about the data feed itself. But go into the Features property and that actually holds an array of objects of all of the earthquakes in the feed. But there are multiple properties and they all hold arrays of their own so we’re going to have to loop through each record and tease out the data I wanted, creating a new PSCustomObject as I go. By creating a PSCustomObject this allowed me to reuse all of the code from before without doing a major rewrite. Here’s how I got the data and created my object:
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
$QuakesData = Invoke-RestMethod –uri "http://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_month.geojson" | |
[array]$Locations = ForEach ($Quake in ($QuakesData.Features | Select –First $Quakes)) | |
{ [PSCustomObject]@{ | |
Title = $Quake.properties.place | |
Magnitude = $Quake.properties.mag | |
TimeDate = [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddMilliseconds($Quake.properties.time)) | |
Lon = $Quake.geometry.coordinates[0] | |
Lat = $Quake.geometry.coordinates[1] | |
} | |
} |
Just loop through the $QuakesData.Features property, which itself is a collection of objects and pull out the data I needed. This is the great thing about PowerShell, it’s so easy to explore these complex objects by just typing $QuakesData.Features and seeing what comes out and then just adding the next property on that, and so on until you get right down to what you want. This kind of thing was so hard with VBScript that I would never do a project like this!
But I had retrofitted the new data source into the script and it’s working great (see the link below if you want to see the full script).
Out-LastEarthquake.ps1
Next 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 realized that with the framework I have worked up for this script I could do! And this script would actually be something that could be useful in a business environment! Don’t you love foreshadowing?
Reblogged this on Sutoprise Avenue, A SutoCom Source.
[…] 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 […]
[…] project is very much like the Last Earthquake script I wrote a little while back, so I figured the hardest part would be finding an API that […]
Hi ;Martin, I am going to study it quite well. I like it very much. For you to know, the information from the page you get the data (even when it is not the most important but the philosophy of the program) do not much the one in page http://earthquake.usgs.gov/earthquakes/index.php maybe you can improve it.
Hi Emilio, sorry but I don’t quite follow?
It is not important. Just I checked the data in that page and compared with the 5 data taken with your program and they differ. They are not the same what makes me think that the data in this page are newer than the ones in the program page (Json). Regards