forked from AhmedMAMahmoud/EAMENA-MachineLearning-ACD
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEAMENA-MLACD-JavaScript-Code
More file actions
1502 lines (1465 loc) · 70.8 KB
/
EAMENA-MLACD-JavaScript-Code
File metadata and controls
1502 lines (1465 loc) · 70.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// To access the EAMENA MLACD JavaScript code copy the following URL (https://code.earthengine.google.com/943a52e4f1c9a1ec52558198a784da0b), or you can copy the full script below however, this means it will not inlcude the assets for the case study demonstration.
// This script is developed to rapidly monitor the changes and threats at and around archaeological sites in Bani Walid region using satellite images.
// This EAMENA Machine Learning ACD script has been developed by EAMENA Research Associate Dr. Ahmed Mahmoud.
// For a comprehensive understanding of the tool, see the full research paper here:
// Mahmoud, et al. 2024, A Novel Machine Learning Automated Change Detection Tool for Monitoring Disturbances and Threats to Archaeological Sites, https://doi.org/10.1016/j.rsase.2024.101396.
// In July 2025 some parts of the developed and published MLACD script have been further automated using AI to improve the user experiance
//*****************************************************************************************************************************************************************//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//**************************************************** Section 1 - Defining Variables & Inputs ******************************************************************//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Description: Adding Feature Collections, in this stage all the feature classes have to be imported and called into the code editor in preparation for
// spatial and statistical analysis; image classification, time series analysis
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//****************************************************** Define the study area *********************************************************************************//
var Study_Area = ee.FeatureCollection(Study_Area).geometry();
Map.centerObject(Study_Area);
Map.addLayer(Study_Area, {color: 'blue'}, 'Study_Area', false);
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//******************************************** Define or import the Archaeological Sites *************************************************************************//
// var Sites = ee.FeatureCollection("projects/ee-eamena-libya/assets/BeniUlid_Sites");
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//************************************************* Define Training Samples & Classification Variables *************************************************************//
// // Define the training samples datasets, colours and labels for each class in your case study
var trainingClasses = [
{TrainingSample: BareSoil, color: '#ffeec3', label: 'BareSoil'}, // class 1
{TrainingSample: Mountain, color: '#170821', label: 'Mountain'}, // class 2
{TrainingSample: Quarry, color: '#c7c6c5', label: 'Quarry'}, // class 3
{TrainingSample: Urban, color: '#cdc2a8', label: 'Urban'}, // class 4
{TrainingSample: Vegetation, color: '#118b29', label: 'Vegetation'}, // class 5
];//Add and define additional training samples (e.g., TrainingSample: Water, color: '#2591ff', label: 'Water') or remove any class that is not relevant to your case study
//****************** Export training samples datasets ********************************************//
// Loop through and export each class
trainingClasses.forEach(function(item) {
Export.table.toDrive({
collection: item.TrainingSample,
description: item.label + '_TrainingSample',
fileNamePrefix: item.label + '_TrainingSample',
fileFormat: 'SHP'
});
});
//***********************************************************************************************************************************************************************//
//******************************************************************************************************************************//
//*********************************** Section 2 - Adding Image collections and Feature collections ***************************//
//******************************************************************************************************************************//
// 2.1 Import Sentinel 2 Multi spectral instrument L2A images
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// cloud mask using Scene Classification Layer (SCL) bit values only the vegetation (4), Bare soils (5),
// Water (6) and unclassified (7) were ketp the rest were masked out //
function s2ClearSky(image) {
var scl = image.select('SCL');
var clear_sky_pixels = scl.eq(4).or(scl.eq(5)).or(scl.eq(6)).or(scl.eq(7));
return image.updateMask(clear_sky_pixels).divide(10000).copyProperties(image, ["system:time_start"])}
// Define a fucntion to add a NDVI band to the image collection for NDVI time series analysis
function addNDVI(image){
var ndvi = image.normalizedDifference(['B8', 'B4']).rename('ndvi')
return image.addBands([ndvi])
}
// Define a fucntion to add a Moisture index band to the image collection for NDMI time series analysis
function addNDMI(image){
var ndmi = image.normalizedDifference(['B8A', 'B11']).rename('ndmi')
return image.addBands([ndmi])
}
// Define a fucntion to add a Normalized Difference Water Index (NDWI) band to the image collection for NDMI time series analysis
function addNDWI(image){
var ndwi = image.normalizedDifference(['B3', 'B8']).rename('ndwi')
return image.addBands([ndwi])
}
//******************************************************************************************************************************//
// Define Initial Inputs by the User to Initiate the processing
// This snippet of the script allows the user to add their own dates of interest, and AOI geometry or point (P), which will be used to filter out the area for the image collection
var startDate = ui.Textbox({
placeholder: 'Start: YYYY-MM-DD',
onChange: function(value) {
startDate.setValue(value);
return(value);
}
});
var endDate = ui.Textbox({
placeholder: 'End: YYYY-MM-DD',
onChange: function(value) {
endDate.setValue(value);
return(value);
}
});
// This function is developed to generate a buffer for each site location by allowing users to define the buffer value in meters
var featureBuffer = ui.Textbox({
placeholder: 'Buffer in meters',
onChange: function (value){
featureBuffer.setValue(value);
return value;
}});
// Create a run button to excuate the first stage of the ACD script
var RunButton = ui.Button({
label: 'Run'
});
// Create a run button to excuate the second stage of the ACD script
var RunButton2 = ui.Button({
label: 'Run'
});
// Create feature collection for user points
var userPoints = []; // Defined for the step of Automated Classification Time Series Charts Point Locations Identified by the User
/****************************************************************************************************************/
// Build arrays automatically for trainingSets, classArray and classesPalette
var trainingSets = trainingClasses.map(function(c) {
return c.TrainingSample; // FeatureCollection for classification
});
var classArray = trainingClasses.map(function(c) {
return c.label; // Class name / label
});
var classesPalette = trainingClasses.map(function(c) {
return c.color; // Hex color for visualization
});
// print('Training Sets:', trainingSets);
// print('Class Array:', classArray);
// print('Classes Palette:', classesPalette);
//******************************************************
// Set and define the classes values
// Create an empty array to store the index values for each class from classArray
var classValues = [];
// Loop through the input array and add the index values to the new list
for (var i = 0; i < classArray.length; i++) {
classValues.push(i);
}
// Print the new list of index values
// print(classValues);
//*********** This snippet is to automate the change detection step and allow the user to choose which classification change results to analyze
// Define the setup function outside the click handler but keep it ready to use
var changeSelector, analyzeButton, changeAnalysisLabel;
var changes = null;
var changesPerSite = null;
// ===== SETUP CHANGE ANALYSIS UI =====
function setupChangeAnalysisUI() {
// Generate change combinations
var changeLabels = [];
for (var i = 0; i < classValues.length; i++) {
for (var j = 0; j < classValues.length; j++) {
changeLabels.push(classArray[i] + " to " + classArray[j]);
}
}
// Create UI elements (only once)
if (!changeSelector) {
changeSelector = ui.Select({
items: changeLabels,
placeholder: 'Select Change Type'
});
analyzeButton = ui.Button({
label: 'Analyse Selected Change',
disabled: true
});
changeAnalysisLabel = ui.Label('7-Change Detection Analysis');
changeAnalysisLabel.style().set('fontWeight', 'bold');
}
}
//************************************************************************************************************//
RunButton.onClick(function(){
var homogeneousBands = ['B1','B2','B3','B4','B5','B6','B7','B8','B8A','B9','B11','B12',
'AOT','WVP','SCL','TCI_R','TCI_G','TCI_B','QA10','QA20','QA60',
'MSK_CLASSI_OPAQUE','MSK_CLASSI_CIRRUS','MSK_CLASSI_SNOW_ICE',
'MSK_CLDPRB','MSK_SNWPRB'];
var startDateValue = startDate.getValue();
var endDateValue = endDate.getValue();
var SenImageCollection = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
.filterDate(startDateValue, endDateValue)
.filterBounds(Study_Area)
.filter(ee.Filter.lt('CLOUD_COVERAGE_ASSESSMENT',0.5))
.map(function(img){
return img.select(homogeneousBands);
});
print('SenImageCollection', SenImageCollection);
// print('SenImageCollection', SenImageCollection);
var listOfimages = SenImageCollection.toList(SenImageCollection.size());
// Map.addLayer(ee.Image(listOfimages.get(0)), {bands: ['B8','B3', 'B2'], min: 0, max: 3000}, '0');
// Get the list of the image acquisition dates
var Imagedates = SenImageCollection.map(function(image){
return ee.Feature(null, {'date': image.date().format('YYYY-MM-dd'), 'id': image.id()});
})
.distinct('date')
.aggregate_array('date');
// print ('Imagedates', Imagedates);
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Create a mosaic of the images that have been aquired on the same day to have a single image that covers the whole study area
// Modified after https://gis.stackexchange.com/users/167933/jean
//https://gis.stackexchange.com/questions/369057/google-earth-engine-roi-area-falling-outside-partial-coverage-by-sentinel-1-tile
function makeMosaics(image) {
var j = ee.Image(image);
var date = ee.Date(j.get('system:time_start'));
// add only hour to exclude images from other paths
var filteredDataset = SenImageCollection.filterDate(date, date.advance(1,'hour'));
// add all image properties to the new image
var toReturn = ee.Image(filteredDataset.mosaic().copyProperties(image,image.propertyNames()));
// add geometries
var geometries = filteredDataset.map(function(i){
return ee.Feature(i.geometry());
});
var mergedGeometries = geometries.union();
return toReturn.set('system:footprint', mergedGeometries.geometry());
}
var mosaicedImages = SenImageCollection.map(makeMosaics);
// Select images where the entire study area falls within the footprint of the image
var filteredmosaicedCollection = mosaicedImages.filter(ee.Filter.contains('.geo', Study_Area));
// print('filteredmosaicedCollection size:', filteredmosaicedCollection.size());
var filteredmosaicedImages = filteredmosaicedCollection;
// print(filteredmosaicedImages, 'filteredmosaicedImages');
//Convert the filtered mosaiced Image collection to a list
var filteredmosaicedImagesList = filteredmosaicedImages.toList(filteredmosaicedImages.size());
// print (filteredmosaicedImagesList);
// Filter out images (i) with null bands in the image collection. remove images with no data
// This has been modified after https://gis.stackexchange.com/users/45066/xunilk, based on
// https://gis.stackexchange.com/questions/387737/filter-null-bands-from-image-collection-in-gee
var newList = filteredmosaicedImagesList.map(function comprobeBandsNumber(i){
var new_list = ee.List([]);
var count = ee.Image(i).bandNames().size();
var comp = ee.Algorithms.If(count.eq(26), i, 0);
new_list = new_list.add(comp);
return new_list;
}).flatten();
//removing zeroes in new list
newList = newList.removeAll([0]);
// print("new list", newList);
// //convert from list to image collection and Add NDVI, NDMI & NDWI bands to the Image Collection
var mosaicedImageCollection = ee.ImageCollection(newList).map(s2ClearSky)
.map(addNDVI).map(addNDMI).map(addNDWI).map(function(image){return image.clip(Study_Area)});
// print (mosaicedImageCollection, "maskedmosaicedImageCollection");
// Get the date for the monthly images
var ImageDates = mosaicedImageCollection.map(function(image){
return ee.Feature(null, {'date': image.date().format('YYYY-MM-dd'), 'id': image.id()});
})
.distinct('date')
.aggregate_array('date');
print ('Image Dates', ImageDates);
// // Add NDVI, NDMI & NDWI bands to the Image Collection
var s2FilteredCollection = mosaicedImageCollection.map(addNDVI).map(addNDMI).map(addNDWI);
print("Sentinel-2 Filtered Image Collection", s2FilteredCollection);
//************************************************************************************************************************//
// *****This section allows users to select a specific year for training data collection ************//
// Global variable to store the selected year's image collection
var trainingCollection;
// Available years for selection (adjust based on your data availability)
var years = ['2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024','2025', '2026', '2027', '2028','2029', '2030'];
// Create UI panel for year selection (positioned at top-left of map)
var trainingYearpanel = ui.Panel({
style: {position: 'top-left', padding: '8px'}
});
// Label explaining the dropdown purpose
var trainingYearlabel = ui.Label({value:'Select Year for Training Data', style:{fontSize: 30, fontWeight: 'bold'}});
// Dropdown menu for year selection - updates trainingCollection when changed
var selectYear = ui.Select({
items: years,
onChange: function(selectedYear) {
// Filter main collection to include only images from selected year
trainingCollection = createTrainingCollection(selectedYear);
// Display the filtered collection in console for verification
print ('Training Image Collection', trainingCollection);
}
});
// Add UI elements to the panel and map
trainingYearpanel.add(trainingYearlabel);
trainingYearpanel.add(selectYear);
Map.add(trainingYearpanel);
// Function that filters the main Sentinel-2 collection to a specific calendar year
function createTrainingCollection(year) {
// Create start date (January 1st of selected year)
var startOfYear = ee.Date.fromYMD(ee.Number.parse(year), 1, 1);
// Create end date (December 31st of selected year)
var endOfYear = ee.Date.fromYMD(ee.Number.parse(year), 12, 31);
// Return filtered collection containing only images from the selected year
return s2FilteredCollection.filterDate(startOfYear, endOfYear);
}
// Initialize trainingCollection with default year when script loads
trainingCollection = createTrainingCollection(selectYear.getValue());
// This function is developed to generate a buffer for each site location based on the value imported by the user
var featureBufferValue = parseFloat(featureBuffer.getValue()); // parseFloat parses a value number defined by the user on the user interface
function buffer(feature){
var feature_buffer = feature.buffer(featureBufferValue);
return feature_buffer;
}
var Sites_buffer = Sites.map(buffer);
// print(Sites, 'Sites');
// Create an empty image into which to paint the features, cast to byte.
var empty = ee.Image().byte();
// Paint all the polygon edges with the same number and width, display.
var Sites_buffer_polygons = empty.paint({
featureCollection: Sites_buffer,
color: 1,
width: 3
});
Map.addLayer(Sites_buffer_polygons, {palette: 'FF0000'}, 'Sites buffer',false);
Map.addLayer(Sites, {color: 'blue'}, 'Sites',false);
//************************************************************************************************************************//
// Merge the training polygons into one FeatureCollection
var sampleDatasetFC = ee.FeatureCollection(trainingSets).flatten();
// Add a random number to each polygon
var polygonsWithRand = sampleDatasetFC.randomColumn('rand', 42);
// Split polygons into training (70%) and validation (30%) sets
var polygonTrain = polygonsWithRand.filter(ee.Filter.lt('rand', 0.7));
var polygonVal = polygonsWithRand.filter(ee.Filter.gte('rand', 0.7));
// visualize polygon split
// Map.addLayer(polygonTrain, {color:'green'}, 'Training Polygons', false);
// Map.addLayer(polygonVal, {color:'orange'}, 'Validation Polygons', false);
// ************************************ STRATIFIED SAMPLING FROM POLYGONS ************************************ //
// Convert polygon sets into raster masks
var polygonTrainImg = ee.Image().byte().paint(polygonTrain, 'landcover').rename('landcover');
var polygonValImg = ee.Image().byte().paint(polygonVal, 'landcover').rename('landcover');
// Collect stratified samples from training polygons
var training_samples = polygonTrainImg.stratifiedSample({
numPoints: 3000, // total points across all classes
classBand: 'landcover',
region: Study_Area,
scale: 10,
classValues: [0,1,2,3,4,5,6,7,8,9],
classPoints: [300,300,300,300,300,300,300,300,300,300],
dropNulls: true,
geometries: true,
seed: 11
});
// Collect stratified samples from validation polygons
var validation_samples = polygonValImg.stratifiedSample({
numPoints: 1000,
classBand: 'landcover',
region: Study_Area,
scale: 10,
classValues: [0,1,2,3,4,5,6,7,8,9],
classPoints: [100,100,100,100,100,100,100,100,100,100],
dropNulls: true,
geometries: true,
seed: 22
});
Map.addLayer(training_samples, {color: 'red'}, 'Training Samples', false);
Map.addLayer(validation_samples, {color: 'blue'}, 'Validation Samples', false);
//*********************************************************************************************************//
// Export Feature Collections
//*********************************************************************************************************//
//Export the study area shapefile
//If you want to export the dataset for each training samples you can change the name of collection instead of BareSoil change it to the new feature collection and then uncomment the lines between 259-264
Export.table.toDrive({
collection: ee.FeatureCollection(Study_Area),
description: 'Study_Area',
fileFormat: 'SHP'
});
//Export the BaniWalid_Sites shapefile
Export.table.toDrive({
collection: ee.FeatureCollection(Sites),
description: 'Sites',
fileFormat: 'shp'
});
//Export the training samples shapefile
Export.table.toDrive({
collection: ee.FeatureCollection(training_samples),
description: 'Training_Samples',
fileFormat: 'shp'
});
//Export the stratified test samples shapefile
Export.table.toDrive({
collection: ee.FeatureCollection(validation_samples),
description: 'validation_samples',
fileFormat: 'shp'
});
//Export the bareSoil training dataset shapefile
Export.table.toDrive({
collection: ee.FeatureCollection(Sites_buffer),
description: 'Sites_buffer',
fileFormat: 'shp'
});
// //Export the mountain training dataset shapefile
// Export.table.toDrive({
// collection: ee.FeatureCollection(Mountain),
// description: 'MountainDataset',
// fileFormat: 'shp'
// });
// // Export the quarry training dataset shapefile
// Export.table.toDrive({
// collection: ee.FeatureCollection(Quarry),
// description: 'QuarryDataset',
// fileFormat: 'shp'
// });
// //Export the urban training dataset shapefile
// Export.table.toDrive({
// collection: ee.FeatureCollection(Urban),
// description: 'UrbanDataset',
// fileFormat: 'shp'
// });
// // Export the vegetation training dataset shapefile
// Export.table.toDrive({
// collection: ee.FeatureCollection(Vegetation),
// description: 'VegetationDataset',
// fileFormat: 'shp'
// });
//************************************************************************************************************************//
//************************************************************************************************************************//
// All the variables used in the select widget must be declared first using "var" to be considered as variables
// Create a drop down menu for the image dates so that the user can select the prior image ID to compare with the post image
var first_ImageDate, first_ImageDateID, second_ImageDate, second_ImageDateID;
var first_ImageIDValue = ui.Select({
items: ImageDates.getInfo(),
placeholder: 'Select First Image',
onChange: function selectedfirst_ImageDate() {
first_ImageDate = first_ImageIDValue.getValue();
first_ImageDateID = ImageDates.indexOf(first_ImageDate);
// print(first_ImageDate);
// print(first_ImageDateID);
},
style: {width: '200px'}
});
// print (first_ImageIDValue);
Map.add(first_ImageIDValue);
// Create a drop down menu for the image dates so that the user can select the post image ID to compare with the prior image
var second_ImageIDValue = ui.Select({
items: ImageDates.getInfo(),
placeholder: 'Select Second Image',
onChange: function selectedsecond_ImageDate() {
second_ImageDate = second_ImageIDValue.getValue();
second_ImageDateID = ImageDates.indexOf(second_ImageDate);
// print(second_ImageDate);
// print(second_ImageDateID);
},
style: {width: '200px'}
});
// print (second_ImageIDValue);
Map.add(second_ImageIDValue);
//******************************************************************************************//
RunButton2.onClick(function(){
//**********************************************************************************************************************//
//*************************************** Section 3 - Image Collection Visualization ***********************************//
//**********************************************************************************************************************//
// Visualzation of a single image//
var listOfimages = s2FilteredCollection.toList(s2FilteredCollection.size());
// print(listOfimages, 'listOfimages');
// This snippt is developed to select the two images using the images dates identifed by the user
var first_Image = ee.Image(listOfimages.get(first_ImageDateID));
var second_Image = ee.Image(listOfimages.get(second_ImageDateID));
// print(first_Image);
// print(second_Image);
Map.addLayer(first_Image, {bands: ['B8','B3', 'B2'], min: 0, max: 0.3}, 'First Image',false);
Map.addLayer(second_Image, {bands: ['B8', 'B3', 'B2'], min: 0, max: 0.3},'Second Image',false);
// // Create RGB visualization images to use as animation frames.
// // Define GIF visualization parameters.
var gifParams = {
'region': Study_Area,
'dimensions': 600,
'crs': SenImageCollection.select('B1').first().projection(),
'framesPerSecond': 1,
// 'min':0,
// 'max':30,
'maxPixels': 1e200
};
var text = require('users/gena/packages:text'); // Import gena's package which allows text overlay on image
var annotations = [
{position: 'left', offset: '1%', margin: '1%', property: 'label', scale: 200} //large scale because image if of the whole world. Use smaller scale otherwise
]
function addText(image){
var timeStamp = ee.Date(image.get('system:time_start')).format().slice(0,10); // get the time stamp of each frame. This can be any string. Date, Years, Hours, etc.
var timeStamp = ee.String('Date: ').cat(ee.String(timeStamp)); //convert time stamp to string
var image = image.visualize({bands: ['B8', 'B3', 'B2'], min: 0, max: 6.5}).set({'label':timeStamp}); // set a property called label for each image
var annotated = text.annotateImage(image, {}, Study_Area, annotations); // create a new image with the label overlayed using gena's package
return annotated
}
var ImageCollectionGif = s2FilteredCollection.map(addText) //add time stamp to all images
// print(ImageCollectionGif.getVideoThumbURL(gifParams)); //print gif
// var filmArgs = {
// dimensions: 128,
// region: Study_Area,
// crs: 'EPSG:3263'
// };
// Print a URL that will produce a filmstrip when accessed.
// print(ImageCollectionGif.getFilmstripThumbURL(filmArgs));
//*************************************************************************************************************//
//**************************** Section 4 - Image Differencing, and Indices NDVI, NDMI, NDWI ******************//
//*************************************************************************************************************//
// Calculate Image Differencing between two dates
var prior_image = first_Image.select(['B1','B2', 'B3', 'B4','B5','B6','B7','B8', 'B8A','B9','B11','B12']);
var post_image = second_Image.select(['B1','B2', 'B3', 'B4','B5','B6','B7','B8', 'B8A','B9','B11','B12']);
var imageDiff = post_image.select(['B8']).subtract(prior_image.select(['B8']));
// print('imageDiff', imageDiff);
Map.addLayer(imageDiff, {min: -1, max: 1, palette:['red', 'yellow', 'green']}, 'Image Difference',false);
//********************************************************************************************************//
// Calculate Normalized Difference Indices (NDVI), (NDMI), (NDWI)
var prior_imageNDVI = ((prior_image.select('B8')).subtract(prior_image.select('B4'))).divide((prior_image.select('B8')).add(prior_image.select('B4')));
var post_imageNDVI = ((post_image.select('B8')).subtract(post_image.select('B4'))).divide((post_image.select('B8')).add(post_image.select('B4')));
var NDVI_Palette = ['red', 'yellow', 'green'];
Map.addLayer(prior_imageNDVI, {min: -1, max: 1, palette: NDVI_Palette}, 'First Image NDVI',false);
Map.addLayer(post_imageNDVI, {min: -1, max: 1, palette: NDVI_Palette}, 'Second Image NDVI',false);
//*******************************************************************************************************************************//
//************ Section 5 - Image Classification - Supervised Classification using Machine Learning Models (i.e. Random Forest)***//
//*******************************************************************************************************************************//
// // Define the spectral samples polygons to understand the responses of each of the classes to different image bands moreover,
// // to understand how different land cover types interact with the enrgey coming from the sun and how features reflect and absorb light
// var spectralSamples = ee.FeatureCollection(
// trainingSets.map(function(ts, i) {
// return ts.first().set('class', classArray[i]);
// })
// );
// Map.addLayer(spectralSamples, {palette: classesPalette},'spectralSamples', false);
// print(spectralSamples, 'spectralSamples');
// Set the colors for the class series to plot the line charts
// for each series and class with the color identified in the classes palette
var classseries = {};
for (var i = 0; i < classArray.length; i++) {
classseries[i] = {
color: classesPalette[i],
label: classArray[i]
};
}
// // Convert spectralSamples to a list of features with a 'class' property
// var labeledSamples = spectralSamples.map(function(feat, i) {
// return feat.set('class', classArray[i]);
// });
// var classificationSpectralChart = ui.Chart.image.regions({ image: prior_image,
// regions: spectralSamples,
// reducer: ee.Reducer.mean(),
// scale: 10,
// seriesProperty: 'class' // <-- Tells the chart to group by class name
// })
// .setOptions({
// title: 'Surface Spectral Reflectance', titleTextStyle: {fontSize:35, italic: false, bold: true},
// vAxis: {title: 'Reflectance', titleTextStyle: {fontSize:30, italic: false, bold: true}, textStyle: {fontSize: 20, bold: true}},
// hAxis: {title: 'Image Band', titleTextStyle: {fontSize:30, italic: false, bold: true}, gridlines: {count:20, color: 'black'}, textStyle: {fontSize: 20, bold: true}},
// interpolateNulls: true,
// lineWidth: 5,
// pointSize: 15,
// series: classseries,
// // seriesNames: classArray, // <-- THIS forces legend labels
// });
// var classificationSpectralChartButton = ui.Button('Press to get the Classification Spectral Reflectance Chart for the Identified Classification Features');
// classificationSpectralChartButton.onClick(function(){
// print(classificationSpectralChart);
// });
// print (classificationSpectralChartButton);
// Set visualization parameters to display the different classes in the classified image min = 0, max = class value of the last class
var class_vis = {min:classValues[0], max: classValues[classValues.length -1], palette: classesPalette};
//***********************************************************************************************************************************************//
//************ Train the Random Forest classifier using the training samples to classify the image collection *****************//
// Define the spectral and index bands to use for classification
var selectedBands = ['B1','B2','B3','B4','B5','B6','B7','B8','B8A','B9','B11','B12','ndvi','ndwi'];
// Add a 'month' band to each image for seasonal context
var s2WithMonthBand = s2FilteredCollection.map(function(image) {
var month = ee.Date(image.get('system:time_start')).get('month').int();
return image
.select(selectedBands)
.addBands(ee.Image.constant(month).rename('month').toInt())
.set('month', month)
.copyProperties(image, ['system:time_start']);
});
// Add a 'month' band to each image for seasonal context
var monthlytrainingCollection = trainingCollection.map(function(image) {
var month = ee.Date(image.get('system:time_start')).get('month').int();
return image
.select(selectedBands)
.addBands(ee.Image.constant(month).rename('month').toInt())
.set('month', month)
.copyProperties(image, ['system:time_start']);
});
// Final list of bands used for classification (spectral + month)
var finalBands = selectedBands.concat(['month']);
// Generate training samples by sampling each image and flattening all into one FeatureCollection
var monthlyTrainingSamples = monthlytrainingCollection.map(function(image) {
return image.sampleRegions({
collection: training_samples,
properties: ['landcover'],
scale: 10,
tileScale: 16
});
}).flatten(); // Combine all per-image samples
// Export the training dataset csv
Export.table.toDrive({
collection: monthlyTrainingSamples,
description: 'Classifier Training Dataset',
fileFormat: 'csv'
});
// Train the Random Forest classifier using the combined training samples
var trainedClassifier = ee.Classifier.smileRandomForest({
numberOfTrees: 100,
variablesPerSplit: null,
minLeafPopulation: 5,
bagFraction: 0.6,
maxNodes: null,
seed: 0
}).train({
features: monthlyTrainingSamples,
classProperty: 'landcover',
inputProperties: finalBands
});
// print(trainedClassifier,'trainedClassifier');
// print('Feature importance:', trainedClassifier.explain().get('importance'));
// Function to classify new Sentinel-2 images using the trained classifier
function classifyImageWithRF(image) {
var month = ee.Date(image.get('system:time_start')).get('month').int();
var imageWithMonth = image
.select(selectedBands)
.addBands(ee.Image.constant(month).rename('month').toInt());
return imageWithMonth
.classify(trainedClassifier)
.copyProperties(image, ['system:time_start']);
}
// Apply classifier to all images in the filtered Sentinel-2 collection
var RFclassified_imageCollection = s2WithMonthBand.map(classifyImageWithRF);
var RFclassified_imageCollectionlist = RFclassified_imageCollection.toList(RFclassified_imageCollection.size());
// print(RFclassified_imageCollectionlist, 'RFclassified_imageCollectionlist');
// Visualize classified Images //
var prior_classifiedImage = ee.Image(RFclassified_imageCollectionlist.get(first_ImageDateID));
var post_classifiedImage = ee.Image(RFclassified_imageCollectionlist.get(second_ImageDateID));
Map.addLayer(prior_classifiedImage, class_vis, 'First Classified Image',false);
Map.addLayer(post_classifiedImage, class_vis, 'Second Classified Image',false);
//*********************************************************************************************************//
// Export Classification Maps
//*********************************************************************************************************//
Export.image.toDrive({
image: ee.Image(RFclassified_imageCollectionlist.get(first_ImageDateID)),
description: 'First_classifiedImage',
folder: 'GEE_Exports',
region: Study_Area,
scale: 10,
maxPixels: 1e12
});
Export.image.toDrive({
image: ee.Image(RFclassified_imageCollectionlist.get(second_ImageDateID)),
description: 'Second_classifiedImage',
folder: 'GEE_Exports',
region: Study_Area,
scale: 10,
maxPixels: 1e12
});
//******************************************************************************************************//
// Defining visualization and generating a legend
//******************************************************************************************************//
// Add a legend
var legendPanel = ui.Panel({
style:{
Position: 'bottom-right',
padding: '5px'
}
});
var legendtitle = ui.Label({
value: 'Classification',
style: {
fontSize: '14px',
fontWeight: 'bold',
margin: '0px'
}
});
legendPanel.add(legendtitle);
var color = classesPalette;
var lc_class = classArray;
var list_legend = function(color, description){
var c = ui.Label({
style:{
backgroundColor: color,
padding: '12px',
fontWeight: 'bold',
margin: '4px'
}
});
var ds = ui.Label({
value: description,
style:{
margin: '4px'
}
});
return ui.Panel({
widgets:[c, ds],
layout: ui.Panel.Layout.Flow('horizontal')
});
};
var aMax = classValues.length;
// print(aMax)
for(var a = 0; a < aMax ; a++){
legendPanel.add(list_legend(color[a], lc_class[a]));
}
Map.add(legendPanel);
//************************************************************************************************************//
//************************** Section 6 - Validation of Classification Results ********************************//
//************************************ Classifier Accuracy Assessment ************************************//
// Sample validation points from training image collection
var validationSamples = monthlytrainingCollection.map(function(image) {
return image.sampleRegions({
collection: validation_samples, // validation FeatureCollection
properties: ['landcover'],
scale: 10,
tileScale: 16
});
}).flatten();
// Classify validation samples using trained model
var validated = validationSamples.classify(trainedClassifier);
// Classifier Confusion Matrix
var confusionMatrix = validated.errorMatrix('landcover', 'classification');
print('Classifier Confusion Matrix:', confusionMatrix);
// Classifier Accuracy metrics
print('Classifier Overall Accuracy:', confusionMatrix.accuracy());
print('Classifier Kappa Coefficient:', confusionMatrix.kappa());
print('Classifier Producer\'s Accuracy:', confusionMatrix.producersAccuracy());
print('Classifier User\'s Accuracy:', confusionMatrix.consumersAccuracy());
// F-score (per class)
var fscore = confusionMatrix.fscore();
print('Classifier F-score per class:', fscore);
// Export Classifier Validation results for external analysis
Export.table.toDrive({
collection: validated,
description: 'ValidationResults_RF',
fileFormat: 'CSV'
});
//////////////////////////////////////////////////////////////////////////////////////////////////
// // Validation for the prior classified image (change validation_samples with samples collected in the field or from high-res image of the same date)
// var PriorRFvalidation_test_samples = prior_classifiedImage.sampleRegions({collection: validation_samples, properties: ['landcover'], scale: 10});
// // print('Prior RF valdiation dataset', PriorRFvalidation_test_samples);
// var PriorRFtestaccuracy = PriorRFvalidation_test_samples.errorMatrix('landcover','classification');
// print('First Classified Image Validation Overall Accuracy:', PriorRFtestaccuracy.accuracy());
// var PriorProducerAccuracy = PriorRFtestaccuracy.producersAccuracy();
// print('First Classified Image Producer Accuracy:',PriorProducerAccuracy);
// var PriorConsumerAccuracy = PriorRFtestaccuracy.consumersAccuracy();
// // var PriorConsumerAccuracy = ee.Array(PriorConsumerAccuracy.values());
// print('First Classified Image Consumer Accuracy:', PriorConsumerAccuracy);
// // // Calculate precision, recall, and F-score
// var FirstClassifiedImagefScore = PriorRFtestaccuracy.fscore();
// // Print the results
// print('First Classified Image Confusion Matrix:', PriorRFtestaccuracy);
// print('First Classified Image F-Score:', FirstClassifiedImagefScore);
// // Validation for the post classified image
// var PostRFvalidation_test_samples = post_classifiedImage.sampleRegions({collection: validation_samples, properties: ['landcover'], scale: 10});
// var PostRFtestaccuracy = PostRFvalidation_test_samples.errorMatrix('landcover','classification');
// print('Second Classified Image Validation Overall Accuracy:', PostRFtestaccuracy.accuracy());
// var PostProducerAccuracy = PostRFtestaccuracy.producersAccuracy();
// print('Second Classified Image Producer Accuracy:',PostProducerAccuracy);
// var PostConsumerAccuracy = PostRFtestaccuracy.consumersAccuracy();
// // var PostConsumerAccuracy = ee.Array(PostConsumerAccuracy.values());
// print('Second Classified Image Consumer Accuracy:', PostConsumerAccuracy);
// // // Calculate precision, recall, and F-score
// var SecondClassifiedImagefScore = PostRFtestaccuracy.fscore();
// // Print the results
// print('Second Classified Image Confusion Matrix:', PostRFtestaccuracy);
// print('Second Classified Image F-Score:', SecondClassifiedImagefScore);
// //***************************************************************************************//
// // Function to compute overall accuracy for a single classified image
// function computeOverallAccuracy(classifiedImage) {
// var validation_test_samples = classifiedImage.sampleRegions({
// collection: validation_samples,
// properties: ['landcover'],
// scale: 10
// });
// var testAccuracy = validation_test_samples.errorMatrix('landcover', 'classification');
// return testAccuracy.accuracy();
// }
// // Map over the classified image collection to compute overall accuracies
// var accuracyList = RFclassified_imageCollection.map(function(image) {
// var accuracy = computeOverallAccuracy(image);
// return image.set('overall_accuracy', accuracy);
// });
// // Extract the overall accuracies and corresponding dates
// var accuracyValues = accuracyList.aggregate_array('overall_accuracy');
// var dates = accuracyList.aggregate_array('system:time_start');
// // Create a chart to plot the overall accuracies over time
// var OverallAccuracychart = ui.Chart.array.values({
// array: accuracyValues,
// axis: 0,
// xLabels: dates
// }).setOptions({
// title: 'Overall Accuracy Over Time',
// hAxis: {title: 'Date'},
// vAxis: {title: 'Overall Accuracy'},
// legend: {position: 'none'},
// lineWidth: 1,
// pointSize: 3
// });
// // Print the chart
// print(OverallAccuracychart);
// //********************************************************
//*******************************************************************************************************************************//
//******************************** Section 7 - Detect the Changes between Classified Images***************************************//
//*******************************************************************************************************************************//
// Calculate the area for each land cover class from all classification images
// Plot a chart to show the time series for area changes in each class
// This snippet of the script (how-to-automate-calculating-area) has been modified after (Daniel Wiell, 2020)
// https://gis.stackexchange.com/users/154371/daniel-wiell?tab=profile
// https://code.earthengine.google.com/d20512d31f5d46d5d9302000ce86bc28
// https://gis.stackexchange.com/questions/361464/google-earth-engine-how-to-automate-calculating-area-for-each-class-for-each-im/361486#361486
function areaByClass(image) {
var classNames = ee.List(classArray);
var groups = ee.Image.pixelArea().addBands(image)
.reduceRegion({
reducer: ee.Reducer.sum().group({
groupField: 1, groupName: 'class'}),
geometry: Study_Area,
scale: 100,
bestEffort: true}).get('groups');
var areaByClass = ee.Dictionary(
ee.List(groups).map(function (AreaDic) {
var AreaDic = ee.Dictionary(AreaDic)
return [
classNames.get(AreaDic.getNumber('class')),
AreaDic.getNumber('sum').divide(1e6) // square km
];
}).flatten());
return ee.Feature(null, areaByClass).copyProperties(image, ["system:time_start"]);
}
var RFClassareas = RFclassified_imageCollectionlist.map(areaByClass);
var AreaClassChart = ui.Chart.feature.byFeature({
features: RFClassareas,
xProperty: 'system:time_start',
// Explicitly set the series (yProperties) in your desired order
yProperties: classArray
})
.setChartType('LineChart')
.setOptions({
interpolateNulls: true,
lineWidth: 5,
pointSize: 15,
title: 'Land Cover Classification Areal Time Series',
titleTextStyle: {fontSize:35, italic: false, bold: true},
hAxis: {
title: 'Date',
format: 'YYYY-MMM',
titleTextStyle: {italic: false, bold: true, fontSize:30},
gridlines: {count:20, color: 'black'},
textStyle: {fontSize: 30, bold: true}
},
vAxis: {
title: 'Area of Classification (sq km)',
titleTextStyle: {italic: false, bold: true, fontSize:30},
textStyle: {fontSize: 30, bold: true}
},
colors: color,
legend: {textStyle: {fontSize: 30, bold: true}}
});
// Export.table.toDrive({
// collection: RFClassareas,
// description: 'RFClassareas',
// fileFormat: 'CSV'
// });
// print(AreaClassbutton);
// print(AreaClassChart);
var AreaClasschartbutton = ui.Button('Press to get the Land Cover Classification Areal Time Series');
AreaClasschartbutton.onClick(function(){
print(AreaClassChart);
});
print(AreaClasschartbutton);
// Classification Differencing //neq divide pixels in the image with change (1) and no change (0)
var Img_classification_diff = prior_classifiedImage.subtract(post_classifiedImage).neq(0);
// print(Img_classification_diff,'Img_classification_diff');
Map.addLayer(Img_classification_diff, {min:0, max: 1, palette:['black', 'red']}, 'Binary Change',false);
// Create RGB visualization classification images to use as animation frames
// Define the Classification GIF visualization parameters.
var ClassificationGifParams = {
'region': Study_Area,
'dimensions': 600,
'crs': SenImageCollection.select('B1').first().projection(),
'framesPerSecond': 1,
'maxPixels': 1e200
};
var text = require('users/gena/packages:text'); // Import gena's package which allows text overlay on image
var annotations = [
{position: 'left', offset: '1%', margin: '1%', property: 'label', scale: 100} //large scale because image if of the whole world. Use smaller scale otherwise
]
function addText(RFclassified_image){
var timeStamp = ee.Date(RFclassified_image.get('system:time_start')).format().slice(0,10); // get the time stamp of each frame. This can be any string. Date, Years, Hours, etc.
var stringtimeStamp = ee.String('Date: ').cat(ee.String(timeStamp)); //convert time stamp to string
var RFclassified_image = RFclassified_image.visualize(class_vis).set({'label':stringtimeStamp}); // set a property called label for each image
var annotated = text.annotateImage(RFclassified_image, {}, Study_Area, annotations); // create a new RFclassified_image with the label overlayed using gena's package
return annotated;
}
var classificationGif = RFclassified_imageCollection.map(addText); //add time stamp to all images
// Print the GIF URL to the console.
// print(classificationGif.getVideoThumbURL(ClassificationGifParams)); //print gif
// // Print a URL that will produce a filmstrip when accessed.
// print(classificationGif.getFilmstripThumbURL(filmArgs));
// // Define arguments for the getFilmstripThumbURL function parameters.
// var filmArgs = {
// dimensions: 128,
// region: Study_Area,
// crs: 'EPSG:32630',
// 'maxPixels': 1e12
// };
// // Print a URL that will produce the filmstrip when accessed.
// print(classificationGif.getFilmstripThumbURL(filmArgs));
// Add ACD-EAMENA in the GIF maps created so that if any one use it will have to autmaotically accredit the EAMENA work
//********************************************************************************************************************//
//******************************** Section 8 - Change detection classification analysis ***************************//
//********************************************************************************************************************//
// Define the series names and values that will be used to plot the change detection time series charts
// Create an empty array to store the series names and values
var timeSeriesTicks = [];
// Create a for loop to generate the timeSeriesTicks array from the classArray
for (var i = 0; i < classArray.length; i++){
var tick = {
v:i, // v is the class value
f:classArray[i] // f is the property class name
};
timeSeriesTicks.push(tick);
}
// print("List of Class Names and Values:", timeSeriesTicks);
// // Extract classification values to site locations in order to detect the changes in land cover classes over time
// // Plot classification Time series chart for a feature of interset
// var ClassificationTimeSerieschart = ui.Chart.image.series({
// imageCollection: RFclassified_imageCollection,
// region: P,
// reducer: ee.Reducer.mode(),
// xProperty:'system:time_start',
// scale:10
// }).setSeriesNames(['class'])
// .setOptions({
// interpolateNulls: true,
// lineWidth: 5,
// pointSize: 15,
// title: ' Classification Time Series at Location (P)', titleTextStyle: {fontSize:35, italic: false, bold: true},
// hAxis: {title: 'Date', format: 'YYYY-MMM', titleTextStyle: {fontSize:30, italic: false, bold: true}, gridlines: {count:20, color: 'black'}, textStyle: {fontSize: 30, bold: true}},
// vAxis: {title: 'Class', titleTextStyle: {fontSize:30, italic: false, bold: true},
// textStyle: {fontSize: 30, bold: true},viewWindow:{min:0, max:5},
// ticks: timeSeriesTicks},
// // ticks values can be adjusted to the classes values which can change from a study area to another, v&f are function used to define the class value and class type, v defines the class value and t defines the class type
// legend: {textStyle: {fontSize: 30, bold: true}},
// });
// // print(ClassificationTimeSerieschart);
// // Display the classification time series chart for a feature of interset
// var ClassificationTimeSerieschartButton = ui.Button('Press to get the Classification Time Series Chart for the Feature of Interest');
// ClassificationTimeSerieschartButton.onClick(function(){
// print(ClassificationTimeSerieschart);
// });
// print(ClassificationTimeSerieschartButton);
//***********************************************************************************************************************************//
//************************* Automated Classification Time Series Charts Point Locations Identified by the User **********************//
// Function to generate time series chart
function generateTimeSeriesChart(point, pointNumber) {
return ui.Chart.image.series({
imageCollection: RFclassified_imageCollection,
region: point,
reducer: ee.Reducer.mode(),
xProperty: 'system:time_start',
scale: 10
}).setSeriesNames(['Point Location ' + pointNumber])
.setOptions({
interpolateNulls: true,
lineWidth: 5,
pointSize: 15,
title: 'Classification Time Series - Point Location ' + pointNumber,
titleTextStyle: {fontSize: 35, italic: false, bold: true},
hAxis: {
title: 'Date',
format: 'YYYY-MMM',
titleTextStyle: {fontSize: 30, italic: false, bold: true},
gridlines: {count: 12, color: 'black'}, // Show only monthly gridlines
minorGridlines: {count: 12}, // Remove intermediate lines
textStyle: {fontSize: 30, bold: true}
},
vAxis: {
title: 'Class',
titleTextStyle: {fontSize: 30, italic: false, bold: true},
textStyle: {fontSize: 30, bold: true},
viewWindow: {min: 0, max: classArray.length-1},
ticks: timeSeriesTicks
},
legend: {textStyle: {fontSize: 30, bold: true}},
// colors: ['red']
});
}
// Map click handler
Map.onClick(function(location) {
var point = ee.Geometry.Point([location.lon, location.lat]);
var pointNumber = userPoints.length + 1;
// Add point to array
userPoints.push(point);
// Create a new layer for just this point
Map.addLayer(
ee.FeatureCollection([ee.Feature(point)]),
{color: 'red'},
'Point Location ' + pointNumber
);
// Generate and show chart
print(generateTimeSeriesChart(point, pointNumber));
// Create NDVI button for this point
createNDVIButton(point, pointNumber);
});
//********************************************************************************************************************************//
//******************** NDVI Time Series at Point Locations Defined by the User ***************************************************//
// Plot NDVI Time series chart for a location of interset
// Function to generate NDVI time series chart
function generateNDVIChart(point, pointNumber) {
return ui.Chart.image.series({
imageCollection: s2FilteredCollection.select('ndvi'),
region: point,
reducer: ee.Reducer.mean(),
scale: 10
}).setOptions({
interpolateNulls: true,
trendlines: {0: {color: 'CC0000'}},
lineWidth: 3,
pointSize: 7,
title: 'NDVI Time Series - Point Location ' + pointNumber,
titleTextStyle: {fontSize: 35, italic: false, bold: true},
vAxis: {
title: 'NDVI',
titleTextStyle: {fontSize: 30, italic: false, bold: true},
textStyle: {fontSize: 30, bold: true}
},
hAxis: {