You may want to consider tackling this problem by adding addional circles at x
intervals with increasing radiuses between each point of the path. This would be very easy to implement and will work for any direction of the cyclone. Obviously Matti's suggested solution to create a polygon by connecting all the tangents would be more accurate, but you can consider this as an possible alternative. The main downside is that it may require some effort to make it look pretty, and it will obviously use more client-side resources than if you were to render a single polygon.
Let's start by recreating your map:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<title>Google Maps Cyclones</title>
<script src="http://maps.google.com/maps/api/js?sensor=false"
type="text/javascript"></script>
</head>
<body>
<div id="map" style="width: 600px; height: 400px"></div>
<script type="text/javascript">
var i;
var mapOptions = {
mapTypeId: google.maps.MapTypeId.TERRAIN,
center: new google.maps.LatLng(28.50, -81.50),
zoom: 5
};
var map = new google.maps.Map(document.getElementById("map"),
mapOptions);
var pathPoints = [
new google.maps.LatLng(25.48, -71.26),
new google.maps.LatLng(25.38, -73.70),
new google.maps.LatLng(25.28, -77.00),
new google.maps.LatLng(25.24, -80.11),
new google.maps.LatLng(25.94, -82.71),
new google.maps.LatLng(27.70, -87.14)
];
pathPoints[0].radius = 80;
pathPoints[1].radius = 100;
pathPoints[2].radius = 200;
pathPoints[3].radius = 300;
pathPoints[4].radius = 350;
pathPoints[5].radius = 550;
new google.maps.Polyline({
path: pathPoints,
strokeColor: '#00FF00',
strokeOpacity: 1.0,
strokeWeight: 3,
map: map
});
for (i = 0; i < pathPoints.length; i++) {
new google.maps.Circle({
center: pathPoints[i],
radius: pathPoints[i].radius * 1000,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeOpacity: 0.5,
strokeWeight: 1,
map: map
});
}
</script>
</body>
</html>
I assume that you heave already arrived to this point, and therefore the above example should be self-explanatory. Basically we have just defined 6 points, along with 6 radiuses, and we have rendered the circles on the map, together with the green path.
Before we continue, we need to define a few methods to be able to calculate the distance and the bearing from one point to another. We will also need a method that will return the destination point when given a bearing and the distance travelled from a source point. Fortunately, there is a very good JavaScript implementation for these methods by Chris Veness at Calculate distance, bearing and more between Latitude/Longitude points. The following methods have been adapted to work with Google's google.maps.LatLng
:
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
Number.prototype.toDeg = function() {
return this * 180 / Math.PI;
}
google.maps.LatLng.prototype.destinationPoint = function(brng, dist) {
dist = dist / 6371;
brng = brng.toRad();
var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();
var lat2 = Math.asin( Math.sin(lat1)*Math.cos(dist) +
Math.cos(lat1)*Math.sin(dist)*Math.cos(brng) );
var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1),
Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new google.maps.LatLng(lat2.toDeg(), lon2.toDeg());
}
google.maps.LatLng.prototype.bearingTo = function(point) {
var lat1 = this.lat().toRad(), lat2 = point.lat().toRad();
var dLon = (point.lng()-this.lng()).toRad();
var y = Math.sin(dLon) * Math.cos(lat2);
var x = Math.cos(lat1)*Math.sin(lat2) -
Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
var brng = Math.atan2(y, x);
return ((brng.toDeg()+360) % 360);
}
google.maps.LatLng.prototype.distanceTo = function(point) {
var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();
var lat2 = point.lat().toRad(), lon2 = point.lng().toRad();
var dLat = lat2 - lat1;
var dLon = lon2 - lon1;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(dLon/2) * Math.sin(dLon/2);
return 6371 * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
}
We would then need to add another loop that renders the intermediate circles inside the for
loop that we used previously to render the original circles. Here is how it can be implemented:
var distanceStep = 50; // Render an intermediate circle every 50km.
for (i = 0; i < pathPoints.length; i++) {
new google.maps.Circle({
center: pathPoints[i],
radius: pathPoints[i].radius * 1000,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeOpacity: 0.5,
strokeWeight: 1,
map: map
});
if (i < (pathPoints.length - 1)) {
distanceToNextPoint = pathPoints[i].distanceTo(pathPoints[i + 1]);
bearingToNextPoint = pathPoints[i].bearingTo(pathPoints[i + 1]);
radius = pathPoints[i].radius;
radiusIncrement = (pathPoints[i + 1].radius - radius) /
(distanceToNextPoint / distanceStep);
for (j = distanceStep;
j < distanceToNextPoint;
j += distanceStep, radius += radiusIncrement) {
new google.maps.Circle({
center: pathPoints[i].destinationPoint(bearingToNextPoint, j),
radius: radius * 1000,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeWeight: 0,
map: map
});
}
}
}
This is what we would get:
And this is how it would look without the black stroke around the original circles:
As you may notice, the main challenge will be to render the circles with a consistent opacity, even when they overlap on each other. There are a few options to achieve this, but that could be the topic of another question.
In any case, the following is the full implementation for this example:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<title>Google Maps Cyclones</title>
<script src="http://maps.google.com/maps/api/js?sensor=false"
type="text/javascript"></script>
</head>
<body>
<div id="map" style="width: 600px; height: 400px"></div>
<script type="text/javascript">
Number.prototype.toRad = function() {
return this * Math.PI / 180;
}
Number.prototype.toDeg = function() {
return this * 180 / Math.PI;
}
google.maps.LatLng.prototype.destinationPoint = function(brng, dist) {
dist = dist / 6371;
brng = brng.toRad();
var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();
var lat2 = Math.asin( Math.sin(lat1)*Math.cos(dist) +
Math.cos(lat1)*Math.sin(dist)*Math.cos(brng) );
var lon2 = lon1 + Math.atan2(Math.sin(brng)*Math.sin(dist)*Math.cos(lat1),
Math.cos(dist)-Math.sin(lat1)*Math.sin(lat2));
if (isNaN(lat2) || isNaN(lon2)) return null;
return new google.maps.LatLng(lat2.toDeg(), lon2.toDeg());
}
google.maps.LatLng.prototype.bearingTo = function(point) {
var lat1 = this.lat().toRad(), lat2 = point.lat().toRad();
var dLon = (point.lng()-this.lng()).toRad();
var y = Math.sin(dLon) * Math.cos(lat2);
var x = Math.cos(lat1)*Math.sin(lat2) -
Math.sin(lat1)*Math.cos(lat2)*Math.cos(dLon);
var brng = Math.atan2(y, x);
return ((brng.toDeg()+360) % 360);
}
google.maps.LatLng.prototype.distanceTo = function(point) {
var lat1 = this.lat().toRad(), lon1 = this.lng().toRad();
var lat2 = point.lat().toRad(), lon2 = point.lng().toRad();
var dLat = lat2 - lat1;
var dLon = lon2 - lon1;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1) * Math.cos(lat2) *
Math.sin(dLon/2) * Math.sin(dLon/2);
return 6371 * (2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)));
}
var i;
var j;
var distanceToNextPoint;
var bearingToNextPoint;
var radius;
var radiusIncrement;
var distanceStep = 50; // Render an intermediate circle every 50km.
var mapOptions = {
mapTypeId: google.maps.MapTypeId.TERRAIN,
center: new google.maps.LatLng(28.50, -81.50),
zoom: 5
};
var map = new google.maps.Map(document.getElementById("map"), mapOptions);
var pathPoints = [
new google.maps.LatLng(25.48, -71.26),
new google.maps.LatLng(25.38, -73.70),
new google.maps.LatLng(25.28, -77.00),
new google.maps.LatLng(25.24, -80.11),
new google.maps.LatLng(25.94, -82.71),
new google.maps.LatLng(27.70, -87.14)
];
pathPoints[0].radius = 80;
pathPoints[1].radius = 100;
pathPoints[2].radius = 200;
pathPoints[3].radius = 300;
pathPoints[4].radius = 350;
pathPoints[5].radius = 550;
new google.maps.Polyline({
path: pathPoints,
strokeColor: '#00FF00',
strokeOpacity: 1.0,
strokeWeight: 3,
map: map
});
for (i = 0; i < pathPoints.length; i++) {
new google.maps.Circle({
center: pathPoints[i],
radius: pathPoints[i].radius * 1000,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeOpacity: 0.5,
strokeWeight: 0,
map: map
});
if (i < (pathPoints.length - 1)) {
distanceToNextPoint = pathPoints[i].distanceTo(pathPoints[i + 1]);
bearingToNextPoint = pathPoints[i].bearingTo(pathPoints[i + 1]);
radius = pathPoints[i].radius;
radiusIncrement = (pathPoints[i + 1].radius - radius) /
(distanceToNextPoint / distanceStep);
for (j = distanceStep;
j < distanceToNextPoint;
j += distanceStep, radius += radiusIncrement) {
new google.maps.Circle({
center: pathPoints[i].destinationPoint(bearingToNextPoint, j),
radius: radius * 1000,
fillColor: '#FF0000',
fillOpacity: 0.2,
strokeWeight: 0,
map: map
});
}
}
}
</script>
</body>
</html>