Search code examples
javascriptangulargoogle-mapsfetch-apideck.gl

Deck.gl TileLayer (WMS) only processing preflight responses


In my Angular 16 app, I'd like to display a WMS layer on a Google Map with Deck.gl - through a combination of TileLayers and BitmapLayers.

Here's my TileLayer config:

import { TileLayer } from "@deck.gl/geo-layers/typed";
import { GeoBoundingBox } from "@deck.gl/geo-layers/typed/tileset-2d";
import { BitmapLayer } from "@deck.gl/layers/typed";
import { parse } from "@loaders.gl/core";

const wmsLayer = new TileLayer({
    id: "xyz",
    tileSize: 256,
    opacity: 0.8,
    renderSubLayers: (props: any) => {
        const { west, south, east, north } = props.tile.bbox;
        return new BitmapLayer(props, {
            image: props.data,
            transparentColor: [255, 255, 255, 0],
            bounds: [west, south, east, north],
        });
    },
    getTileData: async ({ bbox, signal }) => {
        const { west, south, north, east } = bbox as GeoBoundingBox;

        const params = [
            "?Request=GetMap",
            "&Service=WMS",
            "&crs=EPSG:4326",
            "&width=256",
            "&height=256",
            "&dpi=300",
            "&format=image/png",
            "&transparent=TRUE",
            `&BBOX=${[west, south, east, north].join(",")}`,
        ];
        const targetUrl = process.env.WMS_BASE + params.join("");

        const data = await fetch(targetUrl, {
            signal,
        });

        if (signal?.aborted) {
            return null;
        }

        if (data.type === "cors") {
            // Every data response seems to be of type `cors` 🤔🤔  
            return null;
        }

        return parse(data);
    },
});

The issue is, getTileData isn't handling CORS properly because it seems to only pass resolved requests whose response is of type: "cors":

getTileData breakpoint

Note that the initial preflight requests (OPTIONS) plus the subsequent GETs all resolve with 200s but only preflight fetch is passed downstream: enter image description here

How can I ignore preflight requests and let getTileData only process the actual GET requests?

FYI I'm on v8.9.30 and here's my package.json :

"@deck.gl/aggregation-layers": "^8.9.30",
"@deck.gl/carto": "^8.9.30",
"@deck.gl/core": "^8.9.30",
"@deck.gl/experimental-layers": "^6.4.10",
"@deck.gl/extensions": "^8.9.30",
"@deck.gl/geo-layers": "^8.9.30",
"@deck.gl/google-maps": "^8.9.30",
"@deck.gl/layers": "^8.9.30",
"@deck.gl/mesh-layers": "^8.9.30",
"@loaders.gl/core": "^3.4.14",
"@luma.gl/core": "^8.5.21",

PS - I don't have a repro because the services are behind VPNs and auth walls.


Solution

  • TL;DR there's nothing wrong with getTileData or fetch. It's a matter of processing Blobs and/or array buffers.

    Learnings

    1. Beginner's mistake: the fetch response must be awaited. Obtaining an image Blob goes like this:

      const resp = await fetch(...);
      const blob = await resp.blob();
      
    2. There's nothing wrong with a response of type: "cors" – such responses indicate that they were received from a valid cross-origin request.

    3. Blob's cannot easily be parsed by parse() or load() of @loaders.gl/core. However, getTileData may return a Blob and pass it to renderSubLayers.

    4. renderSubLayers's BitmapLayer cannot load Blobs directly. But you can pass Object URLs, namely: URL.createObjectURL(props.data).


    Here's the working code:

    import { GeoBoundingBox, TileLayer } from "@deck.gl/geo-layers/typed";
    import { BitmapLayer } from "@deck.gl/layers/typed";
    
    const wmsLayer = new TileLayer({
        id: "xyz",
        tileSize: 256,
        opacity: 0.8,
        renderSubLayers: (props: any) => {
            const { west, south, east, north } = props.tile.bbox;
            return new BitmapLayer(props, {
                //     ⬇️⬇️⬇️
                image: URL.createObjectURL(props.data as Blob),
                transparentColor: [255, 255, 255, 0],
                bounds: [west, south, east, north],
            });
        },
        getTileData: async ({ bbox, signal }) => {
            const { west, south, north, east } = bbox as GeoBoundingBox;
            const params = [
                "...",
                `&BBOX=${[west, south, east, north].join(",")}`,
            ];
            const targetUrl = process.env.WMS_BASE + params.join("");
            const resp = await fetch(targetUrl, { signal });
            //           ⬇️⬇️⬇️
            const blob = resp?.blob;
            if (signal?.aborted || !blob) {
                return null;
            }
    
            // ⬇️⬇️⬇️
            return blob;
        },
    });