|
| 1 | +//VERSION=3 |
| 2 | + |
| 3 | +// Calculate number of bands needed for all intervals |
| 4 | +// Initialize dates and interval |
| 5 | +// Beware: in JS months are 0 indexed |
| 6 | +var start_date = new Date(2020, 6, 1, 0, 0, 0); |
| 7 | +var end_date = new Date(2020, 6, 30, 0, 0, 0); |
| 8 | +var sampled_dates = sample_timestamps(start_date, end_date, 7, 'day').map(d => withoutTime(d)); |
| 9 | +var nb_bands = sampled_dates.length; |
| 10 | +var n_valid = 0; |
| 11 | +var n_all = 0; |
| 12 | + |
| 13 | +function interval_search(x, arr) { |
| 14 | + let start_idx = 0, end_idx = arr.length - 2; |
| 15 | + |
| 16 | + // Iterate while start not meets end |
| 17 | + while (start_idx <= end_idx) { |
| 18 | + // Find the mid index |
| 19 | + let mid_idx = (start_idx + end_idx) >> 1; |
| 20 | + |
| 21 | + // If element is present at mid, return True |
| 22 | + if (arr[mid_idx] <= x && x < arr[mid_idx + 1]) { |
| 23 | + return mid_idx; |
| 24 | + } |
| 25 | + // Else look in left or right half accordingly |
| 26 | + else if (arr[mid_idx + 1] <= x) start_idx = mid_idx + 1; |
| 27 | + else end_idx = mid_idx - 1; |
| 28 | + } |
| 29 | + if (x == arr[arr.length-1]){ |
| 30 | + return arr.length-2; |
| 31 | + } |
| 32 | + return undefined; |
| 33 | +} |
| 34 | + |
| 35 | +function linearInterpolation(x, x0, y0, x1, y1, no_data_value=NaN) { |
| 36 | + if (x < x0 || x > x1) { |
| 37 | + return no_data_value; |
| 38 | + } |
| 39 | + var a = (y1 - y0) / (x1 - x0); |
| 40 | + var b = -a * x0 + y0; |
| 41 | + return a * x + b; |
| 42 | +} |
| 43 | + |
| 44 | +function lininterp(x_arr, xp_arr, fp_arr, no_data_value=NaN) { |
| 45 | + results = []; |
| 46 | + data_mask = []; |
| 47 | + xp_arr_idx = 0; |
| 48 | + for (var i=0; i<x_arr.length; i++) { |
| 49 | + var x = x_arr[i]; |
| 50 | + n_all+=1; |
| 51 | + interval = interval_search(x, xp_arr); |
| 52 | + if (interval === undefined) { |
| 53 | + data_mask.push(0); |
| 54 | + results.push(no_data_value); |
| 55 | + continue; |
| 56 | + } |
| 57 | + data_mask.push(1); |
| 58 | + n_valid+=1; |
| 59 | + results.push( |
| 60 | + linearInterpolation( |
| 61 | + x, |
| 62 | + xp_arr[interval], |
| 63 | + fp_arr[interval], |
| 64 | + xp_arr[interval+1], |
| 65 | + fp_arr[interval+1], |
| 66 | + no_data_value |
| 67 | + ) |
| 68 | + ); |
| 69 | + } |
| 70 | + return [results, data_mask]; |
| 71 | +} |
| 72 | + |
| 73 | +function interpolated_index(index_a, index_b) { |
| 74 | + // Calculates the index for all bands in array |
| 75 | + var index_data = []; |
| 76 | + for (var i = 0; i < index_a.length; i++){ |
| 77 | + // UINT index returned |
| 78 | + let ind = (index_a[i] - index_b[i]) / (index_a[i] + index_b[i]); |
| 79 | + index_data.push(ind * 10000 + 10000); |
| 80 | + } |
| 81 | + return index_data |
| 82 | +} |
| 83 | + |
| 84 | +function increase(original_date, period, period_unit) { |
| 85 | + date = new Date(original_date) |
| 86 | + switch (period_unit) { |
| 87 | + case 'millisecond': |
| 88 | + return new Date(date.setMilliseconds(date.getMilliseconds()+period)); |
| 89 | + case 'second': |
| 90 | + return new Date(date.setSeconds(date.getSeconds()+period)); |
| 91 | + case 'minute': |
| 92 | + return new Date(date.setMinutes(date.getMinutes()+period)); |
| 93 | + case 'hour': |
| 94 | + return new Date(date.setHours(date.getHours()+period)); |
| 95 | + case 'day': |
| 96 | + return new Date(date.setDate(date.getDate()+period)); |
| 97 | + case 'month': |
| 98 | + return new Date(date.setMonth(date.getMonth()+period)); |
| 99 | + default: |
| 100 | + return undefined |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +function sample_timestamps(start, end, period, period_unit) { |
| 105 | + var cDate = new Date(start); |
| 106 | + var sampled_dates = [] |
| 107 | + while (cDate < end) { |
| 108 | + sampled_dates.push(cDate); |
| 109 | + cDate = increase(cDate, period, period_unit); |
| 110 | + } |
| 111 | + return sampled_dates; |
| 112 | +} |
| 113 | + |
| 114 | +function is_valid(smp) { |
| 115 | + // Check if the sample is valid (i.e. contains no clouds or snow) |
| 116 | + let scl = smp.SCL; |
| 117 | + let dm = smp.dataMask; |
| 118 | + const clouds = [0, 3, 7, 8, 9, 10]; |
| 119 | + |
| 120 | + if (clouds.includes(scl)) { |
| 121 | + return false; |
| 122 | + } |
| 123 | + if (dm !=1 ) { |
| 124 | + return false; |
| 125 | + } |
| 126 | + return true; |
| 127 | +} |
| 128 | + |
| 129 | +function withoutTime(intime) { |
| 130 | + // Return date without time |
| 131 | + intime.setHours(0, 0, 0, 0); |
| 132 | + return intime; |
| 133 | +} |
| 134 | + |
| 135 | +// Sentinel Hub functions |
| 136 | +function setup() { |
| 137 | + // Setup input/output parameters |
| 138 | + return { |
| 139 | + input: [{ |
| 140 | + bands: ["B04", "B08", "SCL", "dataMask"], |
| 141 | + units: "DN" |
| 142 | + }], |
| 143 | + output: [ |
| 144 | + {id: "NDVI", bands: nb_bands, sampleType: SampleType.UINT16}, |
| 145 | + {id: "data_mask", bands: nb_bands, sampleType: SampleType.UINT8} |
| 146 | + ], |
| 147 | + mosaicking: "ORBIT" |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +// Evaluate pixels in the bands |
| 152 | +function evaluatePixel(samples, scenes) { |
| 153 | + |
| 154 | + // Initialise arrays |
| 155 | + var valid_samples = {'B04':[], 'B08':[]}; |
| 156 | + |
| 157 | + var valid_dates = [] |
| 158 | + // Loop over samples. |
| 159 | + for (var i = samples.length-1; i >= 0; i--){ |
| 160 | + if (is_valid(samples[i])) { |
| 161 | + valid_dates.push(withoutTime(new Date(scenes[i].date))); |
| 162 | + valid_samples['B04'].push(samples[i].B04); |
| 163 | + valid_samples['B08'].push(samples[i].B08); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + // Calculate indices and return optimised for UINT16 format (will need unpacking) |
| 168 | + var ndvi = interpolated_index(valid_samples['B08'], valid_samples['B04']) |
| 169 | + |
| 170 | + var [ndvi_interpolated, dm] = lininterp(sampled_dates, valid_dates, ndvi, 0); |
| 171 | + |
| 172 | + // Return all arrays |
| 173 | + return { |
| 174 | + NDVI: ndvi, |
| 175 | + data_mask: dm |
| 176 | + } |
| 177 | +} |
0 commit comments