Skip to content

Instantly share code, notes, and snippets.

@jdbcode
Last active June 22, 2021 21:48
Show Gist options
  • Save jdbcode/181f3009d08d4048f8565c017107f156 to your computer and use it in GitHub Desktop.
Save jdbcode/181f3009d08d4048f8565c017107f156 to your computer and use it in GitHub Desktop.
Landsat 5 and 8 RGB time series explorer
/**
* @license
* Copyright 2021 Justin Braaten
*
* 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
*
* http://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.
*/
// #############################################################################
// ### IMPORT MODULES ###
// #############################################################################
// RGB time series charting module: https://github.com/jdbcode/ee-rgb-timeseries
var rgbTs = require(
'users/jstnbraaten/modules:rgb-timeseries/rgb-timeseries.js');
// #############################################################################
// ### GET URL PARAMS ###
// #############################################################################
var initRun = 'false';
var runUrl = ui.url.get('run', initRun);
ui.url.set('run', runUrl);
var initLon = -121.68804;
var lonUrl = ui.url.get('lon', initLon);
ui.url.set('lon', lonUrl);
var initLat = 36.46517;
var latUrl = ui.url.get('lat', initLat);
ui.url.set('lat', latUrl);
var initIndex = 'NBR';
var indexUrl = ui.url.get('index', initIndex);
ui.url.set('index', indexUrl);
var initRgb = 'SWIR1/NIR/GREEN';
var rgbUrl = ui.url.get('rgb', initRgb);
ui.url.set('rgb', rgbUrl);
var initDuration = 12;
var durationUrl = ui.url.get('duration', initDuration);
ui.url.set('duration', durationUrl);
var initCloud = 30;
var cloudUrl = ui.url.get('cloud', initCloud);
ui.url.set('cloud', cloudUrl);
var initStart = '1984';
var startUrl = ui.url.get('start', initStart);
ui.url.set('start', startUrl);
var initEnd = new Date().getFullYear();
var endUrl = ui.url.get('end', initEnd);
ui.url.set('end', endUrl);
// #############################################################################
// ### DEFINE UI ELEMENTS ###
// #############################################################################
// Style.
var CONTROL_PANEL_WIDTH = '280px';
var CONTROL_PANEL_WIDTH_HIDE = '141px';
var textFont = {fontSize: '12px'};
var headerFont = {
fontSize: '13px', fontWeight: 'bold', margin: '4px 8px 0px 8px'};
var sectionFont = {
fontSize: '16px', color: '#808080', margin: '16px 8px 0px 8px'};
var infoFont = {fontSize: '11px', color: '#505050'};
// Control panel.
var controlPanel = ui.Panel({
style: {position: 'top-left', width: CONTROL_PANEL_WIDTH_HIDE,
maxHeight: '90%'
}});
// Info panel.
var infoElements = ui.Panel(
{style: {shown: false, margin: '0px -8px 0px -8px'}});
// Element panel.
var controlElements = ui.Panel(
{style: {shown: false, margin: '0px -8px 0px -8px'}});
// Instruction panel.
var instr = ui.Label('Click on a location',
{fontSize: '15px', color: '#303030', margin: '0px 0px 6px 0px'});
// Show/hide info panel button.
var infoButton = ui.Button(
{label: 'About ❯', style: {margin: '0px 4px 0px 0px'}});
// Show/hide control panel button.
var controlButton = ui.Button(
{label: 'Options ❯', style: {margin: '0px 0px 0px 0px'}});
// Info/control button panel.
var buttonPanel = ui.Panel(
[infoButton, controlButton],
ui.Panel.Layout.Flow('horizontal'),
{stretch: 'horizontal', margin: '0px 0px 0px 0px'});
// Options label.
var optionsLabel = ui.Label('Options', sectionFont);
optionsLabel.style().set('margin', '16px 8px 2px 8px');
// Information label.
var infoLabel = ui.Label('About', sectionFont);
// Information text.
var aboutLabel = ui.Label(
'This app shows a time series chart for Landsat TM and OLI surface ' +
'reflectance at a given location. Time series ' +
'point colors are defined by RGB assignment to selected bands where ' +
'intensity is based on the area-weighted mean pixel value within a 45 meter ' +
'radius around the clicked point in the map.',
infoFont);
var appCodeLink = ui.Label({
value: 'App source code',
style: {fontSize: '11px', color: '#505050', margin: '-4px 8px 0px 8px'},
targetUrl: 'https://github.com/jdbcode/ee-rgb-timeseries/blob/main/eo-timeseries-explorer.js'
});
// Y-axis index selection.
var indexLabel = ui.Label('Y-axis index', headerFont);
var indexList = ['NBR', 'NDVI', 'Blue', 'Green', 'Red',
'NIR', 'SWIR1', 'SWIR2'];
var indexSelect = ui.Select(
{items: indexList, value: ui.url.get('index'), style: {stretch: 'horizontal'}});
var indexPanel = ui.Panel(
[indexLabel, indexSelect], null, {stretch: 'horizontal'});
// RGB bands selection.
var rgbLabel = ui.Label({value: 'RGB visualization', style: headerFont});
var rgbList = ['SWIR1/NIR/GREEN', 'RED/GREEN/BLUE', 'NIR/RED/GREEN',
'NIR/SWIR1/RED'];
var rgbSelect = ui.Select({
items: rgbList, placeholder: ui.url.get('rgb'),
value: ui.url.get('rgb'), style: {stretch: 'horizontal'}
});
var rgbPanel = ui.Panel([rgbLabel, rgbSelect], null, {stretch: 'horizontal'});
// Duration.
var durationLabel = ui.Label(
{value: 'Duration (months prior)', style: headerFont});
var durationSlider = ui.Slider({
min: 1, max: 24 , value: parseInt(ui.url.get('duration')),
step: 1, style: {stretch: 'horizontal'}
});
var durationPanel = ui.Panel(
[durationLabel, durationSlider], null, {stretch: 'horizontal'});
// Start year
var startYearLabel = ui.Label(
{value: 'Start year', style: headerFont});
var startYearTextbox = ui.Textbox({
value: ui.url.get('start'), style: {stretch: 'horizontal'}
});
var startYearPanel = ui.Panel(
[startYearLabel, startYearTextbox], null, {stretch: 'horizontal'});
// End year
var endYearLabel = ui.Label(
{value: 'End year', style: headerFont});
var endYearTextbox = ui.Textbox({
value: ui.url.get('end'), style: {stretch: 'horizontal'}
});
var endYearPanel = ui.Panel(
[endYearLabel, endYearTextbox], null, {stretch: 'horizontal'});
// Cloud threshold.
var cloudLabel = ui.Label(
{value: 'Cloud threshold % (exclude >)', style: headerFont});
var cloudSlider = ui.Slider({
min: 0, max: 100 , value: parseInt(ui.url.get('cloud')),
step: 1, style: {stretch: 'horizontal'}
});
var cloudPanel = ui.Panel(
[cloudLabel, cloudSlider], null, {stretch: 'horizontal'});
// Panel to hold the chart.
var chartHeight = '325px';
var chartPanel = ui.Panel(
{style: {Height: chartHeight, minHeight: chartHeight, maxHeight: chartHeight}});
// Map widget.
var map = ui.Map();
// Map/chart panel
var mapChartSplitPanel = ui.SplitPanel({
firstPanel: map, //
secondPanel: chartPanel,
orientation: 'vertical',
wipe: false,
});
// Submit changes button.
var submitButton = ui.Button({
label: 'Submit changes',
style: {stretch: 'horizontal', shown: false}
});
// #############################################################################
// ### DEFINE INITIALIZING CONSTANTS ###
// #############################################################################
// Set color of the circle to show on map and images where clicked
var AOI_COLOR = 'ffffff'; //'b300b3';
var COORDS = null;
var CLICKED = false;
// Set region reduction and chart params.
var OPTIONAL_PARAMS = {
reducer: ee.Reducer.mean(),
scale: 20,
crs: 'EPSG:4326',
chartParams: {
pointSize: 11,
legend: {position: 'none'},
hAxis: {title: 'Date', titleTextStyle: {italic: false, bold: true}},
vAxis: {
title: indexSelect.getValue(),
titleTextStyle: {italic: false, bold: true}
},
explorer: {axis: 'horizontal'}
},
chartStyle: {
height: chartHeight,
margin: '0px',
padding: '0px'
}
};
var sensorInfo = {
'Landsat-8 SR': {
scale: 30,
aoiRadius: 45,
index: {
NBR: 'NBR',
NDVI: 'NDVI',
Blue: 'B2',
Green: 'B3',
Red: 'B4',
NIR: 'B5',
SWIR1: 'B6',
SWIR2: 'B7'
},
rgb: {
'SWIR1/NIR/GREEN': {
bands: ['B6', 'B5', 'B3'],
min: [100, 151 , 50],
max: [4500, 4951, 2500],
gamma: [1, 1, 1]
},
'RED/GREEN/BLUE': {
bands: ['B4', 'B3', 'B2'],
min: [0, 50, 50],
max: [2500, 2500, 2500],
gamma: [1.2, 1.2, 1.2]
},
'NIR/RED/GREEN': {
bands: ['B5', 'B4', 'B3'],
min: [151, 0, 50],
max: [4951, 2500, 2500],
gamma: [1, 1, 1]
},
'NIR/SWIR1/RED': {
bands: ['B5', 'B6', 'B3'],
min: [151, 100, 50],
max: [4951, 4500, 2500],
gamma: [1, 1, 1]
}
}
}
};
// #############################################################################
// ### DEFINE FUNCTIONS ###
// #############################################################################
/**
* Selects and renames bands of interest for TM/ETM+.
*/
function renameEtm(img) {
return img.select(
['B1', 'B2', 'B3', 'B4', 'B5', 'B7'],
['B2', 'B3', 'B4', 'B5', 'B6', 'B7']);
}
/**
* Prepar OLI images.
*/
function prepOli(img) {
return addDate(addBands(img));
}
/**
* Prepares TM/ETM+ images.
*/
function prepTm(img) {
return addDate(addBands(renameEtm(img)));
}
/**
* Add NDVI and NBR bands.
*/
function addBands(img) {
var nbr = img.normalizedDifference(['B5', 'B7']).rename(['NBR']);
var ndvi = img.normalizedDifference(['B5', 'B4']).rename('NDVI');
return img.addBands(ee.Image.cat(nbr, ndvi));
}
/**
* Add date property.
*/
function addDate(img) {
var date = img.date().format('YYYY-MM-dd');
return img.set('date', date);
}
/**
* Gathers all Landsat into a collection.
*/
function getLandsatCollection(aoi, startDate, endDate, cloudthresh) {
var tmCol = ee.ImageCollection('LANDSAT/LT05/C01/T1_SR')
.filterBounds(aoi)
.filterDate(startDate, endDate)
.filter(ee.Filter.lt('CLOUD_COVER', cloudthresh))
.map(prepTm);
var oliCol = ee.ImageCollection('LANDSAT/LC08/C01/T1_SR')
.filterBounds(aoi)
.filterDate(startDate, endDate)
.filter(ee.Filter.lt('CLOUD_COVER', cloudthresh))
.map(prepOli);
return tmCol.merge(oliCol);
}
/**
* Generates chart and adds image cards to the image panel.
*/
function renderGraphics(coords) {
var dataType = 'Landsat-8 SR';
// Get the selected RGB combo vis params.
var visParams = sensorInfo[dataType]['rgb'][rgbSelect.getValue()];
// Get the clicked point and buffer it.
var point = ee.Geometry.Point(coords);
var aoiCircle = point.buffer(sensorInfo[dataType]['aoiRadius']);
// Clear previous point from the Map.
map.layers().forEach(function(el) {
map.layers().remove(el);
});
// Add new point to the Map.
map.addLayer(aoiCircle, {color: AOI_COLOR});
map.centerObject(aoiCircle, 14);
// Get collection options.
var cloudThresh = cloudSlider.getValue();
var startDate = ee.Date(startYearTextbox.getValue() + '-01-01');
var endDate = ee.Date(endYearTextbox.getValue() + '-01-01').advance(1, 'year');
// Build the collection.
var col = getLandsatCollection(aoiCircle, startDate, endDate, cloudThresh).sort('system:time_start');
print('len', col.size())
OPTIONAL_PARAMS['chartParams']['vAxis']['title'] = indexSelect.getValue();
OPTIONAL_PARAMS['scale'] = sensorInfo[dataType]['scale'];
// Render the time series chart.
rgbTs.rgbTimeSeriesChart(col, aoiCircle,
sensorInfo[dataType]['index'][indexSelect.getValue()],
sensorInfo[dataType]['rgb'][rgbSelect.getValue()],
chartPanel, OPTIONAL_PARAMS);
}
/**
* Handles map clicks.
*/
function handleMapClick(coords) {
CLICKED = true;
COORDS = [coords.lon, coords.lat];
ui.url.set('run', 'true');
ui.url.set('lon', COORDS[0]);
ui.url.set('lat', COORDS[1]);
renderGraphics(COORDS);
}
/**
* Handles submit button click.
*/
function handleSubmitClick() {
renderGraphics(COORDS);
submitButton.style().set('shown', false);
}
/**
* Sets URL params.
*/
function setParams() {
ui.url.set('index', indexSelect.getValue());
ui.url.set('rgb', rgbSelect.getValue());
ui.url.set('cloud', cloudSlider.getValue());
ui.url.set('start', startYearTextbox.getValue());
ui.url.set('end', endYearTextbox.getValue());
}
/**
* Show/hide the submit button.
*/
function showSubmitButton() {
if(CLICKED) {
submitButton.style().set('shown', true);
}
}
/**
* Handles options changes.
*/
function optionChange() {
showSubmitButton();
setParams();
}
/**
* Show/hide the control panel.
*/
var controlShow = false;
function controlButtonHandler() {
if(controlShow) {
controlShow = false;
controlElements.style().set('shown', false);
controlButton.setLabel('Options ❯');
} else {
controlShow = true;
controlElements.style().set('shown', true);
controlButton.setLabel('Options ❮');
}
if(infoShow | controlShow) {
controlPanel.style().set('width', CONTROL_PANEL_WIDTH);
} else {
controlPanel.style().set('width', CONTROL_PANEL_WIDTH_HIDE);
}
}
/**
* Show/hide the control panel.
*/
var infoShow = false;
function infoButtonHandler() {
if(infoShow) {
infoShow = false;
infoElements.style().set('shown', false);
infoButton.setLabel('About ❯');
} else {
infoShow = true;
infoElements.style().set('shown', true);
infoButton.setLabel('About ❮');
}
if(infoShow | controlShow) {
controlPanel.style().set('width', CONTROL_PANEL_WIDTH);
} else {
controlPanel.style().set('width', CONTROL_PANEL_WIDTH_HIDE);
}
}
// #############################################################################
// ### SETUP UI ELEMENTS ###
// #############################################################################
infoElements.add(infoLabel);
infoElements.add(aboutLabel);
infoElements.add(appCodeLink);
controlElements.add(optionsLabel);
controlElements.add(indexPanel);
controlElements.add(rgbPanel);
controlElements.add(startYearPanel);
controlElements.add(endYearPanel);
controlElements.add(cloudPanel);
controlElements.add(submitButton);
controlPanel.add(instr);
controlPanel.add(buttonPanel);
controlPanel.add(infoElements);
controlPanel.add(controlElements);
map.add(controlPanel);
infoButton.onClick(infoButtonHandler);
controlButton.onClick(controlButtonHandler);
rgbSelect.onChange(optionChange);
indexSelect.onChange(optionChange);
startYearTextbox.onChange(optionChange);
endYearTextbox.onChange(optionChange);
durationSlider.onChange(optionChange);
cloudSlider.onChange(optionChange);
submitButton.onClick(handleSubmitClick);
map.onClick(handleMapClick);
map.style().set('cursor', 'crosshair');
map.setOptions('SATELLITE');
map.setControlVisibility(
{layerList: false, fullscreenControl: false, zoomControl: false});
//map.centerObject(ee.Geometry.Point([-122.91966, 44.24135]), 14);
ui.root.clear();
ui.root.add(mapChartSplitPanel);
if(ui.url.get('run')) {
CLICKED = true;
COORDS = [ui.url.get('lon'), ui.url.get('lat')];
renderGraphics(COORDS);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment