meteo-utils 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meteo_utils/__init__.py +0 -0
- meteo_utils/dem_utils.py +454 -0
- meteo_utils/ecmwf_utils.py +1345 -0
- meteo_utils/gdal_utils.py +244 -0
- meteo_utils/solar_irradiance.py +1049 -0
- meteo_utils-2.1.0.dist-info/METADATA +87 -0
- meteo_utils-2.1.0.dist-info/RECORD +10 -0
- meteo_utils-2.1.0.dist-info/WHEEL +5 -0
- meteo_utils-2.1.0.dist-info/licenses/LICENSE +674 -0
- meteo_utils-2.1.0.dist-info/top_level.txt +1 -0
meteo_utils/__init__.py
ADDED
|
File without changes
|
meteo_utils/dem_utils.py
ADDED
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Created on Mon Apr 2 16:48:56 2018
|
|
4
|
+
|
|
5
|
+
@author: hector
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import glob
|
|
9
|
+
import os
|
|
10
|
+
import os.path as pth
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from osgeo import osr, gdal
|
|
14
|
+
import skimage.morphology as morph
|
|
15
|
+
from . import gdal_utils as gu
|
|
16
|
+
|
|
17
|
+
# Average conversion scale from degrees to meters
|
|
18
|
+
DEG_2_METERS = 111120
|
|
19
|
+
|
|
20
|
+
# Terrain occlusion threshold
|
|
21
|
+
OCCLUSION_THRES = 0
|
|
22
|
+
|
|
23
|
+
# Path to SAGA saga_cmd binary file
|
|
24
|
+
SAGA_CMD_PATH = "saga_cmd"
|
|
25
|
+
|
|
26
|
+
# Process SRTM tiles so that they are of the same resolution, extent and projection as HR dataset
|
|
27
|
+
# and derive slope and aspect.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def srtm_dem(data_dir_dem, template_file, output_dem, srtm_nodata=-32762):
|
|
31
|
+
# Find all SRTM tiles in the DEM directory, create virtual raster and the reproject,
|
|
32
|
+
# resample and subset it based of the HR template.
|
|
33
|
+
srtm_tiles = glob.glob(os.path.join(data_dir_dem, "srtm_??_??.tif"))
|
|
34
|
+
if srtm_tiles:
|
|
35
|
+
if len(srtm_tiles) > 1:
|
|
36
|
+
raw_dem = os.path.join(data_dir_dem, 'temp.tif')
|
|
37
|
+
raw_dem = gu.merge_raster_layers(srtm_tiles, raw_dem)
|
|
38
|
+
else:
|
|
39
|
+
raw_dem = srtm_tiles[0]
|
|
40
|
+
out_fid = gu.resample_with_gdalwarp(raw_dem, template_file,
|
|
41
|
+
resample_alg='cubic')
|
|
42
|
+
dem_data = out_fid.GetRasterBand(1).ReadAsArray().astype(np.float)
|
|
43
|
+
|
|
44
|
+
dem_data[dem_data == srtm_nodata] = np.nan
|
|
45
|
+
gu.save_image(dem_data,
|
|
46
|
+
out_fid.GetGeoTransform(),
|
|
47
|
+
out_fid.GetProjection(),
|
|
48
|
+
output_dem)
|
|
49
|
+
|
|
50
|
+
if os.path.exists(os.path.join(data_dir_dem, 'temp.tif')):
|
|
51
|
+
os.remove(os.path.join(data_dir_dem, 'temp.tif'))
|
|
52
|
+
|
|
53
|
+
def process_dem(data_dir_DEM, template_HR, output_dataset_name, overwrite_existing):
|
|
54
|
+
|
|
55
|
+
dem_file = os.path.join(data_dir_DEM, '%s_dem_hr.tif' % output_dataset_name)
|
|
56
|
+
|
|
57
|
+
if os.path.isfile(dem_file) and not overwrite_existing:
|
|
58
|
+
print('%s already processed' % dem_file)
|
|
59
|
+
else:
|
|
60
|
+
# Find all SRTM tiles in the DEM directory, create virtual raster and the reproject,
|
|
61
|
+
# resample and subset it based of the HR template. This assumes that the tiles are already
|
|
62
|
+
# present since they are downloaded by Sen2Cor.
|
|
63
|
+
srtm_dem(data_dir_DEM, template_HR, dem_file)
|
|
64
|
+
|
|
65
|
+
# Calculate slope and aspect using GDAL
|
|
66
|
+
slope_file = os.path.join(data_dir_DEM, '%s_slope_hr.tif' % output_dataset_name)
|
|
67
|
+
if not os.path.exists(slope_file) or overwrite_existing:
|
|
68
|
+
slope_from_dem(dem_file, slope_file)
|
|
69
|
+
aspect_file = os.path.join(data_dir_DEM, '%s_aspect_hr.tif' % output_dataset_name)
|
|
70
|
+
if not os.path.exists(aspect_file) or overwrite_existing:
|
|
71
|
+
aspect_from_dem(dem_file, aspect_file)
|
|
72
|
+
|
|
73
|
+
return dem_file, slope_file, aspect_file
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def slope_from_dem(dem_file_path, output=None, scale=None):
|
|
77
|
+
|
|
78
|
+
if not output:
|
|
79
|
+
output = dem_file_path.replace('.tif', '_slope.tif')
|
|
80
|
+
if scale is None:
|
|
81
|
+
proj = gu.raster_info(dem_file_path)[0]
|
|
82
|
+
srs = osr.SpatialReference()
|
|
83
|
+
srs.ImportFromWkt(proj)
|
|
84
|
+
if srs.GetAttrValue('UNIT') == "degree":
|
|
85
|
+
# Covert degrees pixel size into meters for trigonometric computations
|
|
86
|
+
scale = DEG_2_METERS
|
|
87
|
+
else:
|
|
88
|
+
scale = 1
|
|
89
|
+
|
|
90
|
+
gdal.DEMProcessing(output, dem_file_path, "slope",
|
|
91
|
+
slopeFormat="degree",
|
|
92
|
+
computeEdges=True,
|
|
93
|
+
scale=scale)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def aspect_from_dem(dem_file_path, output=None):
|
|
97
|
+
|
|
98
|
+
if not output:
|
|
99
|
+
output = pth.splitext(dem_file_path)[0]+'_aspect.tif'
|
|
100
|
+
|
|
101
|
+
gdal.DEMProcessing(output, dem_file_path, "aspect",
|
|
102
|
+
computeEdges=True,
|
|
103
|
+
zeroForFlat=True)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def declination_angle(doy):
|
|
108
|
+
''' Calculates the Earth declination angle
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
doy : float or int
|
|
113
|
+
day of the year
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
declination : float
|
|
118
|
+
Declination angle (radians)
|
|
119
|
+
'''
|
|
120
|
+
declination = np.radians(23.45) * np.sin((2.0 * np.pi * doy / 365.0) - 1.39)
|
|
121
|
+
|
|
122
|
+
return declination
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def hour_angle(ftime, declination, lon, stdlon=0):
|
|
126
|
+
'''Calculates the hour angle
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
ftime : float
|
|
131
|
+
Time of the day (decimal hours)
|
|
132
|
+
declination : float
|
|
133
|
+
Declination angle (radians)
|
|
134
|
+
lon : float
|
|
135
|
+
longitude of the site (degrees).
|
|
136
|
+
stdlon : float
|
|
137
|
+
Longitude of the standard meridian that represent the ftime time zone
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
w : float
|
|
141
|
+
hour angle (radians), negative before noon, positive after noon
|
|
142
|
+
'''
|
|
143
|
+
|
|
144
|
+
EOT = 0.258 * np.cos(declination) - 7.416 * np.sin(declination) - \
|
|
145
|
+
3.648 * np.cos(2.0 * declination) - 9.228 * np.sin(2.0 * declination)
|
|
146
|
+
LC = (stdlon - lon) / 15.
|
|
147
|
+
time_corr = (-EOT / 60.) + LC
|
|
148
|
+
solar_time = ftime - time_corr
|
|
149
|
+
# Get the hour angle
|
|
150
|
+
w = np.radians((solar_time - 12.0) * 15.)
|
|
151
|
+
|
|
152
|
+
return w
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def incidence_angle_tilted(lat, lon, doy, ftime, stdlon=0, aspect=0, slope=0):
|
|
156
|
+
''' Calculates the incidence solar angle over a tilted flat surface
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
lat : float or array
|
|
161
|
+
latitude (degrees)
|
|
162
|
+
lon : float or array
|
|
163
|
+
longitude (degrees)
|
|
164
|
+
doy : int
|
|
165
|
+
day of the year
|
|
166
|
+
ftime : float
|
|
167
|
+
Time of the day (decimal hours)
|
|
168
|
+
stdlon : float
|
|
169
|
+
Longitude of the standard meridian that represent the ftime time zone
|
|
170
|
+
aspect : float or array
|
|
171
|
+
surface azimuth angle, measured clockwise from north (degrees)
|
|
172
|
+
slope : float or array
|
|
173
|
+
slope angle (degrees)
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
cos_theta_i : float or array
|
|
178
|
+
cosine of the incidence angle
|
|
179
|
+
'''
|
|
180
|
+
|
|
181
|
+
# Get the dclination and hour angle
|
|
182
|
+
delta = declination_angle(doy)
|
|
183
|
+
# Hour angle is considered negative before noon, positive after noon
|
|
184
|
+
omega = hour_angle(ftime, delta, lon, stdlon=stdlon)
|
|
185
|
+
|
|
186
|
+
f1, f2, f3 = inclination_factors(lat, slope, aspect)
|
|
187
|
+
|
|
188
|
+
cos_delta = np.cos(delta)
|
|
189
|
+
sin_delta = np.sin(delta)
|
|
190
|
+
cos_omega = np.cos(omega)
|
|
191
|
+
sin_omega = np.sin(omega)
|
|
192
|
+
cos_theta_i = sin_delta * f1 \
|
|
193
|
+
+ cos_delta * cos_omega * f2 \
|
|
194
|
+
+ cos_delta * sin_omega * f3
|
|
195
|
+
|
|
196
|
+
return cos_theta_i
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def shading(dem_file, saa, sza, max_chunk_size=1e9):
|
|
200
|
+
"""
|
|
201
|
+
Adapted from https://github.com/zoran-cuckovic/QGIS-terrain-shading
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
dem_file
|
|
205
|
+
saa
|
|
206
|
+
sza
|
|
207
|
+
max_chunk_size
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
|
|
212
|
+
"""
|
|
213
|
+
def mean_angle(angle_array, degrees=True):
|
|
214
|
+
if degrees is True:
|
|
215
|
+
angle_array = np.radians(angle_array)
|
|
216
|
+
cos_i = np.nanmean(np.cos(angle_array))
|
|
217
|
+
sin_i = np.nanmean(np.sin(angle_array))
|
|
218
|
+
mean_angle = np.arctan2(sin_i, cos_i)
|
|
219
|
+
if degrees is True:
|
|
220
|
+
mean_angle = np.degrees(mean_angle)
|
|
221
|
+
return mean_angle
|
|
222
|
+
|
|
223
|
+
def window_loop(shape, chunk, axis=0, reverse=False, overlap=0):
|
|
224
|
+
"""
|
|
225
|
+
Construct a frame to extract chunks of data from gdal
|
|
226
|
+
(and to insert them properly to a numpy matrix)
|
|
227
|
+
"""
|
|
228
|
+
xsize, ysize = shape if axis == 0 else shape[::-1]
|
|
229
|
+
|
|
230
|
+
if reverse:
|
|
231
|
+
steps = np.arange(xsize // chunk, -1, -1)
|
|
232
|
+
begin = xsize
|
|
233
|
+
else:
|
|
234
|
+
steps = np.arange(1, xsize // chunk + 2)
|
|
235
|
+
begin = 0
|
|
236
|
+
|
|
237
|
+
x, y, x_off, y_off = 0, 0, xsize, ysize
|
|
238
|
+
|
|
239
|
+
for step in steps:
|
|
240
|
+
|
|
241
|
+
end = min(int(chunk * step), xsize)
|
|
242
|
+
if reverse:
|
|
243
|
+
x, x_off = end, begin - end
|
|
244
|
+
else:
|
|
245
|
+
x, x_off = begin, end - begin
|
|
246
|
+
|
|
247
|
+
if overlap and x >= overlap: x -= overlap * int(step)
|
|
248
|
+
|
|
249
|
+
begin = end
|
|
250
|
+
|
|
251
|
+
if x_off < chunk:
|
|
252
|
+
in_view = np.s_[:, : x_off] if not axis else np.s_[: x_off, :]
|
|
253
|
+
else:
|
|
254
|
+
in_view = np.s_[:]
|
|
255
|
+
|
|
256
|
+
if not axis:
|
|
257
|
+
gdal_take = (x, y, x_off, y_off)
|
|
258
|
+
else:
|
|
259
|
+
gdal_take = (y, x, y_off, x_off)
|
|
260
|
+
|
|
261
|
+
yield in_view, gdal_take
|
|
262
|
+
# We need to work with a single beam direction
|
|
263
|
+
direction = mean_angle(saa)
|
|
264
|
+
steep = (45 <= direction <= 135 or 225 <= direction <= 315)
|
|
265
|
+
|
|
266
|
+
s = direction % 90
|
|
267
|
+
if s > 45:
|
|
268
|
+
s = 90 - s
|
|
269
|
+
|
|
270
|
+
slope = np.tan(np.radians(s)) # matrix shear slope
|
|
271
|
+
|
|
272
|
+
# ! attention: x in gdal is y dimension un numpy (the first dimension)
|
|
273
|
+
dem_fid = gdal.Open(dem_file, gdal.GA_ReadOnly)
|
|
274
|
+
xsize, ysize = dem_fid.RasterXSize, dem_fid.RasterYSize
|
|
275
|
+
gt = dem_fid.GetGeoTransform()
|
|
276
|
+
proj = dem_fid.GetProjection()
|
|
277
|
+
# Compute the average pixel sixe
|
|
278
|
+
pixel_size = 0.5 * np.abs(gt[1]) + 0.5 * np.abs(gt[5])
|
|
279
|
+
# Check whether elevation projection units are degrees and convert to meters
|
|
280
|
+
srs = osr.SpatialReference()
|
|
281
|
+
srs.ImportFromWkt(proj)
|
|
282
|
+
if srs.GetAttrValue('UNIT') == "degree":
|
|
283
|
+
# Covert degrees pixel size into meters for trigonometric computations
|
|
284
|
+
pixel_size *= DEG_2_METERS
|
|
285
|
+
# adjust for pixel rotation (pixel diagonal = 45°)
|
|
286
|
+
# (diagonal is clipped by light path, under right angle,
|
|
287
|
+
# which means it becomes the adjacent, and the pixel size the hypothenuse.)
|
|
288
|
+
pixel_size *= np.cos(np.radians(s))
|
|
289
|
+
|
|
290
|
+
tilt = np.tan(np.radians(90. - sza))
|
|
291
|
+
|
|
292
|
+
chunk = min(int(max_chunk_size // (ysize if steep else xsize)),
|
|
293
|
+
(xsize if steep else ysize))
|
|
294
|
+
|
|
295
|
+
if s % 45 > 0:
|
|
296
|
+
# Determine the optimal chunk size (estimate!).
|
|
297
|
+
# The problem is to carry rasterized lines
|
|
298
|
+
# from one chunk to another.
|
|
299
|
+
# So, set chunk size according to minimum rasterisation error
|
|
300
|
+
c = (np.arange(1, chunk) * slope) % 1 # %1 to get decimals only
|
|
301
|
+
c[c > 0.5] -= 1
|
|
302
|
+
# this is not ideal : we cannot predict where it would stop
|
|
303
|
+
chunk -= np.argmin(np.round(abs(c), decimals=2)[::-1]) + 1
|
|
304
|
+
|
|
305
|
+
# 3) ------- SHEAR MATRIX (INDICES) -------------
|
|
306
|
+
chunk_slice = (ysize, chunk) if steep else (chunk, xsize)
|
|
307
|
+
indices_y, indices_x = np.indices(chunk_slice)
|
|
308
|
+
mx_z = np.zeros(chunk_slice)
|
|
309
|
+
mx_z[:] = -99999
|
|
310
|
+
|
|
311
|
+
# this is all upside down ...
|
|
312
|
+
rev_y = 90 <= direction <= 270
|
|
313
|
+
rev_x = not 180 <= direction <= 360
|
|
314
|
+
|
|
315
|
+
if rev_y:
|
|
316
|
+
indices_y = indices_y[::-1, :]
|
|
317
|
+
if not rev_x:
|
|
318
|
+
indices_x = indices_x[:, ::-1]
|
|
319
|
+
|
|
320
|
+
off_a = indices_x + indices_y * slope
|
|
321
|
+
off_b = indices_y + indices_x * slope
|
|
322
|
+
|
|
323
|
+
if steep:
|
|
324
|
+
axis = 0
|
|
325
|
+
# construct a slope to simulate sun angle
|
|
326
|
+
# elevations will be draped over this 2D slope
|
|
327
|
+
off = off_a[:, ::-1] # indices_y * slope + indices_x[:, ::-1]
|
|
328
|
+
|
|
329
|
+
src_y = indices_x[:, ::-1]
|
|
330
|
+
src_x = np.round(off_b).astype(int)
|
|
331
|
+
|
|
332
|
+
else:
|
|
333
|
+
axis = 1
|
|
334
|
+
off = off_b[:, ::-1] # indices_x[:,::-1] * slope + indices_y
|
|
335
|
+
|
|
336
|
+
src_x = indices_y
|
|
337
|
+
src_y = np.round(off_a).astype(int)
|
|
338
|
+
|
|
339
|
+
src = np.s_[src_y, src_x]
|
|
340
|
+
|
|
341
|
+
# Distance to slope's perpendicular is (x + y)/2 * root(2) for 45°
|
|
342
|
+
# because where x = y we have a square with a*root(2) diagonal
|
|
343
|
+
# However, in a rotated pixel grid, the slope is cutting through pixel diagonals
|
|
344
|
+
# so we take cosinus (pixel_size) to get this shortened distance
|
|
345
|
+
off *= pixel_size * np.nanmean(tilt)
|
|
346
|
+
|
|
347
|
+
# create a matrix to hold the sheared matrix
|
|
348
|
+
mx_temp = np.zeros(((np.max(src_y) + 1), np.max(src_x) + 1))
|
|
349
|
+
|
|
350
|
+
t_y, t_x = mx_temp.shape
|
|
351
|
+
|
|
352
|
+
# carrying lines from one chunk to the next (fussy...)
|
|
353
|
+
if steep:
|
|
354
|
+
l = np.s_[-1, : ysize]
|
|
355
|
+
f = np.s_[0, t_x - ysize:]
|
|
356
|
+
else:
|
|
357
|
+
l = np.s_[t_y - xsize:, -1]
|
|
358
|
+
f = np.s_[:xsize, 0]
|
|
359
|
+
|
|
360
|
+
last_line = np.zeros((ysize if steep else xsize))
|
|
361
|
+
|
|
362
|
+
# 4 ----- LOOP THOUGH DATA CHUNKS AND CALCULATE -----------------
|
|
363
|
+
counter = 0
|
|
364
|
+
out_array = np.zeros([ysize, xsize])
|
|
365
|
+
for dem_view, gdal_coords in window_loop(
|
|
366
|
+
shape=(xsize, ysize),
|
|
367
|
+
chunk=chunk,
|
|
368
|
+
axis=not steep,
|
|
369
|
+
reverse=rev_x if steep else rev_y,
|
|
370
|
+
overlap=1):
|
|
371
|
+
|
|
372
|
+
mx_z[dem_view] = dem_fid.ReadAsArray(*gdal_coords).astype(float)
|
|
373
|
+
|
|
374
|
+
# should handle better NoData !!
|
|
375
|
+
# nans will destroy the accumulation sequence
|
|
376
|
+
mask = np.isnan(mx_z)
|
|
377
|
+
mx_z[mask] = -9999
|
|
378
|
+
|
|
379
|
+
mx_temp[src] = mx_z + off
|
|
380
|
+
|
|
381
|
+
mx_temp[f] += -last_line # shadows have negative values, so *-1
|
|
382
|
+
# accumulate maximum shadow depths
|
|
383
|
+
mx_temp -= np.maximum.accumulate(mx_temp, axis=axis)
|
|
384
|
+
# first line has shadow od zero depth (nothing to accum), so copy from previous chunk
|
|
385
|
+
mx_temp[f] = last_line
|
|
386
|
+
|
|
387
|
+
last_line[:] = mx_temp[l] # save for later
|
|
388
|
+
|
|
389
|
+
out = mx_temp[src]
|
|
390
|
+
out[mask] = np.nan
|
|
391
|
+
counter += 1
|
|
392
|
+
out_array[gdal_coords[1]:gdal_coords[1] + gdal_coords[3],
|
|
393
|
+
gdal_coords[0]:gdal_coords[0] + gdal_coords[2]] = out[dem_view]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
return out_array
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def latlon_from_dem(dem_file_path, output=None):
|
|
400
|
+
if not output:
|
|
401
|
+
output = dem_file_path.replace('.tif', '_latlon.tif')
|
|
402
|
+
|
|
403
|
+
proj, gt, size_x, size_y = gu.raster_info(dem_file_path)[0:4]
|
|
404
|
+
dims = size_y, size_x
|
|
405
|
+
rows, cols = np.indices(dims)
|
|
406
|
+
lon = gt[0] + cols * gt[1] + rows * gt[2]
|
|
407
|
+
lat = gt[3] + cols * gt[4] + rows * gt[5]
|
|
408
|
+
srcSRS = osr.SpatialReference()
|
|
409
|
+
srcSRS.ImportFromWkt(proj)
|
|
410
|
+
dstSRS = osr.SpatialReference()
|
|
411
|
+
dstSRS.ImportFromEPSG(4326)
|
|
412
|
+
# GDAL decided to reverse the coordinate transformation axis order
|
|
413
|
+
# https://github.com/OSGeo/gdal/issues/1546
|
|
414
|
+
dstSRS.SetAxisMappingStrategy(osr.OAMS_TRADITIONAL_GIS_ORDER)
|
|
415
|
+
transform = osr.CoordinateTransformation(srcSRS, dstSRS)
|
|
416
|
+
array = np.stack([lon.reshape(-1), lat.reshape(-1)]).T
|
|
417
|
+
coords = np.array(transform.TransformPoints(array)).T
|
|
418
|
+
|
|
419
|
+
lon = coords[0].reshape(dims)
|
|
420
|
+
lat = coords[1].reshape(dims)
|
|
421
|
+
array = np.dstack([lat, lon])
|
|
422
|
+
gu.save_image(array, gt, proj, output, no_data_value=np.nan)
|
|
423
|
+
return lat, lon
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def non_occluded_terrain(dem_file, saa, sza, smooth_radius=1):
|
|
427
|
+
# Get areas that are shaded by obstacles around
|
|
428
|
+
occlusion = shading(dem_file, saa, sza)
|
|
429
|
+
non_shaded = np.logical_and(np.isfinite(occlusion), occlusion >= OCCLUSION_THRES)
|
|
430
|
+
del occlusion
|
|
431
|
+
# Smooth the occlusion map
|
|
432
|
+
if smooth_radius > 0:
|
|
433
|
+
se = morph.disk(smooth_radius)
|
|
434
|
+
non_shaded = morph.binary_closing(non_shaded, footprint=se)
|
|
435
|
+
return non_shaded
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def inclination_factors(lat, slope, aspect):
|
|
439
|
+
# Convert remaining angles into radians,
|
|
440
|
+
# aspect is considered the azimuth from the South being westward positive
|
|
441
|
+
lat, aspect, slope = map(np.radians, [lat, aspect - 180, slope])
|
|
442
|
+
|
|
443
|
+
sin_lat = np.sin(lat)
|
|
444
|
+
cos_lat = np.cos(lat)
|
|
445
|
+
cos_slope = np.cos(slope)
|
|
446
|
+
sin_slope = np.sin(slope)
|
|
447
|
+
cos_az = np.cos(aspect)
|
|
448
|
+
sin_az = np.sin(aspect)
|
|
449
|
+
|
|
450
|
+
f1 = sin_lat * cos_slope - cos_lat * sin_slope * cos_az
|
|
451
|
+
f2 = cos_lat * cos_slope + sin_lat * sin_slope * cos_az
|
|
452
|
+
f3 = sin_slope * sin_az
|
|
453
|
+
return f1, f2, f3
|
|
454
|
+
|