Search code examples
javascripthoverrenderopenlayers

Openlayers Hover render order, hover feature rendering over all other features


I'm looking for a way to render a hover feature vector only over that feature, to essentially render the hover vector over its feature without rendering over other features (specifically, LineString hovers not rendering over proximate markers)

I have hovering set up without issue as per the vector example, the features all rendering in the order I would like (LineStrings are below markers). The issue is that when a LineString is hovered over its hover feature is rendered not only over that LineString but also any marker that the LineString crosses under, and so there is a big streak of the LineString's hover over that marker. I can see how this makes sense just considering how the feature-overlaying vector works, but I can't see how to adjust it as I would like.

I've found several posts that seem to offer incongruous advice on setting a zIndex in the styles or using a renderOrder function, though none have seemed exactly applicable to this situation, or are beyond my n00b capacities. I have tried setting a zIndex on all concerned Styles as well as something along the lines of this renderOrder function, but not to the desired effect. Specifically with the latter, I couldn't figure out where to put the function so that the hover might render according to it.

Any advice is greatly appreciated!

var map = new ol.Map({
    layers: [
    new ol.layer.Tile({
        source: new ol.source.OSM()
    })
    ],
    target: 'map',
    view: new ol.View({
    center: ol.proj.fromLonLat([132.4903, 34.0024]),
    zoom: 4
    })
});

var vectorLayer = new ol.layer.Vector({
    source: new ol.source.Vector({
        url: '../static/gpx/summer3.kml',
        format: new ol.format.KML({
            extractStyles: false
        })
    }),
    style: function(feature) {
        return routeStyle;
    },
});
vectorLayer.getSource().on('addfeature', function(event) {
    event.feature.set('hoverstyle', 'route');
});
map.addLayer(vectorLayer);

var vectorLayer = new ol.layer.Vector({
    source: new ol.source.Vector({
        url: '../static/gpx/testmoo3.kml',
        format: new ol.format.KML({
            extractStyles: false,
        }),
    }),
    style: function(feature) {
        return iconStyle;
    },
});
vectorLayer.getSource().on('addfeature', function(event) {
    event.feature.set('hoverstyle', 'route');
});
map.addLayer(vectorLayer);

var hoverStyles = {
    'moo': new ol.style.Icon({
        anchor: [0.5, 30],
        anchorXUnits: 'fraction',
        anchorYUnits: 'pixels',
        src: '../static/GPX/icon/mooinb.png',
    }),
    'route': new ol.style.Stroke({
        color: 'rgba(236, 26, 201, 0.5)',
        width: 5
    })
};

var routeStyle = new ol.style.Style({
    stroke: new ol.style.Stroke({
        color: 'rgba(209,14,14,.6)',
        width: 4
    })
});

var iconStyle = new ol.style.Style({
    image: new ol.style.Icon({
        anchor: [0.5, 30],
        anchorXUnits: 'fraction',
        anchorYUnits: 'pixels',
        src: '../static/GPX/icon/moo7.png',                
    }),
});

var hoverStyleCache = {}
function hoverStyler(feature, resolution) {
    var hover = feature.get('hoverstyle');
    var geometry = feature.getGeometry().getType();
    console.log(hover);
    while (geometry === 'Point') {
        if (!hoverStyleCache[hover]) {
            hoverStyleCache[hover] = new ol.style.Style({
                image: hoverStyles[hover],
            })
        }
        return [hoverStyleCache[hover]];
    }
    while (geometry === 'LineString') {
        if (!hoverStyleCache[hover]) {
            hoverStyleCache[hover] = new ol.style.Style({
                stroke: hoverStyles[hover]
            })
        }
        return [hoverStyleCache[hover]];
    }
}


var featureOverlay = new ol.layer.Vector({
    source: new ol.source.Vector(),
    map: map,
    style: hoverStyler
});

var highlight;
var hover = function(pixel) {
var feature = map.forEachFeatureAtPixel(pixel, function(feature) {
    return feature;
});

    if (feature !== highlight) {
        if (highlight) {
        featureOverlay.getSource().removeFeature(highlight);
        }
        if (feature) {
        featureOverlay.getSource().addFeature(feature);
        }
        highlight = feature;
    }

};

map.on('pointermove', function(evt) {
    if (evt.dragging) {
        return;
    }
    var pixel = map.getEventPixel(evt.originalEvent);
    hover(pixel);
});

***** EDIT 1 *****

Here is a plunkr that at least illustrates what I mean (with the hover vector overlapping the marker).

I continued fiddling around and did get manage to achieve what I wanted, by separating the point and linestring 'featureOverlay' vector into two separate vectors, though to get that to work I also then had to duplicate everything that it implies (the hoverStyler function, the hover feature, etc).

As well, when I referred above to incongruous advice that came especially to the fore as I was able to set the zIndex on that any vector layer, but not in the Styles as I read in just about every other question I found but to the vector itself. Thus,

var featureOverlay = new ol.layer.Vector({
    source: new ol.source.Vector(),
    map: map,
    style: hoverStyler,
    zIndex: 1,
});

worked (with zIndex similarly applied to ever other vector layer), but because points and linestrings share the feature Overlay that also meant that the marker's hover vector image was below the marker it was meant to hover over: applied in this way both linestring and markers or neither hover above.

I thus tried to come up with a function similar to the hoverStyler that differentiates them:

function zIndexer(feature, resolution) {
    var geometry = feature.getGeometry().getType();
    if (geometry === 'Point') {
        return 5
    }
    if (geometry === 'LineString') { 
        return 1
    }
}

but to no avail (I was unsure what exactly should be returned and tried various inputs; this as others did not work).

So I could settle for duplicated featureOverlay apparatuses, but a handy zIndex function would be of course be preferable :)

(*note: I could not get the zIndex to work in the plunkr as described above, in or out of Styles, hence it does not illustrate my later fiddling but only the original question)

***** EDIT 2 *****

I noted in a comment below on the provided answer that, while the answer works wonderfully, it did make cluster styles styled by function (not mentioned in the original post0 not work. I overcame this with the following vector, which worked but that I am not qualified to say worked well:

var clusterCache = {}
var CLM = new ol.layer.Vector({
    source: new ol.source.Vector(),
    style: function(feature) {
        var size = feature.get('features').length;
        var style = clusterCache[size]
        if (size === 1) {
            return [hoverStyles['moo']]
        }
        else if (size > 1) {
            if (!style) {
                style =  new ol.style.Style({
                    image:new ol.style.Icon({
                        anchor: [0.5, 30],
                        anchorXUnits: 'fraction',
                        anchorYUnits: 'pixels',
                        src: '../static/GPX/icon/moo3Hover.png',
                    }),
                    text: new ol.style.Text({
                        text: size.toString(),
                        font: '12px Calibri,sans-serif',
                        fill: new ol.style.Fill({
                            color: '#fff'
                        }),
                        stroke: new ol.style.Stroke({
                            color: '#000',
                            width: 5
                        }),
                        offsetX: 22,
                        offsetY: 6, 
                    })
                }),
                clusterCache[size] = style;
            }
            return style;
        }
    }
});

hoverLayers['clustermoo'] = CLM;

Solution

  • Approach

    The basic idea is you need separate hover layers for the route and the icons (top to bottom):

    • Icon hover layer
    • Icon layer
    • Route hover layer
    • Route layer

    This will mean that say an unhovered icon will draw over the top of a hovered route.

    ZIndex

    You can't use the style zIndex as it applies only to features within a layer. That is, the layer zIndex has a higher precedence than the style zIndex. In your plunkr I couldn't get the layer zIndex to work at all, but it is implicit in the order the layers are added.

    So, you need to:

    • create separate hover layers
    • setup the layers in the order I gave above
    • when hovering a feature, move it to the appropriate hover layer
    • when unhovering a feature, remove it from the appropriate hover layer

    Style Cache

    Another thing, your style cache implementation was rather dubious. In fact you do not need a style cache as your are using layer styles and they are only assigned/created once.

    Plunkr

    Here is an updated plunkr with the above changes: https://plnkr.co/edit/gpswRq3tjTTy9O0L

    var createHoverLayer = function(style) {
        return new ol.layer.Vector({
            source: new ol.source.Vector(),
            style: style,
            });
    }
    
    var hoverLayers = {};
    hoverLayers['route'] = createHoverLayer([hoverStyles['route']]);
    hoverLayers['moo'] = createHoverLayer([hoverStyles['moo']]);
    
    map.addLayer(routeLayer);
    map.addLayer(hoverLayers['route']);
    map.addLayer(mooLayer);
    map.addLayer(hoverLayers['moo']);
    
    var highlight;
    var hover = function(pixel) {
    var feature = map.forEachFeatureAtPixel(pixel, function(feature) {
        return feature;
    });
        if (feature !== highlight) {
            if (highlight) {
                var highlightType = highlight.get('type');
                hoverLayers[highlightType].getSource().removeFeature(highlight);
            }
            if (feature) {
                var featureType = feature.get('type');
                hoverLayers[featureType].getSource().addFeature(feature);
            }
            highlight = feature;
        }
    };
    

    Alternative Approach

    • do not use separate hover layers at all
    • use per feature styling via style function
    • when the feature is hovered/unhovered, set a 'hovered' property on it to true/false
    • in the style function, check this property and use the appropriate style
    • you will need a style cache with this method