5

I'm using openlayers 3 in a mobile application.

My problem is that I find a big difficulties to select features using my finger even with a touch pen (I have to be more precise in my click or touch, thing which is more painful in my user experience).

I looked for something that can help me like the select tolerance or snapping but I found nothing.

Can someone show me the right way to resolve my problem?

3
  • I assume you use ol.interaction.Select or ol.Map#forEachFeatureAtPixel, right? Unfortunately there currently is no way to configure a selection tolerance. I hope this is something we will add in the near future.
    – erilem
    Jan 3, 2015 at 10:55
  • Yes , I'm using ol.interation.Select And I found anything that refer to a select tolerance , besides I found it in the draw and modify interaction . is there any solution to solve this problem at this time .??
    – b-eyes
    Jan 3, 2015 at 12:40
  • I'm asking if there is also smth that can block the map panning whene using selection interaction
    – b-eyes
    Jan 7, 2015 at 10:15

4 Answers 4

4

I faced the same problem. I used another approach:

First add a feature overlay to your selected feature:

 var avlFeatureOverlay = new ol.FeatureOverlay({
     style: function(feature, resolution) {
     return  avlGetStyle(feature, resolution, true);
     }
 });

Second add a click event to your map:

 map.on('click', function(event) {
     var resolution = map.getView().getResolution();
     var result = avlSelectSWObject(event.coordinate, resolution);
     }
 });

Third create a function "avlSelectSWObject" to handle your event:

 var avlSelectSWObject = function(coordinate, resolution){
     var distance = 15 * resolution; // Your tolerance for a point feature
     avlFeatureOverlay.removeFeature(avlFormerFeature);
     // avlVectorSource is my vector source rename to your own
     var closestFeature = avlVectorSource.getPointFeatureInDistance(coordinate, distance);
     if(closestFeature == null) {
         distance *= 5; // bigger tolerance for line or polygons. Use your own unit
         closestFeature = avlVectorSource.getFeatureInDistance(coordinate, distance);
     }
     if(closestFeature == null){
         return false;
     }         
     avlFeatureOverlay.addFeature(closestFeature); // Add feature to overlay
     avlDisplayProperties(closestFeature);
     return true;
 };

Fourth create two new function on ol.source.vector to calculate distance for a feature with a point geometry or a feature with any geometry type. Why testing for points first? Because if your line and your point shares a equal position, you will always get the line.

The function to catch a feature with a point geometry:

ol.source.Vector.prototype.getPointFeatureInDistance = function(coordinate, distance) {  
    // Find the closet feature with Point as geometry type with in the given distance
    // created from ol.source.Vector.prototype.getClosestFeatureToCoordinate
    var x = coordinate[0];
    var y = coordinate[1];
    var closestFeature = null;
    var previousCityBlockDistance = Infinity;
    var extent = [x-distance, y-distance, x+distance, y+distance];  
    this.forEachFeatureInExtent(extent,function(feature) {
        var geometry = feature.getGeometry();
        var ext = geometry.getExtent();
        if(ext[0] == ext[2] && ext[1] == ext[3])
        {
            var minCityBlockDistance = Math.abs(x - ext[0]) + Math.abs(y - ext[1]);
            if (minCityBlockDistance < previousCityBlockDistance) {
                previousCityBlockDistance = minCityBlockDistance;
                closestFeature = feature;
            }
        }
    });
    return closestFeature;
};

The function to catch a feature with any geometry in a distance (tolerance):

ol.source.Vector.prototype.getFeatureInDistance =
    function(coordinate, distance) {  
    // Find the closet feature with in the given distance
    // created from ol.source.Vector.prototype.getClosestFeatureToCoordinate
    var x = coordinate[0];
    var y = coordinate[1];
    var closestFeature = null;
    var previousCityBlockDistance = Infinity;
    var extent = [x-distance, y-distance, x+distance, y+distance];
    this.forEachFeatureInExtent(extent,function(feature) {
        var geo = feature.getGeometry();
        var coord = geo.getClosestPoint(coordinate);
        var minCityBlockDistance = Math.abs(x - coord[0]) + Math.abs(y - coord[1]);
        if (minCityBlockDistance <= distance && 
            minCityBlockDistance < previousCityBlockDistance) {
              previousCityBlockDistance = minCityBlockDistance;
              closestFeature = feature;
        }
    });
    return closestFeature;
};

In relation to the comment below. You can return multiple values as a vector:

 return [closestFeature,previousCityBlockDistance];

and get them in the calling function:

var result  = avlVectorSource.getPointFeatureInDistance(coordinate, distance);
var closestFeature = result[0];
var mindistance = result[1];

See also: https://stackoverflow.com/questions/2917175/return-multiple-values-in-javascript

3
  • first of all I have to thank you for this helpful answer It would help me a lot in my application, but I'm asking how could I select just one or the closest feature from multiple vector layers (in the select interaction I have just to specify the layers that I want to query in ). as for me I found an other solution by adding a condition just like that select_ = new ol.interaction.Select({ condition: ol.events.condition.always, layers: [vector1, vecto2]}); but it blocks the map panning
    – b-eyes
    Jan 10, 2015 at 9:23
  • In the function i called avlSelectSWObject, call both vector layers and store the resulting features in variables called closestF01, closestF2 etc. Afterward choose the one with the shortest distance. In the functions "getPointFeatureInDistance" and"getFeatureInDistance" You also have to return the var "minCityBlockDistance, to use it in you comparison. See edit in answer above. Instead of a comparison, use the calculated distance for the feature in the 1. layer to be used as tolerance for the next layer. I have not changed in the code above, because I can't test it right now. Jan 10, 2015 at 17:11
  • so many thanks for your help , I can add an amelioration or just a suggestion in the test if the geometry if it's a point or an other geom type you can test by using geometry.getType() == 'Point' instead of testing on the extent (or there is a reason that you did that ??)
    – b-eyes
    Jan 12, 2015 at 0:43
1

After wresting with this for a while I think I've found a solution. The default click selection in OL isn't great. Guys trying to click on iPads had zero love in clicking the features they wanted.

Bear in mind I'm no JS expert so this is probably messy

So on the click event

map.on("click", function(e) {
  var features = featuresAtPixel(e.pixel);
  // do something with features
}

Here there are two distances you can calculate - using coordinates and haversineDistance, or what I went with - pixels and good old pythagoras.

This assumes that you have a variable map with your map, and groupLayers which are all the layers you've got loaded into the map. In my case it was about 30 KMLs. I get the source from the layer and find the features in the visible map.

function featuresAtPixel(pixel) {
    var features = [];
    // get all features in extent
    var featuresInExtent = [];
    var currentExtent = map.getView().calculateExtent(map.getSize());
    // for all visible layers, add features to the full array
    groupLayers.forEach(function (layer){
        if (layer.getVisible()) {
            featuresInExtent = featuresInExtent.concat(
                    layer.getSource().getFeaturesInExtent(currentExtent));
        }
    });
    featuresInExtent.forEach(function(f) {
        var coord = map.getCoordinateFromPixel(pixel);
        // the closest point for this feature
        var closest = f.getGeometry().getClosestPoint(coord);
        var closePixel = map.getPixelFromCoordinate(closest);
        //var dist = distanceBetween(coord, closest);
        var dist = pixelsBetween(pixel, closePixel);
        // tolerance in this case is 10
        if (dist < 10) {
            features.push(f);
        }
    });
    return features;
}

var sphere = new ol.Sphere(6378137);
function distanceBetween(coord1, coord2) {
    return sphere.haversineDistance(coord1, coord2);
}
function pixelsBetween(pixel1, pixel2) {
    return Math.sqrt(Math.pow(pixel1[0]-pixel2[0],2) + Math.pow(pixel1[1]-pixel2[1],2));
}
1

If you use the ol.interaction.Select() it has an option for hitTolerance. Setting it to for example 10 makes it drastically easier to select features.

Example code:

var select = new ol.interaction.Select({
    hitTolerance: 10
});

I think this was introduced in version 3.20.0

0

You can try this:

"Using an invisible (almost transparent) stroke around the points to provide a larger hit target. This approach only slightly improved things and made it hard for the user to hit the right point when they overlapped.

as suggested here http://boundlessgeo.com/2013/02/optimizing-openlayers-for-mobile-applications/

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.