google-mapsgoogle-maps-api-3map-projectionsmercator

is it a bug behind google.maps.geometry.computeOffset?


I am trying to draw a rhumb line constantly pointing at Northeast (45 deg) on google maps. That means if I sail along this line, whenever I look at my compass, I will be always heading to NW. This is the JS code I created with Google maps API. (please replace the API key if you attempt to test it)

I assume no matter I draw one line or multiple lines with different intervals, they will always overlap each other. But the result turned out not. The more points you plot alone the line, the line is closer to 45 deg to the screen. enter image description here Is it due to precision or there is a bug? Is google.maps.geometry.computeOffset doing the right job? Should I trust this function?

function initMap() {
  var mapOptions = {
    zoom: 1,
    center: {
      lat: 30.363,
      lng: 250.044
    }
  };

  var map = new google.maps.Map(document.getElementById('map'), mapOptions);

  var origin = new google.maps.LatLng(-25.363, 214.044);

  var fineLoxo = drawRhumbLine(origin.lat(), origin.lng(), 45, 1000);
  var fineDrawingPath = new google.maps.Polyline({
    path: fineLoxo,
    geodesic: false, // Set to false because loxodrome is not eodesic
    strokeColor: '#000',
    strokeOpacity: 1.0,
    strokeWeight: 5
  });
  fineDrawingPath.setMap(map);


  var brokenLoxoPath = drawRhumbLine(origin.lat(), origin.lng(), 45, 4000000);

  var mediumDrawingPath = new google.maps.Polyline({
    path: brokenLoxoPath,
    geodesic: false, // Set to false because loxodrome is not geodesic
    strokeColor: '#FF0000',
    strokeOpacity: 1.0,
    strokeWeight: 2
  });

  mediumDrawingPath.setMap(map);

  var oneLoxoPath = drawRhumbLine(origin.lat(), origin.lng(), 45, 16000000);
  var straightDrawingPath = new google.maps.Polyline({
    path: oneLoxoPath,
    geodesic: false, // Set to false because loxodrome is not geodesic
    strokeColor: '#00F',
    strokeOpacity: 1.0,
    strokeWeight: 2
  });

  straightDrawingPath.setMap(map);
}

function drawRhumbLine(startLat, startLng, bearingDegrees, interval) {
  var dots = []

  dots.push({
    lat: startLat,
    lng: startLng
  });

  for (let i = 0; i < 16000000 / interval; i++) {
    var tempOrigin = new google.maps.LatLng(startLat, startLng)
    var dest = google.maps.geometry.spherical.computeOffset(tempOrigin, interval, bearingDegrees);
    startLat = dest.lat();
    startLng = dest.lng();
    dots.push({
      lat: startLat,
      lng: startLng
    });
  }
  return dots;
}
<div id="map" style="height: 180px; width: 100%;"></div>

<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk&libraries=geometry&callback=initMap"
        async defer></script>


Solution

  • First to answer your question: Yes, you can trust that google.maps.geometry.spherical.computeOffset functions correctly.

    Secondly your drawRhumbLine functions is not actually calculating a Rhumb line or loxodrome, since it's using google.maps.geometry.spherical.computeOffset. This function as the name implies uses spherical calculations for large circle distances, this is not what you want for loxodromes.

    Instead sice you want a loxodrome, and you have a Mercator projection you should use the Mercator math. This wonderful article explains it quite well. I used it for my version of the drawRhumbLine.

    Your drawRhumbLine function should therefore be this:

    function drawRhumbLine(startLat, startLng, bearingDegrees, interval) {
      var dots = []
    
      const bearing = bearingDegrees * Math.PI / 180;
      const bearingB = (180 - 90 - bearingDegrees) * Math.PI / 180;
      for (let i = 0; i <= 113.137085 / interval; i++) {
        let lat = startLat + i*interval * Math.cos(bearing);
        let lat1 = (lat/2+45)*Math.PI/180;
        let lat0 = (startLat/2+45)*Math.PI/180;
        let dest = {
          lat: startLat + i*interval * Math.cos(bearing),
          lng: startLng + (Math.tan(bearing)*180/Math.PI) * Math.log(Math.tan(lat1) / Math.tan(lat0))
        };
        dots.push(dest);
      }
      return dots;
    }
    

    I put the full example here.