Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Snapping Control #1164

Closed
bjornharrtell opened this issue Oct 25, 2013 · 9 comments
Closed

Snapping Control #1164

bjornharrtell opened this issue Oct 25, 2013 · 9 comments

Comments

@bjornharrtell
Copy link
Contributor


Want to back this issue? Place a bounty on it! We accept bounties via Bountysource.

@tonio
Copy link
Member

tonio commented Jul 3, 2014

Modify interaction supports snapping, what did you have in mind?

@ghost
Copy link

ghost commented Jul 3, 2014

AFAIK the current snapping functionality is limited to preserve existing overlapping vertices on selected features. What I have in mind is more generic snapping, i.e to be able to move a vertex and snap it to another vertex (of another selected feature). This kind of snapping would be useful for both Draw and Modify interactions and would provide similar functionality as in OL2:

http://dev.openlayers.org/releases/OpenLayers-2.13.1/examples/snapping.html

@ttsiodras
Copy link

Seconded - this is a vital part of the way drawInteraction needs to work (snapping to e.g. neighbouring feature vertexes so that a polygon you draw "snaps" to the neighbour, leaving no shim behind).

If one were to implement this on his own, were would he start? e.g. in handlePointerDown, find the closest vertex within some tolerance and if there is one, change the event.x and .y coordinates?

@ttsiodras
Copy link

I covered my own snapping needs, using internal APIs from within OpenLayers 3 code and writing horrible (i.e. stateful) - but working - code. I describe what I did below, in case it proves useful to fellow OL3-coders.

In my case, I needed the drawInteraction to snap on vertexes (vertices?) of polygons in read-only ol.source.Vectors of read-only layers - that don't belong to any modifyInteraction. OL3 snaps perfectly on the vertexes and edges of the polygons INSIDE the drawInteraction - but not to any other polygons. If you go ahead and add your polygons to the drawInteraction, then their vertexes become snapable, but they also become modifiable - which is not what I wanted.

OL3 core coders will probably hate me for pasting this (full of accesses to internal APIs and variables), but I'll just copy it here anyway, with the added caveats, grains of salt, etc - any suggestions for improving it are most welcome.

Here's the executive summary:

  • I basically use the rBush_ acceleration RTree datastructure that all ol.source.Vectors have, to quickly find the feature closest to me (on mousehover)
  • I then find the closest vertex of that feature, and check if its within pixel tolerance
  • if it is, I generate a snappedVertexFeature of one point (so it's a visible preview of where we'll snap at), and store the coordinates, so I can use it on the next mouse click
  • in the onclick handler, I am running too late - OL3 has already added the pixel we clicked on to the drawInteraction's sketchFeature_ and sketchPolygonCoords_ . So I ... (facepalm, shame) look for the pixel's coordinate in these lists, and OVERWRITE them with the last snapped coordinates :-)

Below, the relevant pieces of my TypeScript code (drumroll):

The hover logic:

$(this.map.getViewport()).on('mousemove', (evt:any) => {
    npMapThis.handleMove(evt);
});

handleMove(evt) {
    var npMapThis = this;
    if (npMapThis.currentMode === NpMapMode.NPMAP_MODE_DRAW) {
        // In drawing mode, we want to snap on any vertex of any vector layer
        var pixel = [evt.offsetX, evt.offsetY];
        var map = npMapThis.map;
        var pixelTolerance = 10;

        // Calculate a box of +/- 'pixelTolerance x pixelTolerance' pixels
        // around the point that we clicked
        var lowerLeft = map.getCoordinateFromPixel(
                [pixel[0] - pixelTolerance, pixel[1] + pixelTolerance]);
        var upperRight = map.getCoordinateFromPixel(
                [pixel[0] + pixelTolerance, pixel[1] - pixelTolerance]);
        var box = ol.extent.boundingExtent([lowerLeft, upperRight]);

        // Find the coordinate that the pixel we clicked on maps to
        var pixelCoordinate = map.getCoordinateFromPixel(pixel);

        // Reset the 'result' boolean (whether we snapped or not)
        var snappedToVertex = false;
        npMapThis.layers.forEach(layer => {
            // if we snapped already in a previous layer, abort early
            if (snappedToVertex) return;
            if (layer.isVisible && layer.isVector) { // Ignore tile layers!
                var testLayerSource = layer.source;
                // Vector layers are built with a "feature-extent" RTree...
                var rBush = (<any>testLayerSource).rBush_;
                // Use the RTree to find the features whose extents fall inside
                // our pixelTolerance x pixelTolerance box
                var features = rBush.getInExtent(box);
                if (features.length > 0) {
                    var closestPoint, closestDistance;
                    features.forEach( (f:ol.Feature) => {
                        // if we snapped already on a previous feature, abort early
                        if (snappedToVertex) return;
                        // This feature is a candidate! 
                        var g = f.getGeometry();
                        // For all the polygons inside it...
                        if (g instanceof ol.geom.Polygon) {
                            var x, y;
                            // Find the closest vertex to our hovered pixel coordinates
                            g.flatCoordinates.forEach( (n:number) => {
                                // (the flatCoordinates are an array of width N (N is even,
                                // since the array is of the form [x1,y1,x2,y2,...])
                                // so we need to do this check in two phases: one storing the x...
                                if (isVoid(x)) {
                                    x = n;
                                } else {
                                    // and one completing a coordinate pair (x,y)
                                    var c = [x, n];
                                    // Ignore the point if we've already snapped on it
                                    if (!isVoid(npMapThis.avoidResnapOnSamePoint)) {
                                        var avoidX = npMapThis.avoidResnapOnSamePoint[0];
                                        var avoidY = npMapThis.avoidResnapOnSamePoint[1];
                                        if (avoidX === x && avoidY === n) 
                                            return;
                                    }
                                    var dist = ol.coordinate.squaredDistance(c, pixelCoordinate);
                                    if (isVoid(closestPoint)) {
                                        closestPoint = c;
                                        closestDistance = dist;
                                    } else {
                                        if (dist < closestDistance) {
                                            closestDistance = dist;
                                            closestPoint = c;
                                        }
                                    }
                                    x = undefined;
                                }
                            });
                            // Was the closest vertex within pixelTolerance?
                            if (!isVoid(closestDistance)) {
                                var pixelClosest = map.getPixelFromCoordinate(closestPoint);
                                var pixelClosestDist = Math.sqrt(ol.coordinate.squaredDistance(pixelClosest, pixel));
                                snappedToVertex = pixelClosestDist < pixelTolerance;
                                if (snappedToVertex) {
                                    // It was - update or create the 'snap preview' vertex feature
                                    npMapThis.createOrUpdateVertexFeature(closestPoint);
                                }
                            }
                        }
                    });
                }
            }
        });

        // I have no idea why, but we must ALWAYS remove the snappedVertexFeature here.
        // If we only remove it when we failed to find a close-enough vertex (snappedToVertex=false) 
        // then the point shows up YELLOW (?!@?).
        //
        // But if we show it (via 'createOrUpdateVertexFeature' above) and then ALWAYS destroy it here,
        // it shows up fine. This is exactly the way the modifyInteraction does it
        // ( ol3/src/ol/interaction/modifyinteraction.js, line 576 ) so it's not a bug ;
        // it's a "feature" of OL3 somehow.

        if (!isVoid(npMapThis.snappedVertexFeature)) {
            // We will always remove any freshly created (createOrUpdateVertexFeature) 'snap-preview' point feature.
            // But...
            if (snappedToVertex) {
                // ... we will first save their coordinates, to use in case the user actually clicks
                // (in handleClick below)
                this.lastSnappedFeature = npMapThis.snappedVertexFeature;
            } else {
                this.lastSnappedFeature = null;
            }
            npMapThis.featureOverlayDraw.removeFeature(npMapThis.snappedVertexFeature);
            npMapThis.snappedVertexFeature = null;
        } else {
            // Too far away from vertices - snap off
            this.lastSnappedFeature = null;
        }
    }
}

... and the mouse click logic:

this.map.on(goog.events.EventType.CLICK, function (evt) {
    npMapThis.handleClick(evt);
});

handleClick(evt) {
    var npMapThis = this;
    if (npMapThis.currentMode === NpMapMode.NPMAP_MODE_DRAW) {
        // In draw mode, if we must snap, check for a lastSnappedFeature point!
        if (!isVoid(npMapThis.lastSnappedFeature)) {
            var snappedToX = npMapThis.lastSnappedFeature.getGeometry().flatCoordinates[0];
            var snappedToY = npMapThis.lastSnappedFeature.getGeometry().flatCoordinates[1];

            // We are a bit too late.
            // This handler is called AFTER the default OL3 handler, which has already updated
            // the drawn feature (sketchFeature_ variable of the drawInteraction)
            // Now that we have the coordinates we last snapped on, we need to update
            // the sketchFeature_'s coordinates.
            var coordinates;
            try {
                coordinates = (<any>npMapThis.drawInteraction.sketchFeature_).values_.geometry.flatCoordinates;
            }
            catch(err) {}
            if (!isVoid(coordinates) && !isVoid(coordinates.length)) {
                // The default OL3 handler has already added the coordinates of the pixel we clicked on
                var lastAddedCoordinateToReplace = npMapThis.map.getCoordinateFromPixel(evt.pixel);
                var x = lastAddedCoordinateToReplace[0];
                var y = lastAddedCoordinateToReplace[1];
                // We need to replace them anywhere we find them in the coordinate list, with the snapped ones
                for(var idx=0; idx<coordinates.length; idx+=2) {
                    var xx = coordinates[idx];
                    var yy = coordinates[idx+1];
                    if (xx === x && yy === y) {
                        coordinates[idx] = snappedToX;
                        coordinates[idx+1] = snappedToY;
                    }
                }
                // And we need to do this not only on the sketchFeature_'s coordinates, but also on the
                // sketchPolygonCoords_ - which is somehow used by OL3 elsewhere.
                var coordinateArray;
                try {
                    coordinateArray = (<any>npMapThis.drawInteraction).sketchPolygonCoords_[0];
                }
                catch(err) {}
                if (!isVoid(coordinateArray)) {
                    for(var idx=0; idx<coordinateArray.length; idx++) {
                        var c = coordinateArray[idx];
                        var xx = c[0];
                        var yy = c[1];
                        if (xx === x && yy === y) {
                            c[0] = snappedToX;
                            c[1] = snappedToY;
                        }
                    }
                }
                // Finally, now that we updated the sketch feature, OL3 needs to do some house keeping
                (<any>npMapThis.drawInteraction).updateSketchFeatures_();
                // Don't re-snap on these coordinates in the future
                npMapThis.avoidResnapOnSamePoint = [snappedToX, snappedToY];
            }
            npMapThis.lastSnappedFeature = null;
        }
    }
}

@geographika
Copy link

That's a valiant attempt at implementing this. I've added a bounty on this issue to see if that helps get this functionality added to the core library.

@lenoil
Copy link

lenoil commented Dec 30, 2014

ttsiodras,

I would like use and test your code in javascript.
Can you specify function isvoid()? equivalent to == null I suppose
your code is it available in javascript ?

Thanks

@fperucic
Copy link
Contributor

fperucic commented Jan 3, 2015

I need snap functionalty for a project I'm on, so I'm currenty working on solving this problem.
My idea is to mI need snap functionalty for a project I'm on, so I'm currenty working on solving this problem.
My general idea is to extract snap functionalty from ol.interaction.Modify and make a ol.Snap object which receives some parameters:

  • features: ol.Collection.<ol.Feature>
    • features to snap to
  • interactions: Array.<ol.interaction.Pointer>
    • listen for events on interactions (Modify, Draw or custom interaction)
  • pixelTolerance

Each interaction sholud expose it's current coordinate via snap event. And if ol.Snap is listening for SNAPME events on the interaction it whould bind the snapped coordinates to the event. The interaction whould then use the snapped coordinate.

var event = new ol.SnapEvent(
    ol.SnapEventType.SNAPME,
    event.coordinates
);
this.dispatchEvent(event);
var possiblySnappedCoordinates = event.snappedCoordinate;
/* event.snappedCoordinate is used as current coordinate insead of event.coordinate */

Place to fire a SNAPME event in interaction.Draw is ol.interaction.Draw.prototype.handlePointerMove_,
and in interaction.Modify is ol.interaction.Modify.handleDragEvent_.

This is my idea of solving this problem, any feedback is appreciated.

@ttsiodras
Copy link

@lenoil Yes, isVoid just checks for undefined and null, and no, I wrote the code in TypeScript - but it's very easily translatable, the TypeScript concepts are pretty straightforward to map. If you have any problems with some parts of the syntax, just ask - here, or better yet, directly at my gmail account (since this doesn't really relate to the ticket).

@fperucic fperucic mentioned this issue Jan 9, 2015
@ahocevar
Copy link
Member

Fixed with #3109.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants