Process API-Leaflet Multiple Evalscript

Hi,
I am trying to get NDVI and True Color layers on using Process API-leaflet. I’am student and I’ve just a basic knowledge and i am trying to develop sample process api-leaflet code. Sorry in advance.
The evalscript I applied for the True Color layer is not working. How can I directly use the layers I created in the dashboard without using evalscript or can I define 2 different evalscripts in my code?

Here is my code which works to get NDVI layer correctly.

    # Sentinel Hub OAuth2 + Process API Leaflet

    How to use:

      1) enter sentinelHubNDVI client ID and secret
         (go to SH Dashboard -> User settings -> OAuth clients -> "+")

      2) open this file in browser

  *************************/
  const CLIENT_ID = "a7b4fba9-cc9c-4fdb-9a15-MASKED";
  const CLIENT_SECRET = "Xr-F_M>hTi1O+3xUOEtPcKR-MASKED";

  const fromDate = "2020-07-01T00:00:00.000Z";
  const toDate = "2020-09-01T00:00:00.000Z";
  const dataset = "S2L1C";
  const evalscript = `//VERSION=3

//This script was converted from v1 to v3 using the converter API

//NDVI EVALSCRIPT
//VERSION=3

if (dataMask == 0) return [0,0,0,0];

//ndvi
var val = (B08-B04)/(B08+B04);

if (val<-1.1) return [0,0,0,1];
else if (val<-0.2) return [0.75,0.75,0.75,1];
else if (val<-0.1) return [0.86,0.86,0.86,1];
else if (val<0) return [1,1,0.88,1];
else if (val<0.025) return [1,0.98,0.8,1];
else if (val<0.05) return [0.93,0.91,0.71,1];
else if (val<0.075) return [0.87,0.85,0.61,1];
else if (val<0.1) return [0.8,0.78,0.51,1];
else if (val<0.125) return [0.74,0.72,0.42,1];
else if (val<0.15) return [0.69,0.76,0.38,1];
else if (val<0.175) return [0.64,0.8,0.35,1];
else if (val<0.2) return [0.57,0.75,0.32,1];
else if (val<0.25) return [0.5,0.7,0.28,1];
else if (val<0.3) return [0.44,0.64,0.25,1];
else if (val<0.35) return [0.38,0.59,0.21,1];
else if (val<0.4) return [0.31,0.54,0.18,1];
else if (val<0.45) return [0.25,0.49,0.14,1];
else if (val<0.5) return [0.19,0.43,0.11,1];
else if (val<0.55) return [0.13,0.38,0.07,1];
else if (val<0.6) return [0.06,0.33,0.04,1];
else return [0,0.27,0,1];

  `;

const evalscript1 = `//VERSION=3

                      //TRUE COLOR

//VERSION=3

let minVal = 0.0;
let maxVal = 0.4;

let viz = new HighlightCompressVisualizer(minVal, maxVal);

function evaluatePixel(samples) {
let val = [samples.B04, samples.B03, samples.B02];
val = viz.processList(val);
val.push(samples.dataMask);
return val;
}

function setup() {
return {
input: [{
bands: [
“B02”,
“B03”,
“B04”,
“dataMask”
]
}],
output: {
bands: 4
}
}
}
; // Promise which will fetch Sentinel Hub authentication token: const authTokenPromise = fetch( "https://services.sentinel-hub.com/oauth/token", { method: "post", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: grant_type=client_credentials&client_id=${encodeURIComponent(
CLIENT_ID
)}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`,
}
)
.then((response) => response.json())
.then((auth) => auth[“access_token”]);

  // We need to extend Leaflet's GridLayer to add support for loading images through
  // Sentinel Hub Process API:
  L.GridLayer.SHProcessLayer = L.GridLayer.extend({
        createTile: function (coords, done) {
            const tile = L.DomUtil.create("img", "leaflet-tile");
            const tileSize = this.options.tileSize;
            tile.width = tileSize;
            tile.height = tileSize;
            const nwPoint = coords.multiplyBy(tileSize);
            const sePoint = nwPoint.add([tileSize, tileSize]);
            const nw = L.CRS.EPSG4326.project(
                this._map.unproject(nwPoint, coords.z)
            );
            const se = L.CRS.EPSG4326.project(
                this._map.unproject(sePoint, coords.z)
            );

            authTokenPromise.then((authToken) => {
                // Construct Process API request payload:
                //   https://docs.sentinel-hub.com/api/latest/reference/#tag/process
                const payload = {
                    input: {
                        bounds: {
                            bbox: [nw.x, nw.y, se.x, se.y], // a tile's bounding box
                            geometry: { // remove to disable clipping
                                type: "Polygon",
                                coordinates: [
      [
        [
          37.033538818359375,
          39.246745041633794
        ],
        [
          37.03388214111328,
          39.23777105285819
        ],
        [
          37.04864501953125,
          39.23836935449403
        ],
        [
          37.04804420471191,
          39.24754267396328
        ],
        [
          37.033538818359375,
          39.246745041633794
        ]
      ]
    ]
                            },
                            properties: {
                                crs: "http://www.opengis.net/def/crs/EPSG/0/4326",
                            },
                        },
                        data: [
                            {
                                dataFilter: {
                                    timeRange: {
                                        from: fromDate,
                                        to: toDate,
                                    },
									maxCloudCoverage: 10,
                                    mosaickingOrder: "mostRecent",
                                    
                                },
                                processing: {},
                                type: dataset,
                            },
                        ],
                    },
                    output: {
                        width: 512,
                        height: 512,
                        responses: [
                            {
                                identifier: "default",
                                format: {
                                    type: "image/png",
									
									
									
						
                                },
							
                            },
                        ],
                    },
                    evalscript: evalscript,
					evalscript1: evalscript1,
					
					
                };

                // Fetch the image:
                fetch("https://services.sentinel-hub.com/api/v1/process", {
                    method: "post",
                    headers: {
                        Authorization: "Bearer " + authToken,
                        "Content-Type": "application/json",
                        Accept: "*/*",
                    },
                    body: JSON.stringify(payload),
                })
                    .then((response) => response.blob())
                    .then((blob) => {
                        const objectURL = URL.createObjectURL(blob);
                        tile.onload = () => {
                            URL.revokeObjectURL(objectURL);
                            done(null, tile);
                        };
                        tile.src = objectURL;
                    })
                    .catch((err) => done(err, null));
            });
            return tile;
        },
    });

  L.gridLayer.shProcessLayer = function (opts) {
    return new L.GridLayer.SHProcessLayer(opts);
  };
  const sentinelHubNDVI = L.gridLayer.shProcessLayer();
  const sentinelHubTrueColor = L.gridLayer.shProcessLayer();
  
  // OpenStreetMap layer:
  let osm = L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
    attribution:
      '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',});

    
  
    
  // configure Leaflet:


  let baseMaps = {
    OpenStreetMap: osm,
  };
  let overlayMaps = {
    "NDVI": sentinelHubNDVI,
"True": sentinelHubTrueColor,
    
  };

  let map = L.map("map", {
    center: [39.243276, 37.042575], // lat/lng in EPSG:4326
    zoom: 15,
    layers: [osm, sentinelHubNDVI],
    
  });
  L.control.layers(baseMaps, overlayMaps).addTo(map);
</script>

Hey, thanks for the question.
Sorry for the changes to your post, it was accidental. Now it should be the same as you first posted it.

You are very close to the correct solution.

The only change that is needed is to

  • pass the object with the evalscript to the ShProcessLayer when you are creating the layers

    • the passed object represents additional options for that ShProcessLayer and is merged with other options already accessible to it.
    const sentinelHubNDVI = L.gridLayer.shProcessLayer({evalscript: evalscript});
    const sentinelHubTrueColor = L.gridLayer.shProcessLayer({evalscript: evalscript1});
    
  • use the passed evalscript in the createTile function of the SHProcessLayer

    evalscript: this.options.evalscript,
    

Below, in the collapsible part, is the whole code with the comments, what I changed.

Side note - tips for markdown:
You can use triple backtick (``` ) followed by a new line for the start and end of the code block. This then has another option to set the syntax highlighting if you write the language after the start.

```html (or javascript, python, ...) 
... code 
```
The code (click to expand)
<html>
<head>
  <style>
    #map{
      width: 100vw;
      height: 100vh;
    }
  </style>
  <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
    integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
    crossorigin="" />
  <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
    integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
    crossorigin=""></script>
</head>
</body>

<div id="map"></div>

<script>
  /*************************

  !!! EVERYTHING ABOVE THIS IS AN ASSUMPTION !!!
  Because Markdown treats html tags as formatting if they are not encapsed with triple ` (backtick)).
  Also, I added just the <link ...> and <script src="..." /> tags for Leaflet.
  
  # Sentinel Hub OAuth2 + Process API Leaflet

  How to use:

    1) enter sentinelHubNDVI client ID and secret
      (go to SH Dashboard -> User settings -> OAuth clients -> "+")

    2) open this file in browser

  *************************/
  const CLIENT_ID = "e613dced-c8db-MASKED";
  const CLIENT_SECRET = "jJ#T3bI/K3|JAJq7DsU_MASKED";

  const fromDate = "2020-07-01T00:00:00.000Z";
  const toDate = "2020-09-01T00:00:00.000Z";
  const dataset = "S2L1C";
  const evalscript = `//VERSION=3
    //This script was converted from v1 to v3 using the converter API

    //NDVI EVALSCRIPT

    if (dataMask == 0) return [0,0,0,0];

    //ndvi
    var val = (B08-B04)/(B08+B04);

    if (val<-1.1) return [0,0,0,1];
    else if (val<-0.2) return [0.75,0.75,0.75,1];
    else if (val<-0.1) return [0.86,0.86,0.86,1];
    else if (val<0) return [1,1,0.88,1];
    else if (val<0.025) return [1,0.98,0.8,1];
    else if (val<0.05) return [0.93,0.91,0.71,1];
    else if (val<0.075) return [0.87,0.85,0.61,1];
    else if (val<0.1) return [0.8,0.78,0.51,1];
    else if (val<0.125) return [0.74,0.72,0.42,1];
    else if (val<0.15) return [0.69,0.76,0.38,1];
    else if (val<0.175) return [0.64,0.8,0.35,1];
    else if (val<0.2) return [0.57,0.75,0.32,1];
    else if (val<0.25) return [0.5,0.7,0.28,1];
    else if (val<0.3) return [0.44,0.64,0.25,1];
    else if (val<0.35) return [0.38,0.59,0.21,1];
    else if (val<0.4) return [0.31,0.54,0.18,1];
    else if (val<0.45) return [0.25,0.49,0.14,1];
    else if (val<0.5) return [0.19,0.43,0.11,1];
    else if (val<0.55) return [0.13,0.38,0.07,1];
    else if (val<0.6) return [0.06,0.33,0.04,1];
    else return [0,0.27,0,1];
  `;

  const evalscript1 = `//VERSION=3
    //TRUE COLOR

    let minVal = 0.0;
    let maxVal = 0.4;

    let viz = new HighlightCompressVisualizer(minVal, maxVal);

    function evaluatePixel(samples) {
      let val = [samples.B04, samples.B03, samples.B02];
      val = viz.processList(val);
      val.push(samples.dataMask);
      return val;
    }

    function setup() {
      return {
        input: [{
          bands: [
            "B02",
            "B03",
            "B04",
            "dataMask"
          ]
        }],
        output: {
          bands: 4
        }
      }
    }
  `;

  // Promise which will fetch Sentinel Hub authentication token:
  const authTokenPromise = fetch(
    "https://services.sentinel-hub.com/oauth/token",
    {
      method: "post",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: `grant_type=client_credentials&client_id=${encodeURIComponent(
        CLIENT_ID
      )}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`,
    }
  )
  .then((response) => response.json())
  .then((auth) => auth["access_token"]);

  // We need to extend Leaflet's GridLayer to add support for loading images through
  // Sentinel Hub Process API:
  L.GridLayer.SHProcessLayer = L.GridLayer.extend({
    createTile: function (coords, done) {
      const tile = L.DomUtil.create("img", "leaflet-tile");
      const tileSize = this.options.tileSize;
      tile.width = tileSize;
      tile.height = tileSize;
      const nwPoint = coords.multiplyBy(tileSize);
      const sePoint = nwPoint.add([tileSize, tileSize]);
      const nw = L.CRS.EPSG4326.project(
        this._map.unproject(nwPoint, coords.z)
      );
      const se = L.CRS.EPSG4326.project(
        this._map.unproject(sePoint, coords.z)
      );

      authTokenPromise.then((authToken) => {
        // Construct Process API request payload:
        //   https://docs.sentinel-hub.com/api/latest/reference/#tag/process
        const payload = {
          input: {
            bounds: {
              bbox: [nw.x, nw.y, se.x, se.y], // a tile's bounding box
              geometry: { // remove to disable clipping
                type: "Polygon",
                coordinates: [
                  [
                    [
                      37.033538818359375,
                      39.246745041633794
                    ],
                    [
                      37.03388214111328,
                      39.23777105285819
                    ],
                    [
                      37.04864501953125,
                      39.23836935449403
                    ],
                    [
                      37.04804420471191,
                      39.24754267396328
                    ],
                    [
                      37.033538818359375,
                      39.246745041633794
                    ]
                  ]
                ]
              },
              properties: {
                crs: "http://www.opengis.net/def/crs/EPSG/0/4326",
              },
            },
            data: [
              {
                dataFilter: {
                  timeRange: {
                    from: fromDate,
                    to: toDate,
                  },
                  maxCloudCoverage: 10,
                  mosaickingOrder: "mostRecent",    
                },
                processing: {},
                type: dataset,
              },
            ],
          },
          output: {
            width: 512,
            height: 512,
            responses: [
              {
                identifier: "default",
                format: {
                  type: "image/png",
                },
              },
            ],
          },
          evalscript: this.options.evalscript, // CHANGED: using the evalscript that was passed 
        };

        // Fetch the image:
        fetch("https://services.sentinel-hub.com/api/v1/process", {
          method: "post",
          headers: {
            Authorization: "Bearer " + authToken,
            "Content-Type": "application/json",
            Accept: "*/*",
          },
          body: JSON.stringify(payload),
        })
        .then((response) => response.blob())
        .then((blob) => {
            const objectURL = URL.createObjectURL(blob);
            tile.onload = () => {
                URL.revokeObjectURL(objectURL);
                done(null, tile);
            };
            tile.src = objectURL;
        })
        .catch((err) => done(err, null));
      });
      return tile;
    },
  });

  L.gridLayer.shProcessLayer = function (opts) {
    return new L.GridLayer.SHProcessLayer(opts);
  };

  // CHANGED: passed the object with the correct evalscript to the L.gridLayer.shProcessLayer
  const sentinelHubNDVI = L.gridLayer.shProcessLayer({evalscript: evalscript});
  const sentinelHubTrueColor = L.gridLayer.shProcessLayer({evalscript: evalscript1});
    
  // OpenStreetMap layer:
  let osm = L.tileLayer("http://{s}.tile.osm.org/{z}/{x}/{y}.png", {
    attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
  });

  // configure Leaflet:
  let baseMaps = {
    OpenStreetMap: osm,
  };
  let overlayMaps = {
    "NDVI": sentinelHubNDVI,
    "True": sentinelHubTrueColor,
  };

  let map = L.map("map", {
    center: [39.243276, 37.042575], // lat/lng in EPSG:4326
    zoom: 15,
    layers: [osm, sentinelHubNDVI],
  });
  L.control.layers(baseMaps, overlayMaps).addTo(map);
</script>
</body>
</html>

Hope this helps. Good luck!

Cheers

1 Like

Here’s a bit broader explanation.

The createTile function prepares the payload for the POST request to the Processing API for each tile that is displayed.
Processing API can only accept one evalscript per request. It then uses that evalscript and processes the data and sends a response. So, adding both evalscripts in the payload does not have a desired effect.

What we want to do, is to have a control from the outside of the L.gridLayer.shProcessLayer to set what evalscript should be used. We achieve that by sending an object with options (parameters with which the L.gridLayer.shProcessLayer is then created).
For more advanced use, other parameters can be set in the same way (fromDate, toDate, dataset, the coordinates for the bounds, maxCloudCoverage, mosaickingOrder, response format, etc.), but that’s maybe too advanced for this basic example.

1 Like

Dear Ziga,
Thank you so much for explaining everything in detail and editing the code. I am also researching advanced usage suggestions, I will try to implement it, and thank you for that too!

1 Like

Hey Ziga, I have one more question for you. I would like to list all available dates that were taken images up to 3 months prior to today’s date and apply NDVI to the selected date. I know that there is a distinct function in the Catalog API for this. How can I integrate this function into my process api-leaflet code?

Hey, sorry for late response.

The example below contains code and some explanation of how to use Catalog API.
I didn’t include the code for adding the dates into a DOM element as it makes the example quite long.
The element can be a simple div placed and designed as desired (the only thing is that the z-index should be above what the Leaflet elements z-index is, so that the element is visible (Leaflet has z-index value set to 1000 if I remember correctly).
Or it can also be a custom Control element.

I took the approach with async and await in some places as it is a bit easier to do for me than with the .then() and callbacks.

/*
  Same as in the example above
*/

// Promise which will fetch Sentinel Hub authentication token:
const authTokenPromise = fetch(
  "https://services.sentinel-hub.com/oauth/token",
  {
    method: "post",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: `grant_type=client_credentials&client_id=${encodeURIComponent(
      CLIENT_ID
    )}&client_secret=${encodeURIComponent(CLIENT_SECRET)}`,
  }
)
  .then((response) => response.json())
  .then((auth) => auth["access_token"]);

/*
  Added
  Getting dates
*/

async function getDates(bbox, fromTime, toTime, collection) {
  try {
    const authToken = await authTokenPromise;

    // The catalog responses are paginated (max 100 features / dates returned at once).
    // We need to make multiple requests, each one requesting another "page" of dates.  
    // - check if 'next' field is present in 'context' part of the response,
    // - if it's present, set the 'next' field in our payload to that value and make another request
    // - if it's not present, don't make any more requests

    let payload = {
      "bbox": bbox,
      "datetime": fromTime + "/" + toTime,
      "collections": [collection],
      "limit": 50,
      "distinct": "date",
    };
    let moreResults = true;
    let allDates = [];

    while (moreResults) {
      const response = await fetch("https://services.sentinel-hub.com/api/v1/catalog/search", {
        method: "post",
        headers: {
          Authorization: "Bearer " + authToken,
          "Content-Type": "application/json",
          Accept: "*/*",
        },
        body: JSON.stringify(payload),
      });

      const data = await response.json();
      if (data.context.next) {
        moreResults = true;
        payload.next = data.context.next;
      } else {
        moreResults = false;
      }
      // Dates are returned in the 'features' part of the response.
      // They are ordered from the oldest to the newest date.
      allDates.push(...data.features);
    }

    return allDates;
  }
  catch (err) {
    console.error(err);
  }
}

const bboxForDates = [36, 38, 37, 39]; // bbox that encloses the same geometry as on the map
// bbox can also be retrieved / calculated from the current view on the leaflet map with some additional changes
const fromDateForDates = '2021-01-01T00:00:00Z';
const toDateforDates = '2021-03-29T23:59:59Z';
const collectionForDates = "sentinel-2-l1c"; // this is similar to dataset in the Process API request 

getDates(bboxForDates, fromDateForDates, toDateforDates, collectionForDates).then(dates => {
  // dates are now in ascending order (from older to newer)
  console.log("all the dates for the parameters, older first", { dates });

  // .sort() sorts in-place, so we need to first copy the array into a new one and then sort
  const datesFromNewerToOlder = [...dates].sort((a, b) => (new Date(b).getTime() - new Date(a).getTime()));
  console.log('all the dates for the parameters, newer first', { datesFromNewerToOlder });
  
  // use the dates - put the dates in some DOM element, etc.
});

Hope this helps.
Cheers, Ziga

1 Like