Search code examples
androidpythongoogle-mapsgeopy

Google Maps draw square despite projection


Im trying to draw a square grid on a google maps via android.

If I measure out 250 meter by 250 meter distances on my python server using VincentyDistance in geopy, the resulting grid on the map is significantly rectangular instead of square.

Id mark this up as caused by the projection used by google maps causing disproportionate stretching, though if I instead use google's SphericalUtil.computeOffset, I can generate near perfect squares on the map.

Does this suggest that the distances from computeOffset are wrong? Or that geopy is wrong? Can someone help me understand this behaviour?

Ideally Id like the calculations to be serverside, and ideally I can generate a square grid anywhere on the world, even if it means in reality that square is not square, as long as it appears square on the map projection.


Solution

  • EDIT:

    "Does this suggest that the distances from computeOffset are wrong? ..."

    No, it's not wrong, it's a projection. Projections stretch, squash, skew, ... everything

    The word is projected as a rectangle, twice as large as the height. 360° wide, 180° high. The north and south pole (both just a popint) are stretched as if they were as long as the equator.

    Except ... Google Maps then squashes that map, to almost a square (depending on the zoom). So you must check the bounds that Google Maps gives you


    You can read the degrees-density horizontally, then vertically, from the map bounds. When the tiles are loaded you can read this. If you want you can send this data to the server, with AJAX (not included in this code)

    One thing I can't help is if the pixels on your screen aren't square.

    Well, maybe you can search for the physical pixel density and factor that in as well. Maybe this helps: Mobile web: how to get physical pixel size?

    As an example: 400x400 px map. A red square in the middle, half the width and height of the map. Sorry, it's a javascript/website example, not Python/Android. But I suppose you can use the same principles.

    <!DOCTYPE html>
    <html>
    <head>
    <title>Square on Map</title>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <style>
        html,
        body {
            height: 400px;
            width: 400px;
            padding: 0;
        }
        #map {
            height: 80%;
        }
    </style>
    <script>
        var map;
        // return a polyline.   If you give 2 points, it will simply be a line
        function makePolyline(points, color, close) {
            if (close) {
                points.push(points[0]); // closed polyline, so duplicate the start point as the endpoint
            }
            return new google.maps.Polyline({
                path: points,
                //geodesic: false,
                strokeColor: color, // example: '#FF0000',
                strokeOpacity: 1.0,
                strokeWeight: 2,
                map: map 
            });
        }
    
        function initMap() {
            var myLocation = {
                lat: 43,
                lng: -108
            };
            map = new google.maps.Map(document.getElementById('map'), {
                center: myLocation,
                zoom: 5
            });
    
            // wait until the map is loaded before calculating bounds
            google.maps.event.addListenerOnce(map, 'tilesloaded', function() {
              var mapWidth = document.getElementById('map').offsetWidth;
              var mapHeight = document.getElementById('map').offsetWidth;
              var bounds = map.getBounds();
              var NE = bounds.getNorthEast();
              var SW = bounds.getSouthWest();
    
              // draw a square half the size of the map (200 X 200 px), in the midle  => start at 1/4, stop at 3/4
              points = [{
                lat: SW.lat() + ((NE.lat() - SW.lat()) * 1/4),
                lng: SW.lng() + ((NE.lng() - SW.lng()) * 1/4)
              },
              {
                lat: SW.lat() + ((NE.lat() - SW.lat()) * 1/4),
                lng: SW.lng() + ((NE.lng() - SW.lng()) * 3/4)
              },
              {
                lat: SW.lat() + ((NE.lat() - SW.lat()) * 3/4),
                lng: SW.lng() + ((NE.lng() - SW.lng()) * 3/4)
              },
              {
                lat: SW.lat() + ((NE.lat() - SW.lat()) * 3/4),
                lng: SW.lng() + ((NE.lng() - SW.lng()) * 1/4)
              }];
              makePolyline(points, '#ff0000', true);
    
              // display the numbers in a div.  Feel free to not do this
              document.getElementById('log').innerHTML  = 'map width: ' + mapWidth + 'px - map height: ' + mapHeight + 'px<br/>';
              document.getElementById('log').innerHTML += 'horizontal: ' + ((NE.lng() - SW.lng() ) / mapWidth ).toFixed(4) + ' degrees/px<br/>';
              document.getElementById('log').innerHTML += 'vertical: ' + ((NE.lat() - SW.lat() ) / mapHeight ).toFixed(4) + ' degrees/px<br/>';
            });
        }
    </script>
    </head>
    <body>
      <div id="map"></div>
      <div id="log"></div>
      <script src="https://maps.googleapis.com/maps/api/js?callback=initMap" async defer></script>
    </body>
    </html>