Search code examples
javascriptgoogle-mapsgoogle-maps-api-3google-polyline

Programmatically editing the path of polyline leaves a ghost line


I have lines in the map that can be edited by the user by dragging their vertices, when this happens I check if the vertex that the user has moved is near to any other object, in that case the line has to be attached to that object.

More specifically, when the user moves the line, the event "set_at" is triggered, and in the function called by the listener the line is programmatically edited to be attached to the nearest object (if any). In the case that object exists, the line is attached to the object without any issue (meaning that the line is properly attached and it works). However in the map, the line that the user generated by moving the original one is still visible, but it is translucent. And that line disappears eventually when other operation is performed on the map.

I have tried to clear the path (path.clear()), to create a new path, to remove it from the map (setMap(null)) and place it again (setMap(LocationPicker.map.map)). But nothing seems to work.

google.maps.event.addListener(marker.getPath(), 'set_at', function(vertex, eventC) {
    LocationPicker.controlVertexMovement(marker,vertex, eventC);
});

controlVertexMovement: function(marker, vertex, eventC) {
    var path = marker.getPath();
    var closest = LocationPicker.searchClosestObject(path.j[vertex].lat(), path.j[vertex].lng());
    if(closest!=null){
        path.j[vertex] = closest.latlng;
        LocationPicker.map.overlays[overlayNum].setPath(path);
    }
}

The actual result: https://cdn.pbrd.co/images/HUCyWwu.png The expected result: https://cdn.pbrd.co/images/HUCxBfJ.png

example fiddle

code snippet:

function initialize() {
  var mapOptions = {
    zoom: 3,
    center: new google.maps.LatLng(0, -180),
    mapTypeId: 'terrain'
  };

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

  var flightPlanCoordinates = [
    new google.maps.LatLng(37.772323, -122.214897),
    new google.maps.LatLng(21.291982, -157.821856),
    new google.maps.LatLng(-18.142599, 178.431),
    new google.maps.LatLng(-27.46758, 153.027892)
  ];
  var flightPath = new google.maps.Polyline({
    path: flightPlanCoordinates,
    editable: true,
    strokeColor: '#FF0000',
    strokeOpacity: 1.0,
    strokeWeight: 2,
    map: map
  });

  var deleteMenu = new DeleteMenu();

  google.maps.event.addListener(flightPath, 'rightclick', function(e) {
    // Check if click was on a vertex control point
    if (e.vertex == undefined) {
      return;
    }
    deleteMenu.open(map, flightPath.getPath(), e.vertex);
  });

  google.maps.event.addListener(flightPath.getPath(), 'set_at', function(vertex, eventC) {
    var path = flightPath.getPath();
    path.j[vertex] = new google.maps.LatLng(-34.397, 150.644);
    //marker.setMap(null);
    flightPath.setPath(path);
    //marker.setMap(LocationPicker.map.map);
  });

}

/**
 * A menu that lets a user delete a selected vertex of a path.
 * @constructor
 */
function DeleteMenu() {
  this.div_ = document.createElement('div');
  this.div_.className = 'delete-menu';
  this.div_.innerHTML = 'Delete';

  var menu = this;
  google.maps.event.addDomListener(this.div_, 'click', function() {
    menu.removeVertex();
  });
}
DeleteMenu.prototype = new google.maps.OverlayView();

DeleteMenu.prototype.onAdd = function() {
  var deleteMenu = this;
  var map = this.getMap();
  this.getPanes().floatPane.appendChild(this.div_);

  // mousedown anywhere on the map except on the menu div will close the
  // menu.
  this.divListener_ = google.maps.event.addDomListener(map.getDiv(), 'mousedown', function(e) {
    if (e.target != deleteMenu.div_) {
      deleteMenu.close();
    }
  }, true);
};

DeleteMenu.prototype.onRemove = function() {
  google.maps.event.removeListener(this.divListener_);
  this.div_.parentNode.removeChild(this.div_);

  // clean up
  this.set('position');
  this.set('path');
  this.set('vertex');
};

DeleteMenu.prototype.close = function() {
  this.setMap(null);
};

DeleteMenu.prototype.draw = function() {
  var position = this.get('position');
  var projection = this.getProjection();

  if (!position || !projection) {
    return;
  }

  var point = projection.fromLatLngToDivPixel(position);
  this.div_.style.top = point.y + 'px';
  this.div_.style.left = point.x + 'px';
};

/**
 * Opens the menu at a vertex of a given path.
 */
DeleteMenu.prototype.open = function(map, path, vertex) {
  this.set('position', path.getAt(vertex));
  this.set('path', path);
  this.set('vertex', vertex);
  this.setMap(map);
  this.draw();
};

/**
 * Deletes the vertex from the path.
 */
DeleteMenu.prototype.removeVertex = function() {
  var path = this.get('path');
  var vertex = this.get('vertex');

  if (!path || vertex == undefined) {
    this.close();
    return;
  }

  path.removeAt(vertex);
  this.close();
};

google.maps.event.addDomListener(window, 'load', initialize);
/* 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;
}

.delete-menu {
  position: absolute;
  background: white;
  padding: 3px;
  color: #666;
  font-weight: bold;
  border: 1px solid #999;
  font-family: sans-serif;
  font-size: 12px;
  box-shadow: 1px 3px 3px rgba(0, 0, 0, .3);
  margin-top: -10px;
  margin-left: 10px;
  cursor: pointer;
}

.delete-menu:hover {
  background: #eee;
}
<div id="map"></div>
<!-- Replace the value of the key parameter with your own API key. -->
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk"></script>


Solution

  • path.j is not a documented property. Don't use undocumented properties, they can (and will) change with each release of the API. Only use documented properties/methods.

    Don't use:

    path.j[vertex] = new google.maps.LatLng(-34.397, 150.644);
    

    One option is to use:

    path.setAt(vertex,new google.maps.LatLng(-34.397, 150.644));
    

    Like this:

    google.maps.event.addListenerOnce(flightPath.getPath(), 'set_at', processSetAt);
    
    function processSetAt(vertex, eventC) {
      // turn off editing
      flightPath.setEditable(false);
      // update the path
      var path = flightPath.getPath();
      path.setAt(vertex,new google.maps.LatLng(-34.397, 150.644));
      flightPath.setPath(path);
      // turn editing back on
      flightPath.setEditable(true);
      google.maps.event.addListenerOnce(flightPath.getPath(), 'set_at', processSetAt);
    } 
    

    proof of concept fiddle

    Note that your issue seems to be related to modifying the path of the polyline while it is "editable". The only way I found to remove the "ghost" path was to turn off editing, modify the polyline, then turn editing back on.

    Another option (from the OP's comment):

    Use a flag to avoid an infinite loop on the 'set at' event. Like this:

    google.maps.event.addListener(flightPath.getPath(), 'set_at', function(vertex, eventC) { 
      if(!changingPath){ 
        var path = flightPath.getPath(); 
        changingPath = true; 
        path.setAt(vertex, new google.maps.LatLng(-34.397, 150.644)); 
        changingPath = false; 
      } 
    }); 
    

    fiddle

    code snippet:

    function initialize() {
      var mapOptions = {
        zoom: 3,
        center: new google.maps.LatLng(0, -180),
        mapTypeId: 'terrain'
      };
    
      var map = new google.maps.Map(document.getElementById('map'), mapOptions);
    
      var flightPlanCoordinates = [
        new google.maps.LatLng(37.772323, -122.214897),
        new google.maps.LatLng(21.291982, -157.821856),
        new google.maps.LatLng(-18.142599, 178.431),
        new google.maps.LatLng(-27.46758, 153.027892)
      ];
      var flightPath = new google.maps.Polyline({
        path: flightPlanCoordinates,
        editable: true,
        strokeColor: '#FF0000',
        strokeOpacity: 1.0,
        strokeWeight: 2,
        map: map
      });
    
      var deleteMenu = new DeleteMenu();
    
      google.maps.event.addListener(flightPath, 'rightclick', function(e) {
        // Check if click was on a vertex control point
        if (e.vertex == undefined) {
          return;
        }
        deleteMenu.open(map, flightPath.getPath(), e.vertex);
      });
    
      google.maps.event.addListenerOnce(flightPath.getPath(), 'set_at', processSetAt);
    
      function processSetAt(vertex, eventC) {
        // turn off editing
        flightPath.setEditable(false);
        // update the path
        var path = flightPath.getPath();
        path.setAt(vertex, new google.maps.LatLng(-34.397, 150.644));
        flightPath.setPath(path);
        // turn editing back on
        flightPath.setEditable(true);
        google.maps.event.addListenerOnce(flightPath.getPath(), 'set_at', processSetAt);
      }
    }
    
    /**
     * A menu that lets a user delete a selected vertex of a path.
     * @constructor
     */
    function DeleteMenu() {
      this.div_ = document.createElement('div');
      this.div_.className = 'delete-menu';
      this.div_.innerHTML = 'Delete';
    
      var menu = this;
      google.maps.event.addDomListener(this.div_, 'click', function() {
        menu.removeVertex();
      });
    }
    DeleteMenu.prototype = new google.maps.OverlayView();
    
    DeleteMenu.prototype.onAdd = function() {
      var deleteMenu = this;
      var map = this.getMap();
      this.getPanes().floatPane.appendChild(this.div_);
    
      // mousedown anywhere on the map except on the menu div will close the
      // menu.
      this.divListener_ = google.maps.event.addDomListener(map.getDiv(), 'mousedown', function(e) {
        if (e.target != deleteMenu.div_) {
          deleteMenu.close();
        }
      }, true);
    };
    
    DeleteMenu.prototype.onRemove = function() {
      google.maps.event.removeListener(this.divListener_);
      this.div_.parentNode.removeChild(this.div_);
    
      // clean up
      this.set('position');
      this.set('path');
      this.set('vertex');
    };
    
    DeleteMenu.prototype.close = function() {
      this.setMap(null);
    };
    
    DeleteMenu.prototype.draw = function() {
      var position = this.get('position');
      var projection = this.getProjection();
    
      if (!position || !projection) {
        return;
      }
    
      var point = projection.fromLatLngToDivPixel(position);
      this.div_.style.top = point.y + 'px';
      this.div_.style.left = point.x + 'px';
    };
    
    /**
     * Opens the menu at a vertex of a given path.
     */
    DeleteMenu.prototype.open = function(map, path, vertex) {
      this.set('position', path.getAt(vertex));
      this.set('path', path);
      this.set('vertex', vertex);
      this.setMap(map);
      this.draw();
    };
    
    /**
     * Deletes the vertex from the path.
     */
    DeleteMenu.prototype.removeVertex = function() {
      var path = this.get('path');
      var vertex = this.get('vertex');
    
      if (!path || vertex == undefined) {
        this.close();
        return;
      }
    
      path.removeAt(vertex);
      this.close();
    };
    
    google.maps.event.addDomListener(window, 'load', initialize);
    #map {
      height: 100%;
    }
    
    html,
    body {
      height: 100%;
      margin: 0;
      padding: 0;
    }
    
    .delete-menu {
      position: absolute;
      background: white;
      padding: 3px;
      color: #666;
      font-weight: bold;
      border: 1px solid #999;
      font-family: sans-serif;
      font-size: 12px;
      box-shadow: 1px 3px 3px rgba(0, 0, 0, .3);
      margin-top: -10px;
      margin-left: 10px;
      cursor: pointer;
    }
    
    .delete-menu:hover {
      background: #eee;
    }
    <div id="map"></div>
    <!-- Replace the value of the key parameter with your own API key. -->
    <script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk"></script>