Earth Engine App
Earth Engine Code Editor script
/**
* @license
* Copyright 2021 The Google Earth Engine Community Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @description
* This app shows a CONUS map of the most recent calculation of Palmer Drought
* Severity Index and a monthly mean time series chart for a selected county
* going back to the year 2000. Click on a county polygon and wait a minute to
* view the time series.The app uses URL parameters to save its state for
* sharing with with colleagues. PDSI are represented by the gridMET
* dataset.
*/
/*******************************************************************************
* Model *
******************************************************************************/
var m = {};
m.palette = {
minRgb: [255, 0, 0],
midRgb: [255, 255, 255],
maxRgb: [0, 0, 255],
minVal: -5,
midVal: 0,
maxVal: 5
};
m.counties = ee.FeatureCollection('TIGER/2018/Counties');
m.drought = ee.ImageCollection('GRIDMET/DROUGHT');
m.droughtBand = 'pdsi';
m.dateRange = ['2000-01-01', Date.now()];
/*******************************************************************************
* Components *
******************************************************************************/
var c = {};
// Panel to hold the chart.
c.chartPanel = ui.Panel();
// Map widget.
c.map = ui.Map();
// Map/chart panel
c.mapChartSplitPanel = ui.SplitPanel({
firstPanel: c.map, //
secondPanel: c.chartPanel,
orientation: 'vertical',
wipe: false,
});
// Messages.
c.clickMessage = ui.Label('🖱️ Click a county to view its PDSI time series.');
c.waitMessage = ui.Label('⚙️ Processing, please wait.');
c.noFeaturesMessage = ui.Label('🙃 There are no features intersecting the ' +
'point you clicked, try again.');
// Element panel.
c.aboutPanel = ui.Panel(
{style: {margin: '0px -8px 0px -8px'}});
// Show/hide info panel button.
c.aboutButton = ui.Button(
{label: 'About ❯', style: {margin: '0px 4px 0px 0px'}});
// Information text.
c.descrLabel = ui.Label(
'This app shows a map of the most recent calculation of Palmer ' +
'Drought Severity Index and a monthly mean time series chart ' +
'for a selected county going back to the year 2000. Click on a ' +
'county polygon and wait a minute to view the time series. ' +
'The app uses URL parameters to save its state for sharing what ' +
'you discover with colleagues.');
c.gridmetLabel = ui.Label('Learn more about the gridMET dataset.', null,
'https://developers.google.com/earth-engine/datasets/catalog/GRIDMET_DROUGHT');
c.descrHolder = ui.Panel([c.descrLabel, c.gridmetLabel]);
// Define a legend widget group.
c.legend = {};
c.legend.title = ui.Label();
c.legend.colorbar = ui.Thumbnail(ee.Image.pixelLonLat().select(0));
c.legend.leftLabel = ui.Label('[min]');
c.legend.centerLabel = ui.Label();
c.legend.rightLabel = ui.Label('[max]');
c.legend.labelPanel = ui.Panel({
widgets: [
c.legend.leftLabel,
c.legend.centerLabel,
c.legend.rightLabel,
],
layout: ui.Panel.Layout.flow('horizontal')
});
c.legend.panel = ui.Panel([
c.legend.title,
c.legend.colorbar,
c.legend.labelPanel
]);
/*******************************************************************************
* Composition *
******************************************************************************/
ui.root.clear();
ui.root.add(c.mapChartSplitPanel);
c.map.add(c.legend.panel);
c.map.add(c.aboutPanel);
c.chartPanel.add(c.clickMessage);
c.aboutPanel.add(c.aboutButton);
c.aboutPanel.add(c.descrHolder);
/*******************************************************************************
* Styling *
******************************************************************************/
var s = {};
s.opacityWhiteMed = {
backgroundColor: 'rgba(255, 255, 255, 0.5)'
};
s.opacityWhiteNone = {
backgroundColor: 'rgba(255, 255, 255, 0)'
};
s.chartStyle = {
height: '325px',
margin: '0px',
padding: '0px'
};
c.map.style().set('cursor', 'crosshair');
c.map.setOptions('Grey', {Grey: mapStyle()});
c.chartPanel.style().set({
height: s.chartStyle.height,
minHeight: s.chartStyle.height,
maxHeight: s.chartStyle.height
});
c.aboutPanel.style().set({
position: 'bottom-right',
backgroundColor: 'rgba(255, 255, 255, 0.5)'
});
c.descrLabel.style().set({
margin: '0px',
backgroundColor: 'rgba(255, 255, 255, 0)',
fontSize: '13px',
color: '505050'
});
c.gridmetLabel.style().set({
margin: '4px 0px 0px 0px',
backgroundColor: 'rgba(255, 255, 255, 0)',
fontSize: '13px',
color: '505050'
});
c.descrHolder.style().set({
shown: false,
width: '250px',
margin: '4px 0px 0px 0px',
padding: '8px 8px 8px 8px',
backgroundColor: 'rgba(255, 255, 255, 0.8)',
});
c.aboutButton.style().set({
margin: '0px 0px 0px 0px'
});
c.legend.title.style().set({
fontWeight: 'bold',
fontSize: '12px',
color: '383838'
});
c.legend.title.style().set(s.opacityWhiteNone);
c.legend.colorbar.style().set({
stretch: 'horizontal',
margin: '0px 8px',
maxHeight: '20px'
});
c.legend.leftLabel.style().set({
margin: '4px 8px',
fontSize: '12px'
});
c.legend.leftLabel.style().set(s.opacityWhiteNone);
c.legend.centerLabel.style().set({
margin: '4px 8px',
fontSize: '12px',
textAlign: 'center',
stretch: 'horizontal'
});
c.legend.centerLabel.style().set(s.opacityWhiteNone);
c.legend.rightLabel.style().set({
margin: '4px 8px',
fontSize: '12px'
});
c.legend.rightLabel.style().set(s.opacityWhiteNone);
c.legend.panel.style().set({
position: 'bottom-left',
width: '200px',
padding: '0px'});
c.legend.panel.style().set(s.opacityWhiteMed);
c.legend.labelPanel.style().set(s.opacityWhiteNone);
/*******************************************************************************
* Behaviors *
******************************************************************************/
// Converts RGB integer component to hex string.
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? '0' + hex : hex;
}
// Converts RGB color list to hex color string.
function rgbToHex(rgb) {
return "#" +
componentToHex(rgb[0]) +
componentToHex(rgb[1]) +
componentToHex(rgb[2]);
}
// Identifies the RGB color for a value in a divergent palette.
function getRgbDivergent(cMin, cMid, cMax, min, mid, max, val) {
var dif, frac, c1, c2, r, g, b;
if (val <= mid) {
val = val < min ? min : val;
dif = mid - min;
frac = Math.abs(val/dif);
c1 = cMin;
c2 = cMid;
r = (c1[0] - c2[0]) * frac + c2[0];
g = (c1[1] - c2[1]) * frac + c2[1];
b = (c1[2] - c2[2]) * frac + c2[2];
} else {
val = val > max ? max : val;
dif = max - mid;
frac = Math.abs(val/dif);
c1 = cMid;
c2 = cMax;
r = (c2[0] - c1[0]) * frac + c1[0];
g = (c2[1] - c1[1]) * frac + c1[1];
b = (c2[2] - c1[2]) * frac + c1[2];
}
var rgb = [];
[r, g, b].forEach(function(c) {
rgb.push(Math.round(c));
});
return rgb;
}
// Adds date information to images.
function addYearWeek(img) {
var date = img.date();
var year = date.get('year');
var week = date.get('month');
var id = ee.Algorithms.String(year).cat(ee.Algorithms.String(week));
return img.set({
year: year,
week: week,
date_id: id,
});
}
// Calculates mean monthly PDSI image collection.
function getPdsiSummary() {
var pdsi = m.drought
.filterDate(m.dateRange[0], m.dateRange[1])
.map(addYearWeek)
.select(m.droughtBand);
var distinctId = pdsi.distinct('date_id');
var filter = ee.Filter.equals({leftField: 'date_id', rightField: 'date_id'});
var join = ee.Join.saveAll('id_matches');
var joinCol = ee.ImageCollection(join.apply(distinctId, pdsi, filter));
var pdsiSummary = joinCol.map(function(img) {
var col = ee.ImageCollection.fromImages(img.get('id_matches'));
var meanTime = col.reduceColumns({
reducer: ee.Reducer.mean(),
selectors: ['system:time_start']
}).get('mean');
meanTime = ee.Number(meanTime).round();
return col.median().set('system:time_start', meanTime);
});
return pdsiSummary;
}
// Generates a PDSI time series chart and adds it to chart panel.
function drawChart(coords) {
var point = ee.Geometry.Point(coords.lon, coords.lat);
var aoi = m.counties.filterBounds(point);
var aoiName = aoi.first().get('NAMELSAD');
var aoiViz = aoi.draw({color: 'black', strokeWidth: 2});
var aoiVizLayer = ui.Map.Layer(aoiViz, null, 'Selected county');
c.map.layers().set(2, aoiVizLayer);
var pdsiSummary = getPdsiSummary();
var pdsiDf = pdsiSummary.map(function(img) {
var stat = img.reduceRegion({
'geometry': aoi.geometry(),
'reducer': ee.Reducer.mean(),
'scale': 4000,
'crs': 'EPSG:5070'
});
return ee.Feature(null, stat)
.set({'millis': img.date().millis()});
}).sort('millis');
var pdsiVal = pdsiDf.aggregate_array(m.droughtBand);
var evalInfo = ee.Dictionary({
pdsiVal: pdsiVal,
aoiName: aoiName,
});
evalInfo.evaluate(function(evalInfo) {
var colors = [];
for (var i in evalInfo.pdsiVal) {
var rgb = getRgbDivergent(
m.palette.minRgb, m.palette.midRgb, m.palette.maxRgb,
m.palette.minVal, m.palette.midVal, m.palette.maxVal,
evalInfo.pdsiVal[i]);
colors.push(rgbToHex(rgb));
}
var chart = ui.Chart.feature.groups(pdsiDf, 'millis', 'pdsi', 'millis')
.setChartType('ColumnChart')
.setOptions({
title: evalInfo.aoiName,
bar: {groupWidth: '100%'},
colors: colors,
legend: {position: 'none'},
vAxis: {
title: 'PDSI',
viewWindow: {min: -7, max: 7},
titleTextStyle: {italic: false, bold: true}
},
hAxis: {
minorGridlines: {count: 0}
},
isStacked: true,
explorer: {axis: 'horizontal'}
});
chart.style().set(s.chartStyle);
c.chartPanel.widgets().reset([chart]);
});
}
// Updates the URL parameters for the location clicked.
function updatePtUrl(coords) {
ui.url.set('ptlon', coords.lon);
ui.url.set('ptlat', coords.lat);
}
// Checks to see of clicked point intersects data, calls drawChart if so.
function checkPoint(coords) {
c.chartPanel.widgets().reset([c.waitMessage]);
var geom = ee.Geometry.Point(coords.lon, coords.lat);
var aoi = m.counties.filterBounds(geom)
.filterBounds(m.drought.first().geometry());
aoi.size().evaluate(function(nFeatures) {
if (nFeatures === 0) {
c.chartPanel.widgets().reset([c.noFeaturesMessage]);
return null;
} else {
drawChart(coords);
updatePtUrl(coords);
}
});
}
c.map.onClick(checkPoint);
// Updates URL parameters that control map bounds on load.
function updateUrlParamMap(newMapParams) {
ui.url.set('lat', newMapParams.lat);
ui.url.set('lon', newMapParams.lon);
ui.url.set('zoom', newMapParams.zoom);
}
c.map.onChangeBounds(ui.util.debounce(updateUrlParamMap, 100));
var infoShow = false;
function infoButtonHandler() {
if(infoShow) {
infoShow = false;
c.descrHolder.style().set('shown', false);
c.aboutButton.setLabel('About ❯');
} else {
infoShow = true;
c.descrHolder.style().set('shown', true);
c.aboutButton.setLabel('About ❮');
}
}
c.aboutButton.onClick(infoButtonHandler);
/*******************************************************************************
* Initialize *
******************************************************************************/
// Adds the most recent PDSI image to the map.
function drawRecentPdsi() {
var recent = m.drought.sort('system:time_start', false).first()
.select(m.droughtBand);
var date = recent.date().format('YYYY-MM-dd').getInfo();
var recentRelLayer = ui.Map.Layer(
recent, {min: m.palette.minVal, max: m.palette.maxVal,
palette: ['red', 'white', 'blue']},
'PDSI for ' + date, true, 0.7);
c.map.layers().set(0, recentRelLayer);
drawLegend(date);
}
// Overlays USA counties on the map.
function drawCounties() {
var countiesViz = ee.Image().byte().paint({
featureCollection: m.counties.filterBounds(m.drought.first().geometry()),
color: 1,
width: 1.5
});
var countiesVizLayer = ui.Map.Layer(countiesViz, {palette: ['484848']}, 'Counties');
c.map.layers().set(1, countiesVizLayer);
}
// Adds PDSI legend to the map.
function drawLegend(imgDate) {
c.legend.title.setValue('PDSI for ' + imgDate);
c.legend.colorbar.setParams({
bbox: [0, 0, 1, 0.1],
dimensions: '100x10',
format: 'png',
min: 0,
max: 1,
palette: ['red', 'white', 'blue']
});
c.legend.leftLabel.setValue(m.palette.minVal);
c.legend.centerLabel.setValue(m.palette.midVal);
c.legend.rightLabel.setValue(m.palette.maxVal);
}
// Set map bounds based on URL params.
c.map.setCenter({
lon: ui.url.get('lon', -95.20),
lat: ui.url.get('lat', 34.80),
zoom: ui.url.get('zoom', 4)
});
// Initialize map.
drawRecentPdsi();
drawCounties();
// Draw chart if a point was clicked according to URL params.
if (ui.url.get('ptlon')) {
var coords = {
lon: ui.url.get('ptlon'),
lat: ui.url.get('ptlat')
};
drawChart(coords);
}
/*******************************************************************************
* Map style *
******************************************************************************/
function mapStyle() {
return [
{
"elementType": "geometry",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"elementType": "labels.icon",
"stylers": [
{
"visibility": "off"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"featureType": "administrative.land_parcel",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#bdbdbd"
}
]
},
{
"featureType": "poi",
"elementType": "geometry",
"stylers": [
{
"color": "#eeeeee"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#e5e5e5"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{
"color": "#ffffff"
}
]
},
{
"featureType": "road.arterial",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [
{
"color": "#dadada"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"featureType": "road.local",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
},
{
"featureType": "transit.line",
"elementType": "geometry",
"stylers": [
{
"color": "#e5e5e5"
}
]
},
{
"featureType": "transit.station",
"elementType": "geometry",
"stylers": [
{
"color": "#eeeeee"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#c9c9c9"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
}
];
}