├── LICENSE ├── README.md ├── materials └── images │ └── 2_cover.png └── projects ├── p1-data-availability-timeseries └── main-GEE-APP.js ├── p2-s1-boxplots └── main.js └── p3-lst-uhi └── main.JS /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mirza Waleed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google-Earth-Engine-Projects 2 | A repository containing open-sourced code of my projects on the use of Google Earth Engine for various environmental applications. The codes are written in JavaScript (JS) and Python (PY) languages. Any suggestions and contributions are welcome. 3 | 4 | This repository contains various Google Earth Engine (Javascript and Python) based: 5 | - Applications 6 | - Tools 7 | - Scripts 8 | - YouTube Workshops/SeminarsTutorials 9 | - Blogs 10 | 11 | 12 | Note: Click tool name heading, to see its details & links. 13 | 14 | ## List of Tools 15 | 16 | - [Image Date Acquisition Tool for Visualizing Image Availability for Sentinel-1, Sentinel-2, Landsat-8 and Landsat-9](#1-image-dates-acquisition-tool-for-visualizing-image-availability-for-sentinel-1-sentinel-2-landsat-8-and-landsat-9) 17 | 18 | - [Sentinel-1 Backscatter Profiles for Land-use Using Earth Engine: A Box Plot Approach](#2-sentinel-1-backscatter-profiles-for-land-use-using-earth-engine-a-box-plot-approach) 19 | 20 | - [Land Surface Temperature (LST), Urban Heat Island (UHI), and Urban Thermal Field Variance Index (UTFVI) analysis using Landsat](#3-land-surface-temperature-lst-urban-heat-island-uhi-and-urban-thermal-field-variance-index-utfvi-analysis-using-landsat) 21 | 22 | --- 23 | 24 | ### 1. Image Dates Acquisition Tool for Visualizing Image Availability for Sentinel-1, Sentinel-2, Landsat-8 and Landsat-9 25 | [![Figure](https://imgur.com/j5q117P.png)](https://waleedgis.users.earthengine.app/view/imagedates) 26 | 27 | ( 28 | [GEE App](https://waleedgis.users.earthengine.app/view/imagedates) 29 | | 30 | [Advanced Python Version Notebook](https://github.com/waleedgeo/geotools/blob/main/tools/01_image_dates/image_dates.ipynb) 31 | ) 32 | 33 | ### 2. Sentinel-1 Backscatter Profiles for Land-use Using Earth Engine: A Box Plot Approach 34 | 35 | [![Figure](https://imgur.com/GZknPWq.png)](https://youtu.be/3Yexo6Q--tk) 36 | 37 | ( 38 | [Blog Post](https://waleedgeo.medium.com/sentinel-1-backscatter-profiles-for-land-use-using-earth-engine-a-box-plot-approach-76dba22d378f) 39 | | 40 | [Video Tutorial](https://youtu.be/3Yexo6Q--tk) 41 | | 42 | [GEE JS Code](https://github.com/waleedgeo/geotools/blob/main/tools/02_s1boxplot/s1_boxplot.js)) 43 | 44 | ### 3. Land Surface Temperature (LST), Urban Heat Island (UHI), and Urban Thermal Field Variance Index (UTFVI) analysis using Landsat 45 | 46 | [![Cover](https://imgur.com/j1TKyk0.png)](https://youtu.be/5W84zme9QmE) 47 | 48 | ( 49 | [Video Tutorial](https://youtu.be/5W84zme9QmE) 50 | | 51 | [GEE JS Code](https://github.com/waleedgeo/EarthEngineProjects/blob/main/projects/p3-lst-uhi/main.JS)) 52 | 53 | 54 | --- 55 | 56 | ## Contributing 57 | 58 | Contributions in the form of additional tools, improvements, or bug fixes are welcome! Please open an issue or submit a pull request. 59 | 60 | ## License 61 | 62 | This project is licensed under the MIT License. You are free to use any code/resources given that you give attribution to the original author. 63 | 64 | [If you find my work usefull, consider citing my relevant work.](https://waleedgeo.com/publication/) 65 | -------------------------------------------------------------------------------- /materials/images/2_cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/waleedgeo/EarthEngineProjects/4739c15a9b38013b94a0b24eabe2c4a18a7d566f/materials/images/2_cover.png -------------------------------------------------------------------------------- /projects/p1-data-availability-timeseries/main-GEE-APP.js: -------------------------------------------------------------------------------- 1 | // ***************************************************************************************** 2 | // ***** Image Dates Acquisition Tool - GEE APP CODE ***** 3 | // This tool is used to find the available satellite images 4 | // in a given time period and for a given area of interest. 5 | // 6 | // The tools is also available as a python version at: 7 | // https://github.com/waleedgeo/geotools/blob/main/tools/01_image_dates/image_dates.ipynb 8 | // 9 | // Checkout my webpage: https://waleedgeo.com 10 | // 11 | // Author: Mirza Waleed 12 | // Date: 2023-07-01 13 | // Email: waleedgeo@outlook.com 14 | // 15 | // ***************************************************************************************** 16 | 17 | 18 | // Inputs ********************************************************************************** 19 | 20 | var geometry = ui.import && ui.import("geometry", "geometry", { 21 | "geometries": [], 22 | "displayProperties": [], 23 | "properties": {}, 24 | "color": "#23cba7", 25 | "mode": "Geometry", 26 | "shown": true, 27 | "locked": false 28 | }) || /* color: #23cba7 */ee.Geometry.MultiPoint(); 29 | 30 | var startDateInput = ui.Textbox('Start Date (YYYY-MM-dd):') 31 | 32 | var endDateInput = ui.Textbox('End Date (YYYY-MM-dd):') 33 | 34 | 35 | // ***************************************************************************************** 36 | 37 | var styleBox = { 38 | padding: '0px 0px 0px 0px', 39 | width: '250px', 40 | } 41 | 42 | var styleH1 = { 43 | fontWeight: 'bold', 44 | fontSize: '18px', 45 | margin: '5px 5px 5px 5px', 46 | padding: '0px 0px 0px 0px', 47 | color: 'green' 48 | } 49 | 50 | var styleH2 = { 51 | fontWeight: 'bold', 52 | fontSize: '14px', 53 | margin: '5px 5px', 54 | // padding: '0px 15px 0px 0px', 55 | color: 'black' 56 | } 57 | 58 | var styleP = { 59 | fontSize: '12px', 60 | margin: '5px 5px', 61 | padding: '0px 0px 0px 0px' 62 | } 63 | 64 | var symbol = { 65 | rectangle: '⬛', 66 | polygon: '🔺', 67 | }; 68 | 69 | var drawingTools = Map.drawingTools(); 70 | 71 | drawingTools.setShown(false); 72 | 73 | while (drawingTools.layers().length() > 0) { 74 | var layer = drawingTools.layers().get(0); 75 | drawingTools.layers().remove(layer); 76 | } 77 | 78 | var nullGeometry = 79 | ui.Map.GeometryLayer({ 80 | geometries: null, 81 | name: 'geometry', 82 | color: '23cba7' 83 | }); 84 | 85 | drawingTools.layers().add(nullGeometry); 86 | 87 | function clearGeometry() { 88 | var layers = drawingTools.layers(); 89 | layers.get(0).geometries().remove(layers.get(0).geometries().get(0)); 90 | } 91 | 92 | function drawRectangle() { 93 | clearGeometry(); 94 | drawingTools.setShape('rectangle'); 95 | drawingTools.draw(); 96 | } 97 | 98 | function drawPolygon() { 99 | clearGeometry(); 100 | drawingTools.setShape('polygon'); 101 | drawingTools.draw(); 102 | } 103 | 104 | var chartPanel = ui.Panel({ 105 | style: { 106 | position: 'bottom-left', 107 | padding: '4px', 108 | width: '700px', 109 | shown: false 110 | }, 111 | }) 112 | 113 | //Map.add(chartPanel) 114 | 115 | 116 | 117 | function charting() { 118 | 119 | if (!chartPanel.style().get('shown')) { 120 | chartPanel.style().set('shown', true); 121 | } 122 | 123 | var aoi = drawingTools.layers().get(0).getEeObject(); 124 | 125 | drawingTools.setShape(null); 126 | var startDate = startDateInput.getValue(); 127 | var endDate = endDateInput.getValue(); 128 | 129 | var col1 = ee.ImageCollection("COPERNICUS/S1_GRD") 130 | .filterDate(startDate, endDate).filterBounds(aoi) 131 | var col2 = ee.ImageCollection("COPERNICUS/S2") 132 | .filterDate(startDate, endDate).filterBounds(aoi) 133 | var col3 = ee.ImageCollection("LANDSAT/LC08/C02/T1_L2") 134 | .filterDate(startDate, endDate).filterBounds(aoi) 135 | var col4 = ee.ImageCollection("LANDSAT/LC09/C02/T1_L2") 136 | .filterDate(startDate, endDate).filterBounds(aoi) 137 | 138 | 139 | var col1_range = col1.reduceColumns(ee.Reducer.toList(), ["system:time_start"]) 140 | .values().get(0) 141 | col1_range = ee.List(col1_range) 142 | .map(function(n){ 143 | return ee.Date(n).format("YYYY-MM-dd") 144 | }) 145 | var col2_range = col2.reduceColumns(ee.Reducer.toList(), ["system:time_start"]) 146 | .values().get(0) 147 | col2_range = ee.List(col2_range) 148 | .map(function(n){ 149 | return ee.Date(n).format("YYYY-MM-dd") 150 | }) 151 | var col3_range = col3.reduceColumns(ee.Reducer.toList(), ["system:time_start"]) 152 | .values().get(0) 153 | col3_range = ee.List(col3_range) 154 | .map(function(n){ 155 | return ee.Date(n).format("YYYY-MM-dd") 156 | }) 157 | var col4_range = col4.reduceColumns(ee.Reducer.toList(), ["system:time_start"]) 158 | .values().get(0) 159 | col4_range = ee.List(col4_range) 160 | .map(function(n){ 161 | return ee.Date(n).format("YYYY-MM-dd") 162 | }) 163 | 164 | var all_dates = col1_range.distinct() 165 | .cat(col2_range.distinct()) 166 | .cat(col3_range.distinct()) 167 | .cat(col4_range.distinct()) 168 | .distinct().sort() 169 | 170 | var col1_dict = col1_range.reduce(ee.Reducer.frequencyHistogram()) 171 | var col1_dict = ee.Dictionary(col1_dict) 172 | var col2_dict = col2_range.reduce(ee.Reducer.frequencyHistogram()) 173 | var col2_dict = ee.Dictionary(col2_dict) 174 | var col3_dict = col3_range.reduce(ee.Reducer.frequencyHistogram()) 175 | var col3_dict = ee.Dictionary(col3_dict) 176 | var col4_dict = col4_range.reduce(ee.Reducer.frequencyHistogram()) 177 | var col4_dict = ee.Dictionary(col4_dict) 178 | //print(asc_avail_dict, desc_avail_dict) 179 | 180 | var col1_feat = col1_dict.map(function(date, n){ 181 | return ee.Feature(ee.Geometry.Point(77.58, 13), {label: date, number_images:n, s1: n, s2: 0, l8:0, l9:0, weight:1}) 182 | }).values() 183 | var col2_feat = col2_dict.map(function(date, n){ 184 | return ee.Feature(ee.Geometry.Point(77.58, 13), {label: date, number_images:n, s1: 0, s2: n, l8:0, l9:0, weight:1}) 185 | }).values() 186 | var col3_feat = col3_dict.map(function(date, n){ 187 | return ee.Feature(ee.Geometry.Point(77.58, 13), {label: date, number_images:n, s1: 0, s2: 0, l8:n, l9:0, weight:1}) 188 | }).values() 189 | var col4_feat = col4_dict.map(function(date, n){ 190 | return ee.Feature(ee.Geometry.Point(77.58, 13), {label: date, number_images:n, s1: 0, s2: 0, l8:0, l9:n, weight:1}) 191 | }).values() 192 | 193 | 194 | 195 | var comb_feat = col1_feat.cat(col2_feat).cat(col3_feat).cat(col4_feat) 196 | var comb_col = ee.FeatureCollection(comb_feat); 197 | //print(comb_col) 198 | 199 | // map over dates 200 | var merged_collection = ee.FeatureCollection(all_dates.map(function(date){ 201 | var new_feat_collection1 = comb_col.filter(ee.Filter.equals('label', date)) 202 | var s1_sum = new_feat_collection1.reduceColumns({ 203 | reducer: ee.Reducer.sum(), 204 | selectors: ['s1'], 205 | weightSelectors: ['weight'] 206 | }).get('sum') 207 | var s2_sum = new_feat_collection1.reduceColumns({ 208 | reducer: ee.Reducer.sum(), 209 | selectors: ['s2'], 210 | weightSelectors: ['weight'] 211 | }).get('sum') 212 | var l8_sum = new_feat_collection1.reduceColumns({ 213 | reducer: ee.Reducer.sum(), 214 | selectors: ['l8'], 215 | weightSelectors: ['weight'] 216 | }).get('sum') 217 | var l9_sum = new_feat_collection1.reduceColumns({ 218 | reducer: ee.Reducer.sum(), 219 | selectors: ['l9'], 220 | weightSelectors: ['weight'] 221 | }).get('sum') 222 | 223 | var merged = comb_col.filter(ee.Filter.equals('label', date)) 224 | .union().first() 225 | .set({label:date, s1:s1_sum, s2:s2_sum, l8:l8_sum, l9:l9_sum}) 226 | 227 | 228 | return(merged) 229 | })) 230 | 231 | 232 | // Define the chart and print it to the console. 233 | var chart = ui.Chart.feature 234 | .byFeature({ 235 | features: merged_collection.select('s1', 's2', 'l8', 'l9', 'label'), 236 | xProperty: 'label' 237 | }) 238 | .setChartType('ColumnChart') 239 | .setOptions({ 240 | width: 600, 241 | height: 600, 242 | title: 'Satellite Image Availability', 243 | hAxis: {title: 'Dates', titleTextStyle: {italic: false, bold: true}}, 244 | vAxis: {title: 'Number of images', 245 | titleTextStyle: {italic: false, bold: true} 246 | }, 247 | colors: ['blue', 'green', 'red', 'purple'], 248 | isStacked: 'absolute' 249 | }) 250 | 251 | chartPanel.widgets().reset([chart]); 252 | 253 | } 254 | 255 | 256 | 257 | var panel = { 258 | title: ui.Label({ 259 | value: 'Satellite Image Availability Tool', 260 | style: styleH1 261 | }), 262 | sec_panel: ui.Label({ 263 | value: 'Satellites: Sentinel-1,2, & Landsat-8,9', 264 | style: { 265 | fontWeight: 'bold', 266 | fontSize: '12px', 267 | margin: '5px 5px 5px 5px', 268 | padding: '0px 0px 0px 0px', 269 | color: 'green' 270 | } 271 | }), 272 | sub_title: ui.Label({ 273 | value: 'Input start and end dates, then draw area and see the available images.', 274 | style: styleP 275 | }), 276 | Time: ui.Label({ 277 | value: 'Time Period :', 278 | style: { 279 | fontWeight: 'bold', 280 | fontSize: '14px', 281 | margin: '5px 5px', 282 | // padding: '0px 15px 0px 0px', 283 | color: 'black' 284 | } 285 | }), 286 | 287 | area_list: ui.Label({ 288 | value: 'Drawing tools :', 289 | style: styleH2 290 | }), 291 | draw_rectangle: ui.Button({ 292 | label: symbol.rectangle + ' Rectangle', 293 | onClick: drawRectangle, 294 | style: { 295 | stretch: 'horizontal' 296 | } 297 | }), 298 | draw_poly: ui.Button({ 299 | label: symbol.polygon + ' Polygon', 300 | onClick: drawPolygon, 301 | style: { 302 | stretch: 'horizontal' 303 | } 304 | }), 305 | 306 | } 307 | 308 | var panel_fill = ui.Panel({ 309 | widgets: [ 310 | panel.title, 311 | panel.sec_panel, 312 | panel.sub_title, 313 | panel.Time, 314 | startDateInput, 315 | endDateInput, 316 | panel.area_list, 317 | panel.draw_rectangle, 318 | panel.draw_poly, 319 | ui.Label('About :', styleH2), 320 | ui.Label({ 321 | value: 'Mirza Waleed', 322 | style: { 323 | fontWeight: 'bold', 324 | fontSize: '12px', 325 | margin: '3px 5px' 326 | } 327 | }), 328 | ui.Label({ 329 | value: 'Email : waleedgeo@outlook.com', 330 | style: { 331 | fontSize: '12px', 332 | margin: '3px 5px' 333 | } 334 | }).setUrl('mailto:waleedgeo@outlook.com)'), 335 | ui.Label({ 336 | value: 'Website: waleedgeo.com', 337 | style: { 338 | fontSize: '12px', 339 | margin: '3px 5px' 340 | } 341 | }).setUrl('https://waleedgeo.com'), 342 | ui.Label({ 343 | value: 'LinkedIn: WaleedGeo', 344 | style: { 345 | fontSize: '12px', 346 | margin: '3px 5px' 347 | } 348 | }).setUrl('https://www.linkedin.com/in/waleedgeo'), 349 | ui.Label({ 350 | value: 'Note: An advanced python version of this tool is also available at:', 351 | style: { 352 | fontSize: '12px', 353 | margin: '3px 5px' 354 | } 355 | }), 356 | ui.Label({ 357 | value: 'Advanced Python Version', 358 | style: { 359 | fontSize: '12px', 360 | margin: '3px 5px' 361 | } 362 | }).setUrl('https://github.com/waleedgeo/geotools/blob/main/tools/01_image_dates/image_dates.ipynb'), 363 | ], 364 | style: { 365 | margin: '4px', 366 | position: 'bottom-left', 367 | width: '200' 368 | }, 369 | 370 | }) 371 | 372 | // Map.setCenter( 114.88, -1.17,5) 373 | drawingTools.onDraw(ui.util.debounce(charting, 300)) 374 | drawingTools.onEdit(ui.util.debounce(charting, 300)) 375 | 376 | ui.root.add(panel_fill) 377 | 378 | ui.root.add(chartPanel) 379 | Map.setCenter(0, 0, 2); -------------------------------------------------------------------------------- /projects/p2-s1-boxplots/main.js: -------------------------------------------------------------------------------- 1 | // ***************************************************************************************** 2 | // 3 | // This code visualizes the distribution of Sentinel-1 backscatter values 4 | // for each land cover class, using box plots. 5 | // For reference see the tutorial: https://www.youtube.com/watch?v=3Yexo6Q--tk 6 | // Checkout my webpage: https://waleedgeo.com 7 | // 8 | // Author: Mirza Waleed 9 | // Date: 2023-09-16 10 | // Email: waleedgeo@outlook.com 11 | // 12 | // Note: 13 | // The origional credit for this script goes to Ujaval Gandhi, check his work here https://twitter.com/spatialthoughts/status/1690429199562260480 14 | // I have modified it to work with Sentinel-1 data (VV and VH bands) and to work with the WorldCover dataset. 15 | // ***************************************************************************************** 16 | 17 | 18 | // INPUTS ********************************************************************************** 19 | 20 | var aoi = ee.Geometry.Polygon([[ 21 | [76.816, 13.006],[76.816, 12.901], 22 | [76.899, 12.901],[76.899, 13.006] 23 | ]]); 24 | 25 | var startDate = '2021-01-01' 26 | var endDate = '2022-01-01' 27 | 28 | // optional paramters 29 | 30 | // chart limits 31 | var chartMin = -30 32 | var chartMax = 0 33 | // sentinel 1 parameters 34 | var orbitProperties_pass = 'DESCENDING'// 'ASCENDING' or 'DESCENDING' 35 | 36 | // -------------------------------------- 37 | 38 | // 2. Sentinel-1 filtering 39 | // importing sentinel-1 collection 40 | var s1 = ee.ImageCollection('COPERNICUS/S1_GRD') 41 | .filter(ee.Filter.eq('orbitProperties_pass', orbitProperties_pass)) 42 | .select(['VV', 'VH']) 43 | .map(function(image) { 44 | var edge = image.lt(-30.0); 45 | var maskedImage = image.mask().and(edge.not()); 46 | return image.updateMask(maskedImage); 47 | }); 48 | 49 | var filtered = s1 50 | .filter(ee.Filter.bounds(aoi)) 51 | .filter(ee.Filter.date(startDate, endDate)) 52 | 53 | 54 | // Create a median composite for 2021 55 | var composite = filtered.median(); 56 | 57 | 58 | // 3. Land cover sampling 59 | // We use the ESA WorldCover 2021 dataset 60 | var worldcover = ee.ImageCollection('ESA/WorldCover/v200').first(); 61 | 62 | // The image has 11 classes 63 | // Remap the class values to have continuous values 64 | // from 0 to 10 65 | var classified = worldcover.remap( 66 | [10, 20, 30, 40, 50, 60, 70, 80, 90, 95, 100], 67 | [0, 1 , 2, 3, 4, 5, 6, 7, 8, 9, 10]).rename('classification'); 68 | 69 | // Define a list of class names 70 | var worldCoverClassNames= [ 71 | 'Tree Cover', 'Shrubland', 'Grassland', 'Cropland', 'Built-up', 72 | 'Bare / sparse Vegetation', 'Snow and Ice', 73 | 'Permanent Water Bodies', 'Herbaceous Wetland', 74 | 'Mangroves', 'Moss and Lichen']; 75 | // Define a list of class colors 76 | var worldCoverPalette = [ 77 | '006400', 'ffbb22', 'ffff4c', 'f096ff', 'fa0000', 78 | 'b4b4b4', 'f0f0f0', '0064c8', '0096a0', '00cf75', 79 | 'fae6a0']; 80 | // We define a dictionary with class names 81 | var classNames = ee.Dictionary.fromLists( 82 | ['0','1','2','3','4','5','6','7','8','9', '10'], 83 | worldCoverClassNames 84 | ); 85 | // We define a dictionary with class colors 86 | var classColors = ee.Dictionary.fromLists( 87 | ['0','1','2','3','4','5','6','7','8','9', '10'], 88 | worldCoverPalette 89 | ); 90 | 91 | // 4. Sampling and DataTable creation for Box Plots 92 | // We sample backscatter values from S1 image for each class 93 | var samples = composite.addBands(classified) 94 | .stratifiedSample({ 95 | numPoints: 50, 96 | classBand: 'classification', 97 | region: aoi, 98 | scale: 10, 99 | tileScale: 16, 100 | geometries: true 101 | }); 102 | 103 | // To create a box plot, we need minimum, maximum, 104 | // median and 25- and 75-percentile values for 105 | // each band for each class 106 | var bands = composite.bandNames(); 107 | var properties = bands.add('classification'); 108 | 109 | // Now we have multiple columns, so we have to repeat the reducer 110 | var numBands = bands.length(); 111 | 112 | // We need the index of the group band 113 | var groupIndex = properties.indexOf('classification'); 114 | 115 | // Create a combined reducer for all required statistics 116 | var allReducers = ee.Reducer.median() 117 | .combine({reducer2: ee.Reducer.min(), sharedInputs: true} ) 118 | .combine({reducer2: ee.Reducer.max(), sharedInputs: true} ) 119 | .combine({reducer2: ee.Reducer.percentile([25]), sharedInputs: true} ) 120 | .combine({reducer2: ee.Reducer.percentile([75]), sharedInputs: true} ) 121 | 122 | // Repeat the combined reducer for each band and 123 | // group results by class 124 | var stats = samples.reduceColumns({ 125 | selectors: properties, 126 | reducer: allReducers.repeat(numBands).group({ 127 | groupField: groupIndex}), 128 | }); 129 | var groupStats = ee.List(stats.get('groups')); 130 | print(groupStats); 131 | 132 | // We do some post-processing to format the results 133 | 134 | var spectralStats = ee.FeatureCollection(groupStats.map(function(item) { 135 | var itemDict = ee.Dictionary(item); 136 | var classNumber = itemDict.get('group'); 137 | // Extract the stats 138 | // Create a featute for each statistics for each class 139 | var stats = ee.List(['median', 'min', 'max', 'p25', 'p75']); 140 | // Create a key such as VV_min, VV_max, etc. 141 | var keys = stats.map(function(stat) { 142 | var bandKeys = bands.map(function(bandName) { 143 | return ee.String(stat).cat('_').cat(bandName); 144 | }) 145 | return bandKeys; 146 | }).flatten(); 147 | // Extract the values 148 | var values = stats.map(function(stat) { 149 | return itemDict.get(stat); 150 | }).flatten(); 151 | var properties = ee.Dictionary.fromLists(keys, values); 152 | var withClass = properties 153 | .set('class', classNames.get(classNumber)) 154 | .set('class_number', classNumber); 155 | return ee.Feature(null, withClass); 156 | })); 157 | 158 | 159 | // 5. Charting 160 | // Now we need to create a backscatter signature chart 161 | // for each class. 162 | 163 | // Write a function to create a chart for each class 164 | var createChart = function(className) { 165 | var classFeature = spectralStats.filter(ee.Filter.eq('class', className)).first(); 166 | var classNumber = classFeature.get('class_number'); 167 | var classColor = classColors.get(classNumber); 168 | // X-Axis has Band Names, so we create a row per band 169 | var rowList = bands.map(function(band) { 170 | var stats = ee.List(['median', 'min', 'max', 'p25', 'p75']); 171 | var values = stats.map(function(stat) { 172 | var key = ee.String(stat).cat('_').cat(band); 173 | var value = classFeature.get(key); 174 | return {v: value} 175 | }); 176 | // Row name is the first value 177 | var rowValues = ee.List([{v: band}]); 178 | // Append other values 179 | rowValues = rowValues.cat(values); 180 | 181 | var rowDict = { 182 | c: rowValues 183 | }; 184 | return rowDict; 185 | }); 186 | // We need to convert the server-side rowList and 187 | // classColor objects to client-side javascript object 188 | // use evaluate() 189 | rowList.evaluate(function(rowListClient) { 190 | classColor.evaluate(function(classColor) { 191 | var dataTable = { 192 | cols: [ 193 | {id: 'x', type: 'string', role: 'domain'}, 194 | {id: 'median', type: 'number', role: 'data'}, 195 | {id: 'min', type: 'number', role: 'interval'}, 196 | {id: 'max', type: 'number', role: 'interval'}, 197 | {id: 'firstQuartile', type: 'number', role: 'interval'}, 198 | {id: 'thirdQuartile', type:'number', role: 'interval'}, 199 | ], 200 | rows: rowListClient 201 | }; 202 | 203 | var options = { 204 | title:'BackScatter Profile for Class: ' + className, 205 | vAxis: { 206 | title: 'Backscatter (dB)', 207 | gridlines: { 208 | color: '#d9d9d9' 209 | }, 210 | minorGridlines: { 211 | color: 'transparent' 212 | }, 213 | viewWindow: { 214 | min:chartMin, 215 | max:chartMax 216 | } 217 | }, 218 | hAxis: { 219 | title: 'Bands', 220 | gridlines: { 221 | color: '#d9d9d9' 222 | }, 223 | minorGridlines: { 224 | color: 'transparent' 225 | } 226 | }, 227 | legend: {position: 'none'}, 228 | lineWidth: 1, 229 | interpolateNulls: true, 230 | curveType: 'function', 231 | series: [{'color': classColor}], 232 | intervals: { 233 | barWidth: 0.5, 234 | boxWidth: 0.5, 235 | lineWidth: 1, 236 | style: 'boxes', 237 | fillOpacity: 1, 238 | 239 | }, 240 | interval: { 241 | min: { 242 | style: 'bars', 243 | }, 244 | max: { 245 | style: 'bars', 246 | } 247 | }, 248 | chartArea: {left:100, right:100} 249 | }; 250 | 251 | var chart = ui.Chart(dataTable, 'LineChart', options); 252 | print(chart); 253 | }); 254 | 255 | }) 256 | 257 | 258 | }; 259 | 260 | // We get a list of classes 261 | var classNames = spectralStats.aggregate_array('class'); 262 | // Call the function for each class name to create the chart 263 | print('Creating charts. Please wait...'); 264 | classNames.evaluate(function(classNames) { 265 | for (var i = 0; i < classNames.length; i++) { 266 | createChart(classNames[i]); 267 | } 268 | }); 269 | Map.centerObject(aoi, 12); 270 | Map.addLayer(aoi, {color: 'gray'}, 'AOI'); 271 | var rgbVis = {min: -25, max: 5, bands: ['VV', 'VV', 'VH']}; 272 | Map.addLayer(composite.clip(aoi), rgbVis, '2020 Composite'); 273 | var worldCoverVisParams = {min:0, max:10, palette: worldCoverPalette}; 274 | Map.addLayer(classified.clip(aoi), worldCoverVisParams, 'Landcover'); 275 | print('Stratified Samples', samples); 276 | Map.addLayer(samples, {color: 'red'}, 'Samples'); 277 | print('Average Backscatter Values for Each Class', spectralStats); -------------------------------------------------------------------------------- /projects/p3-lst-uhi/main.JS: -------------------------------------------------------------------------------- 1 | // ***************************************************************************************** 2 | // 3 | // This script calculates the Land Surface Temperature (LST), 4 | // Urban Heat Island (UHI) and the Urban Thermal Field Variance Index (UTFVI) 5 | // for a given area of interest (AOI) and startdate and endate using Landsat 8 data. 6 | // 7 | // For reference see the tutorial: https://youtu.be/5W84zme9QmE 8 | // Check the studies following this approach: https://waleedgeo.com/publication/waleed-leveraging-2022/ 9 | // 10 | // Author: Mirza Waleed 11 | // Date: 2023-09-16 12 | // Email: waleedgeo@outlook.com 13 | // 14 | // ***************************************************************************************** 15 | 16 | 17 | // INPUTS` ********************************************************************************** 18 | 19 | // AOI 20 | var aoi = ee.Geometry.Polygon( 21 | [[[-3.8225909618719167, 40.53241723973516], 22 | [-3.8225909618719167, 40.233239070237204], 23 | [-3.4737750439031667, 40.233239070237204], 24 | [-3.4737750439031667, 40.53241723973516]]]); 25 | var startDate = '2022-05-01' 26 | var endDate = '2022-12-31' 27 | 28 | // ***************************************************************************************** 29 | 30 | // Applies scaling factors. 31 | function applyScaleFactors(image) { 32 | var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2); 33 | var thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0); 34 | return image.addBands(opticalBands, null, true) 35 | .addBands(thermalBands, null, true); 36 | } 37 | 38 | //cloud mask 39 | function maskL8sr(col) { 40 | // Bits 3 and 5 are cloud shadow and cloud, respectively. 41 | var cloudShadowBitMask = (1 << 3); 42 | var cloudsBitMask = (1 << 5); 43 | // Get the pixel QA band. 44 | var qa = col.select('QA_PIXEL'); 45 | // Both flags should be set to zero, indicating clear conditions. 46 | var mask = qa.bitwiseAnd(cloudShadowBitMask).eq(0) 47 | .and(qa.bitwiseAnd(cloudsBitMask).eq(0)); 48 | return col.updateMask(mask); 49 | } 50 | 51 | // Filter the collection, first by the aoi, and then by date. 52 | var image = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2') 53 | .filterDate(startDate, endDate) 54 | .filterBounds(aoi) 55 | .map(applyScaleFactors) 56 | .map(maskL8sr) 57 | .median(); 58 | 59 | var visualization = { 60 | bands: ['SR_B4', 'SR_B3', 'SR_B2'], 61 | min: 0.0, 62 | max: 0.3, 63 | }; 64 | 65 | Map.addLayer(image, visualization, 'True Color (432)', false); 66 | 67 | // NDVI 68 | var ndvi = image.normalizedDifference(['SR_B5', 'SR_B4']).rename('NDVI') 69 | Map.addLayer(ndvi, {min:-1, max:1, palette: ['blue', 'white', 'green']}, 'ndvi', false) 70 | 71 | // ndvi statistics 72 | var ndvi_min = ee.Number(ndvi.reduceRegion({ 73 | reducer: ee.Reducer.min(), 74 | geometry: aoi, 75 | scale: 30, 76 | maxPixels: 1e9 77 | }).values().get(0)) 78 | 79 | var ndvi_max = ee.Number(ndvi.reduceRegion({ 80 | reducer: ee.Reducer.max(), 81 | geometry: aoi, 82 | scale: 30, 83 | maxPixels: 1e9 84 | }).values().get(0)) 85 | 86 | 87 | // fraction of veg 88 | var fv = (ndvi.subtract(ndvi_min).divide(ndvi_max.subtract(ndvi_min))).pow(ee.Number(2)) 89 | .rename('FV') 90 | 91 | 92 | var em = fv.multiply(ee.Number(0.004)).add(ee.Number(0.986)).rename('EM') 93 | 94 | var thermal = image.select('ST_B10').rename('thermal') 95 | 96 | // This code addresses GitHub Issue #1: Incorrect LST formula parameters 97 | // Corrected λ to 11.5 μm and ρ to 14380 μm·K for consistent units. 98 | 99 | var lst = thermal.expression( 100 | '(tb / (1 + ((11.5 * (tb / 14380)) * log(em)))) - 273.15', 101 | { 102 | 'tb': thermal.select('thermal'), // Brightness temperature in Kelvin 103 | 'em': em // Emissivity 104 | } 105 | ).rename('LST'); 106 | 107 | var lst_vis = { 108 | min: 7, 109 | max: 50, 110 | palette: [ 111 | '040274', '040281', '0502a3', '0502b8', '0502ce', '0502e6', 112 | '0602ff', '235cb1', '307ef3', '269db1', '30c8e2', '32d3ef', 113 | '3be285', '3ff38f', '86e26f', '3ae237', 'b5e22e', 'd6e21f', 114 | 'fff705', 'ffd611', 'ffb613', 'ff8b13', 'ff6e08', 'ff500d', 115 | 'ff0000', 'de0101', 'c21301', 'a71001', '911003'] 116 | } 117 | 118 | Map.addLayer(lst, lst_vis, 'LST AOI') 119 | Map.centerObject(aoi, 10) 120 | 121 | // Urban Heat Island *********************************************************************** 122 | 123 | //1. Normalized UHI 124 | 125 | var lst_mean = ee.Number(lst.reduceRegion({ 126 | reducer: ee.Reducer.mean(), 127 | geometry: aoi, 128 | scale: 30, 129 | maxPixels: 1e9 130 | }).values().get(0)) 131 | 132 | 133 | var lst_std = ee.Number(lst.reduceRegion({ 134 | reducer: ee.Reducer.stdDev(), 135 | geometry: aoi, 136 | scale: 30, 137 | maxPixels: 1e9 138 | }).values().get(0)) 139 | 140 | 141 | 142 | print('Mean LST in AOI', lst_mean) 143 | print('STD LST in AOI', lst_std) 144 | 145 | 146 | var uhi = lst.subtract(lst_mean).divide(lst_std).rename('UHI') 147 | 148 | var uhi_vis = { 149 | min: -4, 150 | max: 4, 151 | palette:['313695', '74add1', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', 'e31a1c', 152 | 'b10026'] 153 | } 154 | Map.addLayer(uhi, uhi_vis, 'UHI AOI') 155 | 156 | // Urban Thermal Field variance Index (UTFVI) 157 | 158 | var utfvi = lst.subtract(lst_mean).divide(lst).rename('UTFVI') 159 | var utfvi_vis = { 160 | min: -1, 161 | max: 0.3, 162 | palette:['313695', '74add1', 'fed976', 'feb24c', 'fd8d3c', 'fc4e2a', 'e31a1c', 163 | 'b10026'] 164 | } 165 | Map.addLayer(utfvi, utfvi_vis, 'UTFVI AOI') 166 | 167 | // ***************************************************************************************** 168 | --------------------------------------------------------------------------------