//VERSION=3functionsetup(){return{input:["B04","B08","CLM","dataMask"],output:{bands:1},mosaicking:"ORBIT"};}constNODATA=-32768;// tolerance in either direction, so i.e. +- 5 daysconsttoleranceDays=5;constmsInDay=24*60*60*1000;constmsInYear=365.25*msInDay;constmsInHalfYear=msInYear/2consttoleranceMs=toleranceDays*msInDay;varmetadata=undefined;functionrelDiff(a,b){constdiff=Math.abs(a-b);returndiff>msInHalfYear?msInYear-diff:diff;}functiondatetimeToYearEpoch(date){returndate-newDate(Date.UTC(date.getUTCFullYear(),0,1))}functionsortDatesDescending(d1,d2){constdate1=newDate(d1.dateFrom);constdate2=newDate(d2.dateFrom);returndate2-date1}functionpreProcessScenes(collections){// sortletscenes=collections.scenes.orbits;scenes=scenes.sort(sortDatesDescending);letnewScenes=[];// convert first scene to day of yearconstobserved=newDate(scenes[0].dateFrom);constobsMs=datetimeToYearEpoch(observed)for(leti=0;i<scenes.length;i++){letcurrentDate=newDate(scenes[i].dateFrom);letsceneMs=datetimeToYearEpoch(currentDate)letdt=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;returncollections;}functionupdateOutputMetadata(scenes,inputMetadata,outputMetadata){outputMetadata.userData=metadata;}functioncalcNDVI(sample){returnindex(sample.B08,sample.B04);}functioncalcMaxMin(samples){letndvi=calcNDVI(samples[0])letmax=ndvi;letmin=ndvi;for(leti=1;i<samples.length;++i){ndvi=calcNDVI(samples[i])if(ndvi>max){max=ndvi;}elseif(ndvi<min){min=ndvi;}}return[max,min]}functionisClear(sample){returnsample.CLM==0&&sample.dataMask==1;}functionevaluatePixel(samples){// if the first value isn't clear, stopif(!isClear(samples[0])){return[NODATA]}constclearTs=samples.filter(isClear)constobserved=index(clearTs[0].B08,clearTs[0].B04);letmax=NODATA,min=NODATA,vci=NODATA;if(clearTs.length>0){[max,min]=calcMaxMin(clearTs);vci=(observed-min)/(max-min)}return[vci];}
Please note that in case of Sentinel 2, only a few years of history are available.
The script takes the newest (latest) available scene as the observed one – thus, the observed date can be chosen in the Sentinel Hub Playground GUI (or, in case of API request, via the dataFilter.timeRange.to field). Then, for each previous year the script finds all values within toleranceDays of the most recent date.
Because of the multi-temporal nature of this index, be sure to use it in the temporal version of Sentinel Hub Playground with “Enable temporal data” checked in the “Effects” tab, or in case of API request, set the dataFilter.timeRange.to field far enough back to include all available history.
The actual scenes (dates) used can be returned as meta-data with an API requests by replacing the responses part of the request with: