-
Notifications
You must be signed in to change notification settings - Fork 16
/
MCD10A1.js
288 lines (230 loc) · 9.62 KB
/
MCD10A1.js
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
//----------------------------------------------------------------------------------
//
// Google Earth Engine code to create a combined MODIS snow product, fusing both
// Terra and Aqua MxD10A1 data streams. Both data streams are prone to
// underestimate snow cover (due to MODIS properties and sky conditions). A fused
// products aleviates these issues partially by taking the maximum value of the
// fractional snow cover bands of each data stream.
//
// This raw data is stored in the maxCollection imageCollection. This data is used
// calculate the date of first snow melt and the date of snow accumulation. These
// dates are determined registering the DOY where fractional snow cover drops below
// or the day before it last exceeds 5%, only earliest and latest instances are noted.
// A final product can be derived, namely the maximum snow free period, by taking the
// difference between the snow accumulation and snow melt dates.
//
// Note that this code is writen for the northern hemisphere, and will not work
// for the southern hemisphere without adjustments (sum days between Jan 1. and
// the northern hemisphere snow melt day and from the accumulation day until
// Dec. 31th to get to the snow free period, and obviously reverse the captions
// on the dates melt becomes accumulation and vice versa).
//
// Code written by Koen Hufkens, April 2016
// under a GNU AFFERO GENERAL PUBLIC LICENSE - see repository
//
//----------------------------------------------------------------------------------
// -- settings --
// years to process (from start year t0 to end year t1)
// [2003 is the first year with full coverage]
var t0 = "2003", t1 = "2016";
var t_diff = (t1 - t0) + 1;
// -- functions --
var maxVal = function(feature) {
// load feature and set start time
// when mapping
var img = ee.Image(feature);
var startTime = img.select('NDSI_Snow_Cover').get('system:time_start');
// assign 0 values (which can be confused with NA)
// a small negative value for both bands
var b1c = img.expression(
'b1 == 0 ? -10 : b1', {
'b1': img.select('NDSI_Snow_Cover')
});
var b2c = img.expression(
'b1 == 0 ? -10 : b1', {
'b1': img.select('NDSI_Snow_Cover_1')
});
// take the maximum value of the remapped data
// while unmasking (assigning 0 to NA) the data
var maxval = b1c.unmask(0).max(b2c.unmask(0));
// update the mask (set all 0 values to NA)
maxval = maxval.updateMask(maxval.neq(0));
// substitute in the original 0 values, which
// are currently registered as -10
maxval = img.expression(
'b1 == -10 ? 0 : b1', {
'b1': maxval
});
// associate the start time with the new image
// and return the data
maxval = maxval.set('system:time_start',startTime);
return maxval;
};
// concat two layers from a collection
// into an image with x bands
// (can be iterated)
var cat = function(a, b) {
return ee.Image(b).addBands(a);
};
// rename bands for export to ee.Image() format
var rename_bands = function (img) {
img = ee.Image(img);
var names = img.bandNames();
var prefix = ee.String(img.get("system:time_start")).cat("_");
var new_names = names.map(function(b) {
return prefix.cat(b); });
return img.select(names, new_names);
};
// take the difference between snow accumulation
// and snow melt days (doy values)
var difference = function(feature) {
var img = ee.Image(feature);
var b1 = img.select('snowmelt');
var b2 = img.select('snowacc');
var startTime = b1.get('system:time_start');
return b2.subtract(b1).set('system:time_start',startTime);
};
// set the snow melt date
var returnDoy = function(feature) {
var image = ee.Image(feature);
// Compute time of the image in fractional years relative to the start.
var doy = ee.Date(image.get('system:time_start')).difference(start, 'day').add(1);
// replace values smaller than 5% snow cover with
// their respective DOY
var replacement = image.expression(
'SNOW <= 5 ? DOY : 9999', {
'SNOW': image.select('NDSI_Snow_Cover'),
'DOY': doy
}).toInt();
// mask anything above 5% coverage as empty
var withObs = replacement.neq(9999);
return replacement.updateMask(withObs);
};
// stick two bands together which are linked with an
// inner join
var concatBands = function(feature) {
return ee.Image.cat(feature.get('primary'),feature.get('secondary'));
};
// This function adds a time band to the image,
// for subsequent regression analysis
var createTimeBand = function(feature) {
var image = ee.Image(feature);
return image.addBands(image.metadata('system:time_start'));
};
// Specify an equals filter for image timestamps.
var filterTimeEq = ee.Filter.equals({
leftField: 'system:time_start',
rightField: 'system:time_start'
});
// -- main --
// If a longer time series is selected, calculate statistics
// for each year independently. This can probably be done
// with some sort of reduce statement, but the documentation is a bit
// of a mess when it comes to custom functions.
for (var i = parseInt(t0); i <= parseInt(t1); ++i) {
// int to string
var evalYear = i.toString();
// Load MODIS Fractional Snow Cover product (aqua / terra)
var terraCollection = ee.ImageCollection('MODIS/006/MOD10A1')
.select('NDSI_Snow_Cover')
.filterDate(evalYear.concat("-01-01"),evalYear.concat("-12-31"))
.sort('system:time_start', true);
var aquaCollection = ee.ImageCollection('MODIS/006/MYD10A1')
.select('NDSI_Snow_Cover')
.filterDate(evalYear.concat("-01-01"),evalYear.concat("-12-31"))
.sort('system:time_start', true);
// create a collection of both MOD/MYD data matched
// joined per band using an inner join
var innerJoin = ee.Join.inner();
// Apply the join.
var innerJoinedCollection = innerJoin.apply(terraCollection, aquaCollection, filterTimeEq);
// combine matching images into one image with (2) bands
var joinedCollection = innerJoinedCollection.map(concatBands);
// calculate the maximum value across the two bands MOD/MYD
// see maxVal function above
var yearlyCollection = joinedCollection.map(maxVal);
if (evalYear == 2003){
// pick a location for visual code validation
var loc = ee.Geometry.Point([-118.84257812499999, 55.29665181611441]);
// Register a function to draw a chart when a user clicks on the map.
var mv = ui.Chart.image.series(yearlyCollection, loc, null, 50)
.setOptions({title: 'joined MCD10A1'});
var mod = ui.Chart.image.series(terraCollection, loc, null, 50)
.setOptions({title: 'MOD10A1'});
var myd = ui.Chart.image.series(aquaCollection, loc, null, 50)
.setOptions({title: 'MYD10A1'});
print(mv, mod, myd);
}
// how many bands in the dataset?
// should be close to 365
//print(yearlyCollection.size());
// Get the starting time reference.
var start = ee.Date(yearlyCollection.first().get('system:time_start'));
// subsitute all values < 5% with their respective DOY
var doyCollection = yearlyCollection.map(returnDoy);
// calculate the snow melt and snow fall (accumulation)
var snowMeltAcc = ee.ImageCollection(doyCollection)
.reduce(ee.Reducer.minMax()).unmask(366);
// The latter part is rather ugly, not optimal (but works)
// if first in loop swap with the original layer
if (evalYear == t0){
// snow melt
var tmpImg = snowMeltAcc
.select(['constant_min'],['snowmelt'])
.set('system:time_start',i);
var snowMelt = ee.ImageCollection(tmpImg);
// snow accumulation
var tmpImg = snowMeltAcc
.select(['constant_max'],['snowacc'])
.set('system:time_start',i);
var snowAcc = ee.ImageCollection(tmpImg);
}else{
// add snow melt layers
var tmpImg = snowMeltAcc.select(['constant_min'],['snowmelt'])
.set('system:time_start',i);
var snowMelt = snowMelt.merge(ee.ImageCollection(tmpImg));
// add snow accumulation layers
var tmpImg = snowMeltAcc.select(['constant_max'],['snowacc'])
.set('system:time_start',i);
var snowAcc = snowAcc.merge(ee.ImageCollection(tmpImg));
}
}
// rename bands
var snowMeltlist = snowMelt.toList(t_diff);
snowMeltlist = snowMeltlist.map(rename_bands);
var snowMeltImg = snowMeltlist.slice(1).iterate(cat, snowMeltlist.get(0));
var snowAcclist = snowAcc.toList(t_diff);
snowAcclist = snowAcclist.map(rename_bands);
var snowAccImg = snowAcclist.slice(1).iterate(cat, snowAcclist.get(0));
// Apply the join
var innerJoinedCollection = innerJoin.apply(snowMelt, snowAcc, filterTimeEq);
// combine matching images into one image with (2) band
var joinedCollection = innerJoinedCollection.map(concatBands);
// take the difference of the two bands
var snowFree = joinedCollection.map(difference);
// calculate median snow free season length and
// and create a mask from the result
var snowFreeMedian = ee.ImageCollection(snowFree)
.median();
var snowFreeMask = ee.Image(snowFreeMedian)
.lt(306);
// -- quick regression analysis --
// add a time band
var snowMelt = snowMelt.map(createTimeBand);
// linear regression on the data
var linearFit = ee.ImageCollection(snowMelt).select(['system:time_start', 'snowmelt'])
.reduce(ee.Reducer.linearFit());
// -- plotting results --
// land-water mask for visualization, removes faulty cover
// values for the oceans and water bodies
var hansenImage = ee.Image('UMD/hansen/global_forest_change_2013');
var data = hansenImage.select('datamask');
var waterMask = data.eq(1);
// final mask, excluding water bodies and
// areas with a too short of a winter season
var mask = waterMask.multiply(snowFreeMask);
var filteredFit = ee.Image(linearFit).multiply(mask);
// Plot the snow free trend
Map.addLayer(linearFit
.mask(mask),
{min: 0, max: [-2, 0, 2], bands: ['scale', 'offset', 'scale']}, 'fit');