Staying Connected Initiative
July 2026
Landscape connectivity is essential to allowing wildlife populations to move across fragmented habitats, especially as development and agricultural expansion continue to alter forested corridors. The Staying Connected Initiative (SCI) identifies and protects linkage areas across the northern Appalachians that allow species to move between larger habitat blocks. This analysis focuses on Vermont's portion of the SCI network, evaluating forest loss and conversion within Tier 1 and Tier 2 linkages between 2001 and 2021 to assess where connectivity is most at risk and how threats differ between tiers.
SCI linkage boundaries, delineated and topology-cleaned in ArcGIS Pro into Tier 1 and Tier 2 categories, were clipped to Vermont and analyzed in Google Earth Engine using NLCD land cover data from 2001 and 2021. Forest cover (deciduous, evergreen, and mixed classes) was identified in both years to calculate forest loss and conversion to developed and agricultural land uses. Acreage and percent-of-forest statistics were computed per linkage, annualized to a yearly loss rate, and aggregated by tier to compare overall trends between Tier 1 and Tier 2.
At the individual linkage level, Tier 1 had a slightly higher mean annual forest loss rate (0.084%/yr) than Tier 2 (0.076%/yr), along with higher average conversion to development (0.14% vs 0.10%) and agriculture (0.51% vs 0.43%). But aggregated by tier, the pattern shifts. Tier 2 lost far more total acres, driven mainly by agriculture, while Tier 1 converted a higher percentage of its overall forest base. This suggests that, though smaller, Tier 1 linkages are proportionally more vulnerable, while Tier 2's larger extent drives greater absolute loss despite lower per-linkage rates. Agriculture outpaced development as the dominant driver of loss in both tiers. These results can help SCI partners prioritize conservation where forest loss most directly threatens connectivity, with Tier 1 representing the more urgent priority relative to its size.
This code was created as part of my internship with The Nature Conservancy. In the image above, Red = Forest Loss 2001-2021, Orange = Forest to Developed, and Yellow = Forest to Agriculture.
//Imports
var nlcd2019 = ee.ImageCollection("USGS/NLCD_RELEASES/2019_REL/NLCD"),
nlcd2021 = ee.ImageCollection("USGS/NLCD_RELEASES/2021_REL/NLCD"),
states = ee.FeatureCollection("TIGER/2018/States"),
linkagesT1 = ee.FeatureCollection("projects/middlebury-gee-476017/assets/SCI_TopologyClean_T1"),
linkagesT2 = ee.FeatureCollection("projects/middlebury-gee-476017/assets/SCI_TopologyClean_T2");
// Staying Connected Initiative Linkages
// Bernadette Osborn
//Map.setOptions('HYBRID');
// State of VT boundary
var vt = states.filter(ee.Filter.eq('NAME', 'Vermont'));
Map.centerObject(vt, 8);
// Clip T1 and T2 linkages to VT, tagging Category explicitly
function clipToVT(fc, categoryLabel) {
return fc.filterBounds(vt).map(function(feature) {
return feature
.intersection(vt.geometry(), 1)
.set('Category', categoryLabel);
});
}
var vttable = clipToVT(linkagesT1, 'T1');
var vttable2 = clipToVT(linkagesT2, 'T2');
// Combined collection for maps + overall stats
var vtLinkages = vttable.merge(vttable2);
Map.addLayer(vttable, {color: 'blue'}, 'SCI Linkages - T1');
Map.addLayer(vttable2, {color: 'purple'}, 'SCI Linkages - T2');
print('Sample T1 linkage properties:', vttable.first());
print('Sample T2 linkage properties:', vttable2.first());
print('T1 count:', vttable.size());
print('T2 count:', vttable2.size());
// NLCD Layers (clipped to linkages)
var nlcd2001 = nlcd2019
.filter(ee.Filter.eq('system:index', '2001'))
.first()
.select('landcover')
.clip(vtLinkages);
var nlcd2021Img = nlcd2021
.filter(ee.Filter.eq('system:index', '2021'))
.first()
.select('landcover')
.clip(vtLinkages);
// Forest classes: 41 - deciduous, 42 - evergreen, 43 - mixed
var forest2001 = nlcd2001.remap([41, 42, 43], [1, 1, 1], 0);
var forest2021 = nlcd2021Img.remap([41, 42, 43], [1, 1, 1], 0);
// Developed classes: 21-24
var developed2021 = nlcd2021Img.remap([21, 22, 23, 24], [1, 1, 1, 1], 0);
// Agricultural classes: 81 - pasture/hay, 82 - cultivated crops
var agriculture2021 = nlcd2021Img.remap([81, 82], [1, 1], 0);
// Change layers
var forestLoss = forest2001.eq(1).and(forest2021.eq(0));
var forestToDev = forest2001.eq(1).and(developed2021.eq(1));
var forestToAg = forest2001.eq(1).and(agriculture2021.eq(1));
// Acres image
var acres = ee.Image.pixelArea().divide(4046.8564224);
// Efficient stats
var statsImage = ee.Image.cat([
acres.rename('total_acres'),
acres.updateMask(forest2001).rename('forest2001_ac'),
acres.updateMask(forest2021).rename('forest2021_ac'),
acres.updateMask(forestLoss).rename('forest_loss_ac'),
acres.updateMask(forestToDev).rename('forest_to_dev_ac'),
acres.updateMask(forestToAg).rename('forest_to_ag_ac')
]).unmask(0);
var reduced = statsImage.reduceRegions({
collection: vtLinkages,
reducer: ee.Reducer.sum(),
scale: 30,
tileScale: 4
});
var linkageStats = reduced.map(function(feature) {
var forest2001Ac = ee.Number(feature.get('forest2001_ac'));
var forestLossAc = ee.Number(feature.get('forest_loss_ac'));
var forestToDevAc = ee.Number(feature.get('forest_to_dev_ac'));
var forestToAgAc = ee.Number(feature.get('forest_to_ag_ac'));
var forestLossPct = ee.Algorithms.If(
forest2001Ac.gt(0),
forestLossAc.divide(forest2001Ac).multiply(100),
0
);
var forestToDevPct = ee.Algorithms.If(
forest2001Ac.gt(0),
forestToDevAc.divide(forest2001Ac).multiply(100),
0
);
var forestToAgPct = ee.Algorithms.If(
forest2001Ac.gt(0),
forestToAgAc.divide(forest2001Ac).multiply(100),
0
);
var annualLossRate = ee.Number(forestLossPct).divide(20);
return feature.set({
forest_loss_pct: forestLossPct,
forest_to_dev_pct: forestToDevPct,
forest_to_ag_pct: forestToAgPct,
annual_loss_pct: annualLossRate
});
});
// Maps
Map.addLayer(
forestLoss.selfMask(),
{palette: ['red']},
'Forest Loss 2001-2021'
);
Map.addLayer(
forestToDev.selfMask(),
{palette: ['orange']},
'Forest → Developed'
);
Map.addLayer(
forestToAg.selfMask(),
{palette: ['yellow']},
'Forest → Agriculture'
);
// Choropleth of Annual Forest Loss (%/yr)
var annualLossImg = linkageStats.reduceToImage({
properties: ['annual_loss_pct'],
reducer: ee.Reducer.first()
});
Map.addLayer(
annualLossImg,
{
min: 0,
max: 3,
palette: ['lightgreen', 'green', 'darkgreen']
},
'Annual Forest Loss (%/yr)'
);
// Top 20 most threatened linkages — Forest → Developed
var top20Dev = linkageStats
.sort('forest_to_dev_pct', false)
.limit(20);
print('Top 20 Linkages by Forest → Development (%)', top20Dev);
print(
ui.Chart.feature.byFeature(
top20Dev,
'system:index',
'forest_to_dev_pct'
)
.setChartType('ColumnChart')
.setOptions({
title: 'Top 20 Linkages: Forest → Developed (%)',
legend: {position: 'none'},
hAxis: {title: 'Linkage'},
vAxis: {title: 'Percent'}
})
);
// Top 20 most threatened linkages — Forest → Agriculture
var top20Ag = linkageStats
.sort('forest_to_ag_pct', false)
.limit(20);
print('Top 20 Linkages by Forest → Agriculture (%)', top20Ag);
print(
ui.Chart.feature.byFeature(
top20Ag,
'system:index',
'forest_to_ag_pct'
)
.setChartType('ColumnChart')
.setOptions({
title: 'Top 20 Linkages: Forest → Agriculture (%)',
legend: {position: 'none'},
hAxis: {title: 'Linkage'},
vAxis: {title: 'Percent'}
})
);
// Compare T1 vs T2 (rate of change!!)
var t1Stats = linkageStats.filter(ee.Filter.eq('Category', 'T1'));
var t2Stats = linkageStats.filter(ee.Filter.eq('Category', 'T2'));
print('T1 mean annual forest loss (%/yr):',
t1Stats.aggregate_mean('annual_loss_pct'));
print('T2 mean annual forest loss (%/yr):',
t2Stats.aggregate_mean('annual_loss_pct'));
print('T1 mean forest → developed (%):',
t1Stats.aggregate_mean('forest_to_dev_pct'));
print('T2 mean forest → developed (%):',
t2Stats.aggregate_mean('forest_to_dev_pct'));
print('T1 mean forest → agriculture (%):',
t1Stats.aggregate_mean('forest_to_ag_pct'));
print('T2 mean forest → agriculture (%):',
t2Stats.aggregate_mean('forest_to_ag_pct'));
// Tier-level totals
function tierSummary(stats, tierLabel) {
var devAcres = ee.Number(stats.aggregate_sum('forest_to_dev_ac'));
var agAcres = ee.Number(stats.aggregate_sum('forest_to_ag_ac'));
var combinedAcres = devAcres.add(agAcres);
var forest2001Acres = ee.Number(stats.aggregate_sum('forest2001_ac'));
var devPct = ee.Algorithms.If(
forest2001Acres.gt(0), devAcres.divide(forest2001Acres).multiply(100), 0
);
var agPct = ee.Algorithms.If(
forest2001Acres.gt(0), agAcres.divide(forest2001Acres).multiply(100), 0
);
var combinedPct = ee.Algorithms.If(
forest2001Acres.gt(0), combinedAcres.divide(forest2001Acres).multiply(100), 0
);
return ee.Feature(null, {
Tier: tierLabel,
forest2001_total_ac: forest2001Acres,
developed: devAcres,
agriculture: agAcres,
combined_acres: combinedAcres,
Developed: devPct,
Agriculture: agPct,
combined_pct_of_2001_forest: combinedPct
});
}
var t1Summary = tierSummary(t1Stats, 'T1');
var t2Summary = tierSummary(t2Stats, 'T2');
print('T1 forest loss to Ag + Developed summary:', t1Summary);
print('T2 forest loss to Ag + Developed summary:', t2Summary);
var tierSummaryFC = ee.FeatureCollection([t1Summary, t2Summary]);
print(
ui.Chart.feature.byFeature(
tierSummaryFC,
'Tier',
['agriculture', 'developed']
)
.setChartType('ColumnChart')
.setOptions({
title: 'Acres Converted from Forest by Tier (2001-2021)',
hAxis: {title: 'Tier'},
vAxis: {title: 'Acres'},
isStacked: true
})
);
print(
ui.Chart.feature.byFeature(
tierSummaryFC,
'Tier',
['Agriculture', 'Developed']
)
.setChartType('ColumnChart')
.setOptions({
title: 'Percent of 2001 Forest Converted by Tier',
hAxis: {title: 'Tier'},
vAxis: {title: 'Percent'},
isStacked: true
})
);
// Side-by-side comparison charts
print(
ui.Chart.feature.groups({
features: linkageStats,
xProperty: 'system:index',
yProperty: 'forest_to_dev_pct',
seriesProperty: 'Category'
})
.setChartType('ColumnChart')
.setOptions({
title: 'Forest → Developed (%) by Linkage, T1 vs T2',
hAxis: {title: 'Linkage'},
vAxis: {title: 'Percent'}
})
);
print(
ui.Chart.feature.groups({
features: linkageStats,
xProperty: 'system:index',
yProperty: 'forest_to_ag_pct',
seriesProperty: 'Category'
})
.setChartType('ColumnChart')
.setOptions({
title: 'Forest → Agriculture (%) by Linkage, T1 vs T2',
hAxis: {title: 'Linkage'},
vAxis: {title: 'Percent'}
})
);
// Export!!
Export.table.toDrive({
collection: linkageStats,
description: 'SCI_Vermont_Linkage_ForestLoss_2001_2021',
fileFormat: 'CSV'
});
Export.table.toDrive({
collection: tierSummaryFC,
description: 'SCI_Vermont_Tier_Summary_Ag_Dev_2001_2021',
fileFormat: 'CSV'
});
Spongy Moth Defoliation in Lake George Watershed
December 2025
The spongy moth (Lymantria dispar) is an invasive defoliator that has spread rapidly across eastern North America, weakening and eventually killing trees after repeated seasons of defoliation. Because it feeds on many species, its impact spans a wide range of forest types, making it a significant threat to forest health at a landscape scale. This project maps and analyzes spongy moth defoliation in the Lake George watershed to identify where damage has been most severe and which landowners face the greatest risk, supporting targeted outreach and management. This code was created for a GIS & Natural Environment course.
//Imports
var geometry = ee.FeatureCollection("projects/middlebury-gee-476017/assets/lgw"),
sent = ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED"),
nlcd = ee.ImageCollection("USGS/NLCD_RELEASES/2021_REL/NLCD"),
property = ee.FeatureCollection("projects/middlebury-gee-476017/assets/parcels_asset"),
elevation = ee.Image("USGS/3DEP/10m"),
soil = ee.Image("projects/sat-io/open-datasets/polaris/sand_mean/sand_5_15");
// Function to mask clouds using the Sentinel-2 QA band.
function maskS2clouds(image) {
var qa = image.select('QA60');
// Bits 10 and 11 are clouds and cirrus, respectively.
var cloudBitMask = 1 << 10;
var cirrusBitMask = 1 << 11;
// Both flags should be set to zero, indicating clear conditions.
var mask = qa.bitwiseAnd(cloudBitMask).eq(0).and(
qa.bitwiseAnd(cirrusBitMask).eq(0));
// Return the masked and scaled data, without the QA bands.
return image.updateMask(mask).divide(10000)
.select("B.*")
.copyProperties(image, ["system:time_start"]);
}
// Bring in nlcd and select landcover
var nlcd = nlcd.select('landcover').median();
//Build a forest mask
var forestMask2020 = nlcd.gte(41).and(nlcd.lte(43));
var sent2020 = sent
.filterDate('2020-06-15','2020-10-01')
.filterBounds(geometry)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE',8))
.map(maskS2clouds)
.median() //This creates an image composite
.clip(geometry)
.updateMask(forestMask2020);
Map.centerObject(geometry,10.5);
//Map.addLayer(sent2020,
//{bands:['B4','B3','B2'], min:0,max:0.3,gamma:2.1},'Sentinel',false);
//calculating ndvi
var ndvi2020 = sent2020.normalizedDifference(['B8','B4']);
//-------------------------------------------------------------------------------------------------------------
//Sent2021 filter date and filter bounds
var forestMask2021 = nlcd.gte(41).and(nlcd.lte(43));
var sent2021 = sent
.filterDate('2021-06-15','2021-10-01')
.filterBounds(geometry)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE',8))
.map(maskS2clouds)
.median() //This creates an image composite
.clip(geometry)
.updateMask(forestMask2021);
//Map.addLayer(sent2021,
//{bands:['B4','B3','B2'], min:0,max:0.3,gamma:2.1},'Sentinel',false);
//calculating ndvi
var ndvi2021 = sent2021.normalizedDifference(['B8','B4']);
//-------------------------------------------------------------------------------------------------------------
//Sent2022 filter date and filter bounds
var forestMask2022 = nlcd.gte(41).and(nlcd.lte(43));
var sent2022 = sent
.filterDate('2022-06-15','2022-10-01')
.filterBounds(geometry)
.filter(ee.Filter.lt('CLOUDY_PIXEL_PERCENTAGE',8))
.map(maskS2clouds)
.median() //This creates an image composite
.clip(geometry)
.updateMask(forestMask2022);
//Map.addLayer(sent2022,
//{bands:['B4','B3','B2'], min:0,max:0.3,gamma:2.1},'Sentinel',false);
//calculating ndvi
var ndvi2022 = sent2022.normalizedDifference(['B8','B4']);
//------------------------------------------------------------------------------------------------------------
//Find difference
var defoliated2021 = (ndvi2020.subtract(ndvi2021)).divide(ndvi2020);
var defoliated2022 = (ndvi2020.subtract(ndvi2022)).divide(ndvi2020);
//2021 and 2022 change in ndvi
var change2021 = ee.Image(1).updateMask(defoliated2021.gte(.10));
var change2022 = ee.Image(1).updateMask(defoliated2022.gte(.10));
//2021 change in ndvi
var areaLost21 = change2021.reduceRegions({
collection:geometry,
scale:10,
reducer:ee.Reducer.count()
});
// Add hectares field
areaLost21 = areaLost21.map(function(f) {
var pixels = f.getNumber('count');
var area_ha = pixels.multiply(0.01); // each pixel = 0.01 ha
return f.set('area_ha', area_ha);
});
//2022 change in ndvi
var areaLost22 = change2022.reduceRegions({
collection:geometry,
scale:10,
reducer:ee.Reducer.count()
});
// Add hectares field
areaLost22 = areaLost22.map(function(f) {
var pixels = f.getNumber('count');
var area_ha = pixels.multiply(0.01); // each pixel = 0.01 ha
return f.set('area_ha', area_ha);
});
//Print areaLost
print(areaLost21);
//Map.addLayer(change2021,
//{palette:['forestGreen'],min:0,max:1,opacity:0.5},
//'2021 Change in NDVI');
print(areaLost22);
//Map.addLayer(change2022,
//{palette:['darkGreen'],min:0,max:1,opacity:0.5},
//'2022 Change in NDVI');
// Pixels that were defoliated in both 2021 and 2022
var defoliationArea = function(image, geometry) {
return ee.Number(
image.updateMask(image)
.multiply(ee.Image.pixelArea())
.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: geometry,
scale: 10,
maxPixels: 1e12
}).get('constant')
).divide(10000);
};
var defoliated2122 = change2021.and(change2022);
var overlap_ha = defoliationArea(defoliated2122, geometry);
print('Overlap defoliation (ha):', overlap_ha);
//Map.addLayer(defoliated2122,
//{palette: ['yellow'], min: 0, max: 1, opacity: 0.7},
//'2021 & 2022 Defoliated');
//Question 2: How many hectares of these double defoliation lands were on the combined?
var nlcdLandcover = nlcd.select('landcover');
var deciduousForest = nlcdLandcover.eq(41);
var terrain = ee.Terrain.products(elevation);
var slope = terrain.select('slope');
var aspect = terrain.select('aspect');
var slopePercent = slope.multiply(Math.PI/180).tan().multiply(100);
var steepSlope = slopePercent.gt(10);
var southFacing = aspect.gte(135).and(aspect.lte(225));
var soilSand = soil.select('b1');
var sandySoil = soilSand.gt(50);
var compositeMask = deciduousForest
.and(steepSlope)
.and(southFacing)
.and(sandySoil)
.and(defoliated2122);
var pixelAreaComposite = ee.Image.pixelArea();
var compositeArea = compositeMask
.rename('mask')
.updateMask(compositeMask)
.multiply(ee.Image.pixelArea())
.rename('area')
.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: geometry,
scale: 10,
maxPixels: 1e12
});
var compositeHa = ee.Number(compositeArea.get('area')).divide(10000);
print('Composite area (ha):', compositeHa);
//Question 3: Identify the most affected landowner.
var largeParcels = property.filter(ee.Filter.gte('area_m2', 50000));
var highRiskMask = compositeMask;
var affectedParcels = highRiskMask.reduceRegions({
collection: largeParcels,
reducer: ee.Reducer.sum(),
scale: 10,
});
//Pixel count to area (m2) & calculate fraction
var parcelsWithFraction = affectedParcels.map(function(f) {
var affected_pixels = ee.Number(f.get('sum'));
var affected_m2 = affected_pixels.multiply(10 * 10);
var total_m2 = ee.Number(f.get('area_m2'));
var fraction = affected_m2.divide(total_m2);
return f.set({
'affected_m2': affected_m2,
'affected_fraction': fraction
});
});
//Find top parcel
var mostAffectedParcel = parcelsWithFraction.sort('affected_fraction', false).first();
print('Most affected parcel (highest proportion of high-risk land):', mostAffectedParcel);
//Make a chloropleth map of parcels by percent double defoliation.
var parcelsWithPercent = parcelsWithFraction.map(function(f) {
var pct = ee.Number(f.get('affected_fraction')).multiply(100);
return f.set('percent_defoliated', pct);
});
var percentImage = parcelsWithPercent.reduceToImage({
properties: ['percent_defoliated'],
reducer: ee.Reducer.first()
});
var parcelMask = property.reduceToImage({
properties: ['area_m2'],
reducer: ee.Reducer.first()
}).gt(0);
percentImage = percentImage.updateMask(parcelMask);
var vis = {
min: 0,
max: 5,
palette: ['pink','red','darkred','654321']
};
Map.addLayer(percentImage,vis,'Parcels: % Double Defoliation', true);
Map.addLayer(defoliated2122,
{palette: ['orange'], min: 0, max: 1, opacity: 0.7},
'2021 & 2022 Defoliated');
//Basemap
var basemap = [
{
"elementType": "geometry",
"stylers": [
{
"color": "#212121"
}
]
},
{
"elementType": "labels.icon",
"stylers": [
{
"visibility": "off"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#212121"
}
]
},
{
"featureType": "administrative",
"elementType": "geometry",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "administrative.country",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
},
{
"featureType": "administrative.land_parcel",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "administrative.locality",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#bdbdbd"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#181818"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#1b1b1b"
}
]
},
{
"featureType": "road",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#2c2c2c"
}
]
},
{
"featureType": "road",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#8a8a8a"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry",
"stylers": [
{
"color": "#373737"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [
{
"color": "#3c3c3c"
}
]
},
{
"featureType": "road.highway.controlled_access",
"elementType": "geometry",
"stylers": [
{
"color": "#4e4e4e"
}
]
},
{
"featureType": "road.local",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"featureType": "transit",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#000000"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#3d3d3d"
}
]
}
];
Map.setOptions('basemap',{basemap:basemap});
//Add a legend
var legend = ui.Panel({
style: {
position: 'bottom-left',
padding: '10px',
backgroundColor: 'white'
}
});
legend.add(ui.Label({
value: 'Legend',
style: {fontWeight: 'bold', fontSize: '18px'}
}));
function makeRow(color, name) {
var colorBox = ui.Label({
style: {
backgroundColor: color,
padding: '10px',
margin: '4px',
width: '20px',
}
});
var description = ui.Label({
value: name,
style: {margin: '6px 0px 4px 6px'}
});
return ui.Panel({
widgets: [colorBox, description],
layout: ui.Panel.Layout.Flow('horizontal')
});
}
legend.add(makeRow('pink', '0–1% Double Defoliation'));
legend.add(makeRow('red', '1–2% Double Defoliation'));
legend.add(makeRow('darkred', '2–3% Double Defoliation'));
legend.add(makeRow('654321', '3–5% Double Defoliation'));
legend.add(makeRow('orange', '2021 & 2022 Defoliated'));
Map.add(legend);
// Right side ui.root
var rightPanel = ui.Panel({
layout: ui.Panel.Layout.flow('vertical'),
style: {
width: '450px',
padding: '6px'
}
});
// Title
var title = ui.Label({
value: "Spongy Moth Defoliation in the Lake George Watershed",
style: {
fontWeight: 'bold',
fontSize: '24px',
textAlign: 'left',
margin: '0 0 12px 0' // more bottom margin
}
});
// Natural history
var title_2 = ui.Label({
value: "Spongy Moth Natural History",
style: {fontWeight:'bold', fontSize:'20px', margin: '0 0 6px 0'}
});
var text_2 = ui.Label({
value: 'The spongy moth, also known as the Lymantria dispar moth, has invaded eastern North America, feeding on the foliage of trees in the summer of 2021. These moths can defoliate entire trees, severely weaken them, and after many years lead to tree mortality (Hussain, 2024). Additionally, because it feeds on so many species, the spongy moth threatens diverse forest types making its impact broad and serious (Rindos, 2023).',
style:{fontSize:'12px', margin: '0 0 6px 0'}
});
var citations = ui.Label({
value:'Hussain, N., Alemu Gonsamo, Wang, S., & M. Altaf Arain. (2024). Assessment of spongy moth infestation impacts on forest productivity and carbon loss using the Sentinel-2 satellite remote sensing and eddy covariance flux data. Ecological Processes, 13(1).',
style:{fontSize:'8px', margin: '0 0 4px 0'}
});
var citations_2 = ui.Label({
value:'Rindos, M., & Liebhold, A. M. (2023). The spongy moth, Lymantria dispar. Current Biology, 33(12).',
style:{fontSize:'8px', margin: '0 0 6px 0'}
});
var zoomBtn = ui.Button({
label: 'Zoom to the property!',
onClick: function() {
Map.centerObject(mostAffectedParcel, 16);
}
});
var panel_2 = ui.Panel({
widgets: [title_2, text_2, citations, citations_2, zoomBtn],
style: {margin: '8px 0 10px 0', padding: '0px'} // generous spacing between panels
});
// Web app instructions
var title_3 = ui.Label({
value:"Web App Instructions",
style:{fontWeight:'bold', fontSize:'20px', margin: '0 0 6px 0'}
});
var text_3 = ui.Label({
value: 'To turn layers on and off, click on “Layers” in the top right corner and hit the check box next to the item you would like to turn on and off, and move the slider to make the layer transparent.',
style:{fontSize:'12px', margin: '0 0 6px 0'}
});
var panel_3 = ui.Panel({
widgets: [title_3, text_3],
style: {margin: '8px 0 10px 0', padding: '0px'}
});
// Methods
var title_4 = ui.Label({
value:"Methods",
style:{fontWeight:'bold', fontSize:'20px', margin: '0 0 6px 0'}
});
var text_4 = ui.Label({
value: 'By using Google Earth Engine, I used the Lake George watershed, Sentinel 2 surface reflectance, and the National Land Cover Dataset to find the 2021 and 2022 change in NDVI, calculating the double defoliation of land in hectares. Then I used the elevation data from USGS 3DEP 10m National Map Seamless, creating a composite of double defoliation area by finding high risk land that was deciduous forest, steep slopes greater than 10%, a south facing aspect, and sandy soils. Lastly, I took property data for Warren County in QGIS to calculate the area, and finally imported it as an asset in GEE to find the most affected landowner.',
style:{fontSize:'12px', margin: '0 0 6px 0'}
});
var panel_4 = ui.Panel({
widgets: [title_4, text_4],
style: {margin: '8px 0 10px 0', padding: '0px'}
});
// Results
var title_5 = ui.Label({
value:"Results",
style:{fontWeight:'bold', fontSize:'20px', margin: '0 0 6px 0'}
});
var text_5 = ui.Label({
value: 'In 2021 there was a 5834.54 hectare defoliation, in 2022 there was a 1969.41 hectare defoliation, there was an overlap defoliation in 827.11 hectares, and the high risk land compromised of 184.48 hectares. David Dibelius is the property owner most impacted by the spongy moth with 31% of their property being double defoliated and high risk, meaning they have a large proportion of deciduous trees, steep slopes, south facing aspects, and sandy soils.',
style:{fontSize:'12px', margin: '0 0 6px 0'}
});
var letter = ui.Label({
value: 'Letter to Property Owner',
style: {fontWeight: 'bold', fontSize: '12px', margin: '0 0 4px 0'},
targetUrl: 'https://docs.google.com/document/d/1SdcWMmqfRUxZOPxu9uI-6xXATauHDmD1-pfRPm8XgzU/edit?usp=sharing'
});
var code = ui.Label({
value: 'GEE Code',
style: {fontWeight: 'bold', fontSize: '12px', margin: '0 0 6px 0'},
targetUrl: 'https://code.earthengine.google.com/693e56729cb70a941c572faa889eeb7d'
});
var panel_5 = ui.Panel({
widgets: [title_5, text_5, letter, code],
style: {margin: '8px 0 10px 0', padding: '0px'}
});
// Add all panels to rightPanel
rightPanel
.add(title)
.add(panel_3)
.add(panel_2)
.add(panel_4)
.add(panel_5);
ui.root.add(rightPanel);
Hansen et al. (2013) v. Potapov et al. (2022) Tree Cover Loss
February 2026
Comparing different remote sensing datasets is an important step in understanding how forests are measured and monitored. Because each dataset uses its own definitions and classification methods, the resulting maps can vary even when they describe the same region. This analysis compares the Hansen et al. (2013) and Potapov et al. (2022) tree cover loss datasets for Tanzania, examining their similarities, differences, and how their definitions of forests and tree cover influence the reported results and their usefulness for monitoring forest change.
Tree cover loss statistics from the Hansen et al. (2013) and Potapov et al. (2022) datasets were compared across Tanzania, with attention to how each dataset defines "forest," "tree cover," and "loss." The two datasets largely agree on tree cover loss patterns, with Potapov's data showing the strongest consistency with Hansen's. However, Hansen's map covers a larger area and classifies land as forested more broadly, whereas Potapov's map focuses on denser forest cover and may miss areas like new growth. Hansen defines forest loss as stand-replacement disturbance rather than land-use change, unless stated otherwise, a definitional choice that drives much of the divergence between the two products. Regional agreement between the datasets also varies across Tanzania. Given its broader definition of forest and more comprehensive picture of change, Hansen's dataset is recommended for monitoring tree cover loss in Tanzania. This code was created for a Remote Sensing & Land Use course.
//Imports
var gfc2024 = ee.Image("UMD/hansen/global_forest_change_2024_v1_12");
//#1 Pick Tanzania as study area
var gfc2024 = ee.Image("UMD/hansen/global_forest_change_2024_v1_12");
var treeCover = gfc2024.select('treecover2000');
var countries = ee.FeatureCollection('USDOS/LSIB_SIMPLE/2017');
var tanzania = ee.Feature(
countries
.filter(ee.Filter.eq('country_na', 'Tanzania'))
.first()
);
//#2 Time window 2001–2020
var lossYear = gfc2024.select('lossyear');
var lossIn0120 = lossYear.gte(1).and(lossYear.lte(20));
var areaImage = lossIn0120.multiply(ee.Image.pixelArea());
var stats1 = areaImage.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: tanzania.geometry(),
scale: 30,
maxPixels: 1e15
});
// Visualization settings
var imageVisParam = {
min: 0,
max: 100,
palette: ['white', 'green']
};
// Add to map
Map.centerObject(tanzania, 6);
Map.setOptions('terrain');
Map.addLayer(treeCover.clip(tanzania), imageVisParam, 'Tree Cover 2000');
//Map.addLayer(lossIn0120.clip(tanzania).selfMask(),
//{palette: ['orange']},
//'Hansen Loss');
// Reclassify the rasters
var landmask = ee.Image("projects/glad/OceanMask").lte(1);
var change = ee.Image('projects/glad/GLCLU2020/v2/LCLUC').updateMask(landmask);
var image_remapped = change.remap(
[240,245,251],
[1,1,1]
);
var visParamMap = {
palette: ["blue"]
};
// Add to map
//Map.addLayer(image_remapped.clip(tanzania), visParamMap, 'Potapov Loss');
// Combine the binary tree cover loss bands from Hansen and Potapov using a "local overlay" operation
var local_overlay = lossIn0120.add(image_remapped);
//Map.addLayer(local_overlay.clip(tanzania), {palette: ["red"]}, 'Hansen & Potapov Loss');
// Combined “loss” layer: orange = Hansen only, blue = Potapov only, red = both agree
var potapovMasked = image_remapped.unmask(0);
var hansenOnly = lossIn0120.unmask(0).eq(1).and(potapovMasked.eq(0));
var potapovOnly = potapovMasked.eq(1).and(lossIn0120.unmask(0).eq(0));
var agreement = lossIn0120.unmask(0).eq(1).and(potapovMasked.eq(1));
var combinedRGB = ee.Image(0)
.where(hansenOnly, 1)
.where(potapovOnly, 2)
.where(agreement, 3)
.selfMask();
// Area where both Hansen & Potapov agree on loss
var agreementArea = agreement
.multiply(ee.Image.pixelArea())
.rename('agreement_area');
var agreementStats = agreementArea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: tanzania.geometry(),
scale: 30,
maxPixels: 1e15
});
// Tanzania total area
var tanzaniaArea = tanzania.geometry().area();
// Compute and print percent agreement loss
var agreementAreaSqM = ee.Number(agreementStats.get('agreement_area'));
var percentAgreementLoss = agreementAreaSqM.divide(tanzaniaArea).multiply(100);
print('Agreement % Forest Loss:', percentAgreementLoss);
// Hansen total loss (including agreement pixels)
var hansenArea = lossIn0120
.multiply(ee.Image.pixelArea())
.rename('hansen_area');
var hansenStats = hansenArea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: tanzania.geometry(),
scale: 30,
maxPixels: 1e15
});
var hansenAreaSqM = ee.Number(hansenStats.get('hansen_area'));
var percentHansenLoss = hansenAreaSqM.divide(tanzaniaArea).multiply(100);
print('Hansen % Forest Loss:', percentHansenLoss);
// Potapov total loss (including agreement pixels)
var potapovArea = image_remapped
.multiply(ee.Image.pixelArea())
.rename('potapov_area');
var potapovStats = potapovArea.reduceRegion({
reducer: ee.Reducer.sum(),
geometry: tanzania.geometry(),
scale: 30,
maxPixels: 1e15
});
var potapovAreaSqM = ee.Number(potapovStats.get('potapov_area'));
var percentPotapovLoss = potapovAreaSqM.divide(tanzaniaArea).multiply(100);
print('Potapov % Forest Loss:', percentPotapovLoss);
// Add combined loss layer to map
Map.addLayer(combinedRGB.clip(tanzania), {
min: 1, max: 3,
palette: ['orange', 'blue', 'red']
}, 'Combined Loss Layer');
// Tanzania boundary
var tanzaniaOutline = ee.Image().byte().paint({
featureCollection: tanzania,
color: 1,
width: 1
});
Map.addLayer(tanzaniaOutline, {palette: 'black'}, 'Tanzania Boundary');
//Chart challenge
var percentLossDict = ee.Dictionary({
'Hansen (2001–2020)': percentHansenLoss,
'Potapov (2020)': percentPotapovLoss,
'Agreement Only': percentAgreementLoss
});
var chartData = ee.FeatureCollection(
percentLossDict.keys().map(function(key) {
return ee.Feature(null, {
'Dataset': key,
'Percent Loss': percentLossDict.get(key)
});
})
);
var lossChart = ui.Chart.feature.byFeature(chartData, 'Dataset', 'Percent Loss')
.setChartType('ColumnChart')
.setOptions({
title: 'Percent Forest Loss in Tanzania',
hAxis: {title: 'Dataset'},
vAxis: {title: 'Percent of Country Area (%)'},
legend: {position: 'none'},
colors: ['red'],
fontSize: 14
});
print(lossChart);