Elliot Nelson
Elliot Nelson

Reputation: 11557

Hooking into and extending the polygon editing function on a Google Map

In my use case, we're allowing a user to define "zones" (polygons) on a map. The basic polygon editing functionality, enabled by just setting editable: true, works well. However, I need some additional functionality.

For example, when the user starts dragging a vertex, I want to highlight nearby vertices on other polygons, and if the user drags over one of them, it will "snap" the lat/lng of the vertex they were dragging to be identical to the vertex the dragged over.

Has anyone successfully inserted some "extra" code into the editing process? Are there any intermediate events being fired on those vertex handles (while dragging, mouse moving, etc.) that I can hook into, interpret, and draw some extra things on the map? What I'm hoping for is someone who can tell me "Oh, if polygon.obfuscatedVariable is set, those are drag handles, and you can listen for mousemove on polygon.obfuscatedVariable[3], retrieve the lat/long, etc."

Hacks and jury-rigged solutions are acceptable: since the built-in editing is so close to what I want, I really don't feel like recreating it from scratch.

Upvotes: 1

Views: 760

Answers (1)

Elliot Nelson
Elliot Nelson

Reputation: 11557

I'd since forgotten about this question, but here's our solution to the problem. Hopefully it's helpful!

Short version:

Whenever a mousedown occurs on a shape, check if it's over a vertex, and if it is, use elementFromPoint to save a reference to the actual HTML <div> representing the vertex handle. In subsequent mousemove and mouseup events, you can poll the screen position of this <div> and compare it to the locations of other points on the map.

Long version:

I've yanked out the relevant functions from our application, so you'll need to ignore some of our specific functions and objects etc., but this does show our working "snap-to-point" implementation.

First off, we will be doing pixel-to-lat-lng conversions, so you'll need a simple Overlay defined somewhere (you always need one to do these calculations):

_this.editor.overlay = new g.maps.OverlayView();
_this.editor.overlay.draw = function () {};
_this.editor.overlay.setMap(This.map);

Now, anytime we've initialized a shape on the map, we add a mousedown event handler to it.

g.maps.event.addListener(_this.shape, 'mousedown', function (event) {
  if (event.vertex >= 0) {
    var pixel =  _this.editor.overlay.getProjection().fromLatLngToContainerPixel(_this.shape.getPath().getAt(event.vertex));
    var offset = _this.mapElement.offset();
    var handle = document.elementFromPoint(pixel.x + offset.left, pixel.y + offset.top);

    if (handle) {
      _this.dragHandle = $(handle);
      _this.snappablePoints = _this.editor.snappablePoints(_this);
    } else {
      _this.dragHandle = null;
      _this.snappablePoints = null;
    }
  }
});

(You'll notice the call to snappablePoints, which is just an internal utility function that collects all the points that would be valid for this point to snap to. We do it here because it would be an expensive loop to perform on every single mousemove.)

Now, in our shape's mousemove listener, because we've saved a reference to that <div>, we can poll its screen position and compare it to the screen position of other points on the map. If a point is within a certain pixel range (I think ours is 8 pixels), we save it and hover a little icon telling the user we're going to snap.

g.maps.event.addListener(this.shape, 'mousemove', function (event) {
  var projection, pixel, pixel2, offset, dist, i;

  if (event.vertex >= 0 && this.dragHandle) {
    // If dragHandle is set and we're moving over a vertex, we must be dragging an
    // editable polygon point. 

    offset = this.editor.mapElement.offset();
    pixel = {
      x: this.dragHandle.offset().left - offset.left + this.dragHandle.width() / 2,
      y: this.dragHandle.offset().top - offset.top + this.dragHandle.height() / 2
    };

    // Search through all previously saved snappable points, looking for one within the snap radius.
    projection = this.editor.overlay.getProjection();
    this.snapToPoint = null;
    for(i = 0; i < this.snappablePoints.length; i++) {
      pixel2 = projection.fromLatLngToContainerPixel(this.snappablePoints[i]);
      dist = (pixel.x - pixel2.x) * (pixel.x - pixel2.x) + (pixel.y - pixel2.y) * (pixel.y - pixel2.y);

      if (dist <= SNAP_RADIUS) {
        this.snapToPoint = this.snappablePoints[i];
        $('#zone-editor #snapping').css('left', pixel.x + 10 + offset.left).css('top', pixel.y - 12 + offset.top).show();
        break;
      }
    }

    if (!this.snapToPoint) {
      $('#zone-editor #snapping').hide();
    }
  });

A little cleanup when the user stops moving the mouse:

g.maps.event.addListener(this.shape, 'mouseup', function (event) {
  // Immediately clear dragHandle, so that everybody knows we aren't dragging any more.
  // We'll let the path updated event deal with any actual snapping or point saving.
  _this.dragHandle = null;
  $('#zone-editor #snapping').hide();
});

Last, we actually handle the "snapping", which is really just a tiny bit of logic in an event listener on the shape's path.

g.maps.event.addListener(this.shape.getPath(), 'set_at', function (index, element) {
  if (this.snapToPoint) {
    // The updated point was dragged by the user, and we have a snap-to point.
    // Overwrite the recently saved point and let another path update trigger.
    var point = this.snapToPoint;
    this.snapToPoint = null;
    this.shape.getPath().setAt(index, point);
  } else  {
    // Update our internal list of points and hit the server
    this.refreshPoints();
    this.save();
  };

  // Clear any junk variables whenever the path is updated
  this.dragHandle = null;
  this.snapToPoint = null;
  this.snappablePoints = null;
});

Fin.

Upvotes: 4

Related Questions