Last active
June 22, 2021 21:48
-
-
Save jdbcode/181f3009d08d4048f8565c017107f156 to your computer and use it in GitHub Desktop.
Landsat 5 and 8 RGB time series explorer
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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