Google Maps Marker (calculated with epolys.js) not on polyline if zoom

149 Views Asked by At

My script shows a polyline between several markers. An additional marker (green circle) should visualize the distance already travelled.

The script works, but on high zoom-levels (12-15) the marker for the distance travelled do not "sit" on the polyline anymore, but many meters away. (see screengrab) The markers position is calculated with GetPointAtDistance from the Epolys.js-script.

See demo here: https://jsfiddle.net/faoq2jbr/1/

enter image description here

    <div id="container">
  <div id="map" style="width:100%; height: 400px;"></div>
</div>

<script>
  // initialise map
  function initMap() {
    var options = {
      center: {
        lat: 51.69869842676892,
        lng: 8.188009802432369
      },
      zoom: 14,
      mapId: '1ab596deb8cb9da8',
      mapTypeControl: false,
      streetViewControl: false,
      fullscreenControlOptions: {
        position: google.maps.ControlPosition.RIGHT_BOTTOM
      },
    }
    var map = new google.maps.Map(document.getElementById('map'), options);

    google.maps.LatLng.prototype.distanceFrom = function(newLatLng) {
      var EarthRadiusMeters = 6378137.0; // meters
      var lat1 = this.lat();
      var lon1 = this.lng();
      var lat2 = newLatLng.lat();
      var lon2 = newLatLng.lng();
      var dLat = (lat2 - lat1) * Math.PI / 180;
      var dLon = (lon2 - lon1) * Math.PI / 180;
      var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
        Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
        Math.sin(dLon / 2) * Math.sin(dLon / 2);
      var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
      var d = EarthRadiusMeters * c;
      return d;
    }

    google.maps.Polyline.prototype.GetPointAtDistance = function(metres) {
      // some awkward special cases
      if (metres == 0) return this.getPath().getAt(0);
      if (metres < 0) return null;
      if (this.getPath().getLength() < 2) return null;
      var dist = 0;
      var olddist = 0;
      for (var i = 1;
        (i < this.getPath().getLength() && dist < metres); i++) {
        olddist = dist;
        dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i - 1));
      }
      if (dist < metres) {
        return null;
      }
      var p1 = this.getPath().getAt(i - 2);
      var p2 = this.getPath().getAt(i - 1);
      var m = (metres - olddist) / (dist - olddist);
      return new google.maps.LatLng(p1.lat() + (p2.lat() - p1.lat()) * m, p1.lng() + (p2.lng() - p1.lng()) * m);
    }


    // Define a symbol using SVG path notation, with an opacity of 1.
    const dashedLine = {
      path: "M 0,-1 0,1",
      strokeOpacity: 1,
      scale: 8,
    };

    var markerCoordinates = [{
        lat: 51.17230192226146,
        lng: 7.005455256203302
      },
      {
        lat: 52.017106436819546,
        lng: 8.903316299753124
      },
      {
        lat: 52.1521613855702,
        lng: 9.972045956234473
      },
      {
        lat: 52.12123086563482,
        lng: 11.627830412053509
      },
      {
        lat: 53.6301544474316,
        lng: 11.415718027446243
      },
      {
        lat: 54.08291262244958,
        lng: 12.191652169789096
      },
      {
        lat: 54.3141629859056,
        lng: 13.097095856304708
      }
    ]

    // create markers
    for (i = 0; i < markerCoordinates.length; i++) {
      marker = new google.maps.Marker({
        position: new google.maps.LatLng(markerCoordinates[i]['lat'], markerCoordinates[i]['lng']),
        map: map,
        optimized: true,
      });
    }

    // create polylines
    const stepsRoute = new google.maps.Polyline({
      path: markerCoordinates,
      geodesic: true,
      strokeColor: "#c5d899",
      strokeOpacity: 0.2,
      icons: [{
        icon: dashedLine,
        offset: "0",
        repeat: "35px",
      }, ]
    });
    stepsRoute.setMap(map);

    var polylineLength = google.maps.geometry.spherical.computeLength(stepsRoute.getPath());
    var groupPosition = stepsRoute.GetPointAtDistance(100600);

// add marker at position of the group
    var positionMarker = new google.maps.Marker({
      map: map,
      position: groupPosition,
      icon: {
        path: google.maps.SymbolPath.CIRCLE,
        scale: 10,
        fillOpacity: 1,
        strokeWeight: 2,
        fillColor: '#5384ED',
        strokeColor: '#ffffff',
      },
    });
  };
</script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAvHtXFKU5QUOBQ_gpn2a0DM-3Yjx3H5jQ&callback=initMap&libraries=geometry">
´´´
1

There are 1 best solutions below

0
geocodezip On BEST ANSWER

Looks like the interpolation in GetPointAtDistance in the epoly code is not as accurate as (or at least consistent with) the code in the Google Maps JavaScript API v3 geometry library.

If I replace the existing interpolation:

    return new google.maps.LatLng(p1.lat() + (p2.lat() - p1.lat()) * m, p1.lng() + (p2.lng() - p1.lng()) * m);

with the google.maps.geometry.spherical.interpolate method:

   return google.maps.geometry.spherical.interpolate(p1, p2, m);

The marker ends up on the line (when the line is geodesic: true).

proof of concept fiddle

screenshot of resulting map

code snippet:

// initialise map
function initMap() {
  var options = {
    center: {
      lat: 51.69869842676892,
      lng: 8.188009802432369
    },
    zoom: 14,
    mapId: '1ab596deb8cb9da8',
    mapTypeControl: false,
    streetViewControl: false,
    fullscreenControlOptions: {
      position: google.maps.ControlPosition.RIGHT_BOTTOM
    },
  }
  var map = new google.maps.Map(document.getElementById('map'), options);

  google.maps.LatLng.prototype.distanceFrom = function(newLatLng) {
    var EarthRadiusMeters = 6378137.0; // meters
    var lat1 = this.lat();
    var lon1 = this.lng();
    var lat2 = newLatLng.lat();
    var lon2 = newLatLng.lng();
    var dLat = (lat2 - lat1) * Math.PI / 180;
    var dLon = (lon2 - lon1) * Math.PI / 180;
    var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
      Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
      Math.sin(dLon / 2) * Math.sin(dLon / 2);
    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
    var d = EarthRadiusMeters * c;
    return d;
  }

  google.maps.Polyline.prototype.GetPointAtDistance = function(metres) {
    // some awkward special cases
    if (metres == 0) return this.getPath().getAt(0);
    if (metres < 0) return null;
    if (this.getPath().getLength() < 2) return null;
    var dist = 0;
    var olddist = 0;
    for (var i = 1;
      (i < this.getPath().getLength() && dist < metres); i++) {
      olddist = dist;
      dist += this.getPath().getAt(i).distanceFrom(this.getPath().getAt(i - 1));
    }
    if (dist < metres) {
      return null;
    }
    var p1 = this.getPath().getAt(i - 2);
    var p2 = this.getPath().getAt(i - 1);
    var m = (metres - olddist) / (dist - olddist);
    // updated to use the geometry library function
    return google.maps.geometry.spherical.interpolate(p1, p2, m);
  }


  // Define a symbol using SVG path notation, with an opacity of 1.
  const dashedLine = {
    path: "M 0,-1 0,1",
    strokeOpacity: 1,
    scale: 8,
  };

  var markerCoordinates = [{
      lat: 51.17230192226146,
      lng: 7.005455256203302
    },
    {
      lat: 52.017106436819546,
      lng: 8.903316299753124
    },
    {
      lat: 52.1521613855702,
      lng: 9.972045956234473
    },
    {
      lat: 52.12123086563482,
      lng: 11.627830412053509
    },
    {
      lat: 53.6301544474316,
      lng: 11.415718027446243
    },
    {
      lat: 54.08291262244958,
      lng: 12.191652169789096
    },
    {
      lat: 54.3141629859056,
      lng: 13.097095856304708
    }
  ]

  // create markers
  for (i = 0; i < markerCoordinates.length; i++) {
    marker = new google.maps.Marker({
      position: new google.maps.LatLng(markerCoordinates[i]['lat'], markerCoordinates[i]['lng']),
      map: map,
      optimized: true,
    });
  }

  // create polylines
  const stepsRoute = new google.maps.Polyline({
    path: markerCoordinates,
    geodesic: true,
    strokeColor: "#c5d899",
    strokeOpacity: 0.2,
    icons: [{
      icon: dashedLine,
      offset: "0",
      repeat: "35px",
    }, ]
  });
  stepsRoute.setMap(map);

  var polylineLength = google.maps.geometry.spherical.computeLength(stepsRoute.getPath());
  var groupPosition = stepsRoute.GetPointAtDistance(100600);

  // add marker at position of the group
  var positionMarker = new google.maps.Marker({
    map: map,
    position: groupPosition,
    icon: {
      path: google.maps.SymbolPath.CIRCLE,
      scale: 10,
      fillOpacity: 1,
      strokeWeight: 2,
      fillColor: '#5384ED',
      strokeColor: '#ffffff',
    },
  });

  var positionMarker = new google.maps.Marker({
    map: map,
    position: groupPosition,
  });
};
/* 
 * Always set the map height explicitly to define the size of the div element
 * that contains the map. 
 */

#map {
  height: 100%;
}


/* 
 * Optional: Makes the sample page fill the window. 
 */

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
}
<!DOCTYPE html>
<html>

<head>
  <title>Directions Service</title>
  <script src="https://polyfill.io/v3/polyfill.min.js?features=default"></script>
  <!-- jsFiddle will insert css and js -->
</head>

<body>
  <div id="map"></div>

  <!-- 
      The `defer` attribute causes the callback to execute after the full HTML
      document has been parsed. For non-blocking uses, avoiding race conditions,
      and consistent behavior across browsers, consider loading using Promises
      with https://www.npmjs.com/package/@googlemaps/js-api-loader.
      -->
  <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk&callback=initMap&libraries=geometry" defer></script>
</body>

</html>