Binning data into a hexagonal grid in Google Maps

2019-04-02 00:55发布

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:

enter image description here

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条回答
Lonely孤独者°
2楼-- · 2019-04-02 01:48

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>

查看更多
登录 后发表回答