DOI: 10.7910/dvn/ufc6b5
title: Web-based GIS for spatiotemporal crop climate niche mapping

description: &lt;b&gt;Web-based GIS for spatiotemporal crop climate niche mapping&lt;/b&gt;
&lt;br&gt;&lt;br&gt;

&lt;i&gt;Interactive Google Earth Engine Application—&lt;/i&gt;Version 2, July 2020
&lt;br&gt;
&lt;a&gt;https://cropniche.cartoscience.com&lt;/a&gt;
&lt;br&gt;
&lt;a&gt;https://cartoscience.users.earthengine.app/view/crop-niche&lt;/a&gt;
&lt;br&gt;&lt;br&gt;

&lt;i&gt;Google Earth Engine Code&lt;/i&gt;
&lt;pre&gt;
/* ----------------------------------------------------------------------------------------------------------------------
# CropSuit-GEE
Authors: Brad G. Peter (bpeter@ua.edu), Joseph P. Messina, and Zihan Lin
Organizations: BGP, JPM - University of Alabama; ZL - Michigan State University
Last Modified: 06/28/2020

To cite this code use:
Peter, B. G.; Messina, J. P.; Lin, Z., 2019, "Web-based GIS for spatiotemporal crop climate niche mapping", 
https://doi.org/10.7910/DVN/UFC6B5, Harvard Dataverse, V1
-------------------------------------------------------------------------------------------------------------------------
This is a Google Earth Engine crop climate suitability geocommunication and map export tool designed to support 
agronomic development and deployment of improved crop system technologies. This content is made possible by the 
support of the American People provided to the Feed the Future Innovation Lab for Sustainable Intensification through 
the United States Agency for International Development (USAID). The contents are the sole responsibility of the authors 
and do not necessarily reflect the views of USAID or the United States Government. Program activities are funded by 
USAID under Cooperative Agreement No. AID-OAA-L-14-00006.
-------------------------------------------------------------------------------------------------------------------------
Summarization of input options: There are 14 user options available. The first is a country of interest selection using a 
2-digit FIPS code (link available below). This selection is used to produce a rectangular bounding box for export; however,
other geometries can be selected with minimal modification to the code. Options 2 and 3 specify the complete temporal range
for aggregation (averaged across seasons; single seasons may also be selected). Options 4–7 specify the growing season for 
calculating total seasonal rainfall and average season temperatures and NDVI (NDVI is for export only and is not used in 
suitability determination). Options 8–11 specify the climate parameters for the crop of interest (rainfall and temperature 
max/min). Option 12 enables masking to agriculture, 13 enables exporting of all data layers, and 14 is a text string for 
naming export files.
-------------------------------------------------------------------------------------------------------------------------

••••••••••••••••••••••••••••••••••••••••••• USER OPTIONS ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••• */

// CHIRPS data availability: https://developers.google.com/earth-engine/datasets/catalog/UCSB-CHG_CHIRPS_PENTAD
// MOD11A2 data availability: https://developers.google.com/earth-engine/datasets/catalog/MODIS_006_MOD11A2

var country = 'MI' // [1] https://en.wikipedia.org/wiki/List_of_FIPS_country_codes
var startRange = 2001 // [2]
var endRange = 2017 // [3]
var startSeasonMonth = 11 // [4]
var startSeasonDay = 1 // [5]
var endSeasonMonth = 4 // [6]
var endSeasonDay = 30 // [7]
var precipMin = 750 // [8]
var precipMax = 1200 // [9]
var tempMin = 22 // [10]
var tempMax = 32 // [11]

var maskToAg = 'TRUE' // [12] 'TRUE' (default) or 'FALSE'
var exportLayers = 'TRUE' // [13] 'TRUE' (default) or 'FALSE'
var exportNameHeader = 'crop_suit_maize' // [14] text string for naming export file

// ••••••••••••••••••••••••••••••••• NO USER INPUT BEYOND THIS POINT ••••••••••••••••••••••••••••••••••••••••••••••••••••

// Access precipitation and temperature ImageCollections and a global countries FeatureCollection
var region = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017')
 .filterMetadata('country_co','equals',country)
var precip = ee.ImageCollection('UCSB-CHG/CHIRPS/PENTAD').select('precipitation')
var temp = ee.ImageCollection('MODIS/006/MOD11A2').select(['LST_Day_1km','LST_Night_1km'])
var ndvi = ee.ImageCollection('MODIS/006/MOD13Q1').select(['NDVI'])

// Create layers for masking to agriculture and masking out water bodies
var waterMask = ee.Image('UMD/hansen/global_forest_change_2015').select('datamask').eq(1)
var agModis = ee.ImageCollection('MODIS/006/MCD12Q1').select('LC_Type1').mode()
 .remap([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17],
 [0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0])
var agGC = ee.Image('ESA/GLOBCOVER_L4_200901_200912_V2_3').select('landcover')
 .remap([11,14,20,30,40,50,60,70,90,100,110,120,130,140,150,160,170,180,190,200,210,220,230],
 [1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])
var cropland = ee.Image('USGS/GFSAD1000_V1').neq(0)
var agMask = agModis.add(agGC).add(cropland).gt(0).eq(1)

// Modify user input options for processing with raw data
var years = ee.List.sequence(startRange,endRange)
var bounds = region.geometry().bounds()
var tMinMod = (tempMin+273.15)/0.02
var tMaxMod = (tempMax+273.15)/0.02

// This expression is used to account for growing seasons that overlap the from one year to the next (e.g., Nov-Apr)
var yearMod = ee.Algorithms.If(endSeasonMonth-startSeasonMonth&lt;0,1,0) 

// Calculate seasonal rainfall (sum) and average temperature/NDVI
var bySeason = ee.ImageCollection(years.map(function (y) {
 var startSeason = ee.Date.fromYMD(y, startSeasonMonth,startSeasonDay)
 var endSeason = ee.Date.fromYMD(ee.Number(y).add(yearMod),endSeasonMonth,endSeasonDay)
 var p = precip.filterDate(startSeason,endSeason).sum().rename('p')
 var tDay = temp.select('LST_Day_1km').filterDate(startSeason,endSeason).mean()
 var tNight = temp.select('LST_Night_1km').filterDate(startSeason,endSeason).mean()
 var tMean = tDay.add(tNight).divide(2).rename('t')
 // NDVI is used for data presentation/export only and is not used in the suitability determination
 var nMean = ndvi.mean().rename('n')
 return p.addBands(tMean).addBands(nMean).set('start_date',startSeason).set('end_date',endSeason)
}))

// Average the seasonal data across the temporal range selected
var bySeasonMean = bySeason.mean()
var precipMean = bySeasonMean.select('p').rename('precip_mean')
var tempMean = bySeasonMean.select('t').multiply(0.02).subtract(273.15).rename('temp_mean');
var ndviMean = bySeasonMean.select('n').rename('ndvi_mean')

// Parse into boolean suitable or not suitable categories based on the user input parameters 
var pBool = precipMean.gt(precipMin).add(precipMean.gt(precipMax)).eq(1).rename('precip_suit')
var tBool = bySeasonMean.select('t').gt(tMinMod).add(bySeasonMean.select('t').gt(tMaxMod)).eq(1).rename('temp_suit')
var cBool = pBool.multiply(tBool).rename('combined_suit')

// Clip layers to agriculture if the user selects 'TRUE'
if (maskToAg == 'TRUE') {
 var maskTool = function (i) {
 return i.updateMask(agMask).updateMask(waterMask)
 }
 precipMean = maskTool(precipMean)
 tempMean = maskTool(tempMean)
 ndviMean = maskTool(ndviMean)
 pBool = maskTool(pBool)
 tBool = maskTool(tBool)
 cBool = maskTool(cBool)
}

// Adds layers to map for visualization 
var colors = ['262626','89d82e']
Map.setOptions('HYBRID').centerObject(bounds)
Map.addLayer(precipMean,{},'precip_mean',false)
Map.addLayer(tempMean,{},'temp_mean',false)
Map.addLayer(ndviMean,{},'ndvi_mean',false)
Map.addLayer(pBool,{palette:colors},'precip_boolean',false)
Map.addLayer(tBool,{palette:colors},'temp_boolean',false)
Map.addLayer(cBool,{palette:colors},'combined_boolean',true)
var empty = ee.Image().byte()
var regionVis = empty.paint({
 featureCollection: bounds,
 width: 3
})
Map.addLayer(regionVis,{palette:'white'},country)

// Prints metadata to console for additional evaluation
print('Country bounds: '+ee.Feature(region.first()).get('country_na').getInfo())
print('Years: '+startRange+'-'+endRange)
print('Season: '+startSeasonMonth+'-'+startSeasonDay+' to '+endSeasonMonth+'-'+endSeasonDay)
print('precipMin = '+precipMin+'; precipMax = '+precipMax)
print('tempMin = '+tempMin+'; tempMax = '+tempMax)
print(bySeason)

// Enables export of all layers if the user selects true
if (exportLayers == 'TRUE') {
 var exportTool = function (i,scale,desc) {
 return Export.image.toDrive({
 image: i,
 description: exportNameHeader+desc+'_'+country,
 fileNamePrefix: exportNameHeader+desc+country,
 scale: scale,
 maxPixels: 1e13,
 region: bounds
 })
 }
 exportTool(precipMean,5000,'_precip_mean')
 exportTool(tempMean,1000,'_temp_mean')
 exportTool(ndviMean,250,'_ndvi_mean')
 exportTool(pBool,5000,'_precip_suit_boolean')
 exportTool(tBool,1000,'_temp_suit_boolean')
 exportTool(cBool,1000,'_combined_suit_boolean')
 print('Check tasks tab for export')
}

// Histogram display options (combined suitability grid)
var panel = ui.Panel({
 layout: ui.Panel.Layout.flow('vertical'),
 style: {position:'bottom-left',border:'1px solid gray',padding:'2px',width:'225px'}
})
var histOptions = {
 title: 'Combined suitability',
 fontSize: 11,
 legend: {position:'none'},
 opacity:1,
 hAxis: {textPosition:'none',gridlineColor:'none',baselineColor:'none'},
 series: {0:{color:colors[0]},1:{color:colors[1]}}
}
var histogram = ui.Chart.image.histogram({
 image: cBool.updateMask(cBool.eq(0)).addBands(cBool.updateMask(cBool.eq(1))),
 region: bounds,
 scale: 1000
}).setOptions(histOptions)

Map.add(panel)
panel.add(histogram)

// Legend creation
var makeRow = function(color, name) {
 var colorBox = ui.Label({
 style: {
 backgroundColor: '#' + color,
 padding: '8px',
 margin: '0 0 0 0',
 }
 })
 var description = ui.Label({
 value: name,
 style: {margin: '0 0 2px 4px', fontSize: '13px'}
 })
 return ui.Panel({
 widgets: [colorBox, description],
 layout: ui.Panel.Layout.Flow('horizontal'),
 style: {margin:'0 0 0 15px'}
 })
}
var names = ['Not suitable','Suitable']
for (var i = 0; i &lt; 2; i++) {
 panel.add(makeRow(colors[i], names[i]));
}
panel.add(ui.Label({
 value:'See console tab for metadata and tasks tab for exports',
 style:{margin:'5px 15px 20px 15px'}
}))

/* ------------------------------------------------------------------------------------------------------------------- */
&lt;/pre&gt;
&lt;br&gt;
&lt;i&gt;
This content is made possible by the support of the American People provided to the Feed the Future Innovation Lab for Sustainable Intensification through the United States Agency for International Development (USAID). The contents are the sole responsibility of the authors and do not necessarily reflect the views of USAID or the United States Government. Program activities are funded by USAID under Cooperative Agreement No. AID-OAA-L-14-00006.&lt;/i&gt;