//VERSION=3// Calculate number of bands needed for all intervals// Initialize dates and intervalvarstart_date=newDate("2020-01-01");varend_date=newDate("2021-01-01");varsampled_dates=splitDateIntoEqualIntervals(start_date,end_date,36,1000*60*60*24).map(d=>withoutTime(d));// Defaultsvardefault_band_values={'no_data':0,'min_value':1,// meant for UINT16 and UINT8 minimal value'interp_failed':10000};vardefault_mask_values={'no_data':0,'interp_passed':1,'interp_raised':2,'interp_failed':3};varbands_list=["B01","B02","B03","B04","B05","B06","B07","B08","B8A","B09","B11","B12"];varqmask="QM";varinput_bands=bands_list.concat(["CLP","CLM","dataMask"]);varoutput_bands=bands_list.concat([qmask]);varbands_array_size=9;varbands_array_n=Math.ceil(sampled_dates.length/bands_array_size);varoutput_setup=[];for(vari=0;i<output_bands.length;i++){for(varj=0;j<bands_array_n;j++){varcol_name=output_bands[i]+"_"+(j).toString();varsample_type=col_name.includes(qmask)?SampleType.UINT8:SampleType.UINT16;output_setup.push({id:col_name,bands:bands_array_size,sampleType:sample_type});}}varsh_dates=[];varsum_valid=[];functioninterval_search(x,arr){letstart_idx=0,end_idx=arr.length-2;// Iterate while start not meets endwhile(start_idx<=end_idx){// Find the mid indexletmid_idx=(start_idx+end_idx)>>1;// If element is present at mid, return Trueif(arr[mid_idx]<=x&&x<arr[mid_idx+1]){returnmid_idx;}// Else look in left or right half accordinglyelseif(arr[mid_idx+1]<=x)start_idx=mid_idx+1;elseend_idx=mid_idx-1;}if(x==arr[arr.length-1]){returnarr.length-2;}returnundefined;}functionlinearInterpolation(x,x0,y0,x1,y1){if(x<x0||x>x1){returndefault_band_values['interp_failed'];}vara=(y1-y0)/(x1-x0);varb=-a*x0+y0;returna*x+b;}functionlininterp(x_arr,xp_arr,fp_arr){results=[];quality_mask=[];for(vari=0;i<x_arr.length;i++){varx=x_arr[i];varinterval=interval_search(x,xp_arr);if(typeofinterval=="undefined"){quality_mask.push(default_mask_values['interp_failed']);results.push(default_band_values['interp_failed']);continue;}varinterp_val=linearInterpolation(x,xp_arr[interval],fp_arr[interval],xp_arr[interval+1],fp_arr[interval+1]);if(interp_val<default_band_values['min_value']){quality_mask.push(default_mask_values['interp_raised']);results.push(default_band_values['min_value']);continue;}quality_mask.push(default_mask_values['interp_passed']);results.push(interp_val);}return[results,quality_mask];}functionsplitDateIntoEqualIntervals(startDate,endDate,numberOfIntervals,roundCoefficient){letdiff=endDate.getTime()-startDate.getTime();letintervalLength=diff/numberOfIntervals;letintervals=[];for(leti=0;i<numberOfIntervals;i++){letndate=newDate(startDate.getTime()+i*intervalLength);ndate=newDate(Math.round(ndate.getTime()/roundCoefficient)*roundCoefficient);ndate=newDate(ndate.getTime()+ndate.getTimezoneOffset()*60000);intervals.push(ndate);}returnintervals;}functionis_valid(smp){// Check if the sample is valid (i.e. contains no clouds or snow)letclm=smp.CLM;letclp=smp.CLP;letdm=smp.dataMask;if(clm!=0||clp/255>0.3||dm!=1){returnfalse;}returntrue;}functionis_valid_thr(smp,thr){// Check if the sample is valid (i.e. contains no clouds or snow)letclp=smp.CLP;letdm=smp.dataMask;if(clp/255>thr||dm!=1){returnfalse;}returntrue;}functionwithoutTime(intime){// Return date without timeintime.setHours(0,0,0,0);returnintime;}// Sentinel Hub functionsfunctionsetup(){// Setup input/output parametersreturn{input:[{bands:input_bands,units:"DN"}],output:output_setup,mosaicking:"ORBIT"}}// Evaluate pixels in the bandsfunctionevaluatePixel(samples,scenes){// Initialise arraysvarvalid_indices=[];varvalid_dates=[];// Loop over samples, get valid datesvaralways_no_data=true;for(vari=0;i<samples.length;i++){if(samples[i].dataMask!=0){always_no_data=false;}if(is_valid(samples[i])){valid_indices.push(i);valid_dates.push(withoutTime(newDate(scenes[i].date)));}}// Force at least 2 valid dates and valid dates before and after first and last sampled datesvarclp_thr=0.3;while(valid_indices.length<2||valid_dates[0]>sampled_dates[0]||valid_dates[valid_dates.length-1]<sampled_dates[sampled_dates.length-1]){valid_dates=[];valid_indices=[];for(vari=0;i<samples.length;i++){if(is_valid_thr(samples[i],clp_thr)){valid_indices.push(i);valid_dates.push(scenes[i].date);}}clp_thr+=0.05;if(clp_thr>1){break;}}// Fill datavarvalid_samples={};for(vari=0;i<output_bands.length-1;i++){varband=output_bands[i];valid_samples[band]=[];for(varj=0;j<valid_indices.length;j++){varv_id=valid_indices[j];valid_samples[band].push(samples[v_id][band]);}}// Interpolatevarinterpolated_output={};for(vari=0;i<output_bands.length-1;i++){varband=output_bands[i];var[band_data,mask_data]=lininterp(sampled_dates,valid_dates,valid_samples[band]);if(i>0){varold_mask_data=interpolated_output[qmask];for(varj=0;j<old_mask_data.length;j++){if(old_mask_data[j]>mask_data[j]){mask_data[j]=old_mask_data[j];}}}interpolated_output[band]=band_data;interpolated_output[qmask]=mask_data;}// Reset values in qm if no data availableif(always_no_data){for(vari=0;i<output_bands.length-1;i++){varband=output_bands[i];interpolated_output[band]=newArray(interpolated_output[band].length).fill(default_band_values['no_data']);}interpolated_output[qmask]=newArray(interpolated_output[qmask].length).fill(default_mask_values['no_data']);}// Return all arraysvarchunked_output={}for(varkeyininterpolated_output){for(varj=0;j<interpolated_output[key].length;j++){col_name=key+"_"+parseInt(j/bands_array_size).toString();if(!(col_nameinchunked_output)){chunked_output[col_name]=[];}chunked_output[col_name].push(interpolated_output[key][j]);}}returnchunked_output;}
This evalscript returns a temporally-interpolated stack of band or band indices values.
Warning: In this evalscript the time_interval must be defined both in the request as well as in the evalscript itself.
General description
From the provided time interval and the frequency, a new list of dates is determined on which the temporally interpolated values are estimated.
The evalscript applies cloud masking from the service. If there are not enough valid data poins to fill the time interval, the CLP (cloud probability) threshold is dynamically loosened in order to bring more points in.
More information about the general use of a similar script on this blog post.
Example request usage
request=SentinelHubRequest(evalscript=evalscript,input_data=[SentinelHubRequest.input_data(data_collection=DataCollection.SENTINEL2_L2A,time_interval=time_interval,# must be same as in the evalscript
mosaicking_order='leastRecent',# order is important
other_args={'dataFilter':{'previewMode':'DETAIL'}}# because of `CLM` and `CLP`
)],responses=[SentinelHubRequest.output_response('B01',MimeType.TIFF),SentinelHubRequest.output_response('B02',MimeType.TIFF),SentinelHubRequest.output_response('B03',MimeType.TIFF),SentinelHubRequest.output_response('B04',MimeType.TIFF),SentinelHubRequest.output_response('B05',MimeType.TIFF),SentinelHubRequest.output_response('B06',MimeType.TIFF),SentinelHubRequest.output_response('B07',MimeType.TIFF),SentinelHubRequest.output_response('B08',MimeType.TIFF),SentinelHubRequest.output_response('B8A',MimeType.TIFF),SentinelHubRequest.output_response('B09',MimeType.TIFF),SentinelHubRequest.output_response('B11',MimeType.TIFF),SentinelHubRequest.output_response('B12',MimeType.TIFF),SentinelHubRequest.output_response('QM',MimeType.TIFF),],geometry=geometry,size=bbox_to_dimensions(bbox,resolution)config=config)