Sentinel-2 Global Mosaic best pixel selection script

//VERSION=3 (auto-converted from 2)
//NOTE: This Custom script requires Sentinel Hub API v.2 to operate properly. It is however possible to use some parts of it already now.

function setup() {
  return {
    input: [{
      bands: [
          "B01",
          "B02",
          "B03",
          "B04",
          "B05",
          "B06",
          "B07",
          "B08",
          "B8A",
          "B11",
          "B12",
          "AOT",
          "CLD",
          "SNW",
          "SCL",
          "viewZenithMean",
          "viewAzimuthMean",
          "sunZenithAngles",
          "sunAzimuthAngles"
      ]
    }],
    output: [
        {
          id: "quality_aot",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B11",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B01",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B12",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "quality_cloud_confidence",
          sampleType: "UINT8",
          bands: 1
        },
        {
          id: "B02",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "valid_obs",
          sampleType: "UINT8",
          bands: 1
        },
        {
          id: "B03",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B04",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B05",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B06",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B07",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "view_azimuth_mean",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B08",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "sun_zenith",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "B8A",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "source_index",
          sampleType: "INT16",
          bands: 1
        },
        {
          id: "view_zenith_mean",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "sun_azimuth",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "quality_snow_confidence",
          sampleType: "UINT8",
          bands: 1
        },
        {
          id: "medoid_mos",
          sampleType: "UINT16",
          bands: 1
        },
        {
          id: "quality_scene_classification",
          sampleType: "UINT8",
          bands: 1
        }
    ],
    mosaicking: "TILE"
  }
}


function evaluatePixel(samples, scenes) {
    var filteredSamples = filterByOrbitId(samples, scenes);
    var best = selectRepresentativeSample(filteredSamples);
    if (best === undefined) {
        return {
            B01: [0], B02: [0], B03: [0],
            B04: [0], B05: [0], B06: [0],
            B07: [0], B08: [0], B8A: [0],
            B11: [0], B12: [0],
            source_index: [65535],
            quality_aot: [0],
            quality_cloud_confidence: [0],
            quality_snow_confidence:  [0],
            quality_scene_classification: [0],
            view_zenith_mean: [32768],
            view_azimuth_mean: [32768],
            sun_zenith: [32768],
            sun_azimuth: [32768],
            medoid_mos: [65535],
            valid_obs: [0]
        };
    } else {
        var bestSample = best.sample;
        var mos;
        var sampleIndex = samples.indexOf(bestSample);
        if (isNaN(best.mos)) {
            mos = 65535;
        } else {
            mos = best.mos * 10000;
        }
        return {
            B01: [bestSample.B01 * 10000],
            B02: [bestSample.B02 * 10000],
            B03: [bestSample.B03 * 10000],
            B04: [bestSample.B04 * 10000],
            B05: [bestSample.B05 * 10000],
            B06: [bestSample.B06 * 10000],
            B07: [bestSample.B07 * 10000],
            B08: [bestSample.B08 * 10000],
            B8A: [bestSample.B8A * 10000],
            B11: [bestSample.B11 * 10000],
            B12: [bestSample.B12 * 10000],
            source_index: [sampleIndex],
            quality_aot: [bestSample.AOT * 10000],
            quality_cloud_confidence: [bestSample.CLD],
            quality_snow_confidence: [bestSample.SNW],
            quality_scene_classification: [bestSample.SCL],
            view_zenith_mean: [bestSample.viewZenithMean * 100],
            view_azimuth_mean: [bestSample.viewAzimuthMean * 100],
            sun_zenith: [bestSample.sunZenithAngles * 100],
            sun_azimuth: [bestSample.sunAzimuthAngles * 100],
            medoid_mos: [mos],
            valid_obs: [best.valid_obs]
        };
    }
}

// Utils
function toUInt16(value) {
    return Math.max(0, Math.min(value * 10000, 65535));
}

function filterByOrbitId(samples, scenes) {
    var orbitId = -1;
    
    return samples
        .map(function (sample, i){
                return {s: sample, orbitId: scenes[i].orbitId, tileId: scenes[i].tileId};
            })
        .filter(e => e.s.SCL > 0)
        .sort(function(a, b) {
                if (a.orbitId < b.orbitId) return 1;
                if (a.orbitId > b.orbitId) return -1;

                if (a.tileId < b.tileId) return 1;
                if (a.tileId > b.tileId) return -1;
                return 0;
            })
        .filter(function(e) {
            if (e.orbitId !== orbitId) {
                orbitId = e.orbitId;
                return true;
            } else {
                return false;
            }
            })
        .map(e => e.s);
}

// Mosaic
const minSamplesForMedoid = 4;

function selectRepresentativeSample(samples) {
    var n = samples.length;
    var validSamples = samples.filter(validate);
    var validSamplesNum = validSamples.length;

    if (validSamplesNum == 0) {
        return undefined;
    }

    if (validSamplesNum == 1) {
        return {sample: validSamples[0], mos: NaN, valid_obs: 1};
    }

    if (validSamplesNum >= minSamplesForMedoid) {
        return performMedoid(validSamples);
    } else {
        return performStc(validSamples);
    }
}

function performMedoid(samples) {
    var medoid = computeMedoidIndex(samples);
    return {sample: samples[medoid.index], mos: medoid.spread, valid_obs: samples.length};
}

function performStc(samples) {
    var bestSample = samples[0];
    for (var i = 1; i < samples.length; i++) {
        bestSample = computeStc(samples[i], bestSample);
    }
    return {sample: bestSample, mos: NaN, valid_obs: samples.length};
}

// Validate
function validate(sample) {
    return validateSCL(sample.SCL) && validateViewZenithMean(sample.viewZenithMean);
}

function validateSCL(scl) {
    return scl == 2 || scl == 4 || scl == 5 || scl == 6 || scl == 11;
}

function validateViewZenithMean(vzm) {
    return vzm < 11;
}

function validateSamples(samples) {
    return samples.every(validateSample);
}

function validateSample(sample) {
    return !isNaN(sample) && isFinite(sample);
}

// STC
function computeNdvi(sample) {
    return (sample.B08 - sample.B04) / (sample.B08 + sample.B04);
}

function computeVisualBandsSum(sample) {
    return sample.B02 + sample.B03 + sample.B04;
}

function computeSWIRMean(sample) {
    return (sample.B11 + sample.B12) / 2;
}

function computeNdwi(sample) {
    return (sample.B03 - sample.B08) / (sample.B03 + sample.B08);
}

function computeStc(sampleA, sampleB) {
    var keySwitch = sampleA.SCL * 100 + sampleB.SCL;
    switch (keySwitch) {
        //Vegetation
        case 404:
            var ndviSampleA = computeNdvi(sampleA);
            var ndviSampleB = computeNdvi(sampleB);

            if (ndviSampleA > ndviSampleB && sampleA.CLD <= sampleB.CLD) {
                return sampleA;
            } else {
                if (ndviSampleA < ndviSampleB && sampleA.CLD <= sampleB.CLD) {
                    return sampleA;
                } else {
                    return sampleB;
                }
            }
            break;
        case 405:
        case 504:
            if (computeVisualBandsSum(sampleA) < computeVisualBandsSum(sampleB) && sampleA.CLD <= sampleB.CLD) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 400:
        case 401:
        case 402:
        case 403:
        case 406:
        case 407:
        case 408:
        case 409:
        case 410:
        case 411:
            return sampleA;
            break;
        case 4:
        case 104:
        case 204:
        case 304:
        case 604:
        case 704:
        case 804:
        case 904:
        case 1004:
        case 1104:
            return sampleB;
            break;
        //BARE_SOIL_DESERT
        case 505:
            if (computeVisualBandsSum(sampleA) < computeVisualBandsSum(sampleB) && sampleA.CLD <= sampleB.CLD) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 500:
        case 501:
        case 502:
        case 503:
        case 506:
        case 507:
        case 508:
        case 509:
        case 510:
        case 511:
            return sampleA;
            break;
        case 5:
        case 105:
        case 205:
        case 305:
        case 605:
        case 705:
        case 805:
        case 905:
        case 1005:
        case 1105:
            return sampleB;
            break;
        //SNOW_ICE
        case 1111:
            if (computeVisualBandsSum(sampleA) > computeVisualBandsSum(sampleB) && sampleA.CLD <= sampleB.CLD) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 1100:
        case 1101:
        case 1102:
        case 1103:
        case 1106:
        case 1107:
        case 1108:
        case 1109:
        case 1110:
            return sampleA;
            break;
        case 11:
        case 111:
        case 211:
        case 311:
        case 611:
        case 711:
        case 811:
        case 911:
        case 1011:
            return sampleB;
            break;
        //Water
        case 606:
            if ((computeNdwi(sampleA) > computeNdwi(sampleB) || computeSWIRMean(sampleA) < computeSWIRMean(sampleB)) &&
                    sampleA.CLD <= sampleB.CLD) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 600:
        case 601:
        case 602:
        case 603:
        case 607:
        case 608:
        case 609:
        case 610:
            return sampleA;
            break;
        case 6:
        case 106:
        case 206:
        case 306:
        case 706:
        case 806:
        case 906:
        case 1006:
            return sampleB;
            break;
        //DARK_FEATURE_SHADOW
        case 202:
            if (computeVisualBandsSum(sampleA) > computeVisualBandsSum(sampleB) && sampleA.CLD <= sampleB.CLD) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 200:
        case 201:
        case 203:
        case 207:
        case 208:
        case 209:
        case 210:
            return sampleA;
            break;
        case 2:
        case 102:
        case 302:
        case 702:
        case 802:
        case 902:
        case 1002:
            return sampleB;
            break;
        //CLOUD_SHADOW
        case 303:
            if (computeVisualBandsSum(sampleA) > computeVisualBandsSum(sampleB) && sampleA.CLD <= sampleB.CLD) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 300:
        case 301:
        case 307:
        case 308:
        case 309:
        case 310:
            return sampleA;
            break;
        case 3:
        case 103:
        case 703:
        case 803:
        case 903:
        case 1003:
            return sampleB;
            break;
        //CLOUD_LOW_PROBA
        case 707:
            if (computeVisualBandsSum(sampleA) < computeVisualBandsSum(sampleB)) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 700:
        case 701:
        case 708:
        case 709:
        case 710:
            return sampleA;
            break;
        case 7:
        case 107:
        case 807:
        case 907:
        case 1007:
            return sampleB;
            break;
        //THIN_CIRRUS
        case 1010:
            if (computeVisualBandsSum(sampleA) < computeVisualBandsSum(sampleB)) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 1000:
        case 1001:
        case 1008:
        case 1009:
            return sampleA;
            break;
        case 10:
        case 110:
        case 810:
        case 910:
            return sampleB;
            break;
        //CLOUD_MEDIUM_PROBA
        case 808:
            if (computeVisualBandsSum(sampleA) < computeVisualBandsSum(sampleB)) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 800:
        case 801:
        case 809:
            return sampleA;
            break;
        case 8:
        case 108:
        case 908:
            return sampleB;
            break;
        //CLOUD_HIGH_PROBA
        case 909:
            if (computeVisualBandsSum(sampleA) < computeVisualBandsSum(sampleB)) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 900:
        case 901:
            return sampleA;
            break;
        case 9:
        case 109:
            return sampleB;
            break;
        //SATURATED_DEFECTIVE
        case 101:
            if (computeVisualBandsSum(sampleA) < computeVisualBandsSum(sampleB)) {
                return sampleA;
            } else {
                return sampleB;
            }
            break;
        case 100:
            return sampleA;
            break;
        case 1:
            return sampleB;
            break;
        default:
            return undefined;
    }
}

// Medoid
function distance(a, b) {
    var ret = 0;
    ret += Math.pow(a.B02-b.B02, 2);
    ret += Math.pow(a.B03-b.B03, 2);
    ret += Math.pow(a.B04-b.B04, 2);
    ret += Math.pow(a.B06-b.B06, 2);
    ret += Math.pow(a.B08-b.B08, 2);
    ret += Math.pow(a.B11-b.B11, 2);
    ret += Math.pow(a.B12-b.B12, 2);
    return Math.sqrt(ret);
}

function computeMedoidIndex(samples) {
    var n = samples.length;
    var d = createDistanceMatrix(samples);
    
    var distanceRow;
    var distanceSum;
    var distanceSumMin = Number.POSITIVE_INFINITY;
    var medoidIndex = -1;
    for (var j = 0; j < n; j++) {
        distanceRow = d[j];
        distanceSum = 0.0;
        for (var i = 0; i < n; i++) {
            distanceSum += distanceRow[i];
        }
        if (distanceSum < distanceSumMin) {
            distanceSumMin = distanceSum;
            medoidIndex = j;
        }
    }
    return {index: medoidIndex, spread: distanceSumMin / n};
}

function createDistanceMatrix(samples) {
    var n = samples.length;
    var d = createArray(n, n);
    for (var i = 0; i < n; i++) {
        var a = samples[i];
        for (var j = i + 1; j < n; j++) {
            var b = samples[j]
            d[i][j] = d[j][i] = distance(a, b);
        }
        d[i][i] = 0;
    }
    return d;
}

function createArray(length) {
    var arr = new Array(length || 0),
        i = length;

    if (arguments.length > 1) {
        var args = Array.prototype.slice.call(arguments, 1);
        while(i--) arr[length-1 - i] = createArray.apply(this, args);
    }

    return arr;
}

General description

Sentinel-2 Global Mosaic script is used within S2GM project to select best pixel within the chosen temporal period (10-daily, monthly, quarterly, annual).

Script requires Sentinel Hub API v.2 to run as a whole due to multi-part result. Parts of it can however be used at this point already.