How to detect a point beneath an overlay using Google Maps API
Today I had to do something a bit crazy: create a searchable layer over a Google Map.
I generally don't get APIs made by Google, they are not much better than YOUTUBE APIs. They are basically nonsense and about as easy to understand as a joke made in ancient greek.
I had been using version 2 of the Google Maps API. It was old, a bit complex, but it was working for me. Today I thought it was about time to move on, so I started working on version 3.
The idea is to create an area, using some points on the map to create the shape, and check if a passed address (latitude and longitude) is in the area or not.
But let’s start from the very beginning: how to create a map. Apparently creating a map with the V3 API is really easy. The first thing to do is to attach the following script to your html page:
<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
You will notice that there is a parameter passed to the Google API, which is the sensor. If you set it to true, the API will try to find a GPS (like on an iPhone) and it will use it to determine the user's location. If you just pass ‘false’, it will ignore it.
One of the first and big advantages of the new version of the APIs, is the fact that you no longer need Google API Keys, which caused a lot of problems if you had the same page under different domains. Well, now forget it, this, thank to God (or Gods if you are politeist) is gone.
You will need to create the html element in the document, which is going to be a simple <div> element, as in the following example:
<div id="map_canvas" style="width: 600px; height:600px;"></div>
I've added the width and height of the element with the style attribute, but obviously you can do it with normal css.
Now we have the element which we are going to use to drop our map. So the next step is going to do a bit of javascript. And with the following code you will have a working Google Map for your site:
<script type="text/javascript">
var map;
function initialize(){
var myLatLng = new google.maps.LatLng(53.332251,-6.229965);
var myOptions = {
zoom: 10,
center: myLatLng,
mapTypeId: google.maps.MapTypeId.TERRAIN
};
var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
}
</script>
Now, let me explain this script line-by-line. The first line is just the declaration of the map object, it could be inside the function, but to have a global variable will be handy for later.
The first line of the function just declares a new object; it specifies the coordinates of the center point of our map.
The second line is an object that contains three parameters:
- zoom: this indicates the level of zoom of our map, the bigger the number, the smaller the area covered by our map.
- center: this just passes the previous declared coordinates of the center of our map
-
mapTypeId: this specifies which kind of map we want to draw, in this case we picked TERRAIN, but there are four options for this parameter:
- ROADMAP displays the normal, default 2D tiles of Google Maps.
- SATELLITE displays photographic tiles.
- HYBRID displays a mix of photographic tiles and a tile layer for prominent features (roads, city names).
- TERRAIN displays physical relief tiles for displaying elevation and water features (mountains, rivers, etc.).
The next line of our code will create the map object, and what you need to do is just to tell it where to put the map (our previously created <div>, and with which options (the previous variable). Now we just need to launch the function when the document loads. You can simply do that with the "onload" attribute of the <body> element, as Google shows in their examples:
<body onload="initialize()">
Or you can just use jQuery, and launch it with the .ready() command:
$().ready(function(){
initialize();
});
Honestly, it doesn't matter which you choose. I prefer the second one only because I love jQuery, but there is no real advantage in using one over the other.
Now that we have the map, the next step will be to draw an area on our map, but how we can do that?
It's actually pretty easy to do, we just have to understand how to use the Google Maps Polygon!
The Polygon essentially is just a list of points on the map, which are going to be connected with a stroke. If you are a Flash Developer, you will find a lot of similarities with the beginFill and lineTo commands of Actionscript 2.0.
The first step to do this, is to create a global var, so after our maps variable, we will declare our new variable which will be the object for our Polygon:
var map; var dublinTriangle;
Then inside the function we will create an array that contains all our points in the map, and these points will be used to create our shape in the map:
var dublinCoords = [ new google.maps.LatLng(53.335342,-6.227812399999948), new google.maps.LatLng(53.344339,-6.265383), new google.maps.LatLng(53.3359108,-6.289094299999988), new google.maps.LatLng(53.335342,-6.227812399999948) ];
I just picked up few random places on the map, and as you see, it's just a simple array with coordinates.
The only thing that we have to bear in mind, is that the first and last points have to be the same. In order to complete the shape the last point must end on the same coordinates as the first.
Once our Polygon object is created, we will pass our coordinates to it:
dublinTriangle = new google.maps.Polygon({
paths: dublinCoords,
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: "#FF0000",
fillOpacity: 0.35
});
Again, let me explain this line-by-line. The first parameter just passes our coordinates, so it will tell google to create a shape with those points.
The second, third and fourth parameter are just the colour, the opacity (between 0 and 1) and the weight of the stroke. The fifth and the sixth define the colour and opacity of the fill.
So, now we have the Polygon object created, what we need to do is apply it to the map:
dublinTriangle.setMap(map);
As you can see, now we have a map with our shape on it. But now you can see that there is a little problem. The map is not centered to our shape, but rather it is mapped to the coordinates we specified at the beginning of our script. So the next step will be to center the map.
To do so we will need to use the ‘Bounds’ object of the Google APIs.
So on our global vars, we are going to declare this new object:
var bounds = new google.maps.LatLngBounds();
In our function, we will need to pass the points we used to create the shape, to the ‘bounds’ object. To do this we just read the dublinCoords array, and after the creation of this array, insert these three lines:
for (i = 0; i < dublinCoords.length; i++) {
bounds.extend(dublinCoords[i]);
}
The method "extend" adds the points to the bounds object. Now to center our map, we just need to tell the "map" object to center itself to the bounds passed using the method "fitBounds", as showed in the line below:
map.fitBounds(bounds);
Perfect! Now we have our map centered to the center point of the shape we had previously drawn. The very last step will be the most complicated, or at least compared with what we have done so far.
But let start with the easy stuff: the Html.
<label for="address">Address</label> <input type="text" name="address" id="address" /> <input type="submit" value="Search" onclick="codeAddress()" />
With this code we have two input fields and a label, it's pretty minimal, and there is no form because we don't need to send the data to any page. The submit button will just call a function, codeAddress, which it will send the value of the address field to Google, and Google will return the coordinates of the address (if he can finds them!) to our map.
There is nothing else to explain on the html, as I said, it's pretty minimal and stupid, like modern art.
On the javascript side we will need few things, and the first is two new global variables, so alongside the previous ones, we will need to add the following:
var geocoder = new google.maps.Geocoder(); var marker;
The first one will be an object that we will use to send the address to Google and then return the information we need from it. The second one will be the object to create the pointer on the map.
But lets see the entire codeAddress function, and then I'll explain it line-by-line:
function codeAddress() {
var address = document.getElementById("address").value;
geocoder.geocode( { 'address': address}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.setCenter(results[0].geometry.location);
var isWithinPolygon = dublinTriangle.containsLatLng(results[0].geometry.location);
marker = new google.maps.Marker({
map: map,
position: results[0].geometry.location
});
} else {
alert("Geocode was not successful for the following reason: " + status);
}
});
}
The first thing that this function does when it's called is to get the current value of the address field:
var address = document.getElementById("address").value;
Secondly it calls the geocoder object, we pass the address to it, and it will return two variables, where the first one is an object containing the result of our search, if Google found something, if not, it will be empty, and in this case we will need to check the "status" string.
But if everything went well, we will use the following command to center the map on the position of the coordinates found.
map.setCenter(results[0].geometry.location);
If you don't need this, and you want to keep the map centered with the area we created before, just remove this line.
The next line will create a Marker that points to the coordinates found, and the options for this object are pretty easy to understand. The "map" attribute just tells the marker what the map should point at, and the position is where the marker has to be placed.
Now there is only one last thing to do - to check if the address we searched for is in the area or not.
To do this we can't use the Google APIs, or at least not directly. So we’ll use a script written by Matt Williamson (thank you Matt) to do the trick. You’ll find the script on github.com. Once you download it into your folder, add the following to your html page:
<script type="text/javascript" src="/path-to-your-js-folder/maps.google.polygon.containsLatLng.js"></script>
Next we will call the function that will check if the coordinates passed are in or out the area. So inside the result of our Geocode calls, if everything went ok, we will need to add this line:
var isWithinPolygon = dublinTriangle.containsLatLng(results[0].geometry.location);
The result of the var isWithinPolygon will be "true" if the coordinates we passed are in the "dublinTriangle" object, and obviously "false" if they will not.
Then we can use this variable to execute some code or another, that's it!
So the very final javascript code, will be the following:
var map;
var dublinTriangle;
var bounds = new google.maps.LatLngBounds();
var geocoder = new google.maps.Geocoder();
var marker;
function initialize(){
var myLatLng = new google.maps.LatLng(53.332251,-6.229965);
var myOptions = {
zoom: 13,
center: myLatLng,
mapTypeId: google.maps.MapTypeId.TERRAIN
};
map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
var dublinCoords = [
new google.maps.LatLng(53.335342,-6.227812399999948),
new google.maps.LatLng(53.344339,-6.265383),
new google.maps.LatLng(53.3359108,-6.289094299999988),
new google.maps.LatLng(53.335342,-6.227812399999948)
];
for (i = 0; i < dublinCoords.length; i++) {
bounds.extend(dublinCoords[i]);
}
map.fitBounds(bounds);
dublinTriangle = new google.maps.Polygon({
paths: dublinCoords,
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: "#FF0000",
fillOpacity: 0.35
});
dublinTriangle.setMap(map);
}
function codeAddress() {
var address = document.getElementById("address").value;
geocoder.geocode( { 'address': address}, function(results, status) {
if (status == google.maps.GeocoderStatus.OK) {
map.setCenter(results[0].geometry.location);
var isWithinPolygon = dublinTriangle.containsLatLng(results[0].geometry.location);
marker = new google.maps.Marker({
map: map,
position: results[0].geometry.location
});
} else {
alert("Geocode was not successful for the following reason: " + status);
}
});
}