openlayers

How do I get the selected style on a polygon feature to be "on top" when it shares a boundary with an unselected feature in OpenLayers?


I am working with OpenLayers 9.1.0. in a React application. I have a vector tile layer with a style function set up so when a user clicks on a polygon it reflects a selected style. It works to some degree.

export const normalAOIStyle = new Style({
    stroke: new Stroke({
        color: '#004C73',
        width: 1,
    }),
    fill: new Fill({
        color: 'rgba(255, 255, 255, 0)',
    }),
})

export const selectedAOIStyle = new Style({
    stroke: new Stroke({
        color: 'rgb(10, 250, 242, 1)',
        width: 2,
    }),
    fill: new Fill({
        color: 'rgba(10, 250, 242, 0.1)',
    }),
})

const hucStyleFunc = (feature: Feature): Style => {
    const selectedAOIs = useMapStore.getState().selectedAOIs
    if (Object.keys(selectedAOIs).includes(addLeadingZero(feature.getId() as number))) {
        return selectedAOIStyle
    } else {
        return normalAOIStyle
    }
}

const mvt = new VectorTileLayer({
    opacity: 1,
    extent: extent,
    declutter: true,
    source: createVectorTileSource(wrkspaceName, layerName, tg, undefined, idField),
    style: hucStyleFunc,
    properties: { name: layerName, isAOI: isAOI },
    zIndex: 2,
})

While the feature is selected and styled correctly, it draws in such a way that a selected polygon ends up looking like this, where part of the polygon boundary is drawn on top in the right color (electric blue) and then the other part of the boundary is drawn below the normal style. Is there a way to fix this behavior that does not involve creating an entirely separate layer to sit on top and hold the selections?

Selected polygon showing one part of the boundary with the selected style on top and the other part of the boundary with the normal style on top

Update:

I tried using the renderOrder property on the VectorTileLayer, but unfortunately renderOrder only occurs on first load, it isn't taken into account when re-styling features.

If you don't mind your layer being entirely redrawn resulting in a momentary blink/flash affect, you could use this function to render selected features last, and then call yourLayer.getSource().refresh() and it will re-render the layer, paying respect to your renderOrder property and style everything correctly. This should really only be used for smaller selections, probably less than 300 features or it might be too slow.

For me, this solution doesn't work because of the layer blinking on redraw; I don't want my users to see that every time they click a feature to select it, but I'll put it here for others anyway.

First, write an OrderFunction:

export const renderSelectedLast = (featureA: FeatureLike, featureB: FeatureLike) => {
    // Take your array of selected feature IDs and create a Map out of them for faster lookups.
    const selectedFeatures = ['feature_id1', 'feature_id2', 'feature_id3']
    const selectedFtIdxMap = new Map()
    selectedAOIs.forEach((feature, index) => {
        selectedFtIdxMap.set(feature, index)
    })
    
    const featureAId = featureA.getId()
    const featureBId = featureB.getId()

    const featureAIncluded = selectedFtIdxMap.has(featureAId)
    const featureBIncluded = selectedFtIdxMap.has(featureBId)

    if (featureAIncluded && !featureBIncluded) {
        return 1 //Render featureB before featureA
    } else if (!featureAIncluded && featureBIncluded) {
        return -1 //Render featureA before featureB
    } else if (featureAIncluded && featureBIncluded) {
        //Render in selected AOI array order
        return selectedFtIdxMap.get(featureAId) - selectedFtIdxMap.get(featureBId)
    } else {
        return 0 //Use source order
    }
}

Add the renderOrder property to your VectorTileLayer definition, set to your OrderFunction:

const mvt = new VectorTileLayer({
    opacity: 1,
    extent: extent,
    declutter: true,
    source: createVectorTileSource(wrkspaceName, layerName, tg, undefined, idField),
    style: hucStyleFunc,
    properties: { name: layerName, isAOI: isAOI },
    zIndex: 2,
    renderOrder: renderSelectedLast
})

Then, wherever you're monitoring your user's clicks on a feature (usually in an onClick event of some kind), you can run this line to re-render/re-draw the layer which will run the function above:

yourVectorTileLayer.getSource().refresh()

Now it will load how you want:

The selected polygon renders with the proper style for the entire polygon boundary

I tried using yourVectorTileLayer.getSource().changed() to see if that would stop the blink/flash, but it doesn't.


Solution

  • I should have read the docs more closely. Style has a zIndex property itself.

    All I had to do was add the zIndex property to my two Style objects, giving a higher zIndex to my selected style and it now renders the selected-style features on top of the normal-styled features:

    export const normalAOIStyle = new Style({
        stroke: new Stroke({
            color: '#004C73',
            width: 1,
        }),
        fill: new Fill({
            color: 'rgba(255, 255, 255, 0)',
        }),
        // The current zIndex on my VectorTileLayer overall is set to 2, 
        // so I set 2 here as well
        zIndex: 2 
    })
    
    export const selectedAOIStyle = new Style({
        stroke: new Stroke({
            color: 'rgb(10, 250, 242, 1)',
            width: 2,
        }),
        fill: new Fill({
            color: 'rgba(10, 250, 242, 0.1)',
        }),
        // I want selected feature styles to sit "on top" of normal feature styles
        // so I gave this Style a higher zIndex number
        zIndex: 3
    })