1

I am trying to create a draggable marker that is confined to a polyline. I have read this post (Confine dragging of Google Maps V3 Marker to Polyline), but I do not want to create the points that the marker can move along. Are there other ways to do this without having to create a points array for the marker? If anyone can point me in the right direction, it is much appreciated.

Community
  • 1
  • 1
kryptonkal
  • 874
  • 8
  • 23
  • 1
    I have found a library snaptoroute that uses mouse movement, but it is based on v2 api. I have modified it to work with draggable markers and updated it to work with v3. Once complete, I will update this question with working code. – kryptonkal Aug 04 '15 at 20:21
  • Update The solution below works when tested, but there may be changes to the gmaps api that are causing the example fiddle to error out. Please be advised that there may be additional changes to make it work with the most recent gmaps api. – kryptonkal Jul 23 '20 at 21:42

1 Answers1

4

From what I understand, you have to load the polyline points into an array. It seems that there is no way around this. I am not sure how the the directions api snaps to roads, but I am assuming that it is based on this concept (loading points into an array).

I have found an older maps v2 library that updates the marker based on mouse movement events, which loads the line data on zoom end. I have updated the code to work with api v3 and replaced the mouse events with drag events.

To use this library, initialize like this:

var snapToRoute = new SnapToRoute(map_instance, initial_marker, polyline);

The library can be found here: SnapToRoute

** Update ** example fiddle

Here is my modified version:

function SnapToRoute(map, marker, polyline) {
    this.routePixels_ = [];
    this.normalProj_ = map.getProjection();
    this.map_ = map;
    this.marker_ = marker;
    this.polyline_ = polyline;

    this.init_();
}

SnapToRoute.prototype.init_ = function () {
    this.loadLineData_();
    this.loadMapListener_();
};

SnapToRoute.prototype.updateTargets = function (marker, polyline) {
    this.marker_ = marker || this.marker_;
    this.polyline_ = polyline || this.polyline_;
    this.loadLineData_();
};

SnapToRoute.prototype.loadMapListener_ = function () {
    var me = this;

    google.maps.event.addListener(me.marker_, "dragend", function (evt) {
        me.updateMarkerLocation_(evt.latLng);
    });

    google.maps.event.addListener(me.marker_, "drag", function (evt) {
        me.updateMarkerLocation_(evt.latLng);
    });

    google.maps.event.addListener(me.map_, "zoomend", function (evt) {
        me.loadLineData_();
    });
};

SnapToRoute.prototype.loadLineData_ = function () {
    var zoom = this.map_.getZoom();
    this.routePixels_ = [];
    var path = this.polyline_.getPath();
    for (var i = 0; i < path.getLength() ; i++) {
        var Px = this.normalProj_.fromLatLngToPoint(path.getAt(i));
        this.routePixels_.push(Px);
    }
};

SnapToRoute.prototype.updateMarkerLocation_ = function (mouseLatLng) {
    var markerLatLng = this.getClosestLatLng(mouseLatLng);
    this.marker_.setPosition(markerLatLng);
};

SnapToRoute.prototype.getClosestLatLng = function (latlng) {
    var r = this.distanceToLines_(latlng);
    return this.normalProj_.fromPointToLatLng(new google.maps.Point(r.x, r.y));
};

SnapToRoute.prototype.getDistAlongRoute = function (latlng) {
    if (typeof (opt_latlng) === 'undefined') {
        latlng = this.marker_.getLatLng();
    }
    var r = this.distanceToLines_(latlng);
    return this.getDistToLine_(r.i, r.to);
};

SnapToRoute.prototype.distanceToLines_ = function (mouseLatLng) {
    var zoom = this.map_.getZoom();
    var mousePx = this.normalProj_.fromLatLngToPoint(mouseLatLng);
    var routePixels_ = this.routePixels_;
    return this.getClosestPointOnLines_(mousePx, routePixels_);
};

SnapToRoute.prototype.getDistToLine_ = function (line, to) {
   var routeOverlay = this.polyline_;
   var d = 0;
    for (var n = 1; n < line; n++) {
        d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(n - 1), routeOverlay.getAt(n));
    }
    d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(line - 1), routeOverlay.getAt(line)) * to;
    return d;
};

SnapToRoute.prototype.getClosestPointOnLines_ = function (pXy, aXys) {
    var minDist;
    var to;
    var from;
    var x;
    var y;
    var i;
    var dist;

    if (aXys.length > 1) {
        for (var n = 1; n < aXys.length ; n++) {
            if (aXys[n].x !== aXys[n - 1].x) {
                var a = (aXys[n].y - aXys[n - 1].y) / (aXys[n].x - aXys[n - 1].x);
                var b = aXys[n].y - a * aXys[n].x;
                dist = Math.abs(a * pXy.x + b - pXy.y) / Math.sqrt(a * a + 1);
            } else {
                dist = Math.abs(pXy.x - aXys[n].x);
            }

            var rl2 = Math.pow(aXys[n].y - aXys[n - 1].y, 2) + Math.pow(aXys[n].x - aXys[n - 1].x, 2);
            var ln2 = Math.pow(aXys[n].y - pXy.y, 2) + Math.pow(aXys[n].x - pXy.x, 2);
            var lnm12 = Math.pow(aXys[n - 1].y - pXy.y, 2) + Math.pow(aXys[n - 1].x - pXy.x, 2);
            var dist2 = Math.pow(dist, 2);
            var calcrl2 = ln2 - dist2 + lnm12 - dist2;
            if (calcrl2 > rl2) {
                dist = Math.sqrt(Math.min(ln2, lnm12));
            }

            if ((minDist == null) || (minDist > dist)) {
                to = Math.sqrt(lnm12 - dist2) / Math.sqrt(rl2);
                from = Math.sqrt(ln2 - dist2) / Math.sqrt(rl2);
                minDist = dist;
                i = n;
            }
        }
        if (to > 1) {
            to = 1;
        }
        if (from > 1) {
            to = 0;
            from = 1;
        }
        var dx = aXys[i - 1].x - aXys[i].x;
        var dy = aXys[i - 1].y - aXys[i].y;

        x = aXys[i - 1].x - (dx * to);
        y = aXys[i - 1].y - (dy * to);
    }
    return { 'x': x, 'y': y, 'i': i, 'to': to, 'from': from };
};

example fiddle

code snippet:

var geocoder;
var directionsDisplay;
var directionsService = new google.maps.DirectionsService();
var map;
var polyline = new google.maps.Polyline({
  path: [],
  strokeColor: '#FF0000',
  strokeWeight: 3
});
var marker;

function initialize() {
  directionsDisplay = new google.maps.DirectionsRenderer();
  map = new google.maps.Map(
    document.getElementById("map_canvas"), {
      center: new google.maps.LatLng(37.4419, -122.1419),
      zoom: 13,
      mapTypeId: google.maps.MapTypeId.ROADMAP
    });
  calcRoute("New York, NY", "Baltimore, MD");

  directionsDisplay.setMap(map);

}
google.maps.event.addDomListener(window, "load", initialize);

function calcRoute(start, end) {
  var request = {
    origin: start,
    destination: end,
    travelMode: google.maps.TravelMode.DRIVING
  };
  directionsService.route(request, function(response, status) {
    if (status == google.maps.DirectionsStatus.OK) {
      // directionsDisplay.setDirections(response);
      renderRoute(response);
    }
  });
}

function renderRoute(response) {
  var bounds = new google.maps.LatLngBounds();
  var route = response.routes[0];
  var summaryPanel = document.getElementById("directions_panel");
  var detailsPanel = document.getElementById("direction_details");
  var path = response.routes[0].overview_path;
  var legs = response.routes[0].legs;
  for (i = 0; i < legs.length; i++) {
    if (i == 0) {
      marker = new google.maps.Marker({
        position: legs[i].start_location,
        draggable: true,
        map: map
      });
    }
    var steps = legs[i].steps;
    for (j = 0; j < steps.length; j++) {
      var nextSegment = steps[j].path;
      for (k = 0; k < nextSegment.length; k++) {
        polyline.getPath().push(nextSegment[k]);
        bounds.extend(nextSegment[k]);
      }
    }
  }

  polyline.setMap(map);
  map.fitBounds(bounds);
  var snapToRoute = new SnapToRoute(map, marker, polyline);

}


function SnapToRoute(map, marker, polyline) {
  this.routePixels_ = [];
  this.normalProj_ = map.getProjection();
  this.map_ = map;
  this.marker_ = marker;
  this.editable_ = Boolean(false);
  this.polyline_ = polyline;

  this.init_();
}

SnapToRoute.prototype.init_ = function() {
  this.loadLineData_();
  this.loadMapListener_();
};

SnapToRoute.prototype.updateTargets = function(marker, polyline) {
  this.marker_ = marker || this.marker_;
  this.polyline_ = polyline || this.polyline_;
  this.loadLineData_();
};

SnapToRoute.prototype.loadMapListener_ = function() {
  var me = this;

  google.maps.event.addListener(me.marker_, "dragend", function(evt) {
    me.updateMarkerLocation_(evt.latLng);
  });

  google.maps.event.addListener(me.marker_, "drag", function(evt) {
    me.updateMarkerLocation_(evt.latLng);
  });

  google.maps.event.addListener(me.map_, "zoomend", function(evt) {
    me.loadLineData_();
  });
};

SnapToRoute.prototype.loadLineData_ = function() {
  var zoom = this.map_.getZoom();
  this.routePixels_ = [];
  var path = this.polyline_.getPath();
  for (var i = 0; i < path.getLength(); i++) {
    var Px = this.normalProj_.fromLatLngToPoint(path.getAt(i));
    this.routePixels_.push(Px);
  }
};

SnapToRoute.prototype.updateMarkerLocation_ = function(mouseLatLng) {
  var markerLatLng = this.getClosestLatLng(mouseLatLng);
  this.marker_.setPosition(markerLatLng);
};

SnapToRoute.prototype.getClosestLatLng = function(latlng) {
  var r = this.distanceToLines_(latlng);
  return this.normalProj_.fromPointToLatLng(new google.maps.Point(r.x, r.y));
};

SnapToRoute.prototype.getDistAlongRoute = function(latlng) {
  if (typeof(opt_latlng) === 'undefined') {
    latlng = this.marker_.getLatLng();
  }
  var r = this.distanceToLines_(latlng);
  return this.getDistToLine_(r.i, r.to);
};

SnapToRoute.prototype.distanceToLines_ = function(mouseLatLng) {
  var zoom = this.map_.getZoom();
  var mousePx = this.normalProj_.fromLatLngToPoint(mouseLatLng);
  var routePixels_ = this.routePixels_;
  return this.getClosestPointOnLines_(mousePx, routePixels_);
};

SnapToRoute.prototype.getDistToLine_ = function(line, to) {
  var routeOverlay = this.polyline_;
  var d = 0;
  for (var n = 1; n < line; n++) {
    d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(n - 1), routeOverlay.getAt(n));
  }
  d += google.maps.geometry.spherical.computeDistanceBetween(routeOverlay.getAt(line - 1), routeOverlay.getAt(line)) * to;
  return d;
};

SnapToRoute.prototype.getClosestPointOnLines_ = function(pXy, aXys) {
  var minDist;
  var to;
  var from;
  var x;
  var y;
  var i;
  var dist;

  if (aXys.length > 1) {
    for (var n = 1; n < aXys.length; n++) {
      if (aXys[n].x !== aXys[n - 1].x) {
        var a = (aXys[n].y - aXys[n - 1].y) / (aXys[n].x - aXys[n - 1].x);
        var b = aXys[n].y - a * aXys[n].x;
        dist = Math.abs(a * pXy.x + b - pXy.y) / Math.sqrt(a * a + 1);
      } else {
        dist = Math.abs(pXy.x - aXys[n].x);
      }

      var rl2 = Math.pow(aXys[n].y - aXys[n - 1].y, 2) + Math.pow(aXys[n].x - aXys[n - 1].x, 2);
      var ln2 = Math.pow(aXys[n].y - pXy.y, 2) + Math.pow(aXys[n].x - pXy.x, 2);
      var lnm12 = Math.pow(aXys[n - 1].y - pXy.y, 2) + Math.pow(aXys[n - 1].x - pXy.x, 2);
      var dist2 = Math.pow(dist, 2);
      var calcrl2 = ln2 - dist2 + lnm12 - dist2;
      if (calcrl2 > rl2) {
        dist = Math.sqrt(Math.min(ln2, lnm12));
      }

      if ((minDist == null) || (minDist > dist)) {
        to = Math.sqrt(lnm12 - dist2) / Math.sqrt(rl2);
        from = Math.sqrt(ln2 - dist2) / Math.sqrt(rl2);
        minDist = dist;
        i = n;
      }
    }
    if (to > 1) {
      to = 1;
    }
    if (from > 1) {
      to = 0;
      from = 1;
    }
    var dx = aXys[i - 1].x - aXys[i].x;
    var dy = aXys[i - 1].y - aXys[i].y;

    x = aXys[i - 1].x - (dx * to);
    y = aXys[i - 1].y - (dy * to);
  }
  return {
    'x': x,
    'y': y,
    'i': i,
    'to': to,
    'from': from
  };
};
html,
body,
#map_canvas {
  height: 100%;
  width: 100%;
  margin: 0px;
  padding: 0px
}
<script src="https://maps.googleapis.com/maps/api/js"></script>
<div id="map_canvas" style="border: 2px solid #3872ac;"></div>
kryptonkal
  • 874
  • 8
  • 23