Reduce Http Request To Your Geoserver From Openlayer

If you see this article, you might be frustated handle openlayer that making too much request to your geoserver. I faced this problem also and try to figure it out for a couple days. But first, let me tell you about my experiences.




At first, My manager and me was thinking to limit the map interaction (panning/moving) to not able out of my raster map. But the problem is, in my case,  the other layer might be out of my raster map and it contains hundreds or even thousand of marker.

Of course, I can do a mathematical function to get the extent between these layers to get minX, minY, maxX and maxY. But, the trade are very costly, it will affect performance, since it running a heavy computation work on client side.

If you don't know, the openlayer gonna create a new request (TileWMS layer) to your geoserver even when the layer (raster map) is not visible to your viewport. So, when you moving to Africa, while your layer is in Indonesia, it still requesting a new request because the boundingBox (BBOX) has changed. So here is my approach.


1. Get the coordinates of your Viewport

What is viewport? Viewport here is a boundingBox of your base map. For example, you show a map on a component with 500x500px. It is a square, the map will show as a square. Take a look at right top corner, left top corner, right bottom corner and left bottom corner. The outer point of that is also a coordinate, right? that means the view port. Everything that inside those coordinates, is considered "inside" the viewport.

How to get the viewport? Take a look at this code below:



const mapExtentViewport = map.getView().calculateExtent(map.getSize())


that will return a number[] with this order: minX, minY, maxX, maxY using EPSG:3857



2. Paradigm

The big picture is, if your raster map are inside/partially inside the viewport, we are gonna load it. if no then skip.

We got our viewport. The challenge is to get your raster map layer boundingBox. I already try it using openlayer getSource().getExtent() but it returns undefined. Luckily, the backend already give me the boundingBox of it.

I am really sorry to you who read this, but please take another reasearch to get boundingBox of your raster map by yourself.

The backend give me this data: minX, minY, maxX, maxY of the raster maps. Since I have multiple raster maps, I just looping it and create a layer for every loop. Take a look at the code here:

   
const { map, ecwLayers } = this.props

    ecwLayers.forEach((ecw, idx) => {
      const layer = new TileLayer({
        useInterimTilesOnError: true,
        source: new TileWMS({
          url: GEO_SERVICE_API_URL,
          tileLoadFunction: (tile, src) => this.tileLoader2(tile, src, ecw),
          params: {
            LAYERS: ecw.layername,
            VERSION: '1.1.1',
            FORMAT: 'image/png',
            TILED: true,
          },
        }),
        visible: this.props.selectedEcwLayer?.layername === ecw.layername,
        properties: {
          ...ecw,
          type: 'ecw',
        },
        zIndex: 1,
      })

      map?.addLayer(layer)
    })




Focus on tileLoader2 and parameter that passed onto it. It have 'ecw' that contains my boundingBox of the raster map.



3. fx() as tileLoader2

We have tileLoader2 function, this function is gonna check the rule wether we create a new request or just skip it.

My rule is:
We fetch a new request if the raster/layer is fully inside or partially inside or intersecting with our map viewport.

import { 
containsCoordinate, 
containsExtent, 
intersects, 
} from 'ol/extent'

 tileLoader2 = (tile: any, src: any, ecw: any) => {
    const { map } = this.props
    if (!map) {
      return
    }

    if (!ecw['nativeBound']?.minx || 
    !ecw['nativeBound']?.miny || 
    !ecw['nativeBound'].maxx || 
    !ecw['nativeBound'].maxy) {
      this.loadTile(tile, src)
      return
    }

    const zoom = map?.getView().getZoom()
    const mapExtentViewport = map.getView().calculateExtent(map.getSize())
    
    const feature = [
    ecw['nativeBound'].minx, 
    ecw['nativeBound'].miny, 
    ecw['nativeBound'].maxx, 
    ecw['nativeBound'].maxy
    ]
    
    const extentFeature = transformExtent(feature, 'EPSG:4326', 'EPSG:3857')
    const isFullyVisible = containsExtent(mapExtentViewport, extentFeature)
    if (this.shouldRequestBecauseOfZoomChange(zoom, isFullyVisible)) {
      //new request to geoserver due to zoom level change
      this.loadTile(tile, src)
      return
    }



    //@ts-ignore
    if (isFullyVisible && this.state.isFullyVisible) {
      tile.setState(2)
      return
    }

    const isVisibleToViewPort = containsExtent(mapExtentViewport, extentFeature) || 
    containsCoordinate(mapExtentViewport, extentFeature) || 
    intersects(mapExtentViewport, extentFeature)

    if (isVisibleToViewPort) {

      this.loadTile(tile, src)
    } else {
      tile.setState(2)
    }

  }



For now, please take a look at extentFeature variable, it get my nativeBound/boundingBox from my backend and converts it to openlayer format (EPSG3857). And then checking it with: containsExtent (for a fully inside checking), containsCoordinate, and intersects (for partially visible detection).

If the value if "isVisibleToViewPort" is true, the hit this.loadTile() function which is a custom function to create a xhr request. You can use your own or just use the default like this: https://openlayers.org/en/latest/apidoc/module-ol_Tile.html

When it's not visible/partially visible? We skip it. BUT I have found a weird things.

If you just skipping it without doing anything, looks like openlayers gonna throw an exception and it will go into 'dead' mode. So, I use tile.setState(2) to prevent that problem.

The '2' as params means it is marked as 'success' get new tiles. You cannot use other value like '3' to marked it as failed. I suggest you to use these two state.


Important Notes:
The tile.setStaate is very depends on what layer you used. In this case, I use TileWMS, it contains setState method. The other layer might not need that, or might be need different approach. Here is the example of using the Vector Layer (it uses setProjection etc): https://github.com/openlayers/openlayers/issues/8276


I will explains the other lines inside tileLoader2 next.


4. Stop creating request when previous state is same and no zoom change

When a raster map is fully visible, and then you move/pan it and still the raster is fully inside, I dont want to create a new request. Simply because I already got it, no zoom level (means no need to request a raster with different resolution).

Take a look at step number 3. You will see this line:

const zoom = map?.getView().getZoom()

It gives you a newest zoom level after interaction. Save it to a state.

You see shouldRequestBecauseOfZoomChange(), this function I made is just for checking: if the previous zoom level and previous raster is fully visible, store it on a state, and returns a boolean value. True means, previous value is different from newest value (might be user panning it too far, or zooming changes), so it should create a new request to gesoerver. If it's exact same (means no zoom change, and the map is still FULLY visible), the no request will be created.


  shouldRequestBecauseOfZoomChange = (zoom: number | undefined, isFullyVisible: boolean): boolean => {
    if (!zoom) {
      return true
    }
    //@ts-ignore
    if (zoom != this.state.previousZoom) {
      this.setState((state) => ({
        ...state,
        previousZoom: zoom,
        isFullyVisible
      }))
      return true
    }
    return false
  }


Foot notes:
- If you are using functional component, it might be difficult to you. Try use useCallback on tileLoader function
- I only apply this approach on TileWMS
- I am using openlayers v6.1x.x
- I am using nextJS
- the boundingBox of the raster map is from backend. Theorically it is possible to get via frontend, but somehow openlayers give me undefined value. So i skip that approach.
- This approach is gonna create a new request only when: panning/moving that makes raster map partially visible OR zoom events
- I am thinking to prevent load a request even when partially visible, but looks like impossible to make it because the 'partially visible' can be anywhere.

*some of the detail especially the UI, I cannot give it to you because it is a private material. If you have some spesific question, please ask through comment section


Post a Comment

0 Comments