Search code examples
javascriptgoogle-mapsmapsgoogle-polyline

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


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">
´´´

Solution

  • 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>