merged upstream main and resolved conflicts
This commit is contained in:
commit
a1958c944f
10
HISTORY.rst
10
HISTORY.rst
|
@ -52,6 +52,8 @@ Unreleased Changes
|
|||
* Fixed a bug where sampledata downloads failed silently (and progress bar
|
||||
became innacurate) if the Workbench did not have write permission to
|
||||
the download location. https://github.com/natcap/invest/issues/1070
|
||||
* Changing the language setting will now cause the app to relaunch
|
||||
(`#1168 <https://github.com/natcap/invest/issues/1168>`_),
|
||||
* Forest Carbon
|
||||
* The biophysical table is now case-insensitive.
|
||||
* HRA
|
||||
|
@ -96,6 +98,14 @@ Unreleased Changes
|
|||
set to 0. The old behavior was not well documented and caused some
|
||||
confusion when nodata pixels did not line up. It's safer not to fill in
|
||||
unknown data. (`#1317 <https://github.com/natcap/invest/issues/1317>`_)
|
||||
* Negative monthly quickflow values will now be set to 0. This is because
|
||||
very small negative values occasionally result from valid data, but they
|
||||
should be interpreted as 0.
|
||||
(`#1318 <https://github.com/natcap/invest/issues/1318>`_)
|
||||
* In the monthly quickflow calculation, QF_im will be set to 0 on any pixel
|
||||
where s_i / a_im > 100. This is done to avoid overflow errors when
|
||||
calculating edge cases where the result would round down to 0 anyway.
|
||||
(`#1318 <https://github.com/natcap/invest/issues/1318>`_)
|
||||
* Urban Flood Risk
|
||||
* Fixed a bug where the model incorrectly raised an error if the
|
||||
biophysical table contained a row of all 0s.
|
||||
|
|
|
@ -549,10 +549,10 @@ def _determine_valid_viewpoints(dem_path, structures_path):
|
|||
|
||||
# Coordinates in map units to pass to viewshed algorithm
|
||||
geometry = point.GetGeometryRef()
|
||||
if geometry.GetGeometryType() != ogr.wkbPoint:
|
||||
if geometry.GetGeometryName() != 'POINT':
|
||||
raise AssertionError(
|
||||
f"Feature {point.GetFID()} is not a Point geometry. "
|
||||
"Features must be a Point.")
|
||||
f"Feature {point.GetFID()} must be a POINT geometry, "
|
||||
f"not {geometry.GetGeometryName()}")
|
||||
|
||||
viewpoint = (geometry.GetX(), geometry.GetY())
|
||||
|
||||
|
|
|
@ -622,7 +622,7 @@ def _execute(args):
|
|||
# TypeError when n_workers is None.
|
||||
n_workers = -1 # Synchronous mode.
|
||||
task_graph = taskgraph.TaskGraph(
|
||||
cache_dir, n_workers, reporting_interval=5.0)
|
||||
cache_dir, n_workers, reporting_interval=5)
|
||||
|
||||
LOGGER.info('Building file registry')
|
||||
file_registry = utils.build_file_registry(
|
||||
|
@ -770,14 +770,13 @@ def _execute(args):
|
|||
climate_zone_rain_events_month = dict([
|
||||
(cz_id, cz_rain_events_lookup[cz_id][month_label]) for
|
||||
cz_id in cz_rain_events_lookup])
|
||||
n_events_nodata = -1
|
||||
n_events_task = task_graph.add_task(
|
||||
func=utils.reclassify_raster,
|
||||
args=(
|
||||
(file_registry['cz_aligned_raster_path'], 1),
|
||||
climate_zone_rain_events_month,
|
||||
file_registry['n_events_path_list'][month_id],
|
||||
gdal.GDT_Float32, n_events_nodata,
|
||||
gdal.GDT_Float32, TARGET_NODATA,
|
||||
reclass_error_details),
|
||||
target_path_list=[
|
||||
file_registry['n_events_path_list'][month_id]],
|
||||
|
@ -827,8 +826,6 @@ def _execute(args):
|
|||
func=_calculate_monthly_quick_flow,
|
||||
args=(
|
||||
file_registry['precip_path_aligned_list'][month_index],
|
||||
file_registry['lulc_aligned_path'],
|
||||
file_registry['cn_path'],
|
||||
file_registry['n_events_path_list'][month_index],
|
||||
file_registry['stream_path'],
|
||||
file_registry['si_path'],
|
||||
|
@ -858,13 +855,12 @@ def _execute(args):
|
|||
kc_lookup = dict([
|
||||
(lucode, biophysical_table[lucode]['kc_%d' % (month_index+1)])
|
||||
for lucode in biophysical_table])
|
||||
kc_nodata = -1 # a reasonable nodata value
|
||||
kc_task = task_graph.add_task(
|
||||
func=utils.reclassify_raster,
|
||||
args=(
|
||||
(file_registry['lulc_aligned_path'], 1), kc_lookup,
|
||||
file_registry['kc_path_list'][month_index],
|
||||
gdal.GDT_Float32, kc_nodata, reclass_error_details),
|
||||
gdal.GDT_Float32, TARGET_NODATA, reclass_error_details),
|
||||
target_path_list=[file_registry['kc_path_list'][month_index]],
|
||||
dependent_task_list=[align_task],
|
||||
task_name='classify kc month %d' % month_index)
|
||||
|
@ -978,7 +974,7 @@ def _calculate_vri(l_path, target_vri_path):
|
|||
None.
|
||||
|
||||
"""
|
||||
qb_sum = 0.0
|
||||
qb_sum = 0
|
||||
qb_valid_count = 0
|
||||
l_nodata = pygeoprocessing.get_raster_info(l_path)['nodata'][0]
|
||||
|
||||
|
@ -1039,109 +1035,154 @@ def _calculate_annual_qfi(qfm_path_list, target_qf_path):
|
|||
qfi_sum_op, target_qf_path, gdal.GDT_Float32, qf_nodata)
|
||||
|
||||
|
||||
def _calculate_monthly_quick_flow(
|
||||
precip_path, lulc_raster_path, cn_path, n_events_raster_path,
|
||||
stream_path, si_path, qf_monthly_path):
|
||||
def _calculate_monthly_quick_flow(precip_path, n_events_path, stream_path,
|
||||
si_path, qf_monthly_path):
|
||||
"""Calculate quick flow for a month.
|
||||
|
||||
Args:
|
||||
precip_path (string): path to file that correspond to monthly
|
||||
precipitation
|
||||
lulc_raster_path (string): path to landcover raster
|
||||
cn_path (string): path to curve number raster
|
||||
n_events_raster_path (string): a path to a raster where each pixel
|
||||
precip_path (string): path to monthly precipitation raster
|
||||
n_events_path (string): a path to a raster where each pixel
|
||||
indicates the number of rain events.
|
||||
stream_path (string): path to stream mask raster where 1 indicates a
|
||||
stream pixel, 0 is a non-stream but otherwise valid area from the
|
||||
original DEM, and nodata indicates areas outside the valid DEM.
|
||||
si_path (string): path to raster that has potential maximum retention
|
||||
qf_monthly_path_list (list of string): list of paths to output monthly
|
||||
rasters.
|
||||
qf_monthly_path (string): path to output monthly QF raster.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
p_nodata = pygeoprocessing.get_raster_info(precip_path)['nodata'][0]
|
||||
n_nodata = pygeoprocessing.get_raster_info(n_events_path)['nodata'][0]
|
||||
stream_nodata = pygeoprocessing.get_raster_info(stream_path)['nodata'][0]
|
||||
si_nodata = pygeoprocessing.get_raster_info(si_path)['nodata'][0]
|
||||
|
||||
qf_nodata = -1
|
||||
p_nodata = pygeoprocessing.get_raster_info(precip_path)['nodata'][0]
|
||||
n_events_nodata = pygeoprocessing.get_raster_info(
|
||||
n_events_raster_path)['nodata'][0]
|
||||
stream_nodata = pygeoprocessing.get_raster_info(stream_path)['nodata'][0]
|
||||
|
||||
def qf_op(p_im, s_i, n_events, stream_array):
|
||||
def qf_op(p_im, s_i, n_m, stream):
|
||||
"""Calculate quick flow as in Eq [1] in user's guide.
|
||||
|
||||
Args:
|
||||
p_im (numpy.array): precipitation at pixel i on month m
|
||||
s_i (numpy.array): factor that is 1000/CN_i - 10
|
||||
(Equation 1b from user's guide)
|
||||
n_events (numpy.array): number of rain events on the pixel
|
||||
stream_mask (numpy.array): 1 if stream, otherwise not a stream
|
||||
pixel.
|
||||
n_m (numpy.array): number of rain events on pixel i in month m
|
||||
stream (numpy.array): 1 if stream, otherwise not a stream pixel.
|
||||
|
||||
Returns:
|
||||
quick flow (numpy.array)
|
||||
|
||||
"""
|
||||
# s_i is an intermediate output which will always have a defined
|
||||
# nodata value
|
||||
valid_mask = ((p_im != 0.0) &
|
||||
(stream_array != 1) &
|
||||
(n_events > 0) &
|
||||
~utils.array_equals_nodata(s_i, si_nodata))
|
||||
if p_nodata is not None:
|
||||
valid_mask &= ~utils.array_equals_nodata(p_im, p_nodata)
|
||||
if n_events_nodata is not None:
|
||||
valid_mask &= ~utils.array_equals_nodata(n_events, n_events_nodata)
|
||||
# stream_nodata is the only input that carry over nodata values from
|
||||
valid_p_mask = ~utils.array_equals_nodata(p_im, p_nodata)
|
||||
valid_n_mask = ~utils.array_equals_nodata(n_m, n_nodata)
|
||||
# precip mask: both p_im and n_m are defined and greater than 0
|
||||
precip_mask = valid_p_mask & valid_n_mask & (p_im > 0) & (n_m > 0)
|
||||
stream_mask = stream == 1
|
||||
# stream_nodata is the only input that carries over nodata values from
|
||||
# the aligned DEM.
|
||||
if stream_nodata is not None:
|
||||
valid_mask &= ~utils.array_equals_nodata(
|
||||
stream_array, stream_nodata)
|
||||
valid_mask = (
|
||||
valid_p_mask &
|
||||
valid_n_mask &
|
||||
~utils.array_equals_nodata(stream, stream_nodata) &
|
||||
~utils.array_equals_nodata(s_i, si_nodata))
|
||||
|
||||
valid_n_events = n_events[valid_mask]
|
||||
valid_si = s_i[valid_mask]
|
||||
# QF is defined in terms of three cases:
|
||||
#
|
||||
# 1. Where there is no precipitation, QF = 0
|
||||
# (even if stream or s_i is undefined)
|
||||
#
|
||||
# 2. Where there is precipitation and we're on a stream, QF = P
|
||||
# (even if s_i is undefined)
|
||||
#
|
||||
# 3. Where there is precipitation and we're not on a stream, use the
|
||||
# quickflow equation (only if all four inputs are defined):
|
||||
# QF_im = 25.4 * n_m * (
|
||||
# (a_im - s_i) * exp(-0.2 * s_i / a_im) +
|
||||
# s_i^2 / a_im * exp(0.8 * s_i / a_im) * E1(s_i / a_im)
|
||||
# )
|
||||
#
|
||||
# When evaluating the QF equation, there are a few edge cases:
|
||||
#
|
||||
# 3a. Where s_i = 0, you get NaN and a warning from numpy because
|
||||
# E1(0 / a_im) = infinity. In this case, per conversation with
|
||||
# Rafa, the final term of the equation should evaluate to 0, and
|
||||
# the equation can be simplified to QF_im = P_im
|
||||
# (which makes sense because if s_i = 0, no water is retained).
|
||||
#
|
||||
# Solution: Preemptively set QF_im equal to P_im where s_i = 0 in
|
||||
# order to avoid calculations with infinity.
|
||||
#
|
||||
# 3b. When the ratio s_i / a_im becomes large, QF approaches 0.
|
||||
# [NOTE: I don't know how to prove this mathematically, but it
|
||||
# holds true when I tested with reasonable values of s_i and a_im].
|
||||
# The exp() term becomes very large, while the E1() term becomes
|
||||
# very small.
|
||||
#
|
||||
# Per conversation with Rafa and Lisa, large s_i / a_im ratios
|
||||
# shouldn't happen often with real world data. But if they did, it
|
||||
# would be a situation where there is very little precipitation
|
||||
# spread out over relatively many rain events and the soil is very
|
||||
# absorbent, so logically, QF should be effectively zero.
|
||||
#
|
||||
# To avoid overflow, we set a threshold of 100 for the s_i / a_im
|
||||
# ratio. Where s_i / a_im > 100, we set QF to 0. 100 was chosen
|
||||
# because it's a nice whole number that gets us close to the
|
||||
# float32 max without surpassing it (exp(0.8*100) = 5e34). When
|
||||
# s_i / a_im = 100, the actual result of the QF equation is on the
|
||||
# order of 1e-6, so it should be rounded down to 0 anyway.
|
||||
#
|
||||
# 3c. Otherwise, evaluate the QF equation as usual.
|
||||
#
|
||||
# 3d. With certain inputs [for example: n_m = 10, CN = 50, p_im = 30],
|
||||
# it's possible that the QF equation evaluates to a very small
|
||||
# negative value. Per conversation with Lisa and Rafa, this is an
|
||||
# edge case that the equation was not designed for. Negative QF
|
||||
# doesn't make sense, so we set any negative QF values to 0.
|
||||
|
||||
# qf_im is the quickflow at pixel i on month m
|
||||
qf_im = numpy.full(p_im.shape, TARGET_NODATA, dtype=numpy.float32)
|
||||
|
||||
# case 1: where there is no precipitation
|
||||
qf_im[~precip_mask] = 0
|
||||
|
||||
# case 2: where there is precipitation and we're on a stream
|
||||
qf_im[precip_mask & stream_mask] = p_im[precip_mask & stream_mask]
|
||||
|
||||
# case 3: where there is precipitation and we're not on a stream
|
||||
case_3_mask = valid_mask & precip_mask & ~stream_mask
|
||||
|
||||
# for consistent indexing, make a_im the same shape as the other
|
||||
# arrays even though we only use it in case 3
|
||||
a_im = numpy.full(p_im.shape, numpy.nan, dtype=numpy.float32)
|
||||
# a_im is the mean rain depth on a rainy day at pixel i on month m
|
||||
# the 25.4 converts inches to mm since Si is in inches
|
||||
a_im = numpy.empty(valid_n_events.shape)
|
||||
a_im = p_im[valid_mask] / (valid_n_events * 25.4)
|
||||
qf_im = numpy.empty(p_im.shape)
|
||||
qf_im[:] = qf_nodata
|
||||
# the 25.4 converts inches to mm since s_i is in inches
|
||||
a_im[case_3_mask] = p_im[case_3_mask] / (n_m[case_3_mask] * 25.4)
|
||||
|
||||
# Precompute the last two terms in quickflow so we can handle a
|
||||
# numerical instability when s_i is large and/or a_im is small
|
||||
# on large valid_si/a_im this number will be zero and the latter
|
||||
# exponent will also be zero because of a divide by zero. rather than
|
||||
# raise that numerical warning, just handle it manually
|
||||
E1 = scipy.special.expn(1, valid_si / a_im)
|
||||
E1[valid_si == 0] = 0
|
||||
nonzero_e1_mask = E1 != 0
|
||||
exp_result = numpy.zeros(valid_si.shape)
|
||||
exp_result[nonzero_e1_mask] = numpy.exp(
|
||||
(0.8 * valid_si[nonzero_e1_mask]) / a_im[nonzero_e1_mask] +
|
||||
numpy.log(E1[nonzero_e1_mask]))
|
||||
# case 3a: when s_i = 0, qf = p
|
||||
case_3a_mask = case_3_mask & (s_i == 0)
|
||||
qf_im[case_3a_mask] = p_im[case_3a_mask]
|
||||
|
||||
# qf_im is the quickflow at pixel i on month m Eq. [1]
|
||||
qf_im[valid_mask] = (25.4 * valid_n_events * (
|
||||
(a_im - valid_si) * numpy.exp(-0.2 * valid_si / a_im) +
|
||||
valid_si ** 2 / a_im * exp_result))
|
||||
# case 3b: set quickflow to 0 when the s_i/a_im ratio is too large
|
||||
case_3b_mask = case_3_mask & (s_i / a_im > 100)
|
||||
qf_im[case_3b_mask] = 0
|
||||
|
||||
# case 3c: evaluate the equation as usual
|
||||
case_3c_mask = case_3_mask & ~(case_3a_mask | case_3b_mask)
|
||||
qf_im[case_3c_mask] = (
|
||||
25.4 * n_m[case_3c_mask] * (
|
||||
((a_im[case_3c_mask] - s_i[case_3c_mask]) *
|
||||
numpy.exp(-0.2 * s_i[case_3c_mask] / a_im[case_3c_mask])) +
|
||||
(s_i[case_3c_mask] ** 2 / a_im[case_3c_mask] *
|
||||
numpy.exp(0.8 * s_i[case_3c_mask] / a_im[case_3c_mask]) *
|
||||
scipy.special.exp1(s_i[case_3c_mask] / a_im[case_3c_mask]))
|
||||
)
|
||||
)
|
||||
|
||||
# case 3d: set any negative values to 0
|
||||
qf_im[valid_mask & (qf_im < 0)] = 0
|
||||
|
||||
# if precip is 0, then QF should be zero
|
||||
qf_im[(p_im == 0) | (n_events == 0)] = 0.0
|
||||
# if we're on a stream, set quickflow to the precipitation
|
||||
valid_stream_precip_mask = stream_array == 1
|
||||
if p_nodata is not None:
|
||||
valid_stream_precip_mask &= ~utils.array_equals_nodata(
|
||||
p_im, p_nodata)
|
||||
qf_im[valid_stream_precip_mask] = p_im[valid_stream_precip_mask]
|
||||
return qf_im
|
||||
|
||||
pygeoprocessing.raster_calculator(
|
||||
[(path, 1) for path in [
|
||||
precip_path, si_path, n_events_raster_path, stream_path]], qf_op,
|
||||
qf_monthly_path, gdal.GDT_Float32, qf_nodata)
|
||||
precip_path, si_path, n_events_path, stream_path]],
|
||||
qf_op, qf_monthly_path, gdal.GDT_Float32, TARGET_NODATA)
|
||||
|
||||
|
||||
def _calculate_curve_number_raster(
|
||||
|
@ -1172,7 +1213,6 @@ def _calculate_curve_number_raster(
|
|||
4: 'cn_d',
|
||||
}
|
||||
# curve numbers are always positive so -1 a good nodata choice
|
||||
cn_nodata = -1
|
||||
lulc_to_soil = {}
|
||||
lulc_nodata = pygeoprocessing.get_raster_info(
|
||||
lulc_raster_path)['nodata'][0]
|
||||
|
@ -1195,7 +1235,7 @@ def _calculate_curve_number_raster(
|
|||
else:
|
||||
# handle the lulc nodata with cn nodata
|
||||
lulc_to_soil[soil_id]['lulc_values'].append(lulc_nodata)
|
||||
lulc_to_soil[soil_id]['cn_values'].append(cn_nodata)
|
||||
lulc_to_soil[soil_id]['cn_values'].append(TARGET_NODATA)
|
||||
|
||||
# Making the landcover array a float32 in case the user provides a
|
||||
# float landcover map like Kate did.
|
||||
|
@ -1213,7 +1253,7 @@ def _calculate_curve_number_raster(
|
|||
def cn_op(lulc_array, soil_group_array):
|
||||
"""Map lulc code and soil to a curve number."""
|
||||
cn_result = numpy.empty(lulc_array.shape)
|
||||
cn_result[:] = cn_nodata
|
||||
cn_result[:] = TARGET_NODATA
|
||||
|
||||
# if lulc_array value not in lulc_to_soil[soil_group_id]['lulc_values']
|
||||
# then numpy.digitize will not bin properly and cause an IndexError
|
||||
|
@ -1252,10 +1292,9 @@ def _calculate_curve_number_raster(
|
|||
cn_result[current_soil_mask] = cn_values[current_soil_mask]
|
||||
return cn_result
|
||||
|
||||
cn_nodata = -1
|
||||
pygeoprocessing.raster_calculator(
|
||||
[(lulc_raster_path, 1), (soil_group_path, 1)], cn_op, cn_path,
|
||||
gdal.GDT_Float32, cn_nodata)
|
||||
gdal.GDT_Float32, TARGET_NODATA)
|
||||
|
||||
|
||||
def _calculate_si_raster(cn_path, stream_path, si_path):
|
||||
|
@ -1269,7 +1308,6 @@ def _calculate_si_raster(cn_path, stream_path, si_path):
|
|||
Returns:
|
||||
None
|
||||
"""
|
||||
si_nodata = -1
|
||||
cn_nodata = pygeoprocessing.get_raster_info(cn_path)['nodata'][0]
|
||||
|
||||
def si_op(ci_factor, stream_mask):
|
||||
|
@ -1278,17 +1316,17 @@ def _calculate_si_raster(cn_path, stream_path, si_path):
|
|||
~utils.array_equals_nodata(ci_factor, cn_nodata) &
|
||||
(ci_factor > 0))
|
||||
si_array = numpy.empty(ci_factor.shape)
|
||||
si_array[:] = si_nodata
|
||||
si_array[:] = TARGET_NODATA
|
||||
# multiply by the stream mask != 1 so we get 0s on the stream and
|
||||
# unaffected results everywhere else
|
||||
si_array[valid_mask] = (
|
||||
(1000.0 / ci_factor[valid_mask] - 10) * (
|
||||
(1000 / ci_factor[valid_mask] - 10) * (
|
||||
stream_mask[valid_mask] != 1))
|
||||
return si_array
|
||||
|
||||
pygeoprocessing.raster_calculator(
|
||||
[(cn_path, 1), (stream_path, 1)], si_op, si_path, gdal.GDT_Float32,
|
||||
si_nodata)
|
||||
TARGET_NODATA)
|
||||
|
||||
|
||||
def _aggregate_recharge(
|
||||
|
@ -1350,7 +1388,7 @@ def _aggregate_recharge(
|
|||
"no coverage for polygon %s", ', '.join(
|
||||
[str(poly_feat.GetField(_)) for _ in range(
|
||||
poly_feat.GetFieldCount())]))
|
||||
value = 0.0
|
||||
value = 0
|
||||
elif op_type == 'sum':
|
||||
value = aggregate_stats[poly_index]['sum']
|
||||
poly_feat.SetField(aggregate_field_id, float(value))
|
||||
|
|
|
@ -126,7 +126,8 @@ class ScenicQualityTests(unittest.TestCase):
|
|||
with self.assertRaises(AssertionError) as cm:
|
||||
scenic_quality._determine_valid_viewpoints(
|
||||
dem_path, viewpoints_path)
|
||||
self.assertIn('Feature 1 is not a Point geometry', str(cm.exception))
|
||||
self.assertIn('Feature 1 must be a POINT geometry, not LINESTRING',
|
||||
str(cm.exception))
|
||||
|
||||
def test_exception_when_no_structures_aoi_overlap(self):
|
||||
"""SQ: model raises exception when AOI does not overlap structures."""
|
||||
|
|
|
@ -974,12 +974,6 @@ class SeasonalWaterYieldRegressionTests(unittest.TestCase):
|
|||
precip_array = numpy.array([
|
||||
[10, 10],
|
||||
[10, 10]], dtype=numpy.float32)
|
||||
lulc_array = numpy.array([
|
||||
[1, 1],
|
||||
[2, 2]], dtype=numpy.float32)
|
||||
cn_array = numpy.array([
|
||||
[40, 40],
|
||||
[80, 80]], dtype=numpy.float32)
|
||||
si_array = numpy.array([
|
||||
[15, 15],
|
||||
[2.5, 2.5]], dtype=numpy.float32)
|
||||
|
@ -990,13 +984,12 @@ class SeasonalWaterYieldRegressionTests(unittest.TestCase):
|
|||
[0, 0],
|
||||
[0, 0]], dtype=numpy.float32)
|
||||
|
||||
# results calculated by wolfram alpha
|
||||
expected_quickflow_array = numpy.array([
|
||||
[-4.82284552e-36, -4.82284552e-36],
|
||||
[ 6.19275831e-01, 6.19275831e-01]])
|
||||
[0, 0],
|
||||
[0.61928378, 0.61928378]])
|
||||
|
||||
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
|
||||
lulc_path = os.path.join(self.workspace_dir, 'lulc.tif')
|
||||
cn_path = os.path.join(self.workspace_dir, 'cn.tif')
|
||||
si_path = os.path.join(self.workspace_dir, 'si.tif')
|
||||
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
|
||||
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
|
||||
|
@ -1008,13 +1001,11 @@ class SeasonalWaterYieldRegressionTests(unittest.TestCase):
|
|||
|
||||
# write all the test arrays to raster files
|
||||
for array, path in [(precip_array, precip_path),
|
||||
(lulc_array, lulc_path),
|
||||
(n_events_array, n_events_path)]:
|
||||
# make the nodata value undefined for user inputs
|
||||
pygeoprocessing.numpy_array_to_raster(
|
||||
array, None, (1, -1), (1180000, 690000), project_wkt, path)
|
||||
for array, path in [(cn_array, cn_path),
|
||||
(si_array, si_path),
|
||||
for array, path in [(si_array, si_path),
|
||||
(stream_mask, stream_path)]:
|
||||
# define a nodata value for intermediate outputs
|
||||
pygeoprocessing.numpy_array_to_raster(
|
||||
|
@ -1022,13 +1013,119 @@ class SeasonalWaterYieldRegressionTests(unittest.TestCase):
|
|||
|
||||
# save the quickflow results raster to quickflow.tif
|
||||
seasonal_water_yield._calculate_monthly_quick_flow(
|
||||
precip_path, lulc_path, cn_path, n_events_path, stream_path,
|
||||
si_path, output_path)
|
||||
precip_path, n_events_path, stream_path, si_path, output_path)
|
||||
# read the raster output back in to a numpy array
|
||||
quickflow_array = pygeoprocessing.raster_to_numpy_array(output_path)
|
||||
# assert each element is close to the expected value
|
||||
self.assertTrue(numpy.isclose(
|
||||
quickflow_array, expected_quickflow_array).all())
|
||||
numpy.testing.assert_allclose(
|
||||
quickflow_array, expected_quickflow_array, atol=1e-5)
|
||||
|
||||
def test_monthly_quickflow_si_zero(self):
|
||||
"""Test `_calculate_monthly_quick_flow` when s_i is zero"""
|
||||
from natcap.invest.seasonal_water_yield import seasonal_water_yield
|
||||
|
||||
# QF should be equal to P when s_i is 0
|
||||
precip_array = numpy.array([[10.5]], dtype=numpy.float32)
|
||||
si_array = numpy.array([[0]], dtype=numpy.float32)
|
||||
n_events_array = numpy.array([[10]], dtype=numpy.float32)
|
||||
stream_mask = numpy.array([[0]], dtype=numpy.float32)
|
||||
expected_quickflow_array = numpy.array([[10.5]])
|
||||
|
||||
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
|
||||
si_path = os.path.join(self.workspace_dir, 'si.tif')
|
||||
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
|
||||
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
|
||||
|
||||
srs = osr.SpatialReference()
|
||||
srs.ImportFromEPSG(26910) # UTM Zone 10N
|
||||
project_wkt = srs.ExportToWkt()
|
||||
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
|
||||
|
||||
# write all the test arrays to raster files
|
||||
for array, path in [(precip_array, precip_path),
|
||||
(n_events_array, n_events_path),
|
||||
(si_array, si_path),
|
||||
(stream_mask, stream_path)]:
|
||||
# define a nodata value for intermediate outputs
|
||||
pygeoprocessing.numpy_array_to_raster(
|
||||
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
|
||||
seasonal_water_yield._calculate_monthly_quick_flow(
|
||||
precip_path, n_events_path, stream_path, si_path, output_path)
|
||||
numpy.testing.assert_allclose(
|
||||
pygeoprocessing.raster_to_numpy_array(output_path),
|
||||
expected_quickflow_array, atol=1e-5)
|
||||
|
||||
def test_monthly_quickflow_large_si_aim_ratio(self):
|
||||
"""Test `_calculate_monthly_quick_flow` with large s_i/a_im ratio"""
|
||||
from natcap.invest.seasonal_water_yield import seasonal_water_yield
|
||||
|
||||
# with these values, the QF equation would overflow float32 if
|
||||
# we didn't catch it early
|
||||
precip_array = numpy.array([[6]], dtype=numpy.float32)
|
||||
si_array = numpy.array([[23.33]], dtype=numpy.float32)
|
||||
n_events_array = numpy.array([[10]], dtype=numpy.float32)
|
||||
stream_mask = numpy.array([[0]], dtype=numpy.float32)
|
||||
expected_quickflow_array = numpy.array([[0]])
|
||||
|
||||
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
|
||||
si_path = os.path.join(self.workspace_dir, 'si.tif')
|
||||
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
|
||||
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
|
||||
|
||||
srs = osr.SpatialReference()
|
||||
srs.ImportFromEPSG(26910) # UTM Zone 10N
|
||||
project_wkt = srs.ExportToWkt()
|
||||
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
|
||||
|
||||
# write all the test arrays to raster files
|
||||
for array, path in [(precip_array, precip_path),
|
||||
(n_events_array, n_events_path),
|
||||
(si_array, si_path),
|
||||
(stream_mask, stream_path)]:
|
||||
# define a nodata value for intermediate outputs
|
||||
pygeoprocessing.numpy_array_to_raster(
|
||||
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
|
||||
seasonal_water_yield._calculate_monthly_quick_flow(
|
||||
precip_path, n_events_path, stream_path, si_path, output_path)
|
||||
numpy.testing.assert_allclose(
|
||||
pygeoprocessing.raster_to_numpy_array(output_path),
|
||||
expected_quickflow_array, atol=1e-5)
|
||||
|
||||
def test_monthly_quickflow_negative_values_set_to_zero(self):
|
||||
"""Test `_calculate_monthly_quick_flow` with negative QF result"""
|
||||
from natcap.invest.seasonal_water_yield import seasonal_water_yield
|
||||
|
||||
# with these values, the QF equation evaluates to a small negative
|
||||
# number. assert that it is set to zero
|
||||
precip_array = numpy.array([[30]], dtype=numpy.float32)
|
||||
si_array = numpy.array([[10]], dtype=numpy.float32)
|
||||
n_events_array = numpy.array([[10]], dtype=numpy.float32)
|
||||
stream_mask = numpy.array([[0]], dtype=numpy.float32)
|
||||
expected_quickflow_array = numpy.array([[0]])
|
||||
|
||||
precip_path = os.path.join(self.workspace_dir, 'precip.tif')
|
||||
si_path = os.path.join(self.workspace_dir, 'si.tif')
|
||||
n_events_path = os.path.join(self.workspace_dir, 'n_events.tif')
|
||||
stream_path = os.path.join(self.workspace_dir, 'stream.tif')
|
||||
|
||||
srs = osr.SpatialReference()
|
||||
srs.ImportFromEPSG(26910) # UTM Zone 10N
|
||||
project_wkt = srs.ExportToWkt()
|
||||
output_path = os.path.join(self.workspace_dir, 'quickflow.tif')
|
||||
|
||||
# write all the test arrays to raster files
|
||||
for array, path in [(precip_array, precip_path),
|
||||
(n_events_array, n_events_path),
|
||||
(si_array, si_path),
|
||||
(stream_mask, stream_path)]:
|
||||
# define a nodata value for intermediate outputs
|
||||
pygeoprocessing.numpy_array_to_raster(
|
||||
array, -1, (1, -1), (1180000, 690000), project_wkt, path)
|
||||
seasonal_water_yield._calculate_monthly_quick_flow(
|
||||
precip_path, n_events_path, stream_path, si_path, output_path)
|
||||
numpy.testing.assert_allclose(
|
||||
pygeoprocessing.raster_to_numpy_array(output_path),
|
||||
expected_quickflow_array, atol=1e-5)
|
||||
|
||||
def test_calculate_annual_qfi_different_nodata_areas(self):
|
||||
"""Test with qf rasters with different areas of nodata."""
|
||||
|
@ -1079,8 +1176,8 @@ class SeasonalWaterYieldRegressionTests(unittest.TestCase):
|
|||
[100, 100],
|
||||
[200, 200]], dtype=numpy.float32)
|
||||
quickflow_array = numpy.array([
|
||||
[-4.8e-36, -4.822e-36],
|
||||
[ 6.1e-01, 6.1e-01]], dtype=numpy.float32)
|
||||
[0, 0],
|
||||
[0.61, 0.61]], dtype=numpy.float32)
|
||||
flow_dir_array = numpy.array([
|
||||
[15, 25],
|
||||
[50, 50]], dtype=numpy.float32)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
class Store {
|
||||
export default class Store {
|
||||
constructor(options) {
|
||||
this.defaults = options.defaults || {};
|
||||
this.store = this.defaults;
|
||||
|
@ -20,5 +20,3 @@ class Store {
|
|||
this.store = this.defaults;
|
||||
}
|
||||
}
|
||||
|
||||
export default Store;
|
||||
|
|
|
@ -6,6 +6,7 @@ export const ipcMainChannels = {
|
|||
GET_ELECTRON_PATHS: 'get-electron-paths',
|
||||
GET_N_CPUS: 'get-n-cpus',
|
||||
GET_SETTING: 'get-setting',
|
||||
GET_LANGUAGE: 'get-language',
|
||||
INVEST_KILL: 'invest-kill',
|
||||
INVEST_READ_LOG: 'invest-read-log',
|
||||
INVEST_RUN: 'invest-run',
|
||||
|
|
|
@ -4,12 +4,13 @@ import path from 'path';
|
|||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
screen,
|
||||
nativeTheme,
|
||||
Menu,
|
||||
ipcMain
|
||||
} from 'electron';
|
||||
|
||||
import Store from 'electron-store';
|
||||
|
||||
import {
|
||||
createPythonFlaskProcess,
|
||||
getFlaskIsReady,
|
||||
|
@ -72,6 +73,11 @@ export const createWindow = async () => {
|
|||
logger.info(`Running invest-workbench version ${pkg.version}`);
|
||||
nativeTheme.themeSource = 'light'; // override OS/browser setting
|
||||
|
||||
// read language setting from storage and switch to that language
|
||||
// default to en if no language setting exists
|
||||
const store = new Store();
|
||||
i18n.changeLanguage(store.get('language', 'en'));
|
||||
|
||||
splashScreen = new BrowserWindow({
|
||||
width: 574, // dims set to match the image in splash.html
|
||||
height: 500,
|
||||
|
@ -112,14 +118,6 @@ export const createWindow = async () => {
|
|||
menuTemplate(mainWindow, ELECTRON_DEV_MODE, i18n)
|
||||
)
|
||||
);
|
||||
// when language changes, rebuild the menu bar in new language
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
Menu.setApplicationMenu(
|
||||
Menu.buildFromTemplate(
|
||||
menuTemplate(mainWindow, ELECTRON_DEV_MODE, i18n)
|
||||
)
|
||||
);
|
||||
});
|
||||
mainWindow.loadURL(path.join(BASE_URL, 'index.html'));
|
||||
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import i18n from 'i18next';
|
||||
import { ipcMain } from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { app, ipcMain } from 'electron';
|
||||
import { getLogger } from './logger';
|
||||
import { ipcMainChannels } from './ipcMainChannels';
|
||||
|
||||
const logger = getLogger(__filename.split('/').slice(-1)[0]);
|
||||
|
||||
const store = new Store();
|
||||
|
||||
export default function setupChangeLanguage() {
|
||||
ipcMain.on(ipcMainChannels.GET_LANGUAGE, (event) => {
|
||||
// default to en if no language setting exists
|
||||
event.returnValue = store.get('language', 'en');
|
||||
});
|
||||
|
||||
ipcMain.handle(
|
||||
ipcMainChannels.CHANGE_LANGUAGE,
|
||||
(e, languageCode) => {
|
||||
logger.debug('changing language to', languageCode);
|
||||
i18n.changeLanguage(languageCode);
|
||||
store.set('language', languageCode);
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ export default {
|
|||
PORT: PORT, // where the flask app is running
|
||||
ELECTRON_LOG_PATH: electronLogPath,
|
||||
USERGUIDE_PATH: userguidePath,
|
||||
LANGUAGE: ipcRenderer.sendSync(ipcMainChannels.GET_LANGUAGE),
|
||||
logger: {
|
||||
debug: (message) => ipcRenderer.send(ipcMainChannels.LOGGER, 'debug', message),
|
||||
info: (message) => ipcRenderer.send(ipcMainChannels.LOGGER, 'info', message),
|
||||
|
|
|
@ -24,8 +24,6 @@ import { getInvestModelNames } from './server_requests';
|
|||
import InvestJob from './InvestJob';
|
||||
import { dragOverHandlerNone } from './utils';
|
||||
|
||||
import { ipcMainChannels } from '../main/ipcMainChannels';
|
||||
|
||||
const { ipcRenderer } = window.Workbench.electron;
|
||||
|
||||
/** This component manages any application state that should persist
|
||||
|
@ -62,8 +60,7 @@ export default class App extends React.Component {
|
|||
recentJobs: recentJobs,
|
||||
showDownloadModal: this.props.isFirstRun,
|
||||
});
|
||||
const language = await ipcRenderer.invoke(ipcMainChannels.GET_SETTING, 'language');
|
||||
await i18n.changeLanguage(language);
|
||||
await i18n.changeLanguage(window.Workbench.LANGUAGE);
|
||||
ipcRenderer.on('download-status', (downloadedNofN) => {
|
||||
this.setState({
|
||||
downloadedNofN: downloadedNofN,
|
||||
|
|
|
@ -75,7 +75,7 @@ export default function ResourcesTab(props) {
|
|||
}
|
||||
|
||||
const { t, i18n } = useTranslation();
|
||||
const userGuideURL = `${window.Workbench.USERGUIDE_PATH}/${i18n.language}/${docs}`;
|
||||
const userGuideURL = `${window.Workbench.USERGUIDE_PATH}/${window.Workbench.LANGUAGE}/${docs}`;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
|
|
@ -28,15 +28,17 @@ class SettingsModal extends React.Component {
|
|||
this.state = {
|
||||
show: false,
|
||||
languageOptions: null,
|
||||
language: null,
|
||||
loggingLevel: null,
|
||||
taskgraphLoggingLevel: null,
|
||||
nWorkers: null
|
||||
nWorkers: null,
|
||||
language: window.Workbench.LANGUAGE,
|
||||
showConfirmLanguageChange: false,
|
||||
};
|
||||
this.handleShow = this.handleShow.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.setSettings = this.setSettings.bind(this);
|
||||
this.handleChangeLanguage = this.handleChangeLanguage.bind(this);
|
||||
this.switchToDownloadModal = this.switchToDownloadModal.bind(this);
|
||||
}
|
||||
|
||||
|
@ -81,6 +83,15 @@ class SettingsModal extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
handleChangeLanguage() {
|
||||
// if language has changed, refresh the app
|
||||
if (this.state.language !== window.Workbench.LANGUAGE) {
|
||||
// tell the main process to update the language setting in storage
|
||||
// and then relaunch the app
|
||||
ipcRenderer.invoke(ipcMainChannels.CHANGE_LANGUAGE, this.state.language);
|
||||
}
|
||||
}
|
||||
|
||||
switchToDownloadModal() {
|
||||
this.props.showDownloadModal();
|
||||
this.handleClose();
|
||||
|
@ -94,21 +105,22 @@ class SettingsModal extends React.Component {
|
|||
loggingLevel,
|
||||
taskgraphLoggingLevel,
|
||||
nWorkers,
|
||||
showConfirmLanguageChange,
|
||||
} = this.state;
|
||||
const { clearJobsStorage, nCPU, t } = this.props;
|
||||
|
||||
const nWorkersOptions = [
|
||||
[-1, `${t('Synchronous')} (-1)`],
|
||||
[0, `${t('Threaded task management')} (0)`]
|
||||
[0, `${t('Threaded task management')} (0)`],
|
||||
];
|
||||
for (let i = 1; i <= nCPU; i += 1) {
|
||||
nWorkersOptions.push([i, `${i} ${t('CPUs')}`]);
|
||||
}
|
||||
const logLevelOptions = { // map value to display name
|
||||
'DEBUG': t('DEBUG'),
|
||||
'INFO': t('INFO'),
|
||||
'WARNING': t('WARNING'),
|
||||
'ERROR': t('ERROR')
|
||||
DEBUG: t('DEBUG'),
|
||||
INFO: t('INFO'),
|
||||
WARNING: t('WARNING'),
|
||||
ERROR: t('ERROR'),
|
||||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -145,18 +157,19 @@ class SettingsModal extends React.Component {
|
|||
<Form.Label column sm="8" htmlFor="language-select">
|
||||
<MdTranslate className="language-icon" />
|
||||
{t('Language')}
|
||||
<Form.Text className="text-nowrap" muted>
|
||||
<MdWarningAmber className="align-text-bottom ml-3" />
|
||||
{t('Changing this setting will refresh the app and close all tabs')}
|
||||
</Form.Text>
|
||||
</Form.Label>
|
||||
<Col sm="4">
|
||||
<Form.Control
|
||||
id="language-select"
|
||||
as="select"
|
||||
name="language"
|
||||
value={language}
|
||||
onChange={this.handleChange}
|
||||
value={window.Workbench.LANGUAGE}
|
||||
onChange={
|
||||
(event) => this.setState({
|
||||
showConfirmLanguageChange: true,
|
||||
language: event.target.value
|
||||
})}
|
||||
>
|
||||
{Object.entries(languageOptions).map((entry) => {
|
||||
const [value, displayName] = entry;
|
||||
|
@ -273,6 +286,30 @@ class SettingsModal extends React.Component {
|
|||
<span>{t('no invest workspaces will be deleted')}</span>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
{
|
||||
(languageOptions) ? (
|
||||
<Modal show={showConfirmLanguageChange} className="confirm-modal" >
|
||||
<Modal.Header>
|
||||
<Modal.Title as="h5" >{t('Warning')}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
{t('Changing this setting will close your tabs and relaunch the app.')}
|
||||
</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => this.setState({ showConfirmLanguageChange: false })}
|
||||
>{t('Cancel')}</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={this.handleChangeLanguage}
|
||||
>{t('Change to ') + languageOptions[language]}</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
) : <React.Fragment />
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -362,7 +362,7 @@ function AboutModal(props) {
|
|||
// create link to users guide entry for this arg
|
||||
// anchor name is the arg name, with underscores replaced with hyphens
|
||||
const userguideURL = `
|
||||
${window.Workbench.USERGUIDE_PATH}/${i18n.language}/${userguide}#${argkey.replace(/_/g, '-')}`;
|
||||
${window.Workbench.USERGUIDE_PATH}/${window.Workbench.LANGUAGE}/${userguide}#${argkey.replace(/_/g, '-')}`;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Button
|
||||
|
|
|
@ -4,7 +4,6 @@ import { Translation } from 'react-i18next';
|
|||
|
||||
import i18n from '../i18n/i18n';
|
||||
import { handleClickExternalURL } from './handlers';
|
||||
import { getSettingsValue } from '../components/SettingsModal/SettingsStorage';
|
||||
import { ipcMainChannels } from '../../main/ipcMainChannels';
|
||||
import investLogo from '../static/invest-logo.png';
|
||||
|
||||
|
@ -15,8 +14,7 @@ async function getInvestVersion() {
|
|||
return investVersion;
|
||||
}
|
||||
|
||||
const language = await getSettingsValue('language');
|
||||
await i18n.changeLanguage(language);
|
||||
await i18n.changeLanguage(window.Workbench.LANGUAGE);
|
||||
const investVersion = await getInvestVersion();
|
||||
ReactDom.render(
|
||||
<Translation>
|
||||
|
|
|
@ -7,12 +7,10 @@ import {
|
|||
handleClickExternalURL,
|
||||
handleClickFindLogfiles
|
||||
} from './handlers';
|
||||
import { getSettingsValue } from '../components/SettingsModal/SettingsStorage';
|
||||
import investLogo from '../static/invest-logo.png';
|
||||
import natcapLogo from '../static/NatCapLogo.jpg';
|
||||
|
||||
const language = await getSettingsValue('language');
|
||||
await i18n.changeLanguage(language);
|
||||
await i18n.changeLanguage(window.Workbench.LANGUAGE);
|
||||
ReactDom.render(
|
||||
<Translation>
|
||||
{(t, { i18n }) => (
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
import { ipcMainChannels } from '../main/ipcMainChannels';
|
||||
|
||||
const HOSTNAME = 'http://127.0.0.1';
|
||||
const { logger, PORT } = window.Workbench;
|
||||
const { ipcRenderer } = window.Workbench.electron;
|
||||
const { logger, PORT, LANGUAGE } = window.Workbench;
|
||||
const PREFIX = 'api';
|
||||
|
||||
// The Flask server sends UTF-8 encoded responses by default
|
||||
|
@ -17,13 +14,12 @@ const PREFIX = 'api';
|
|||
* @returns {Promise} resolves object
|
||||
*/
|
||||
export async function getInvestModelNames() {
|
||||
const language = await ipcRenderer.invoke(ipcMainChannels.GET_SETTING, 'language');
|
||||
return (
|
||||
window.fetch(`${HOSTNAME}:${PORT}/${PREFIX}/models?language=${language}`, {
|
||||
window.fetch(`${HOSTNAME}:${PORT}/${PREFIX}/models?language=${LANGUAGE}`, {
|
||||
method: 'get',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.catch((error) => { logger.error(`${error.stack}`) })
|
||||
.catch((error) => { logger.error(`${error.stack}`); })
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -34,9 +30,8 @@ export async function getInvestModelNames() {
|
|||
* @returns {Promise} resolves object
|
||||
*/
|
||||
export async function getSpec(payload) {
|
||||
const language = await ipcRenderer.invoke(ipcMainChannels.GET_SETTING, 'language');
|
||||
return (
|
||||
window.fetch(`${HOSTNAME}:${PORT}/${PREFIX}/getspec?language=${language}`, {
|
||||
window.fetch(`${HOSTNAME}:${PORT}/${PREFIX}/getspec?language=${LANGUAGE}`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify(payload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
@ -56,9 +51,8 @@ export async function getSpec(payload) {
|
|||
* @returns {Promise} resolves array
|
||||
*/
|
||||
export async function fetchValidation(payload) {
|
||||
const language = await ipcRenderer.invoke(ipcMainChannels.GET_SETTING, 'language');
|
||||
return (
|
||||
window.fetch(`${HOSTNAME}:${PORT}/${PREFIX}/validate?language=${language}`, {
|
||||
window.fetch(`${HOSTNAME}:${PORT}/${PREFIX}/validate?language=${LANGUAGE}`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify(payload),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
@ -102,7 +96,7 @@ export function getVectorColumnNames(payload) {
|
|||
return (
|
||||
window.fetch(`${HOSTNAME}:${PORT}/${PREFIX}/colnames`, {
|
||||
method: 'post',
|
||||
body: JSON.stringify({vector_path: payload}),
|
||||
body: JSON.stringify({ vector_path: payload }),
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
.then((response) => response.json())
|
||||
|
@ -189,7 +183,6 @@ export function writeParametersToFile(payload) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the mapping of supported language codes to display names.
|
||||
*
|
||||
|
|
|
@ -572,6 +572,11 @@ input[type=text]::placeholder {
|
|||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.confirm-modal .modal-content {
|
||||
background-color: papayawhip;
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
.error-boundary {
|
||||
max-width:600px;
|
||||
margin: 0 auto;
|
||||
|
|
|
@ -12,6 +12,7 @@ if (!process.env.ELECTRON_LOG_LEVEL) {
|
|||
if (global.window) {
|
||||
// mock the work of preload.js here:
|
||||
const api = require('../src/preload/api').default;
|
||||
api.LANGUAGE = 'en';
|
||||
global.window.Workbench = api;
|
||||
|
||||
// normally electron main passes port to preload.
|
||||
|
|
|
@ -193,6 +193,7 @@ describe('createWindow', () => {
|
|||
const expectedOnChannels = [
|
||||
ipcMainChannels.DOWNLOAD_URL,
|
||||
ipcMainChannels.GET_ELECTRON_PATHS,
|
||||
ipcMainChannels.GET_LANGUAGE,
|
||||
ipcMainChannels.INVEST_RUN,
|
||||
ipcMainChannels.INVEST_KILL,
|
||||
ipcMainChannels.INVEST_READ_LOG,
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ipcRenderer } from 'electron';
|
|||
import {
|
||||
render, waitFor, within
|
||||
} from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
|
@ -16,20 +15,13 @@ import {
|
|||
getSupportedLanguages
|
||||
} from '../../src/renderer/server_requests';
|
||||
import InvestJob from '../../src/renderer/InvestJob';
|
||||
// import {
|
||||
// getSettingsValue, saveSettingsStore
|
||||
// } from '../../src/renderer/components/SettingsModal/SettingsStorage';
|
||||
import { setupSettingsHandlers } from '../../src/main/settingsStore';
|
||||
import { ipcMainChannels } from '../../src/main/ipcMainChannels';
|
||||
import {
|
||||
setupInvestRunHandlers,
|
||||
setupInvestLogReaderHandler,
|
||||
} from '../../src/main/setupInvestHandlers';
|
||||
import writeInvestParameters from '../../src/main/writeInvestParameters';
|
||||
import { removeIpcMainListeners } from '../../src/main/main';
|
||||
|
||||
import {
|
||||
getSettingsValue,
|
||||
} from '../../src/renderer/components/SettingsModal/SettingsStorage';
|
||||
import { mockUISpec } from './utils';
|
||||
|
||||
// It's quite a pain to dynamically mock a const from a module,
|
||||
// here we do it by importing as another object, then
|
||||
// we can overwrite the object we want to mock later
|
||||
|
@ -399,33 +391,12 @@ describe('InVEST global settings: dialog interactions', () => {
|
|||
const tgLoggingLabelText = 'Taskgraph logging threshold';
|
||||
const languageLabelText = 'Language';
|
||||
|
||||
const { location } = global.window;
|
||||
beforeAll(() => {
|
||||
// // Because changing the language triggers a location.reload
|
||||
// delete global.window.location;
|
||||
// Object.defineProperty(global.window, 'location', {
|
||||
// configurable: true,
|
||||
// value: { reload: jest.fn() },
|
||||
// });
|
||||
setupSettingsHandlers();
|
||||
});
|
||||
|
||||
// afterAll(() => {
|
||||
// removeIpcMainListeners();
|
||||
// // window.location.reload is not implemented in jsdom
|
||||
// delete global.window.location;
|
||||
// Object.defineProperty(global.window, 'location', {
|
||||
// configurable: true,
|
||||
// value: { reload: jest.fn() },
|
||||
// });
|
||||
// });
|
||||
|
||||
afterAll(() => {
|
||||
removeIpcMainListeners();
|
||||
Object.defineProperty(global.window, 'location', {
|
||||
configurable: true,
|
||||
value: location,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -434,19 +405,16 @@ describe('InVEST global settings: dialog interactions', () => {
|
|||
ipcRenderer.invoke.mockImplementation(() => Promise.resolve());
|
||||
});
|
||||
|
||||
// afterEach(async () => {
|
||||
// await clearSettingsStore();
|
||||
// });
|
||||
|
||||
test('Invest settings save on change', async () => {
|
||||
const nWorkersLabel = 'Threaded task management (0)';
|
||||
const nWorkersValue = '0';
|
||||
const loggingLevel = 'DEBUG';
|
||||
const tgLoggingLevel = 'DEBUG';
|
||||
const languageValue = 'es';
|
||||
const spyInvoke = jest.spyOn(ipcRenderer, 'invoke');
|
||||
|
||||
const {
|
||||
getByText, getByRole, getByLabelText, findByRole,
|
||||
getByText, getByRole, getByLabelText, findByRole, findByText,
|
||||
} = render(
|
||||
<App />
|
||||
);
|
||||
|
@ -455,7 +423,6 @@ describe('InVEST global settings: dialog interactions', () => {
|
|||
const nWorkersInput = getByLabelText(nWorkersLabelText, { exact: false });
|
||||
const loggingInput = getByLabelText(loggingLabelText);
|
||||
const tgLoggingInput = getByLabelText(tgLoggingLabelText);
|
||||
const languageInput = getByLabelText(languageLabelText, { exact: false });
|
||||
|
||||
await userEvent.selectOptions(nWorkersInput, [getByText(nWorkersLabel)]);
|
||||
await waitFor(() => { expect(nWorkersInput).toHaveValue(nWorkersValue); });
|
||||
|
@ -463,23 +430,25 @@ describe('InVEST global settings: dialog interactions', () => {
|
|||
await waitFor(() => { expect(loggingInput).toHaveValue(loggingLevel); });
|
||||
await userEvent.selectOptions(tgLoggingInput, [tgLoggingLevel]);
|
||||
await waitFor(() => { expect(tgLoggingInput).toHaveValue(tgLoggingLevel); });
|
||||
await userEvent.selectOptions(languageInput, [languageValue]);
|
||||
await waitFor(() => { expect(languageInput).toHaveValue(languageValue); });
|
||||
await userEvent.click(getByRole('button', { name: 'close settings' }));
|
||||
|
||||
// Check values were saved in app
|
||||
await userEvent.click(await findByRole('button', { name: 'settings' }));
|
||||
const languageInput = getByLabelText(languageLabelText, { exact: false });
|
||||
await waitFor(() => {
|
||||
expect(nWorkersInput).toHaveValue(nWorkersValue);
|
||||
expect(loggingInput).toHaveValue(loggingLevel);
|
||||
expect(tgLoggingInput).toHaveValue(tgLoggingLevel);
|
||||
expect(languageInput).toHaveValue(languageValue);
|
||||
expect(languageInput).toHaveValue('en');
|
||||
});
|
||||
|
||||
// expect(settingsStore.get('nWorkers')).toBe(nWorkersValue);
|
||||
// expect(settingsStore.get('loggingLevel')).toBe(loggingLevel);
|
||||
// expect(settingsStore.get('taskgraphLoggingLevel')).toBe(tgLoggingLevel);
|
||||
// expect(settingsStore.get('language')).toBe(languageValue);
|
||||
expect(await getSettingsValue('nWorkers')).toBe(nWorkersValue);
|
||||
expect(await getSettingsValue('loggingLevel')).toBe(loggingLevel);
|
||||
expect(await getSettingsValue('taskgraphLoggingLevel')).toBe(tgLoggingLevel);
|
||||
|
||||
await userEvent.selectOptions(languageInput, [languageValue]);
|
||||
await userEvent.click(await findByText('Change to spanish'));
|
||||
expect(spyInvoke).toHaveBeenCalledWith(ipcMainChannels.CHANGE_LANGUAGE, languageValue);
|
||||
});
|
||||
|
||||
// test('Load invest settings from storage and test Reset', async () => {
|
||||
|
@ -516,13 +485,14 @@ describe('InVEST global settings: dialog interactions', () => {
|
|||
// expect(languageInput).toHaveValue(expectedSettings.language);
|
||||
// });
|
||||
|
||||
// // Test Reset sets values to default
|
||||
// Test Reset sets values to default
|
||||
// await userEvent.click(getByText('Reset to Defaults'));
|
||||
// await waitFor(() => {
|
||||
// expect(nWorkersInput).toHaveValue(defaultSettings.nWorkers);
|
||||
// expect(loggingInput).toHaveValue(defaultSettings.loggingLevel);
|
||||
// expect(tgLoggingInput).toHaveValue(defaultSettings.tgLoggingLevel);
|
||||
// expect(languageInput).toHaveValue(defaultSettings.language);
|
||||
// // should NOT change the language setting - it's handled differently
|
||||
// expect(languageInput).toHaveValue(expectedSettings.language);
|
||||
// });
|
||||
// });
|
||||
|
||||
|
@ -544,40 +514,3 @@ describe('InVEST global settings: dialog interactions', () => {
|
|||
expect(queryByText('Settings')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Translation', () => {
|
||||
const { location } = global.window;
|
||||
beforeAll(async () => {
|
||||
getInvestModelNames.mockResolvedValue({});
|
||||
getSupportedLanguages.mockResolvedValue({ en: 'english', ll: 'foo' });
|
||||
|
||||
delete global.window.location;
|
||||
Object.defineProperty(global.window, 'location', {
|
||||
configurable: true,
|
||||
value: { reload: jest.fn() },
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(global.window, 'location', {
|
||||
configurable: true,
|
||||
value: location,
|
||||
});
|
||||
});
|
||||
|
||||
test('Text rerenders in new language when language setting changes', async () => {
|
||||
const { findByLabelText } = render(<App />);
|
||||
|
||||
await userEvent.click(await findByLabelText('settings'));
|
||||
const languageInput = await findByLabelText('Language', { exact: false });
|
||||
expect(languageInput).toHaveValue('en');
|
||||
|
||||
await userEvent.selectOptions(languageInput, 'll');
|
||||
// await waitFor(() => {
|
||||
// expect(global.window.location.reload).toHaveBeenCalled();
|
||||
// });
|
||||
// // because we can't reload the window in the test environment,
|
||||
// // components won't actually rerender in the new language
|
||||
// expect(languageInput).toHaveValue('ll');
|
||||
});
|
||||
});
|
||||
|
|
1288
workbench/yarn.lock
1288
workbench/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue