Binning data into a hexagonal grid in Google Maps

2019-04-02 00:58发布

问题:

I'm trying to display geospatial data in a hexagonal grid on a Google Map.

In order to do so, given a hexagon tile grid size X I need to be able to convert ({lat, lng}) coordinates into the ({lat, lng}) centers of the hexagon grid tiles that contain them.

In the end, I would like to be able to display data on a Google Map like this:

Does anybody have any insight into how this is done?

I've tried porting this Python hexagon binning script, binner.py to Javascript but it doesn't seem to be working properly- the output values are all the same as the input ones.

For the sake of this example, I don't care if there are multiple polygons in a single location, I just need to figure out how to bin them into the correct coordinates.

Code below, (Plunker here!)

var map;
var pointCount = 0;
var locations = [];
var gridWidth = 200000; // hex tile size in meters
var bounds;

var places = [
  [44.13, -69.51],
  [45.23, -67.42],
  [46.33, -66.53],
  [44.43, -65.24],
  [46.53, -64.15],
  [44.63, -63.06],
  [44.73, -62.17],
  [43.83, -63.28],
  [44.93, -64.39],
  [44.13, -65.41],
  [41.23, -66.52],
  [44.33, -67.63],
  [42.43, -68.74],
  [44.53, -69.65],
  [40.63, -70.97],
]

var SQRT3 = 1.73205080756887729352744634150587236;

$(document).ready(function(){

  bounds = new google.maps.LatLngBounds();

  map = new google.maps.Map(document.getElementById("map_canvas"), {center: {lat: 0, lng: 0}, zoom: 2});

  // Adding a marker just so we can visualize where the actual data points are.
  // In the end, we want to see the hex tile that contain them
  places.forEach(function(place, p){

    latlng = new google.maps.LatLng({lat: place[0], lng: place[1]});
    marker = new google.maps.Marker({position: latlng, map: map})

    // Fitting to bounds so the map is zoomed to the right place
    bounds.extend(latlng);
  });

  map.fitBounds(bounds);

  // Now, we draw our hexagons! (or try to)
  locations = makeBins(places);

  locations.forEach(function(place, p){
    drawHorizontalHexagon(map, place, gridWidth);
  })


});


  function drawHorizontalHexagon(map,position,radius){
    var coordinates = [];
    for(var angle= 0;angle < 360; angle+=60) {
       coordinates.push(google.maps.geometry.spherical.computeOffset(position, radius, angle));    
    }

    // Construct the polygon.
    var polygon = new google.maps.Polygon({
        paths: coordinates,
        position: position,
        strokeColor: '#FF0000',
        strokeOpacity: 0.8,
        strokeWeight: 2,
        fillColor: '#FF0000',
        fillOpacity: 0.35,
        geodesic: true
    });
    polygon.setMap(map);
}

// Below is my attempt at porting binner.py to Javascript.
// Source: https://github.com/coryfoo/hexbins/blob/master/hexbin/binner.py

function distance(x1, y1, x2, y2){
  console.log(x1, y1, x2, y2);
  result =  Math.sqrt(((x1 - x2) * (x1 - x2)) + ((y1 - y2) * (y1 - y2)));
  console.log("Distance: ", result);
  return result;
}

function nearestCenterPoint(value, scale){
    div = (value / (scale / 2));
    mod = value % (scale / 2);

    if(div % 2 == 1){
      increment = 1;
    } else {
      increment = 0;
    }

    rounded = (scale / 2) * (div + increment);

    if(div % 2 === 0){
      increment = 1;
    } else {
      increment = 0;
    }

    rounded_scaled = (scale / 2) * (div + increment)
    result = [rounded, rounded_scaled];

    return result;
}

function makeBins(data){
  bins = [];

  data.forEach(function(place, p){
    x = place[0];
    y = place[1];

    console.log("Original location:", x, y);

    px_nearest = nearestCenterPoint(x, gridWidth);

    py_nearest = nearestCenterPoint(y, gridWidth * SQRT3);

    z1 = distance(x, y, px_nearest[0], py_nearest[0]);

    z2 = distance(x, y, px_nearest[1], py_nearest[1]);

    console.log(z1, z2);

    if(z1 > z2){
      bin = new google.maps.LatLng({lat: px_nearest[0], lng: py_nearest[0]});
       console.log("Final location:", px_nearest[0], py_nearest[0]);
    } else {
      bin = new google.maps.LatLng({lat: px_nearest[1], lng: py_nearest[1]});
       console.log("Final location:", px_nearest[1], py_nearest[1]);
    }

    bins.push(bin);

  })
  return bins;
}   

回答1:

Use google.maps.geometry.poly.containsLocation.

for (var i = 0; i < hexgrid.length; i++) {
  if (google.maps.geometry.poly.containsLocation(place, hexgrid[i])) {
    if (!hexgrid[i].contains) {
      hexgrid[i].contains = 0;
    }
    hexgrid[i].contains++
  }
}

Example based off this related question: How can I make a Google Maps API v3 hexagon tiled map, preferably coordinate-based?. The number in the white box in the center of each hexagon is the number of markers contained by it.

proof of concept fiddle

code snippet:

var map = null;
var hexgrid = [];

function initMap() {
  var myOptions = {
    zoom: 8,
    center: new google.maps.LatLng(43, -79.5),
    mapTypeControl: true,
    mapTypeControlOptions: {
      style: google.maps.MapTypeControlStyle.DROPDOWN_MENU
    },
    navigationControl: true,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  }
  map = new google.maps.Map(document.getElementById("map"),
    myOptions);
  createHexGrid();
  var bounds = new google.maps.LatLngBounds();
  // Seed our dataset with random locations
  for (var i = 0; i < hexgrid.length; i++) {
    var hexbounds = new google.maps.LatLngBounds();
    for (var j = 0; j < hexgrid[i].getPath().getLength(); j++) {
      bounds.extend(hexgrid[i].getPath().getAt(j));
      hexbounds.extend(hexgrid[i].getPath().getAt(j));
    }
    hexgrid[i].bounds = hexbounds;
  }
  var span = bounds.toSpan();
  var locations = [];
  for (pointCount = 0; pointCount < 50; pointCount++) {
    place = new google.maps.LatLng(Math.random() * span.lat() + bounds.getSouthWest().lat(), Math.random() * span.lng() + bounds.getSouthWest().lng());
    bounds.extend(place);
    locations.push(place);
    var mark = new google.maps.Marker({
      map: map,
      position: place
    });
    // bin points in hexgrid
    for (var i = 0; i < hexgrid.length; i++) {
      if (google.maps.geometry.poly.containsLocation(place, hexgrid[i])) {
        if (!hexgrid[i].contains) {
          hexgrid[i].contains = 0;
        }
        hexgrid[i].contains++
      }
    }
  }
  // add labels
  for (var i = 0; i < hexgrid.length; i++) {
    if (typeof hexgrid[i].contains == 'undefined') {
      hexgrid[i].contains = 0;
    }
    var labelText = "<div style='background-color:white'>" + hexgrid[i].contains + "</div>";


    var myOptions = {
      content: labelText,
      boxStyle: {
        border: "1px solid black",
        textAlign: "center",
        fontSize: "8pt",
        width: "20px"
      },
      disableAutoPan: true,
      pixelOffset: new google.maps.Size(-10, 0),
      position: hexgrid[i].bounds.getCenter(),
      closeBoxURL: "",
      isHidden: false,
      pane: "floatPane",
      enableEventPropagation: true
    };

    var ibLabel = new InfoBox(myOptions);
    ibLabel.open(map);
  }

}

function createHexGrid() {
  // === Hexagonal grid ===
  var point = new google.maps.LatLng(42, -78.8);
  map.setCenter(point);
  var hex1 = google.maps.Polygon.RegularPoly(point, 25000, 6, 90, "#000000", 1, 1, "#00ff00", 0.5);
  hex1.setMap(map);
  var d = 2 * 25000 * Math.cos(Math.PI / 6);
  hexgrid.push(hex1);
  var hex30 = google.maps.Polygon.RegularPoly(EOffsetBearing(point, d, 30), 25000, 6, 90, "#000000", 1, 1, "#00ffff", 0.5);
  hex30.setMap(map);
  hexgrid.push(hex30);
  var hex90 = google.maps.Polygon.RegularPoly(EOffsetBearing(point, d, 90), 25000, 6, 90, "#000000", 1, 1, "#ffff00", 0.5);
  hex90.setMap(map);
  hexgrid.push(hex90);
  var hex150 = google.maps.Polygon.RegularPoly(EOffsetBearing(point, d, 150), 25000, 6, 90, "#000000", 1, 1, "#00ffff", 0.5);
  hex150.setMap(map);
  hexgrid.push(hex150);
  var hex210 = google.maps.Polygon.RegularPoly(EOffsetBearing(point, d, 210), 25000, 6, 90, "#000000", 1, 1, "#ffff00", 0.5);
  hex210.setMap(map);
  hexgrid.push(hex210);
  hex270 = google.maps.Polygon.RegularPoly(EOffsetBearing(point, d, 270), 25000, 6, 90, "#000000", 1, 1, "#ffff00", 0.5);
  hex270.setMap(map);
  hexgrid.push(hex270);
  var hex330 = google.maps.Polygon.RegularPoly(EOffsetBearing(point, d, 330), 25000, 6, 90, "#000000", 1, 1, "#ffff00", 0.5);
  hex330.setMap(map);
  hexgrid.push(hex330);
  var hex30_2 = google.maps.Polygon.RegularPoly(EOffsetBearing(EOffsetBearing(point, d, 30), d, 90), 25000, 6, 90, "#000000", 1, 1, "#ff0000", 0.5);
  hex30_2.setMap(map);
  hexgrid.push(hex30_2);
  var hex150_2 = google.maps.Polygon.RegularPoly(EOffsetBearing(EOffsetBearing(point, d, 150), d, 90), 25000, 6, 90, "#000000", 1, 1, "#0000ff", 0.5);
  hex150_2.setMap(map);
  hexgrid.push(hex150_2);
  var hex90_2 = google.maps.Polygon.RegularPoly(EOffsetBearing(EOffsetBearing(point, d, 90), d, 90), 25000, 6, 90, "#000000", 1, 1, "#00ff00", 0.5);
  hex90_2.setMap(map);
  hexgrid.push(hex90_2);

  // This Javascript is based on code provided by the
  // Community Church Javascript Team
  // http://www.bisphamchurch.org.uk/   
  // http://econym.org.uk/gmap/

  //]]>
}
google.maps.event.addDomListener(window, 'load', initMap);

// EShapes.js
//
// Based on an idea, and some lines of code, by "thetoy" 
//
//   This Javascript is provided by Mike Williams
//   Community Church Javascript Team
//   http://www.bisphamchurch.org.uk/   
//   http://econym.org.uk/gmap/
//
//   This work is licenced under a Creative Commons Licence
//   http://creativecommons.org/licenses/by/2.0/uk/
//
// Version 0.0 04/Apr/2008 Not quite finished yet
// Version 1.0 10/Apr/2008 Initial release
// Version 3.0 12/Oct/2011 Ported to v3 by Lawrence Ross

google.maps.Polygon.Shape = function(point, r1, r2, r3, r4, rotation, vertexCount, strokeColour, strokeWeight, Strokepacity, fillColour, fillOpacity, opts, tilt) {
  var rot = -rotation * Math.PI / 180;
  var points = [];
  var latConv = google.maps.geometry.spherical.computeDistanceBetween(point, new google.maps.LatLng(point.lat() + 0.1, point.lng())) * 10;
  var lngConv = google.maps.geometry.spherical.computeDistanceBetween(point, new google.maps.LatLng(point.lat(), point.lng() + 0.1)) * 10;
  var step = (360 / vertexCount) || 10;

  var flop = -1;
  if (tilt) {
    var I1 = 180 / vertexCount;
  } else {
    var I1 = 0;
  }
  for (var i = I1; i <= 360.001 + I1; i += step) {
    var r1a = flop ? r1 : r3;
    var r2a = flop ? r2 : r4;
    flop = -1 - flop;
    var y = r1a * Math.cos(i * Math.PI / 180);
    var x = r2a * Math.sin(i * Math.PI / 180);
    var lng = (x * Math.cos(rot) - y * Math.sin(rot)) / lngConv;
    var lat = (y * Math.cos(rot) + x * Math.sin(rot)) / latConv;

    points.push(new google.maps.LatLng(point.lat() + lat, point.lng() + lng));
  }
  return (new google.maps.Polygon({
    paths: points,
    strokeColor: strokeColour,
    strokeWeight: strokeWeight,
    strokeOpacity: Strokepacity,
    fillColor: fillColour,
    fillOpacity: fillOpacity
  }))
}

google.maps.Polygon.RegularPoly = function(point, radius, vertexCount, rotation, strokeColour, strokeWeight, Strokepacity, fillColour, fillOpacity, opts) {
  rotation = rotation || 0;
  var tilt = !(vertexCount & 1);
  return google.maps.Polygon.Shape(point, radius, radius, radius, radius, rotation, vertexCount, strokeColour, strokeWeight, Strokepacity, fillColour, fillOpacity, opts, tilt)
}

function EOffsetBearing(point, dist, bearing) {
  var latConv = google.maps.geometry.spherical.computeDistanceBetween(point, new google.maps.LatLng(point.lat() + 0.1, point.lng())) * 10;
  var lngConv = google.maps.geometry.spherical.computeDistanceBetween(point, new google.maps.LatLng(point.lat(), point.lng() + 0.1)) * 10;
  var lat = dist * Math.cos(bearing * Math.PI / 180) / latConv;
  var lng = dist * Math.sin(bearing * Math.PI / 180) / lngConv;
  return new google.maps.LatLng(point.lat() + lat, point.lng() + lng)
}
html,
body,
#map {
  height: 100%;
  width: 100%;
  margin: 0px;
  padding: 0px
}
<script src="https://maps.googleapis.com/maps/api/js?libraries=geometry"></script>
<script src="https://google-maps-utility-library-v3.googlecode.com/svn/trunk/infobox/src/infobox.js"></script>
<div id="map"></div>