


//* JavaScript code to be implemented in Google Earth Engine(c) 
// developed by F. C. Conesa and H.A. Orengo
// to accompany the paper: 

// Conesa, F.C.; Orengo, H.A.; Lobo, A. and Petrie, C.A. 2022. 
// 'An algorithm to detect endangered Cultural Heritage by agri-cultural expansion in drylands at a global scale' 
// submitted to Remote Sensing.

//                ------------- ooo -------------

// To execute the algorithm paste this code into Google Earth Engine editor and press "run"

//                ------------- ooo -------------

// For more information on how this code works and how to apply it refer to the text of the article.
// Suggestions and code improvements are welcome! Please, contact:
// Francesc C. Conesa at fconesa@icac.cat
// Hector A. Orengo at horengo@icac.cat

// Notes, please read carefully:

// 1. To apply these mapping to your own areas: 
//    a.  Import your own vector dataset (site table) in KML or shapefile using the NEW Table Upload option
//        in the 'Assets' tab and substitute the asset ID.
//    b.  Eliminate the polygon "AOI" variable and draw your own polygon (as a geometry)
//        delimiting your own Area of Interest using the Geometry Imports menu in the top right
//        corner of the map area below.
// 2. The images resulting from this script can be transferred to your Assets or Google Drive running
//    the analysis from the 'Tasks' tab on the top right of the screen and selecting the Drive option.
// 3. No outputs have been included in the Map Window below since some of the operations can exceed the
//    the computing time allowed by GEE but they can be proceesed as tasks in the GEE server.
// 4. Specific instructions of how the algorithm works have been included within the code, please,
//    read these carefully and consult the paper's text to understand how to best apply it.

//                ------------- ooo ------------- */


// ============================= IMPUT VECTOR DATASET =========================================

// Call asset with your own achaeological vector table
var table = ee.FeatureCollection("projects/ee-francesccecilia/assets/subsetMounds_10");

// Print total number of sites in the input vector database
var tableCount = table.size().getInfo();
print('Input vector table', tableCount + " sites");


// ============================= AREA OF INTEREST AND VISUALISATION PRE-SETTINGS =================================================

// Define a central point in your study area using the Inspector tab (copy/paste X,Y coords, in WGS84 decimal degrees)
// Replace with your own coords here, and scale to center the map in your study area
Map.setCenter(71.8116, 28.9206, 10);
// Default map view is set to satellite
Map.setOptions('SATELLITE');

// Create a polygon geometry delimiting the Region of Interest (AOI)
// By default, the AOI covers the Cholistan Desert
// The user can eliminate this polygon and draw a new AOI using the Geometry Imports tool in the map view
var AOI = ee.Geometry.Polygon(
[[[72.45830862199341, 29.750552355244295],
[71.56292287980591, 29.420943340410837],
[71.01360647355591, 29.167036496936127],
[70.50823537980591, 28.710347184314795],
[69.81335012589966, 28.145148616022762],
[69.81403677140747, 27.806470493744765],
[70.13524030890267, 27.80691077970534],
[70.14361534618399, 27.81924011961056],
[70.19595210629575, 27.865568170389185],
[70.22910634738344, 27.90336008925868],
[70.30078110977706, 27.93859381059637],
[70.36959504400818, 28.010891479325696],
[70.43631096947502, 28.020339734987342],
[70.50166169909733, 28.038239115709796],
[70.55217408774433, 28.026853985776395],
[70.58619708587139, 28.013031425772674],
[70.59686024591633, 27.994368049466157],
[70.67757741784033, 27.92112382776193],
[70.67482388569022, 27.875304760850973],
[70.68504668856156, 27.831124438046768],
[70.73134544986533, 27.760441143885107],
[70.73799881516959, 27.744538198666564],
[70.7611263284465, 27.721341098010463],
[70.78838123782589, 27.716671141396315],
[70.87299856497816, 27.704965787566607],
[70.90542874721541, 27.711473892612446],
[70.96206249621889, 27.73053659203472],
[71.00032589621992, 27.748971687642825],
[71.20075266566528, 27.83535362519099],
[71.38139621599032, 27.872048361937328],
[71.66534371633725, 27.879391737019766],
[71.89597316155891, 27.96174525590272],
[71.93064543920536, 28.12628319926123],
[72.00685697697509, 28.222437609592077],
[72.13421194230591, 28.316959172219075],
[72.20704561235095, 28.39904278337407],
[72.30060300945013, 28.67054467287233],
[72.35306763729557, 28.73223060323902],
[72.40490211802103, 28.78305813071367],
[72.48232155059634, 28.813692704333132],
[72.7344705855187, 28.94884380122061],
[72.94630488645794, 29.02877070048368],
[73.00350515519654, 29.153844875278295],
[73.08520807734783, 29.235790953385802],
[73.10989574272867, 29.283124550218936],
[72.98565237199341, 29.42572796212171],
[72.76592580949341, 29.717162871938747]]]);

// Print AOI area and visualize AOI geometry   
print('AOI area:', AOI.area().divide(1000 * 1000).int(), 'km2'); // casts integer
var AOIVis = {opacity:0.7, color: '21130d'};
Map.addLayer(AOI, AOIVis, "AOI");

// Define a yearly color-based visualisation style for the output raster 
// Default parameters are set for the algorithm visualisation of the last 5 years (2017-2022)
var AgriExpVis = {"opacity":1,"bands":["NDVI_first"],
  "min":2017,"max":2022,
  "palette":["13b133",  // light green, 2017 
              "ece614", // pale yellow, 2018
              "ffcd07", // pale orange, 2919
              "ec6b14", // dark orange, 2020 
              "dc0c10", // red, 2021 
              "8d191c"]}; // dark red, 2022 
              
// User-defined prompts
// NDVI threshold (range 0-1, default is set to 0.4)
var NDVImin = prompt('What is maximum index vegetation threshold? Range values between 0-1', 0.4);
// Buffer distance(default is set to 50 meters)
// Please choose these value taking into account the average size
// and the vector format of your imput table (point or polygon data) 
var DistBuffer = prompt('What is the distance buffer, in meters?', 50);
// Print user-defined values in the console panel
print("Input parameters", 'Maximun NDVI threshold:', NDVImin, 'Distance buffer (in m):', DistBuffer);
  

// ============================= SENTINEL 2 HARMONISED COLLECTION =============================

// Function to mask clouds using the Sentinel-2 QA band 
function maskS2clouds(image) {
  var qa = image.select('QA60');
  var cloudBitMask = 1 << 10;
  var cirrusBitMask = 1 << 11;
  var mask = qa.bitwiseAnd(cloudBitMask).eq(0)
      .and(qa.bitwiseAnd(cirrusBitMask).eq(0));
  return image.updateMask(mask).divide(10000)
        .select("B.*")
      .copyProperties(image, ["system:time_start"]);
}

// Import the image collection, filtered by geometry and cloud-free mask
var S2Hcol = ee.ImageCollection('COPERNICUS/S2_HARMONIZED')
    .filterBounds(AOI)
    // Default max date range is fixed to November 2022 to facilitate output reproducibility
    // Important: uncomment this line to get uptaded, almost near-time results as new S2 scenes are available
    .filterDate(ee.DateRange('2017-01-01', '2022-11-05')) 
    // Pre-filter to get less cloudy granules.
    .filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE', 20))
    // Apply the cloud mask function 
    .map(maskS2clouds);

// Print available scenes and the image date range over your AOI
var range = S2Hcol.reduceColumns(ee.Reducer.minMax(), ["system:time_start"]);
var count = S2Hcol.size();
print("Image collection:", S2Hcol,
      'Date range: ', ee.Date(range.get('min')), ee.Date(range.get('max')), 
      'Scenes count: ', count);


// ============================= MULTI-TEMPORAL MAX NDVI COMPOSITE ============================

// Write NDVI function  
var addNDVI = function(image) {
  var NDVI = image.normalizedDifference(['B8', 'B4']).rename('NDVI');  // NDVI formula
    return image.addBands(NDVI);
};

// AgriExp function
var applyNDVI = function(year) {
  return S2Hcol.filterDate(ee.Date.fromYMD(year, 1, 1), ee.Date.fromYMD(year, 12, 31))
    .filterBounds(AOI)
    .map(addNDVI)
    .select("NDVI")
    .max() // creates a NVDV quality mosaic
    .gte(ee.Number(Number(NDVImin))) // applies index threshold defined by the user
    .focal_median({kernel: ee.Kernel.square({radius:15, units: 'meters'})}) // filters isolated classified pixels 
    .selfMask() // masks nulls
    .multiply(ee.Image.constant(year)) // returns multi-temporal annual value; if none is null 
    .int16() // reduces final raster and it keeps full year labels (eg 2022, 2021, etc)
    .clip(AOI); // clip raster to user defined AOI (uncomment line for visualisation over filterBounds area)
};

// Create a yearly sequence to map the AgriExp function
var years = ee.List.sequence(2017, 2022).map(applyNDVI);

// Create a multi-temporal aggregte composite
var AgriExp = ee.ImageCollection.fromImages(years).reduce(ee.Reducer.firstNonNull());
// Visualise output raster with default yearly visualisation
Map.addLayer(AgriExp, AgriExpVis, "AgriExp");


// ============================= MAP BUFFERS ==================================================

// Create and map distance buffers for each location in the imput table
var functBuff = function(feature) {
  return feature.buffer(ee.Number(Number(DistBuffer))); // defined by prompt
};
var buffers = table.map(functBuff); // .map

// Visualise buffers
Map.addLayer(buffers,{color: 'black'},"Table buffers");


// ============================= MAP NEW ENDANGERED SPATIAL VARIABLES =========================

// Add total buffer area (in Ha)
var addArea = function(feature) {
  return feature.set({areaBuffer: feature.geometry().area().divide(100 * 100)});
};
var buffersArea = buffers.map(addArea);

// Add % area with agricultural encroaching
var buffersAgriPercent = AgriExp.mask(AgriExp).mask().reduceRegions({
  collection: buffersArea,
  scale: 1, // reduce time
  reducer: ee.Reducer.mean().setOutputs(["areaPercentAgri"])
});

// Reclassify to a binary yes/no categorical variable (affected; not affected)  
var list = ee.List(["No", "Yes"]); // string
var buffersReclass = buffersAgriPercent.map(function(f) {
  var val = ee.Number(f.get('areaPercentAgri'));
  var newVal = ee.Number.expression({expression: // reclass using a ternary operator
    "(x > 0) ? 1" + // Affected surface area = Yes
    ": 0", // Affected surface area = No
    vars: {x: val} 
  });
  return f.set('areaImpact', list.get(newVal)); // returns a new classified string
});

// Reclassify and add endangered surface categorical levels (after Hammer et al. 2018)
var list_levels = ee.List(["None", "Low", "Moderate", "High", "Extreme"]); // string
var buffersReclass_levels = buffersReclass.map(function(f) {
  var val = ee.Number(f.get('areaPercentAgri'));
  var newVal_levels = ee.Number.expression({expression:  // reclass using a ternary operator
    "(x > .75) ? 4" + // >75% affected surface area = Total impact (urgent priority)
    ": (x > 0.5) ? 3" + // >50% affected surface area = High impact
    ": (x > 0.25) ? 2" + // >25% affected surface area = Moderate impact
    ": (x > 0) ? 1" + // >25% affected surface area = Moderate impact
    ": 0", // No impact = None
    vars: {x: val} 
  });
  return f.set('areaLevels', list_levels.get(newVal_levels)); // returns a new classified string
});

// Create reducers for AgriExpt multi-temporal zonal statistics (min, max, and mode impact years)
// Get the first (min) and the last (max) years of agricultural encroachement for any pixel within the buffer area
// Get the year with more impact (mode)
var reducers = ee.Reducer.min().setOutputs(["firstYR"]) // get first year
  .combine({
  reducer2: ee.Reducer.max().setOutputs(["lastYR"]), // get last year
  sharedInputs: true // a single pass
}).combine({
  reducer2: ee.Reducer.mode(null, null, 2022).setOutputs(["mostYR"]), // get most year
  sharedInputs: true 
}); // Note: returns "null" for those pixels without impact

// Apply and add reducers as table atributes
var buffersYears = AgriExp.reduceRegions({ // use clipped AgriExp
  collection: buffersReclass_levels,
  scale: 1, // reduce time
  reducer: reducers,
});


// ============================= EXPORT OUTPUTS ================================================

// Prepare strings to be included in the final ouput names
// Get string info for the last acquisition date - it marks the most recent version of the algorithm ouputs
var lastDate = ee.Date(range.get("max")).format("YYYYMMdd").getInfo();
// Get string info for the user-defined NDVI threshold value (T) 
var T = ee.String(NDVImin).replace("\\.", "_").getInfo();

// Export AgriExp output (8bit classified raster)
var AgriExp = AgriExp.remap(
    [2022, 2021, 2020, 2019, 2018, 2017], // original 18bit pixel values
    [22, 21, 20, 19, 18, 17], null) // replace to a lighter 0-255 range with 8bit pixel values;
    .byte(); // transform to a final output of 8bit raster; note that "0" is null
Export.image.toDrive({
  image: AgriExp,
  description: lastDate + '_T' + T + '_AgriExp',
  scale: 10,
  maxPixels: 1e12,
  region: AOI
});

// Export ouput table
Export.table.toDrive({
  collection: buffersYears,
  description: lastDate + '_T' + T + '_AgriExp_buffers' + '_dist' + DistBuffer,
});


// ============================= GEE CONSOLE VISUALISATION ============================================


// NOTE: If using small-sized datasets as table imput, try out the following prints
// to visualise aggregate statistics and multi-categorical counts directly in the GEE console
// Computational requirements might be higher for large-sized datasets (eg > 300 points or polygons)
//print("Total endangered sites:", buffersYears.aggregate_histogram("areaImpact"))
//print("total endangered count by levels:", buffersYears.aggregate_histogram("areaLevels"))
//print("Max, min, mean and sd for endangered areas:", buffersYears.aggregate_stats("areaPercentAgri"))

// Add opacity slider for a direct AgriExp raster comparison in GEE
var slider = ui.Slider();
slider.setValue(1);
slider.onChange(function(value) {
  Map.layers().get(1).setOpacity(value); // Set order accordingly to "Layers" menu, AgriExpt raster is (1)
});
print("AgriExp opacity", slider);

var slider = ui.Slider();
slider.setValue(1);
slider.onChange(function(value) {
  Map.layers().get(0).setOpacity(value); // Set order accordingly to "Layers" menu, AgriExpt raster is (1)
});
print("AOI opacity", slider);


// ============================= GEE MAP VISUALISATION - NDVI CHARTS ==================================

// Inspect point coordinates with multi-temporal NDVI charts to validate and complement AgriExp classifications

// Set crosshair cursor
Map.style().set('cursor', 'crosshair');

// Map NDVI over S2 collection and select NDVI band
var S2Hcol_NDVI = S2Hcol.map(addNDVI).select("NDVI")
  .filterDate(ee.DateRange('2017-01-01', '2022-11-05'));

// Create a panel to hold and display the chart widget
var panel = ui.Panel();
panel.style().set('width', '600px');

// Create an intro panel with labels
var intro = ui.Panel([
  ui.Label({
    value: 'NDVI Chart Inspector',
    style: {fontSize: '20px', fontWeight: 'bold'}
  }),
  ui.Label('Click a point on the map to inspect.')
]);
panel.add(intro);

// Create panels to display lon/lat values
var lon = ui.Label();
var lat = ui.Label();
panel.add(ui.Panel([lon, lat], ui.Panel.Layout.flow('horizontal')));

// Register a callback on the default map to be invoked when the map is clicked.
Map.onClick(function(coords) {
  // Update the lon/lat panel with values from the click event.
  lon.setValue('lon: ' + coords.lon.toFixed(2)),
  lat.setValue('lat: ' + coords.lat.toFixed(2));

  // Add a red dot for the point clicked on.
  var point = ee.Geometry.Point(coords.lon, coords.lat);
  var dot = ui.Map.Layer(point, {color: 'FF0000', pointSize: 3}, "Point (coords) to inspect");
  Map.layers().set(3, dot);

  // Set chart style properties
  var chartStyle = {
  curveType: 'function',
  title: 'Vegetation index value (max) over time',
  hAxis: {
    title: 'Month - year',
    titleTextStyle: {italic: false, bold: true},
    gridlines: {color: 'FFFFFF'},
  },
  vAxis: {
    title: 'NDVI',
    titleTextStyle: {italic: false, bold: true},
    gridlines: {color: 'FFFFFF'},
    format: 'short',
    baselineColor: 'FFFFFF',
  },
  lineWidth: 2, colors:  ['0f8755'], // dark green
  chartArea: {backgroundColor: 'EBEBEB'}
};

  // Add NDVI chart as widget
  var ndviChart = ui.Chart.image.series(S2Hcol_NDVI, point, ee.Reducer.max(), 10); // display NDVI max values
  ndviChart.setOptions(chartStyle);
  ndviChart.setChartType('AreaChart');
  panel.widgets().set(2, ndviChart);
});

// Add the panels to the ui.root
ui.root.insert(0, panel); // hold chart to the left side of the GEE map


//
//
// ============================= END ==========================================================