Search code examples
javascriptopenlayers

Make features rendered with a custom renderer selectable


Given is a LineString, a start and an end point. A parallel to this LineString should be plotted (using openlayers). Please have a look at the image for an example (the black line is the parallel, the blue line the original line, start/end points aren't plotted).

enter image description here

To achive this I set a custom renderer within the style of the vector layer (ol/style/Style~RenderFunction). This function takes the pixel coordinates of the original line, calculates the parallel and plot it into the canvas.

Problem: The created parallel line can't be selected by the user by e.g. clicking on it when using ol/interaction/Select~Select. I want to provide information about the feature to user as soon as selected.

Question: How is it possible to make this feature selectable? Are there any additional steps necessary within the renderer-function?

The custom renderer function looks like that:

function parallelRenderer(pixelCoordinates, state) {
    
     const parallelCoordinates = computeParallelCoordinates(pixelCoordinates);

    const canvas = state.context;
    canvas.beginPath();
    canvas.moveTo(parallelCoordinates[0][0], parallelCoordinates[0][1]);

    for (let i=1; i<parallelCoordinates.length; i++) {
        canvas.lineTo(parallelCoordinates[i][0], parallelCoordinates[i][1]);
    }

    canvas.stroke();
}

Background:

  • Start/end points are created by the user using ol/interactions/Draw
  • The created line is selectable when providing ol/style/Stroke~Stroke instead of the renderer function
  • I'm using openlayers version 6.5.0

Update: There is a related question (and also the answer of Mike in this thread). The difference is, that real world coordinates are used there to compute the position of the parallel line. I wanted to solve that in pixel space... The reason is that I want to keep the distance between the lines constant. Using real world coordinates the distance is dependent on the resolution. It seems that this problem can be solved by recalculating the real world geometry every time the resolution changes.


Solution

  • Hit detection for custom renderers has been suggested but never implemented https://github.com/openlayers/openlayers/issues/8136

    You could use a style function to style a single linestring as parallel linestrings. This code uses math.intersect from https://cdnjs.cloudflare.com/ajax/libs/mathjs/5.4.1/math.min.js to calculate the positions of the offset vertices

    function styleFunction(feature, resolution) {
        var colors = ['green', 'yellow', 'red'];
        var width = 4;
        var styles = [];
        for (var line = 0; line < colors.length; line++) {
            var dist = width * resolution * (line - (colors.length-1)/2);
            var geom = feature.getGeometry();
            var coords = [];
            var counter = 0;
            geom.forEachSegment(function(from, to) {
                var angle = Math.atan2(to[1] - from[1], to[0] - from[0]);
                var newFrom = [
                    Math.sin(angle) * dist + from[0],
                    -Math.cos(angle) * dist + from[1]
                ];
                var newTo = [
                    Math.sin(angle) * dist + to[0],
                    -Math.cos(angle) * dist + to[1]
                ];
                coords.push(newFrom);
                coords.push(newTo);
                if (coords.length > 2) {
                    var intersection = math.intersect(coords[counter], coords[counter+1], coords[counter+2], coords[counter+3]);
                    coords[counter+1] = (intersection) ? intersection : coords[counter+1];
                    coords[counter+2] = (intersection) ? intersection : coords[counter+2];
                    counter += 2;
                }
            });
            styles.push(
                new ol.style.Style({
                    geometry: new ol.geom.LineString(coords),
                    stroke: new ol.style.Stroke({
                        color: colors[line],
                        width: width
                    })
                })
            );
        }
        return styles;
    };