From d4e29630a2a602874bba86dd9b095850de0464b6 Mon Sep 17 00:00:00 2001
From: michdn <28901045+michdn@users.noreply.github.com>
Date: Tue, 15 Nov 2022 13:40:10 -0600
Subject: [PATCH] Updated LST product to MODIS 6.1. Added evaluate()
asynchronous calls to prevent UI from hanging at different calculation steps.
Removed .If() statements. Aligned formatting and style to GEE style.
---
EPIDEMIA_REACH_v3.4_Ethiopia.js | 1109 +++++++++++++++++++++++++++++++
1 file changed, 1109 insertions(+)
create mode 100644 EPIDEMIA_REACH_v3.4_Ethiopia.js
diff --git a/EPIDEMIA_REACH_v3.4_Ethiopia.js b/EPIDEMIA_REACH_v3.4_Ethiopia.js
new file mode 100644
index 0000000..f7c7b35
--- /dev/null
+++ b/EPIDEMIA_REACH_v3.4_Ethiopia.js
@@ -0,0 +1,1109 @@
+////////////////////////////////////////////////////////////////////////////////
+// EPIDEMIA Data Downloader (Version 3.3-ETH)
+// Ethiopia National (ETH) version
+// Coded by Dr. Mike Wimberly, Dr. Dawn Nekorchuk
+// Contributions from: K. Ramharan Reddy
+// University of Oklahoma, Department of Geography and Environmental Sustainability
+// mcwimberly@ou.edu, dawn.nekorchuk@ou.edu
+// Released 2022-11-15
+////////////////////////////////////////////////////////////////////////////////
+
+
+// Data Imports & Global variables
+var woredas = ee.FeatureCollection(
+ 'users/dawneko/public/Eth_Admin_Woreda_2019_20200702');
+ // Create region outer boundary to filter products.
+var ethiopia = woredas.geometry().bounds();
+var gpm = ee.ImageCollection('NASA/GPM_L3/IMERG_V06');
+//Updated MOD11A2 product
+var lstTerra8 = ee.ImageCollection('MODIS/061/MOD11A2')
+ // After MCST outage
+ .filterDate('2001-06-26', Date.now());
+var brdfReflect = ee.ImageCollection('MODIS/006/MCD43A4');
+var brdfQa = ee.ImageCollection('MODIS/006/MCD43A2');
+
+
+// For interactions with UI & map
+
+// Will be set later with parsed user input:
+// User requested start and end dates.
+// Initializing with a 0 date (1970-01-01).
+var reqStartDate = ee.Date(0);
+var reqEndDate = ee.Date(0);
+// Modified start date to capture previous scene of 8-day MODIS data
+var lstStartDate = ee.Date(0);
+// Potential modified start dates if there is no data
+// available in user request period.
+// Collections will be filtered afterwards but it needs to run
+// the rest of the code to generate empty file for export.
+var brdfStartDate = ee.Date(0);
+var precipStartDate = ee.Date(0);
+
+// For calculated daily environmental data.
+var dailyPrecip = ee.ImageCollection([]);
+var dailyLst = ee.ImageCollection([]);
+var dailyBrdf = ee.ImageCollection([]);
+
+// For flattened (table) results for export.
+var precipFlat = ee.FeatureCollection([]);
+var lstFlat = ee.FeatureCollection([]);
+var brdfFlat = ee.FeatureCollection([]);
+
+// Specific filenames for export.
+var precipFilename = '';
+var lstFilename = '';
+var brdfFilename = '';
+
+// Declare global widgets.
+var startDateInput;
+var endDateInput;
+var panel;
+var calcButton;
+var downloadButton;
+
+// Reset results to prevent accidental data confusion:
+function resetResults() {
+ dailyPrecip = ee.ImageCollection([]);
+ dailyLst = ee.ImageCollection([]);
+ dailyBrdf = ee.ImageCollection([]);
+
+ precipFlat = ee.FeatureCollection([]);
+ lstFlat = ee.FeatureCollection([]);
+ brdfFlat = ee.FeatureCollection([]);
+
+ precipFilename = '';
+ lstFilename = '';
+ brdfFilename = '';
+}
+
+// Main Calculation function
+
+// Main function to be kicked off upon user click on Calculate button
+// 1. Date Prep
+// 2*. Precipitation
+// 3*. LST
+// 4*. BRDF / Spectral
+// *Sections 2, 3, 4: contain subsections for filtering, calculating, summarizing
+// 5. Export setup (separate function for export)
+
+function calculateEnvVars(userStartDate, userEndDate) {
+ // Step 1: Start Date prep
+
+ // Parse user dates
+ reqStartDate = ee.Date(userStartDate);
+ reqEndDate = ee.Date(userEndDate);
+ print('user req start date', reqStartDate);
+ print('user req end date', reqEndDate);
+
+ // LST Dates
+ // LST MODIS is every 8 days, and user date will likely not match.
+ // Want to get the latest previous image date
+ // i.e. the date the closest, but prior to, the user requested date.
+ // Will filter to requested later.
+ // Get date of first image.
+ var lstEarliestDate = lstTerra8.first().date();
+ // Filter collection to dates from beginning to requested start date.
+ var priorLstImgcol = lstTerra8.filterDate(lstEarliestDate, reqStartDate);
+ // Get the latest (max) date of this collection of earlier images.
+ var lstPrevMax = priorLstImgcol.reduceColumns({
+ reducer: ee.Reducer.max(),
+ selectors: ['system:time_start']
+ });
+ lstStartDate = ee.Date(lstPrevMax.get('max'));
+ print('lstStartDate', lstStartDate);
+
+ // Last available data dates
+ // Different variables have different data lags.
+ // Data may not be available in user range.
+ // To prevent errors from stopping script,
+ // grab last available (if relevant) & filter at end.
+
+ // Precipitation:
+ // Calculate date of most recent measurement for gpm (of all time)
+ var gpmAllMax = gpm.reduceColumns({
+ reducer: ee.Reducer.max(),
+ selectors: ['system:time_start']
+ });
+ var gpmAllEndDateTime = ee.Date(gpmAllMax.get('max'));
+ // GPM every 30 minutes, so get just date part
+ var gpmAllEndDate = ee.Date.fromYMD({
+ year: gpmAllEndDateTime.get('year'),
+ month: gpmAllEndDateTime.get('month'),
+ day: gpmAllEndDateTime.get('day')
+ });
+ // If data ends before requested start, take last data date,
+ // otherwise use requested date.
+ var precipStartDate = ee.Date(gpmAllEndDate.millis()
+ .min(reqStartDate.millis()));
+ print('precipStartDate', precipStartDate);
+
+ // BRDF
+ // Calculate date of most recent measurement for brdf (of all time).
+ var brdfAllMax = brdfReflect.reduceColumns({
+ reducer: ee.Reducer.max(),
+ selectors: ['system:time_start']
+ });
+ var brdfAllEndDate = ee.Date(brdfAllMax.get('max'));
+ // If data ends before requested start, take last data date,
+ // otherwise use the requested date.
+ var brdfStartDate = ee.Date(brdfAllEndDate.millis()
+ .min(reqStartDate.millis()));
+ print('brdfStartDate', brdfStartDate);
+ print('brdfEndDate', brdfAllEndDate);
+
+ // Step 2: Precipitation
+
+ // Step 2a: Precipitation filtering and dates
+
+ // Filter gpm by date, using modified start if necessary.
+ var gpmFiltered = gpm
+ .filterDate(precipStartDate, reqEndDate.advance(1, 'day'))
+ .filterBounds(ethiopia)
+ .select('precipitationCal');
+
+ // Calculate date of most recent measurement for gpm
+ // (in modified requested window).
+ var gpmMax = gpmFiltered.reduceColumns({
+ reducer: ee.Reducer.max(),
+ selectors: ['system:time_start']
+ });
+ var gpmEndDate = ee.Date(gpmMax.get('max'));
+ var precipEndDate = gpmEndDate;
+ print('precipEndDate ', precipEndDate);
+
+ // Create list of dates for the precipitation time series
+ var precipDays = precipEndDate.difference(precipStartDate, 'day');
+ var precipDatesPrep = ee.List.sequence(0, precipDays, 1);
+ function makePrecipDates(n) {
+ return precipStartDate.advance(n, 'day');
+ }
+ var precipDates = precipDatesPrep.map(makePrecipDates);
+
+ // Step 2b: Calculate daily precipitation
+
+ // Function to calculate daily precipitation
+ function calcDailyPrecip(curdate) {
+ var curyear = ee.Date(curdate).get('year');
+ var curdoy = ee.Date(curdate).getRelative('day', 'year').add(1);
+ var totprec = gpmFiltered.select('precipitationCal')
+ .filterDate(ee.Date(curdate),
+ ee.Date(curdate).advance(1, 'day'))
+ .sum()
+ // Every half-hour.
+ .multiply(0.5)
+ .rename('totprec');
+ return totprec
+ .set('doy', curdoy)
+ .set('year', curyear)
+ .set('system:time_start', curdate);
+ }
+ // Map function over list of dates.
+ var dailyPrecipExtended =
+ ee.ImageCollection.fromImages(precipDates.map(calcDailyPrecip));
+
+ // Filter back to original user requested start date.
+ dailyPrecip = dailyPrecipExtended
+ .filterDate(reqStartDate, precipEndDate.advance(1, 'day'));
+
+ // Step 2c: Summarize daily precipitation by woreda
+
+ // Filter precip data for zonal summaries.
+ var precipSummary = dailyPrecip
+ .filterDate(reqStartDate, reqEndDate.advance(1, 'day'));
+ // Function to calculate zonal statistics for precipitation by woreda.
+ function sumZonalPrecip(image) {
+ // To get the doy and year,
+ // convert the metadata to grids and then summarize.
+ var image2 = image.addBands([
+ image.metadata('doy').int(),
+ image.metadata('year').int()
+ ]);
+ // Reduce by regions to get zonal means for each county.
+ var output = image2.select(['year', 'doy', 'totprec'])
+ .reduceRegions({
+ collection: woredas,
+ reducer: ee.Reducer.mean(),
+ scale: 1000});
+ return output;
+ }
+ // Map the zonal statistics function over the filtered precip data.
+ var precipWoreda = precipSummary.map(sumZonalPrecip);
+ // Flatten the results for export.
+ precipFlat = precipWoreda.flatten();
+
+ // Step 3: LST
+
+ // Step 3a: Calculate LST variables
+
+ // Filter Terra LST by altered LST start date.
+ // Rarely, but at the end of the year if the last image is late in the year
+ // with only a few days in its period, it will sometimes not grab
+ // the next image. Add extra padding to reqEndDate and
+ // it will be trimmed at the end.
+ var lstFiltered = lstTerra8
+ .filterDate(lstStartDate, reqEndDate.advance(8, 'day'))
+ .filterBounds(ethiopia)
+ .select('LST_Day_1km', 'QC_Day', 'LST_Night_1km', 'QC_Night');
+
+ // Filter Terra LST by QA information.
+ function filterLstQA(image) {
+ var qaday = image.select(['QC_Day']);
+ var qanight = image.select(['QC_Night']);
+ var dayshift = qaday.rightShift(6);
+ var nightshift = qanight.rightShift(6);
+ var daymask = dayshift.lte(2);
+ var nightmask = nightshift.lte(2);
+ var outimage = ee.Image(image.select(['LST_Day_1km', 'LST_Night_1km']));
+ var outmask = ee.Image([daymask, nightmask]);
+ return outimage.updateMask(outmask);
+ }
+ var lstFilteredQA = lstFiltered.map(filterLstQA);
+
+ // Rescale temperature data and convert to degrees Celsius (C).
+ function rescaleLst(image) {
+ var lst_day = image.select('LST_Day_1km')
+ .multiply(0.02)
+ .subtract(273.15)
+ .rename('lst_day');
+ var lst_night = image.select('LST_Night_1km')
+ .multiply(0.02)
+ .subtract(273.15)
+ .rename('lst_night');
+ var lst_mean = image.expression(
+ '(day + night) / 2', {
+ 'day': lst_day.select('lst_day'),
+ 'night': lst_night.select('lst_night')
+ }
+ ).rename('lst_mean');
+ return image.addBands(lst_day)
+ .addBands(lst_night)
+ .addBands(lst_mean);
+ }
+ var lstVars = lstFilteredQA.map(rescaleLst);
+
+ // Create list of dates for time series.
+ var lstRange = lstVars.reduceColumns({
+ reducer: ee.Reducer.max(),
+ selectors: ['system:time_start']
+ });
+ var lstEndDate = ee.Date(lstRange.get('max')).advance(7, 'day');
+ var lstDays = lstEndDate.difference(lstStartDate, 'day');
+ var lstDatesPrep = ee.List.sequence(0, lstDays, 1);
+ function makeLstDates(n) {
+ return lstStartDate.advance(n, 'day');
+ }
+ var lstDates = lstDatesPrep.map(makeLstDates);
+
+ // Step 3b: Calculate daily LST
+
+ // Function to calculate daily LST by assigning the 8-day composite summary
+ // to each day in the composite period.
+ function calcDailyLst(curdate) {
+ var curyear = ee.Date(curdate).get('year');
+ var curdoy = ee.Date(curdate).getRelative('day', 'year').add(1);
+ var moddoy = curdoy.divide(8).ceil().subtract(1).multiply(8).add(1);
+ var basedate = ee.Date.fromYMD(curyear, 1, 1);
+ var moddate = basedate.advance(moddoy.subtract(1), 'day');
+ var lst_day = lstVars
+ .select('lst_day')
+ .filterDate(moddate, moddate.advance(1, 'day'))
+ .first()
+ .rename('lst_day');
+ var lst_night = lstVars
+ .select('lst_night')
+ .filterDate(moddate, moddate.advance(1, 'day'))
+ .first()
+ .rename('lst_night');
+ var lst_mean = lstVars
+ .select('lst_mean')
+ .filterDate(moddate, moddate.advance(1, 'day'))
+ .first()
+ .rename('lst_mean');
+ return lst_day
+ .addBands(lst_night)
+ .addBands(lst_mean)
+ .set('doy', curdoy)
+ .set('year', curyear)
+ .set('system:time_start', curdate);
+ }
+ // Map the function over the image collection.
+ var dailyLstExtended = ee.ImageCollection.fromImages(lstDates.map(calcDailyLst));
+
+ // Filter back to original user requested start date.
+ dailyLst = dailyLstExtended
+ .filterDate(reqStartDate, lstEndDate.advance(1, 'day'));
+
+ // Step 3c: Summarize daily LST by woreda
+
+ // Filter lst data for zonal summaries.
+ var lstSummary = dailyLst
+ .filterDate(reqStartDate, reqEndDate.advance(1, 'day'));
+ // Function to calculate zonal statistics for lst by woreda
+ function sumZonalLst(image) {
+ // To get the doy and year, we convert the metadata to grids
+ // and then summarize.
+ var image2 = image.addBands([
+ image.metadata('doy').int(),
+ image.metadata('year').int()
+ ]);
+ // Reduce by regions to get zonal means for each county
+ // ORDER is important, must correspond to selection below.
+ var reducers = ee.Reducer.mean().combine({ //doy
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'year'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'lst_day'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'lst_night'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'lst_mean'})
+ .combine({
+ reducer2: ee.Reducer.count(), //using the second lstday
+ outputPrefix: 'pixels_lstd'})
+ .combine({
+ reducer2: ee.Reducer.count(), //using the second lstnight
+ outputPrefix: 'pixels_lstn'})
+ .combine({
+ reducer2: ee.Reducer.count(), //using the second lstmean
+ outputPrefix: 'pixels_lstm'})
+ .combine({
+ reducer2: ee.Reducer.countEvery(),
+ outputPrefix: 'pixels_total'});
+ // ORDER is important, must correspond to reducers above.
+ var output = image2
+ .select(['doy', 'year', 'lst_day', 'lst_night', 'lst_mean',
+ 'lst_day', 'lst_night', 'lst_mean'],
+ ['doy', 'year', 'lst_day', 'lst_night', 'lst_mean',
+ 'dayToCount', 'nightToCount', 'meanToCount'])
+ .reduceRegions({
+ collection: woredas,
+ reducer: reducers,
+ scale: 1000
+ });
+ return output;
+ }
+ // Map the zonal statistics function over the filtered lst data.
+ var lstWoreda = lstSummary.map(sumZonalLst);
+ // Rename fields
+ var lstNamesOld = ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME',
+ 'yearmean', 'mean',
+ 'lst_daymean', 'lst_nightmean', 'lst_meanmean',
+ 'pixels_lstdcount', 'pixels_lstncount',
+ 'pixels_lstmcount', 'pixels_totalcount'];
+ var lstNamesNew = ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME',
+ 'year', 'doy',
+ 'lst_day', 'lst_night', 'lst_mean',
+ 'pixels_lstd', 'pixels_lstn',
+ 'pixels_lstm', 'pixels_total'];
+ // Flatten the results for export.
+ lstFlat = lstWoreda.flatten().select(lstNamesOld, lstNamesNew, false);
+
+ // Step 4: BRDF / Spectral Indices
+
+ // Step 4a: Calculate spectral indices
+
+ // Filter BRDF-Adjusted Reflectance by Date
+ var brdfReflectVars = brdfReflect
+ .filterDate(brdfStartDate, reqEndDate.advance(1, 'day'))
+ .filterBounds(ethiopia)
+ .select(['Nadir_Reflectance_Band1', 'Nadir_Reflectance_Band2',
+ 'Nadir_Reflectance_Band3', 'Nadir_Reflectance_Band4',
+ 'Nadir_Reflectance_Band5', 'Nadir_Reflectance_Band6',
+ 'Nadir_Reflectance_Band7'],
+ ['red', 'nir', 'blue', 'green', 'swir1', 'swir2', 'swir3']);
+
+ // Filter BRDF QA by date.
+ var brdfReflectQa = brdfQa
+ .filterDate(brdfStartDate, reqEndDate.advance(1, 'day'))
+ .filterBounds(ethiopia)
+ .select(['BRDF_Albedo_Band_Quality_Band1', 'BRDF_Albedo_Band_Quality_Band2',
+ 'BRDF_Albedo_Band_Quality_Band3', 'BRDF_Albedo_Band_Quality_Band4',
+ 'BRDF_Albedo_Band_Quality_Band5', 'BRDF_Albedo_Band_Quality_Band6',
+ 'BRDF_Albedo_Band_Quality_Band7', 'BRDF_Albedo_LandWaterType'],
+ ['qa1', 'qa2', 'qa3', 'qa4', 'qa5', 'qa6', 'qa7', 'water']);
+
+ // Join the 2 collections.
+ var idJoin = ee.Filter.equals({
+ leftField: 'system:time_end',
+ rightField: 'system:time_end'
+ });
+ // Define the join.
+ var innerJoin = ee.Join.inner('NBAR', 'QA');
+ // Apply the join.
+ var brdfJoined = innerJoin.apply(brdfReflectVars, brdfReflectQa, idJoin);
+
+ // Add QA bands to the NBAR collection
+ function addQaBands(image){
+ var nbar = ee.Image(image.get('NBAR'));
+ var qa = ee.Image(image.get('QA')).select(['qa2']);
+ var water = ee.Image(image.get('QA')).select(['water']);
+ return nbar.addBands([qa, water]);
+ }
+ var brdfMerged = ee.ImageCollection(brdfJoined.map(addQaBands));
+
+ // Function to mask out pixels based on qa and water/land flags:
+ function filterBrdf(image) {
+ // Using QA info for the NIR band.
+ var qaband = image.select(['qa2']);
+ var wband = image.select(['water']);
+ var qamask = qaband.lte(2).and(wband.eq(1));
+ var nir_r = image.select('nir').multiply(0.0001).rename('nir_r');
+ var red_r = image.select('red').multiply(0.0001).rename('red_r');
+ var swir1_r = image.select('swir1').multiply(0.0001).rename('swir1_r');
+ var swir2_r = image.select('swir2').multiply(0.0001).rename('swir2_r');
+ var blue_r = image.select('blue').multiply(0.0001).rename('blue_r');
+ return image.addBands(nir_r)
+ .addBands(red_r)
+ .addBands(swir1_r)
+ .addBands(swir2_r)
+ .addBands(blue_r)
+ .updateMask(qamask);
+ }
+ var brdfFilteredVars = brdfMerged.map(filterBrdf);
+
+ // Function to calculate spectral indices:
+ function calcBrdfIndices(image) {
+ var curyear = ee.Date(image.get('system:time_start')).get('year');
+ var curdoy = ee.Date(image.get('system:time_start'))
+ .getRelative('day', 'year').add(1);
+ var ndvi = image.normalizedDifference(['nir_r', 'red_r'])
+ .rename('ndvi');
+ var savi = image.expression(
+ '1.5 * (nir - red) / (nir + red + 0.5)', {
+ 'nir': image.select('nir_r'),
+ 'red': image.select('red_r')
+ }
+ ).rename('savi');
+ var evi = image.expression(
+ '2.5 * (nir - red) / (nir + 6 * red - 7.5 * blue + 1)', {
+ 'nir': image.select('nir_r'),
+ 'red': image.select('red_r'),
+ 'blue': image.select('blue_r')
+ }
+ ).rename('evi');
+ var ndwi5 = image.normalizedDifference(['nir_r', 'swir1_r'])
+ .rename('ndwi5');
+ var ndwi6 = image.normalizedDifference(['nir_r', 'swir2_r'])
+ .rename('ndwi6');
+
+ return image.addBands(ndvi)
+ .addBands(savi)
+ .addBands(evi)
+ .addBands(ndwi5)
+ .addBands(ndwi6)
+ .set('doy', curdoy)
+ .set('year', curyear);
+ }
+ // Map function over image collection.
+ brdfFilteredVars = brdfFilteredVars.map(calcBrdfIndices);
+
+ // Create list of dates for full time series.
+ var brdfRange = brdfFilteredVars.reduceColumns({
+ reducer: ee.Reducer.max(),
+ selectors: ['system:time_start']
+ });
+ var brdfEndDate = ee.Date(brdfRange.get('max'));
+ var brdfDays = brdfEndDate.difference(brdfStartDate, 'day');
+ var brdfDatesPrep = ee.List.sequence(0, brdfDays, 1);
+ function makeBrdfDates(n) {
+ return brdfStartDate.advance(n, 'day');
+ }
+ var brdfDates = brdfDatesPrep.map(makeBrdfDates);
+
+ // List of dates that exist in BRDF data.
+ var brdfDatesExist = brdfFilteredVars
+ .aggregate_array('system:time_start');
+
+ // Step 4b: Calculate daily spectral indices
+
+ // Get daily brdf values.
+ function calcDailyBrdfExists(curdate) {
+ curdate = ee.Date(curdate);
+ var curyear = curdate.get('year');
+ var curdoy = curdate.getRelative('day', 'year').add(1);
+ var brdfTemp = brdfFilteredVars
+ .filterDate(curdate, curdate.advance(1, 'day'));
+ var outImg = brdfTemp.first();
+ return outImg;
+ }
+ var dailyBrdfExtExists =
+ ee.ImageCollection.fromImages(brdfDatesExist.map(calcDailyBrdfExists));
+
+ // Create empty result, to fill in dates when BRDF data does not exist.
+ function calcDailyBrdfFiller(curdate) {
+ curdate = ee.Date(curdate);
+ var curyear = curdate.get('year');
+ var curdoy = curdate.getRelative('day', 'year').add(1);
+ var brdfTemp = brdfFilteredVars
+ .filterDate(curdate, curdate.advance(1, 'day'));
+ var brdfSize = brdfTemp.size();
+ var outImg = ee.Image.constant(0).selfMask()
+ .addBands(ee.Image.constant(0).selfMask())
+ .addBands(ee.Image.constant(0).selfMask())
+ .addBands(ee.Image.constant(0).selfMask())
+ .addBands(ee.Image.constant(0).selfMask())
+ .rename(['ndvi', 'evi', 'savi', 'ndwi5', 'ndwi6'])
+ .set('doy', curdoy)
+ .set('year', curyear)
+ .set('system:time_start', curdate)
+ .set('brdfSize', brdfSize);
+ return outImg;
+ }
+ // Create filler for all dates.
+ var dailyBrdfExtendedFiller =
+ ee.ImageCollection.fromImages(brdfDates.map(calcDailyBrdfFiller));
+ // But only use if and when size was 0.
+ var dailyBrdfExtFillFilt = dailyBrdfExtendedFiller
+ .filter(ee.Filter.eq('brdfSize', 0));
+
+ // Merge the two collections.
+ var dailyBrdfExtended = dailyBrdfExtExists
+ .merge(dailyBrdfExtFillFilt);
+
+ // Filter back to original user requested start date.
+ dailyBrdf = dailyBrdfExtended
+ .filterDate(reqStartDate, brdfEndDate.advance(1, 'day'));
+
+
+ // Step 4c: Summarize daily spectral indices by woreda
+
+ // Filter spectral indices for zonal summaries.
+ var brdfSummary = dailyBrdf
+ .filterDate(reqStartDate, reqEndDate.advance(1, 'day'));
+ // Function to calculate zonal statistics for spectral indices by county:
+ function sumZonalBrdf(image) {
+ // To get the doy and year, we convert the metadata to grids and then summarize
+ var image2 = image.addBands([
+ image.metadata('doy').int(),
+ image.metadata('year').int()]);
+ // Reduce by regions to get zonal means for each feature.
+ // ORDER is important, must correspond to selection below.
+ var reducers = ee.Reducer.mean().combine({ //doy
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'year'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'ndvi'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'savi'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'evi'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'ndwi5'})
+ .combine({
+ reducer2: ee.Reducer.mean(),
+ outputPrefix: 'ndwi6'})
+ .combine({
+ reducer2: ee.Reducer.count(), //using the 'extra' ndvi
+ outputPrefix: 'good_pixels'})
+ .combine({
+ reducer2: ee.Reducer.countEvery(), //0-input reducer, does not need a band
+ outputPrefix: 'total_pixels'});
+ // ORDER is important, must correspond to reducers above.
+ var output = image2
+ // The extra ndvi at the end is for counting pixels.
+ .select(['doy', 'year', 'ndvi', 'savi', 'evi', 'ndwi5', 'ndwi6', 'ndvi'],
+ ['doy', 'year', 'ndvi', 'savi', 'evi', 'ndwi5', 'ndwi6', 'tocount'])
+ .reduceRegions({
+ collection: woredas,
+ reducer: reducers,
+ scale: 500}); //NBAR product 500 meter, using same scale for LST reducers
+ return output;
+ }
+ // Map the zonal statistics function over the filtered spectral index data.
+ var brdfWoreda = brdfSummary.map(sumZonalBrdf);
+
+ // Flatten the results for export
+ var brdfNamesOld = ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME',
+ 'yearmean', 'mean',
+ 'ndvimean', 'savimean', 'evimean', 'ndwi5mean', 'ndwi6mean',
+ 'good_pixelscount', 'total_pixelscount', ];
+ var brdfNamesNew = ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME',
+ 'year', 'doy',
+ 'ndvi', 'savi', 'evi', 'ndwi5', 'ndwi6',
+ 'pixels_ndvi', 'pixels_total'];
+ brdfFlat = brdfWoreda.flatten().select(brdfNamesOld, brdfNamesNew, false);
+
+ // Step 5: Exporting Set-up
+
+ //To prevent the UI from hanging while it is calculating
+ // the end dates for the download file names (old getInfo() calls)
+ // We create a function that we will call asynchronously via evaluate()
+ // That will do the waiting for results without hanging the UI.
+
+ function afterCalculate(data){
+ var precipSummaryEndDate = data.precipDate; //data[0];
+ precipFilename = precipPrefix
+ .concat('_', userStartDate,
+ '_', precipSummaryEndDate);
+
+ var lstSummaryEndDate = data.lstDate; //data[1];
+ lstFilename = lstPrefix
+ .concat('_', userStartDate,
+ '_', lstSummaryEndDate);
+
+ var brdfSummaryEndDate = data.brdfDate; //data[2];
+ brdfFilename = brdfPrefix
+ .concat('_', userStartDate,
+ '_', brdfSummaryEndDate);
+
+ print(precipFilename, lstFilename, brdfFilename);
+
+ displayResults();
+ }
+
+ //Dictionary collector for things to evaluate
+ //var dataList = [];
+ var fileDateDictionary = {};
+
+ //Precipitation
+ var precipPrefix = 'export_precip_data';
+ var precipLastDate = ee.Date(reqEndDate.millis()
+ .min(precipEndDate.millis())).format('yyyy-MM-dd');
+ //dataList.push(precipLastDate);
+ fileDateDictionary.precipDate = precipLastDate;
+
+ //LST
+ var lstPrefix = 'export_lst_data';
+ var lstLastDate = ee.Date(reqEndDate.millis()
+ .min(lstEndDate.millis())).format('yyyy-MM-dd');
+ //dataList.push(lstLastDate);
+ fileDateDictionary.lstDate = lstLastDate;
+
+
+ //BRDF
+ var brdfPrefix = 'export_spectral_data';
+ var brdfLastDate = ee.Date(reqEndDate.millis()
+ .min(brdfEndDate.millis())).format('yyyy-MM-dd');
+ //dataList.push(brdfLastDate);
+ fileDateDictionary.brdfDate = brdfLastDate;
+
+ //Now call asynchronous evaluation
+ //ee.List(dataList).evaluate(afterCalculate);
+ ee.Dictionary(fileDateDictionary).evaluate(afterCalculate);
+
+} //end calculateEnvVars
+
+// Function for Drive exporting
+
+// For when script is run in Code Editor with access to Tasks:
+function exportToDrive(){
+
+ // Export flattened tables to Google Drive.
+ // Need to click 'RUN in the Tasks tab to configure and start each export.
+ Export.table.toDrive({
+ collection: precipFlat,
+ description: precipFilename,
+ selectors: ['NewPCODE', 'R_NAME','Z_NAME','W_NAME', 'year', 'doy', 'totprec']
+ });
+ Export.table.toDrive({
+ collection: lstFlat,
+ description: lstFilename,
+ selectors: ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME', 'year', 'doy',
+ 'lst_day', 'lst_night', 'lst_mean',
+ 'pixels_lstd', 'pixels_lstn', 'pixels_lstm', 'pixels_total']
+ });
+ Export.table.toDrive({
+ collection: brdfFlat,
+ description: brdfFilename,
+ selectors: ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME', 'year', 'doy',
+ 'ndvi', 'savi', 'evi', 'ndwi5', 'ndwi6',
+ 'pixels_ndvi', 'pixels_total']
+ });
+}
+
+// Separate function for final exporting in app and new UI panel:
+function exportSummaries(){
+
+ //Because this can also hang the UI,
+ // we will create these asynchronously.
+
+ function generateUrls(ignoreData){
+
+ //Flattened tables are global
+ // so we are not using flatDictionary
+ // which really only existed to run evaluate from
+ // Quite possibly a better way to do this, but
+ // might also involve issues with callbacks / variable passing
+ var precipURL = precipFlat
+ .getDownloadURL({
+ format: 'csv',
+ filename: precipFilename,
+ selectors: ['NewPCODE', 'R_NAME','W_NAME','Z_NAME', 'year', 'doy', 'totprec']
+ });
+ var lstURL = lstFlat
+ .getDownloadURL({
+ format: 'csv',
+ filename: lstFilename,
+ selectors: ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME', 'year', 'doy',
+ 'lst_day', 'lst_night', 'lst_mean',
+ 'pixels_lstd', 'pixels_lstn', 'pixels_lstm', 'pixels_total']
+ });
+ var brdfURL = brdfFlat
+ .getDownloadURL({
+ format: 'csv',
+ filename: brdfFilename,
+ selectors: ['NewPCODE', 'R_NAME', 'Z_NAME', 'W_NAME', 'year', 'doy',
+ 'ndvi', 'savi', 'evi', 'ndwi5', 'ndwi6',
+ 'pixels_ndvi', 'pixels_total']
+ });
+
+ // Add download links to UI.
+ // Adapted from TC Chakraborty Global Surface UHI Explorer.
+ // Link construction:
+ var linkSection = ui.Chart(
+ [
+ ['Download data'],
+ ['' +
+ 'Precipitation'],
+ ['' +
+ 'Land Surface Temperatures'],
+ ['' +
+ 'Spectral Indicies'],
+ ],
+ 'Table', {allowHtml: true});
+ // Make link panel.
+ downloadPanel = ui.Panel({
+ widgets: [linkSection],
+ layout: ui.Panel.Layout.Flow('vertical')
+ });
+ sidePanel.add(downloadPanel);
+
+ //Update button text
+ downloadButton.setLabel('(See below)');
+
+ }
+
+ //flattened table dictionary to run eval off of
+ var flatDictionary = {
+ precipFlatKey: precipFlat,
+ lstFlatKey: lstFlat,
+ brdfFlatKey: brdfFlat
+ };
+
+ // Generate URLs asynchronously, and displays links when done
+ ee.Dictionary(flatDictionary).evaluate(generateUrls);
+
+}
+
+// User interface (UI)
+
+// Initialize some UI-related variables.
+var map = ui.Map();
+var sidePanel = ui.Panel();
+var resultsPanel = ui.Panel();
+var downloadPanel = ui.Panel();
+// Will be used in UI default dates.
+var now = Date.now();
+
+var config = {
+ // 28 days before today
+ initialStartDate: ee.Date(now)
+ .advance(-28, 'days')
+ .format('YYYY-MM-dd').getInfo(),
+ // Today
+ initialEndDate: ee.Date(now)
+ .format('YYYY-MM-dd').getInfo(),
+ initialCalcButtonText: 'Click to summarize',
+};
+
+// Palettes for environmental variable maps:
+var palettePrecip = ['f7fbff', '08306b'];
+var paletteLST = ['fff5f0', '67000d'];
+var paletteSpectral = ['ffffe5', '004529'];
+
+
+function makeSidePanel(title, description) {
+ title = ui.Label({
+ value: title,
+ style: {
+ fontSize: '18px',
+ fontWeight: '400',
+ padding: '10px',
+ }
+ });
+ description = ui.Label({
+ value: description,
+ style: {
+ color: 'gray',
+ padding: '10px',
+ }
+ });
+ return ui.Panel({
+ widgets: [title, description],
+ style: {
+ height: '100%',
+ width: '30%',
+ },
+ });
+}
+
+function initializeWidgets() {
+
+ panel = ui.Panel();
+
+ // Start date box:
+ var startDateLabel = ui.Label({
+ value: 'Start Date for Summary (YYYY-MM-DD). ' +
+ 'For this script, the earliest start date is 2001-06-26 for LST data.',
+ });
+ panel.add(startDateLabel);
+ startDateInput = ui.Textbox({
+ value: config.initialStartDate,
+ onChange: function(value) {
+ // Reset calculation button.
+ calcButton.setLabel(config.initialCalcButtonText);
+ // Reset results and summaries.
+ panel.remove(resultsPanel);
+ sidePanel.remove(downloadPanel);
+ resetResults();
+ // Reset map.
+ map.clear();
+ drawBaseMap();
+ // Set value.
+ startDateInput.setValue(value);
+ return(value);
+ }
+ });
+ panel.add(startDateInput);
+
+ // End date box:
+ var endDateLabel = ui.Label({
+ value: 'End Date for Summary (YYYY-MM-DD):',
+ });
+ panel.add(endDateLabel);
+ endDateInput = ui.Textbox({
+ value: config.initialEndDate,
+ onChange: function(value) {
+ // Reset calculation button.
+ calcButton.setLabel(config.initialCalcButtonText);
+ // Reset results and summary.
+ panel.remove(resultsPanel);
+ sidePanel.remove(downloadPanel);
+ resetResults();
+ // Reset map.
+ map.clear();
+ drawBaseMap();
+ // Set value.
+ endDateInput.setValue(value);
+ return(value);
+ }
+ });
+ panel.add(endDateInput);
+
+ // Calculate button
+ var calcButtonLabel = ui.Label({
+ value: '2. Calculate environmental variables for selected dates. ' +
+ 'These steps will take several seconds, please be patient.',
+ style: {fontWeight: 'bold'}
+ });
+ panel.add(calcButtonLabel);
+ calcButton = ui.Button({
+ label: config.initialCalcButtonText,
+ onClick: function(button) {
+ button.setLabel('(Calculating)');
+ // Call main calculation script with user set dates.
+ calculateEnvVars(startDateInput.getValue(),
+ endDateInput.getValue());
+ }
+ });
+ panel.add(calcButton);
+ return panel;
+}
+
+function displayResults() {
+ // Only run this function once all the data has been populated.
+ // Create new results panel & display.
+ resultsPanel = createResultsPanel(startDateInput.getValue(),
+ endDateInput.getValue());
+ panel.add(resultsPanel);
+ calcButton.setLabel('(See below)');
+ // Add tasks for Drive Export.
+ exportToDrive();
+}
+
+function drawEnvMap(dtRange) {
+ // dtRange is from from a UI date slider
+ // and is the date range to show the envirnonmental variables.
+
+ // Filter image collections based on slider value.
+ var brdfDisp = dailyBrdf
+ .filterDate(dtRange.start(), dtRange.end());
+ var lstDisp = dailyLst
+ .filterDate(dtRange.start(), dtRange.end());
+ var precipDisp = dailyPrecip
+ .filterDate(dtRange.start(), dtRange.end());
+
+ // Select the image (should be only one) from each collection.
+ var precipImage = precipDisp.first().select('totprec');
+ var lstdImage = lstDisp.first().select('lst_day');
+ var lstmImage = lstDisp.first().select('lst_mean');
+ var ndviImage = brdfDisp.first().select('ndvi');
+ var ndwi6Image = brdfDisp.first().select('ndwi6');
+
+ // Reset map.
+ map.clear();
+ drawBaseMap();
+
+ // Add layers to the map viewer.
+ // Showing precipitation by default,
+ // others hidden until users pick them from layers drop down menu.
+ map.addLayer({eeObject: precipImage,
+ visParams: {min: 0, max: 20, palette: palettePrecip},
+ name:'Precipitation',
+ shown: true,
+ opacity: 0.75});
+ map.addLayer({eeObject: lstdImage,
+ visParams: {min: 0, max: 40, palette: paletteLST},
+ name: 'LST Day',
+ shown: false,
+ opacity: 0.75});
+ map.addLayer({eeObject: lstmImage,
+ visParams: {min: 0, max: 40, palette: paletteLST},
+ name: 'LST Mean',
+ shown: false,
+ opacity: 0.75});
+ map.addLayer({eeObject: ndviImage,
+ visParams: {min: 0, max: 1, palette: paletteSpectral},
+ name: 'NDVI',
+ shown: false,
+ opacity: 0.75});
+ map.addLayer({eeObject: ndwi6Image,
+ visParams: {min: 0, max: 1, palette: paletteSpectral},
+ name: 'NDWI6',
+ shown: false,
+ opacity: 0.75});
+}
+
+function createResultsPanel(userStartDate, userEndDate) {
+
+ // Date slider for displaying env data on map
+ var dateLabel = ui.Label({
+ value: 'Optional, for VISUALIZATION of layers on the map: ' +
+ 'Pick a date to show environmental data. ' +
+ 'Choose layers from the layer menu in the upper right of map (using the checkbox). ' +
+ 'Some layers may not yet be available close to the current date.'});
+ var dateDisplay = ui.DateSlider({
+ start: userStartDate,
+ end: ee.Date(userEndDate).advance(1, 'day')
+ .format('YYYY-MM-dd').getInfo(),
+ value: userStartDate,
+ onChange: function(value){
+ // Note: value is a DateRange.
+ // Draw the updated map.
+ drawEnvMap(value);
+ }
+ });
+ // Wrap in a panel to group label and slider.
+ var pickDateDisplay = ui.Panel([
+ dateLabel,
+ dateDisplay
+ ]);
+
+ var dayCount = ee.Date(userEndDate)
+ .difference(ee.Date(userStartDate), 'days')
+ .add(1);
+
+ var dayLabel = ui.Label('Number of days selected: ' + dayCount.getInfo());
+
+ // Download button for user to click:
+ downloadButton = ui.Button({
+ label:'3. Get download links for woreda summary CSV files',
+ onClick: function(button) {
+ button.setLabel('(Generating download links)');
+ //Create links and show,
+ //and create Tasks (for when in code editor).
+ exportSummaries();
+ }
+ });
+
+ // Set ~9 months of data at a time as a hard limit
+ // trying to prevent timeout errors for the user.
+ downloadButton.style().set('shown', Boolean(dayCount.lte(280).getInfo()));
+
+ var resultsUI = ui.Panel([pickDateDisplay,
+ dayLabel,
+ downloadButton]);
+ return resultsUI;
+}
+
+
+function drawBaseMap() {
+
+ // Display the outline of woredas as a black line with no fill.
+ // Create an empty image into which to paint the features, cast to byte.
+ var empty = ee.Image().byte();
+ // Paint all the polygon edges with the same number and width, display.
+ var outline = empty.paint({
+ featureCollection: woredas,
+ color: 1,
+ width: 1
+ });
+ map.addLayer(outline, {palette: '000000'}, 'Woredas');
+}
+
+function init() {
+
+ // Set up default map:
+ map.setCenter(40.5, 9.5, 6);
+ drawBaseMap();
+
+ sidePanel = makeSidePanel(
+ 'Retrieving Environmental Analytics for Climate and Health (REACH)',
+ 'Generates daily environmental data summarized by woreda, ' +
+ 'for use in the EPIDEMIA system for forecasting malaria. ' +
+ 'Version 3.4-ETH.'
+ );
+
+ // Links to an external references.
+ var packageLink = ui.Label(
+ 'R package epidemiar',
+ {},
+ 'https://github.com/EcoGRAPH/epidemiar');
+ //sidePanel.add(packageLink);
+ var websiteLink = ui.Label(
+ 'EcoGRAPH EPIDEMIA webpage',
+ {},
+ 'http://ecograph.net/epidemia/');
+ //sidePanel.add(websiteLink);
+ var codeLink = ui.Label(
+ 'Access to Code Editor version (must have Google Earth Engine account)',
+ {},
+ 'https://code.earthengine.google.com/d5ed6dd135dfb5d8bb8e3c86292456cb');
+ var moreInfoPanel = ui.Panel({
+ widgets: [//ui.Label('', {}),
+ //packageLink,
+ websiteLink,
+ codeLink],
+ style: {padding: '0px 0px 0px 10px'}}
+ );
+ sidePanel.add(moreInfoPanel);
+
+ var descriptionText =
+ '1. Enter a date range to download data. ' +
+ 'Please request only a few months at one time, ' +
+ 'otherwise it may time out.';
+ sidePanel.add(ui.Label({
+ value: descriptionText,
+ style: {fontWeight: 'bold'}
+ }));
+ var widgetPanel = initializeWidgets();
+ sidePanel.add(widgetPanel);
+
+ var splitPanel = ui.SplitPanel({
+ firstPanel: sidePanel,
+ secondPanel: map,
+ });
+ ui.root.clear();
+ ui.root.add(splitPanel);
+}
+
+init();
+
+