diff --git a/planetary-variables/soil-water-content/index.md b/planetary-variables/soil-water-content/index.md index 4fa2e41d..41b84178 100644 --- a/planetary-variables/soil-water-content/index.md +++ b/planetary-variables/soil-water-content/index.md @@ -9,6 +9,7 @@ parent: Planetary Variables Soil Water Content (SWC) measures the amount of water in a unit volume of soil, which is crucial information for drought monitoring, water management, and climate risk assessment. -Planet's SWC product provides near-daily measurements at spatial resolutions of 100 m and 1000 m. It is unhindered by clouds and has a long and consistent data record of more than 20 years. Please check [here](https://docs.sentinel-hub.com/api/latest/data/planetary-variables/soil-water-content/#available-bands) for a list of available bands. +Planet's SWC product provides near-daily measurements at spatial resolutions of 100 m and 1000 m. It is unhindered by clouds and has a long and consistent data record of more than 20 years. Please check [here](https://docs.sentinel-hub.com/api/latest/data/planetary-variables/soil-water-content/#available-bands) for a list of available bands. Soil Water Content is measured in $$m^3/m^3$$. -- [Soil Water Content Visualization]({% link planetary-variables/soil-water-content/soil-water-content-visualization/index.md %}) +- [Soil Water Content Visualization]({% link planetary-variables/soil-water-content/soil-water-content-visualization/index.md %}) +- [Soil Water Content Anomaly]({% link planetary-variables/soil-water-content/soil-water-content-anomaly/index.md %}) diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/anomaly.png b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/anomaly.png new file mode 100644 index 00000000..7bd6db8e Binary files /dev/null and b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/anomaly.png differ diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/single_date.png b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/single_date.png new file mode 100644 index 00000000..695c60f6 Binary files /dev/null and b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/single_date.png differ diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/swc.png b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/swc.png new file mode 100644 index 00000000..17cfe9fa Binary files /dev/null and b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/swc.png differ diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/timespan.png b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/timespan.png new file mode 100644 index 00000000..ec0f0fc0 Binary files /dev/null and b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/timespan.png differ diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/true_color.png b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/true_color.png new file mode 100644 index 00000000..844fc69a Binary files /dev/null and b/planetary-variables/soil-water-content/soil-water-content-anomaly/fig/true_color.png differ diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/index.md b/planetary-variables/soil-water-content/soil-water-content-anomaly/index.md new file mode 100644 index 00000000..a5451977 --- /dev/null +++ b/planetary-variables/soil-water-content/soil-water-content-anomaly/index.md @@ -0,0 +1,54 @@ +--- +title: Soil Water Content Anomaly +grand_parent: Planetary Variables +parent: Soil Water Content +layout: script +nav_exclude: false +scripts: + - [Visualization, script.js] + - [Raw Values, raw.js] +--- + +## Description of representative images + +The visualization represents negative soil water content anomalies (less water content than on average) in shades of red and positive soil water content anomalies (more water content than on average) in hues of blue . + +Soil Water Content Anomaly (C band 1000 m) on June 3rd, 2023 Graz, Austria. + +| True Color Image of AOI (June 9th) | Soil Water Content (SWC) | Standardized Anomaly of SWC | +| :--------------------------------: | :--------------------------------: | :----------------------------------------------------: | +| ![True Color](fig/true_color.png) | ![Soil Water Content](fig/swc.png) | ![Soil Water Content Anomaly example](fig/anomaly.png) | + +## General description + +This script calculates the standardized anomaly of the soil water content for a particular date. It takes all values of the same day of the year in previous years and calculates the mean and standard deviation of the value. The anomaly is then defined as the current value subtracted by the mean of the reference period. To get the _standardized_ anomaly, the absolute anomaly value is then divided by the standard deviation of the reference period. + +The standardized anomaly can be compared between different areas and different sensors and is the one produced by this script. If the absolute anomaly is desired the last step in the evalscript of dividing by the standard deviation can be removed. This then results in anomalies in the unit of measurement. In this case $$m^3/m^3$$ below or above the mean water content during the reference period. + +## Notes on usage + +### EO Browser + +To use this script in the EO Browser, a time span needs to be set in the interface. To do this, visualize the date you want to calculate an anomaly for. Then in the Visualize panel, hit the green `Timespan` button. + +![Visualize Panel Interface](fig/single_date.png) + +In the interface which then appears, select the time range you want to use as reference period. In this case, we select a time range from 2012 to 2024, which is 12 years. Be aware that this will only include data which is available, so if you ordered data for 5 years but specify a time range of 10 years, only the 5 years you have ordered will be included. + +_Please note_: The date that is compared to the reference period is always the most recent date with data in the selected time span. + +![Time Span Interface](fig/timespan.png) + +### Reference Period + +The reference period represents which dates get included for each year and is determined by the variable `toleranceDays` in the evalscript. This variable determines how many days adjacent to the selected day are included in the calculation. If the day for which an anomaly is computed is the 10th of January 2024 and `toleranceDays` is 0, only data in previous years that are also exactly on the 10th of January will be considered. If `toleranceDays` is 1, for each year in the reference period, one day before and after the 10th of January will also be considered and included in the calculation. + +### Visualization + +In the visualization script you can modify the color scale by changing the variables `vmin` and `vmax`. Those are the maximum and minimum values of the color ramp. If the anomaly is only very slight, you might want to change `vmin` and `vmax` to lower values to be able to see slight differences better. + +## References + +- [Product specifications](https://planet.widen.net/s/5xtzljjwgg) +- [Data sheet](https://planet.widen.net/s/cv7bfjhhd5) +- [Sentinel Hub documentation about Soil Water Content](https://docs.sentinel-hub.com/api/latest/data/planetary-variables/soil-water-content/) diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/raw.js b/planetary-variables/soil-water-content/soil-water-content-anomaly/raw.js new file mode 100644 index 00000000..84a9997c --- /dev/null +++ b/planetary-variables/soil-water-content/soil-water-content-anomaly/raw.js @@ -0,0 +1,99 @@ +// tolerance in either direction, so i.e. +- 5 days +const toleranceDays = 1; + +const NODATA = NaN; +const band = "SWC"; + +function setup() { + return { + input: [band, "dataMask"], + output: { bands: 1, sampleType: "FLOAT32" }, + mosaicking: "ORBIT", + }; +} + +const msInDay = 24 * 60 * 60 * 1000; +const msInYear = 365.25 * msInDay; +const msInHalfYear = msInYear / 2; +const toleranceMs = toleranceDays * msInDay; + +var metadata = undefined; + +function relDiff(a, b) { + const diff = Math.abs(a - b); + return diff > msInHalfYear ? msInYear - diff : diff; +} + +function datetimeToYearEpoch(date) { + return date - new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); +} + +function sortDatesDescending(d1, d2) { + const date1 = new Date(d1.dateFrom); + const date2 = new Date(d2.dateFrom); + return date2 - date1; +} + +function preProcessScenes(collections) { + // sort + let scenes = collections.scenes.orbits; + scenes = scenes.sort(sortDatesDescending); + let newScenes = []; + // convert first scene to day of year + const observed = new Date(scenes[0].dateFrom); + const obsMs = datetimeToYearEpoch(observed); + for (let i = 0; i < scenes.length; i++) { + let currentDate = new Date(scenes[i].dateFrom); + let sceneMs = datetimeToYearEpoch(currentDate); + let dt = relDiff(obsMs, sceneMs); + if (dt <= toleranceMs) { + newScenes.push(scenes[i]); + } + } + + metadata = { + observed: observed.toISOString(), + historical: newScenes.slice(1).map((scene) => scene.dateFrom), + }; + + collections.scenes.orbits = newScenes; + return collections; +} + +function updateOutputMetadata(scenes, inputMetadata, outputMetadata) { + outputMetadata.userData = metadata; +} + +function sum(array) { + let sum = 0; + for (let i = array.length; i--; ) { + sum += array[i]; + } + return sum; +} + +function mean(array) { + return sum(array) / array.length; +} + +function std(array, mean) { + let sum = 0; + for (let i = 0; i < array.length; i++) { + sum += Math.pow(array[i] - mean, 2); + } + return Math.sqrt(sum / array.length); +} + +function evaluatePixel(samples) { + const values = []; + for (let i = samples.length; i--; ) { + if (samples[i].dataMask) { + values.push(samples[i][band]); + } + } + if (values.length === 0) return [NODATA]; + const valsMean = mean(values); + const valsStd = std(values, valsMean); + const anomaly = samples[0][band] - valsMean; + return [anomaly / valsStd]; +} diff --git a/planetary-variables/soil-water-content/soil-water-content-anomaly/script.js b/planetary-variables/soil-water-content/soil-water-content-anomaly/script.js new file mode 100644 index 00000000..75e803b9 --- /dev/null +++ b/planetary-variables/soil-water-content/soil-water-content-anomaly/script.js @@ -0,0 +1,129 @@ +// Visualization +const vmin = -2; +const vmax = 2; +// tolerance in either direction, so i.e. +- 1 days +const toleranceDays = 1; + +const band = "SWC"; +const NODATA = NaN; + +function setup() { + return { + input: [band, "dataMask"], + output: { bands: 4 }, + mosaicking: "ORBIT", + }; +} + +const msInDay = 24 * 60 * 60 * 1000; +const msInYear = 365.25 * msInDay; +const msInHalfYear = msInYear / 2; +const toleranceMs = toleranceDays * msInDay; + +function updateColormap(vmin, vmax) { + const numIntervals = cmap.length; + const intervalLength = (vmax - vmin) / (numIntervals - 1); + for (let i = 0; i < numIntervals; i++) { + cmap[i][0] = vmin + intervalLength * i; + } +} + +const cmap = [ + [-3, 0x67001f], + [-2, 0xb2182b], + [-1, 0xd6604d], + [-0.5, 0xf4a582], + [-0.25, 0xfddbc7], + [0, 0xf7f7f7], + [0.25, 0xd1e5f0], + [0.5, 0x92c5de], + [1, 0x4393c3], + [2, 0x2166ac], + [3, 0x053061], +]; + +updateColormap(vmin, vmax); +const visualizer = new ColorRampVisualizer(cmap); + +var metadata = undefined; + +function relDiff(a, b) { + const diff = Math.abs(a - b); + return diff > msInHalfYear ? msInYear - diff : diff; +} + +function datetimeToYearEpoch(date) { + return date - new Date(Date.UTC(date.getUTCFullYear(), 0, 1)); +} + +function sortDatesDescending(d1, d2) { + const date1 = new Date(d1.dateFrom); + const date2 = new Date(d2.dateFrom); + return date2 - date1; +} + +function preProcessScenes(collections) { + // sort + let scenes = collections.scenes.orbits; + scenes = scenes.sort(sortDatesDescending); + let newScenes = []; + // convert first scene to day of year + const observed = new Date(scenes[0].dateFrom); + const obsMs = datetimeToYearEpoch(observed); + for (let i = 0; i < scenes.length; i++) { + let currentDate = new Date(scenes[i].dateFrom); + let sceneMs = datetimeToYearEpoch(currentDate); + let dt = relDiff(obsMs, sceneMs); + if (dt <= toleranceMs) { + newScenes.push(scenes[i]); + } + } + + metadata = { + observed: observed.toISOString(), + historical: newScenes.slice(1).map((scene) => scene.dateFrom), + }; + + collections.scenes.orbits = newScenes; + return collections; +} + +function updateOutputMetadata(scenes, inputMetadata, outputMetadata) { + outputMetadata.userData = metadata; +} + +function sum(array) { + let sum = 0; + for (let i = array.length; i--; ) { + sum += array[i]; + } + return sum; +} + +function mean(array) { + return sum(array) / array.length; +} + +function std(array, mean) { + let sum = 0; + for (let i = 0; i < array.length; i++) { + sum += Math.pow(array[i] - mean, 2); + } + return Math.sqrt(sum / array.length); +} + +function evaluatePixel(samples) { + const values = []; + for (let i = samples.length; i--; ) { + if (samples[i].dataMask) { + values.push(samples[i][band]); + } + } + if (values.length === 0) return [0, 0, 0, 0]; + const valsMean = mean(values); + const valsStd = std(values, valsMean); + const anomaly = samples[0][band] - valsMean; + const val = anomaly / valsStd; + let imgVals = visualizer.process(val); + return [...imgVals, samples[0].dataMask]; +} diff --git a/sentinel-2/vegetation_condition_index/script.js b/sentinel-2/vegetation_condition_index/script.js index a27c65a1..c35b9d01 100755 --- a/sentinel-2/vegetation_condition_index/script.js +++ b/sentinel-2/vegetation_condition_index/script.js @@ -1,104 +1,105 @@ //VERSION=3 function setup() { - return { - input: ["B04", "B08", "CLM", "dataMask"], - output: { bands: 1 }, - mosaicking: "ORBIT" - }; -} - -const NODATA = -32768; - -// tolerance in either direction, so i.e. +- 5 days -const toleranceDays = 5; -const msInDay = 24 * 60 * 60 * 1000; -const msInYear = 365.25 * msInDay; -const msInHalfYear = msInYear / 2 -const toleranceMs = toleranceDays * msInDay; - -var metadata = undefined; - -function relDiff(a, b) { - const diff = Math.abs(a - b); - return diff > msInHalfYear ? msInYear - diff : diff; -} - -function datetimeToYearEpoch(date) { - return date - new Date(Date.UTC(date.getUTCFullYear(), 0, 1)) -} - -function sortDatesDescending(d1, d2) { - const date1 = new Date(d1.dateFrom); - const date2 = new Date(d2.dateFrom); - return date2 - date1 -} - -function preProcessScenes(collections) { - // sort - let scenes = collections.scenes.orbits; - scenes = scenes.sort(sortDatesDescending); - let newScenes = []; - // convert first scene to day of year - const observed = new Date(scenes[0].dateFrom); - const obsMs = datetimeToYearEpoch(observed) - for (let i = 0; i < scenes.length; i++) { - let currentDate = new Date(scenes[i].dateFrom); - let sceneMs = datetimeToYearEpoch(currentDate) - let dt = relDiff(obsMs, sceneMs) - if (dt <= toleranceMs) { - newScenes.push(scenes[i]); + return { + input: ["B04", "B08", "CLM", "dataMask"], + output: { bands: 1 }, + mosaicking: "ORBIT" + }; + } + + const NODATA = -32768; + + // tolerance in either direction, so i.e. +- 5 days + const toleranceDays = 5; + const msInDay = 24 * 60 * 60 * 1000; + const msInYear = 365.25 * msInDay; + const msInHalfYear = msInYear / 2 + const toleranceMs = toleranceDays * msInDay; + + var metadata = undefined; + + function relDiff(a, b) { + const diff = Math.abs(a - b); + return diff > msInHalfYear ? msInYear - diff : diff; + } + + function datetimeToYearEpoch(date) { + return date - new Date(Date.UTC(date.getUTCFullYear(), 0, 1)) + } + + function sortDatesDescending(d1, d2) { + const date1 = new Date(d1.dateFrom); + const date2 = new Date(d2.dateFrom); + return date2 - date1 + } + + function preProcessScenes(collections) { + // sort + let scenes = collections.scenes.orbits; + scenes = scenes.sort(sortDatesDescending); + let newScenes = []; + // convert first scene to day of year + const observed = new Date(scenes[0].dateFrom); + const obsMs = datetimeToYearEpoch(observed) + for (let i = 0; i < scenes.length; i++) { + let currentDate = new Date(scenes[i].dateFrom); + let sceneMs = datetimeToYearEpoch(currentDate) + let dt = relDiff(obsMs, sceneMs) + if (dt <= toleranceMs) { + newScenes.push(scenes[i]); + } } + + metadata = { + observed: observed.toISOString(), + historical: newScenes.slice(1).map(scene => scene.dateFrom) + } + + collections.scenes.orbits = newScenes; + return collections; + } + + + function updateOutputMetadata(scenes, inputMetadata, outputMetadata) { + outputMetadata.userData = metadata; } - - metadata = { - observed: observed.toISOString(), - historical: newScenes.slice(1).map(scene => scene.dateFrom) + + function calcNDVI(sample) { + return index(sample.B08, sample.B04); } - - collections.scenes.orbits = newScenes; - return collections; -} - - -function updateOutputMetadata(scenes, inputMetadata, outputMetadata) { - outputMetadata.userData = metadata; -} - -function calcNDVI(sample) { - return index(sample.B08, sample.B04); -} - -function calcMaxMin(samples) { - let ndvi = calcNDVI(samples[0]) - let max = ndvi; - let min = ndvi; - for (let i = 1; i < samples.length; ++i) { - ndvi = calcNDVI(samples[i]) - if (ndvi > max) { - max = ndvi; - } else if (ndvi < min) { - min = ndvi; + + function calcMaxMin(samples) { + let ndvi = calcNDVI(samples[0]) + let max = ndvi; + let min = ndvi; + for (let i = 1; i < samples.length; ++i) { + ndvi = calcNDVI(samples[i]) + if (ndvi > max) { + max = ndvi; + } else if (ndvi < min) { + min = ndvi; + } } + return [max, min] } - return [max, min] -} - -function isClear(sample) { - return sample.CLM == 0 && sample.dataMask == 1; -} - -function evaluatePixel(samples) { - // if the first value isn't clear, stop - if (!isClear(samples[0])) { - return [NODATA] + + function isClear(sample) { + return sample.CLM == 0 && sample.dataMask == 1; } - const clearTs = samples.filter(isClear) - const observed = index(clearTs[0].B08, clearTs[0].B04); - let max = NODATA, min = NODATA, vci = NODATA; - if (clearTs.length > 0) { - [max, min] = calcMaxMin(clearTs); - vci = (observed - min) / (max - min) + + function evaluatePixel(samples) { + // if the first value isn't clear, stop + if (!isClear(samples[0])) { + return [NODATA] + } + const clearTs = samples.filter(isClear) + const observed = index(clearTs[0].B08, clearTs[0].B04); + let max = NODATA, min = NODATA, vci = NODATA; + if (clearTs.length > 0) { + [max, min] = calcMaxMin(clearTs); + vci = (observed - min) / (max - min) + } + + return [vci]; } - - return [vci]; -} + \ No newline at end of file