Google Maps heatmap layer point radius
Asked Answered
W

4

12

I want to be able to specify the radius of points effectively in meters. The API is configured so that the radius property is held to be constant for pixels, so that zooming in causes the heatmap to erode (I know you can make the heatmap not erode with the dissipating property, but this raises other issues, i.e., having to manually mess with the radius to get my heatmap to display properly. Here is the heatmaps reference.

Specifically, I'm trying to display a probability distribution on a map. I have the distribution in image form, and want to map the 0-1 weights to a heatmap layer. (I can, and don't want to, overlay the images).

Any suggestions?

Whin answered 6/9, 2012 at 0:24 Comment(1)
I think you will need to use the fact that at each zoom level, the distances per pixel double, then redraw a heatmap for each zoom level?Libriform
S
14

Ok, I tried some things:

Using the Mercator Projection example (check the source) to extract the x,y pixel coordinates of any point from a latLng, to later use the geometry library, specifically the computeOffset function get another latLng a distance "DM" (in meters) to the right of the previous one, get the difference (in pixels) as an absolute value "DP" and from there you get your "pixelsPerMeter" ratio DP/DM.

So then, if you want your radius to be 100 meters you just set the properties to {radius:Math.floor(desiredRadiusPerPointInMeters*pixelsPerMeter)}

And to handle the change in zoom just use a listener

 google.maps.event.addListener(map, 'zoom_changed', function () {
          heatmap.setOptions({radius:getNewRadius()});
      });

I uploaded a small example (try zooming), you can check if the distance looks right with the button on the bottom.

Stoops answered 6/9, 2012 at 2:31 Comment(3)
I think I have it almost working, however, the TILE_SIZE parameter is throwing me off. Why is it set to 256, is this arbitrary? Does the map, by default, have a tile size of (256x256)? How can I get the correct tile size?Whin
thanks. one thing to note is that when you zoom out too far, each point can't fit in a pixel anymore and the radii are set to zero. adding this code in the getNewRadius() method makes it so the points are rendered as nondissipating (inaccurate), but at least they're rendered(dissipating=false at init) if(totalPixelSize == 0 && dissipating == true) { heatmap.setOptions({dissipating: false}); totalPixelSize=.01; dissipate=false;} else if(totalPixelSize > 0 && dissipate==false) {heatmap.setOptions({dissipating: true}); dissipate=true;} else if(totalPixelSize == 0) { totalPixelSize=.01;}Whin
@nrhine1 The number 256 comes from Because the basic Mercator Google Maps tile is 256 x 256 pixels, the usable world coordinate space is {0-256}, {0-256} in this documentation page, thanks for the feedback regarding the dissipation :)Stoops
S
2

Based on a Google group forum post and this GIS post, I came up with a simple yet complete solution along the same lines.

First, define a function to get the radius in meters for a given zoom level: Because there are scaling differences for different latitudes, you need to feed in someLatValue, for example the center of the map your plan on using. Although an approximation, it will be good enough for accurate results up to the size of a small country. You also need to specify the size of the radius you want in meters.

You could change the function to read these values in as parameters if you prefer (e.g., getting the lat of the center of the current map view and/or a radius based on property of the data), but if they are static, this making them globals is easier.

var someLatValue = 35.726332;
var desiredRadiusInMeters = 1500;

function getHeatmapRadius(){
  metersPerPx = 156543.03392 * Math.cos(someLatValue * Math.PI / 180) / Math.pow(2,theMap.getZoom());
  return desiredRadiusInMeters / metersPerPx;
};

This returns the (approximate) radius in pixels for a desired number of meters and zoom level around a particular latitude. The value 156543.03392 is based on the approximate radius of the Earth that google actually uses for Google Maps.

So, say you have a heatmap like this:

fixedRadiusHeatmap = new google.maps.visualization.HeatmapLayer({
  data: myCoords,
  map: theMap
});

In order to set the initial view, just call the function before adding the heatmap to your map.

fixedRadiusHeatmap.setOptions({radius: getHeatmapRadius()});
fixedRadiusHeatmap.setMap(theMap);

Then you need to reset the radius every time the map is zoomed, which can be done like this:

google.maps.event.addListener(theMap, 'zoom_changed', function () {
  fixedRadiusHeatmap.setOptions({radius: getHeatmapRadius()});
});

In my case it lags a bit, so it shows the (stupidly aggregated and poorly thought out) default heatmap before the fixed radius one appears. Maybe there is a way to delay the rendering to make the transition smoother, but that's a different problem.

Sweater answered 25/1, 2019 at 5:37 Comment(1)
This answer is clean and works very well to maintain consistent size and was easy to set the radius to an exact mileageCrin
L
1

For anyone who'd like to have a nicely packaged coffeescript version of @lccarrasco's jsbin example, you can view the gist of the MercatorProjection coffeescript class I created using his code.

Once you have the class loaded, you can use it with the following:

map = new google.maps.Map(...)
heatmap = new google.maps.visualization.HeatmapLayer({map: map})
google.maps.event.addListener(map, 'zoom_changed', () ->
  projection = new MercatorProjection()
  heatmap.setOptions(radius: projection.getNewRadius(map, 15))
)

Where '15' is the radius in meters which you can play with or set programmatically by other means to get your heatmap to look like you want it.

Lubricant answered 26/6, 2014 at 16:58 Comment(1)
Be sure to include the geometry library, ie: <script src='maps.googleapis.com/maps/api/…'> . Thanks @Lubricant for the class! works greatJunket
S
1

I solved this by using the listener that @lccarrasco used but in my getNewRadius() function i made the radius change relative to the zoom.

ie. var radius = (somemultiplicationfactor)/(Math.pow(2,(20-zoom)));

This works as the zoom ratio is 2:1 for each zoom

Strouse answered 5/4, 2016 at 3:21 Comment(1)
what about return multiplicator * Math.pow(2,zoom); ?Gilleod

© 2022 - 2024 — McMap. All rights reserved.