Edit

NDVI with a Dynamic Color Ramp

cog14 ndvi3

Min NDVI
Max NDVI

NDVI from a COG with a dynamic color ramp

The GeoTIFF layer in this example draws from two Sentinel 2 sources: a red band and a near infrared band. The layer style includes a color expression that calculates the Normalized Difference Vegetation Index (NDVI) from values in the two bands. The interpolate expression is used to map NDVI values to colors. The "stop" values for the color ramp are derived from application provided style variables. Using the inputs above, the min and max colors and values can be adjusted. The layer.updateStyleVariables() method is called to update the variables used in the interpolated color expression.

main.js
import {scale as chromaScale} from 'chroma-js';
import Map from 'ol/Map.js';
import TileLayer from 'ol/layer/WebGLTile.js';
import GeoTIFF from 'ol/source/GeoTIFF.js';

const segments = 10;

const defaultMinColor = '#0300AD';
const defaultMaxColor = '#00ff00';

const defaultMinValue = -0.5;
const defaultMaxValue = 0.7;

const minColorInput = document.getElementById('min-color');
minColorInput.value = defaultMinColor;

const maxColorInput = document.getElementById('max-color');
maxColorInput.value = defaultMaxColor;

const minValueOutput = document.getElementById('min-value-output');
const minValueInput = document.getElementById('min-value-input');
minValueInput.value = defaultMinValue.toString();

const maxValueOutput = document.getElementById('max-value-output');
const maxValueInput = document.getElementById('max-value-input');
maxValueInput.value = defaultMaxValue.toString();

function getVariables() {
  const variables = {};

  const minColor = minColorInput.value;
  const maxColor = maxColorInput.value;
  const scale = chromaScale([minColor, maxColor]).mode('lab');

  const minValue = parseFloat(minValueInput.value);
  const maxValue = parseFloat(maxValueInput.value);
  const delta = (maxValue - minValue) / segments;

  for (let i = 0; i <= segments; ++i) {
    const color = scale(i / segments).rgb();
    const value = minValue + i * delta;
    variables[`value${i}`] = value;
    variables[`red${i}`] = color[0];
    variables[`green${i}`] = color[1];
    variables[`blue${i}`] = color[2];
  }
  return variables;
}

function colors() {
  const stops = [];
  for (let i = 0; i <= segments; ++i) {
    stops[i * 2] = ['var', `value${i}`];
    const red = ['var', `red${i}`];
    const green = ['var', `green${i}`];
    const blue = ['var', `blue${i}`];
    stops[i * 2 + 1] = ['color', red, green, blue];
  }
  return stops;
}

const ndvi = [
  '/',
  ['-', ['band', 2], ['band', 1]],
  ['+', ['band', 2], ['band', 1]],
];

const source = new GeoTIFF({
  sources: [
    {
      // visible red, band 1 in the style expression above
      url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/B04.tif',
      max: 10000,
    },
    {
      // near infrared, band 2 in the style expression above
      url: 'https://sentinel-cogs.s3.us-west-2.amazonaws.com/sentinel-s2-l2a-cogs/36/Q/WD/2020/7/S2A_36QWD_20200701_0_L2A/B08.tif',
      max: 10000,
    },
  ],
});

const layer = new TileLayer({
  style: {
    variables: getVariables(),
    color: ['interpolate', ['linear'], ndvi, ...colors()],
  },
  source: source,
});

function update() {
  layer.updateStyleVariables(getVariables());
  minValueOutput.innerText = parseFloat(minValueInput.value).toFixed(1);
  maxValueOutput.innerText = parseFloat(maxValueInput.value).toFixed(1);
}

minColorInput.addEventListener('input', update);
maxColorInput.addEventListener('input', update);
minValueInput.addEventListener('input', update);
maxValueInput.addEventListener('input', update);
update();

const map = new Map({
  target: 'map',
  layers: [layer],
  view: source.getView(),
});
index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>NDVI with a Dynamic Color Ramp</title>
    <link rel="stylesheet" href="node_modules/ol/ol.css">
    <style>
      .map {
        width: 100%;
        height: 400px;
      }
      .data {
        text-align: right;
        font-family: monospace;
      }
      td {
        padding-right: 10px;
      }
      input[type="range"] {
        vertical-align: text-bottom;
      }
    </style>
  </head>
  <body>
    <div id="map" class="map"></div>
    <table>
      <tbody>
        <tr>
          <td>Min NDVI</td>
          <td><input type="range" id="min-value-input" min="-1.0" max="-0.1" step="0.01"></td>
          <td class="data" id="min-value-output"></td>
          <td><input type="color" id="min-color"></td>
        </tr>
        <tr>
          <td>Max NDVI</td>
          <td><input type="range" id="max-value-input" min="0.1" max="1.0" step="0.01"></td>
          <td class="data" id="max-value-output"></td>
          <td><input type="color" id="max-color"></td>
        </tr>
      </tbody>
    </table>

    <script type="module" src="main.js"></script>
  </body>
</html>
package.json
{
  "name": "cog-colors",
  "dependencies": {
    "ol": "10.5.0",
    "chroma-js": "^3.1.1"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}