gms_object.py 88.9 KB
Newer Older
1
2
3
4
5
# -*- coding: utf-8 -*-

import collections
import copy
import datetime
6
import functools
7
8
9
10
11
12
13
import glob
import json
import os
import re
import shutil
import sys
import warnings
14
import logging
15
from collections import OrderedDict
16
from itertools import chain
17
from typing import Iterable, List, Union, TYPE_CHECKING  # noqa F401  # flake8 issue
18
19
20

import numpy as np
import spectral
21
from spectral.io import envi
22
from numba import jit
23
from pandas import DataFrame, read_csv
24
from nested_dict import nested_dict
25

26
27
28
29
try:
    from osgeo import gdalnumeric
except ImportError:
    import gdalnumeric
30

31
from geoarray import GeoArray
32
from py_tools_ds.geo.coord_grid import is_coord_grid_equal
33
from py_tools_ds.geo.projection import EPSG2WKT
34
35
36
from py_tools_ds.geo.map_info import geotransform2mapinfo, mapinfo2geotransform
from py_tools_ds.geo.coord_calc import calc_FullDataset_corner_positions
from py_tools_ds.geo.coord_trafo import pixelToLatLon, pixelToMapYX
37
from sicor.options import get_options as get_ac_options
38

39
from ..misc.logging import GMS_logger as DatasetLogger
40
from ..model.mgrs_tile import MGRS_tile
41
from ..model.metadata import METADATA, get_dict_LayerOptTherm, metaDict_to_metaODict
42
43
44
from ..model.dataset import Dataset
from ..misc import path_generator as PG
from ..misc import database_tools as DB_T
45
from ..options.config import GMS_config as CFG
46
47
48
from ..algorithms import geoprocessing as GEOP
from ..io import input_reader as INP_R
from ..io import output_writer as OUT_W
49
50
from ..misc import helper_functions as HLP_F
from ..misc import definition_dicts as DEF_D
51
from ..misc.locks import MultiSlotLock
52

53
54
55
if TYPE_CHECKING:
    from ..algorithms.L1C_P import L1C_object  # noqa F401  # flake8 issue

56
__author__ = 'Daniel Scheffler'
57
58


59
class GMS_object(Dataset):
60
61
62
63
    # class attributes
    # NOTE: these attributes can be modified and seen by ALL GMS_object instances
    proc_status_all_GMSobjs = nested_dict()

64
65
66
67
    def __init__(self, pathImage=''):
        # get all attributes of base class "Dataset"
        super(GMS_object, self).__init__()

68
        # add private attributes
69
        self._dict_LayerOptTherm = None
70
71
        self._cloud_masking_algorithm = None
        self._meta_odict = None
72
        self._coreg_info = None
73

74
        self.job_ID = CFG.ID
75
        # FIXME not needed anymore?:
76
        # self.dataset_ID = int(DB_T.get_info_from_postgreSQLdb(CFG.conn_database, 'scenes', ['datasetid'],
77
78
79
        #                                {'id': self.scene_ID})[0][0]) if self.scene_ID !=-9999 else -9999
        self.scenes_proc_ID = None  # set by Output writer after creation/update of db record in table scenes_proc
        self.mgrs_tiles_proc_ID = None  # set by Output writer after creation/update of db rec in table mgrs_tiles_proc
80
        self.MGRS_info = None
Daniel Scheffler's avatar
Daniel Scheffler committed
81
82
        self.lonlat_arr = None  # set by self.write_tiles_to_ENVIfile
        self.trueDataCornerUTM = None  # set by self.from_tiles
83
84

        # set pathes
85
86
87
88
        self.path_cloud_class_obj = ''

        # handle initialization arguments
        if pathImage:
89
90
            # run the setter for 'arr' of the base class 'Dataset' which creates an Instance of GeoArray
            self.arr = pathImage
91
92
93
94

    def __getstate__(self):
        """Defines how the attributes of GMS object are pickled."""

Daniel Scheffler's avatar
Bugfix    
Daniel Scheffler committed
95
        self.close_loggers()
96
        del self.pathGen  # path generator can only be used for the current processing level
97
98

        # delete arrays if their in-mem size is to big to be pickled
99
        # => (avoids MaybeEncodingError: Error sending result: '[<gms_preprocessing.algorithms.L2C_P.L2C_object
100
        #    object at 0x7fc44f6399e8>]'. Reason: 'error("'i' format requires -2147483648 <= number <= 2147483647",)')
101
        if self.proc_level == 'L2C' and CFG.inmem_serialization:
102
103
            # FIXME check by bandname
            if self.mask_nodata is not None and self.masks.bands > 1 and self.mask_clouds is not None:
104
                del self.masks
105

Daniel Scheffler's avatar
Bugfix    
Daniel Scheffler committed
106
107
        return self.__dict__

108
    def set_pathes(self):
109
110
111
112
113
114
        self.baseN = self.pathGen.get_baseN()
        self.path_procdata = self.pathGen.get_path_procdata()
        self.ExtractedFolder = self.pathGen.get_path_tempdir()
        self.path_logfile = self.pathGen.get_path_logfile()
        self.pathGen = PG.path_generator(self.__dict__)  # passes a logger in addition to previous attributes
        self.path_archive = self.pathGen.get_local_archive_path_baseN()
115

116
        if not CFG.inmem_serialization:
Daniel Scheffler's avatar
Daniel Scheffler committed
117
            self.path_InFilePreprocessor = os.path.join(self.ExtractedFolder, '%s%s_DN.bsq'
118
119
                                                        % (self.entity_ID,
                                                           ('_%s' % self.subsystem if self.subsystem else '')))
120
        else:  # keep data in memory
121
            self.path_InFilePreprocessor = None  # None: keeps all produced data in memory (numpy array attributes)
122
123
124
125
126
127

        self.path_MetaPreprocessor = self.path_archive

    def validate_pathes(self):
        if not os.path.isfile(self.path_archive) and not os.path.isdir(self.path_archive):
            self.logger.info("The %s dataset '%s' has not been processed earlier and no corresponding raw data archive"
128
                             "has been found at %s." % (self.sensor, self.entity_ID, self.path_archive))
129
            self.logger.info('Trying to download the dataset...')
130
            self.path_archive_valid = self._data_downloader(self.sensor, self.entity_ID)
131
132
133
        else:
            self.path_archive_valid = True

134
        if not CFG.inmem_serialization and self.ExtractedFolder and not os.path.isdir(self.ExtractedFolder):
135
136
137
138
139
140
141
            os.makedirs(self.ExtractedFolder)

        assert os.path.exists(self.path_archive), 'Invalid path to RAW data. File %s does not exist at %s.' \
                                                  % (os.path.basename(self.path_archive),
                                                     os.path.dirname(self.path_archive))
        assert isinstance(self.path_archive, str), 'Invalid path to RAW data. Got %s instead of string or unicode.' \
                                                   % type(self.path_archive)
142
        if not CFG.inmem_serialization and self.ExtractedFolder:
143
144
            assert os.path.exists(self.path_archive), \
                'Invalid path for temporary files. Directory %s does not exist.' % self.ExtractedFolder
145
146
147
148
149
150
151
152

    @property
    def logger(self):
        if self._loggers_disabled:
            return None
        if self._logger and self._logger.handlers[:]:
            return self._logger
        else:
153
            self._logger = DatasetLogger('log__' + self.baseN, fmt_suffix=self.scene_ID, path_logfile=self.path_logfile,
154
                                         log_level=CFG.log_level, append=True)
155
156
157
158
            return self._logger

    @logger.setter
    def logger(self, logger):
159
        assert isinstance(logger, logging.Logger) or logger in ['not set', None], \
160
            "GMS_obj.logger can not be set to %s." % logger
161
162

        # save prior logs
163
        # if logger is None and self._logger is not None:
164
        #    self.log += self.logger.captured_stream
165
166
        self._logger = logger

167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
    @property
    def proc_status(self):
        # type: () -> str
        """
        Get the processing status of the current GMS_object (subclass) instance for the current processing level.

        Possible values: 'initialized', 'running', 'finished', 'failed'
        """
        # NOTE: self.proc_status_all_GMSobjs is a class attribute (visible and modifyable from any other subsystem)
        return self.proc_status_all_GMSobjs[self.scene_ID][self.subsystem][self.proc_level]

    @proc_status.setter
    def proc_status(self, new_status):
        # type: (str) -> None
        self.proc_status_all_GMSobjs[self.scene_ID][self.subsystem][self.proc_level] = new_status

183
184
    @property
    def GMS_identifier(self):
185
186
        return collections.OrderedDict(zip(
            ['image_type', 'Satellite', 'Sensor', 'Subsystem', 'proc_level', 'dataset_ID', 'logger'],
187
188
            [self.image_type, self.satellite, self.sensor, self.subsystem, self.proc_level, self.dataset_ID,
             self.logger]))
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204

    @property
    def MetaObj(self):
        if self._meta_odict:
            # if there is already a meta_odict -> create a new MetaObj from it (ensures synchronization!)
            self._MetaObj = METADATA(self.GMS_identifier).from_odict(self._meta_odict)
            del self.meta_odict
        elif not self._MetaObj:
            # if there is no meta_odict and no MetaObj -> create MetaObj by reading metadata from disk
            pass  # reading from disk should use L1A_P.L1A_object.import_metadata -> so just return None

        return self._MetaObj

    @MetaObj.setter
    def MetaObj(self, MetaObj):
        assert isinstance(MetaObj, METADATA), "'MetaObj' can only be set to an instance of METADATA class. " \
205
                                              "Got %s." % type(MetaObj)
206
207
208
        self._MetaObj = MetaObj

        # update meta_odict
209
        del self.meta_odict  # it is recreated if getter is used the next time
210
211
212

    @MetaObj.deleter
    def MetaObj(self):
213
214
215
216
217
        if hasattr(self, '_MetaObj') and self._MetaObj and hasattr(self._MetaObj, 'logger') and \
                self._MetaObj.logger not in [None, 'not set']:
            self._MetaObj.logger.close()
            self._MetaObj.logger = None

218
219
220
221
222
        self._MetaObj = None

    @property
    def meta_odict(self):
        if self._MetaObj:
223
            # if there is already a MetaObj -> create new meta_odict from it (ensures synchronization!)
224
225
226
227
            self._meta_odict = self._MetaObj.to_odict()
            del self.MetaObj
        elif not self._meta_odict:
            # if there is no MetaObj and no meta_odict -> use MetaObj getter to read metadata from disk
228
            pass  # reading from disk should use L1A_P.L1A_object.import_metadata -> so just return None
229
230
231
232
233
234
            self._meta_odict = None

        return self._meta_odict

    @meta_odict.setter
    def meta_odict(self, odict):
235
236
        assert isinstance(odict, (collections.OrderedDict, dict)), "'meta_odict' can only be set to an instance of " \
                                                                   "collections.OrderedDict. Got %s." % type(odict)
237
238
239
        self._meta_odict = odict

        # update MetaObj
240
        del self.MetaObj  # it is recreated if getter is used the next time
241
242
243
244
245

    @meta_odict.deleter
    def meta_odict(self):
        self._meta_odict = None

246
247
248
249
250
    @property
    def dict_LayerOptTherm(self):
        if self._dict_LayerOptTherm:
            return self._dict_LayerOptTherm
        elif self.LayerBandsAssignment:
251
            self._dict_LayerOptTherm = get_dict_LayerOptTherm(self.identifier, self.LayerBandsAssignment)
252
253
254
255
256
257
258
            return self._dict_LayerOptTherm
        else:
            return None

    @property
    def georef(self):
        """Returns True if the current dataset can serve as spatial reference."""
Daniel Scheffler's avatar
Daniel Scheffler committed
259

260
261
262
263
        return True if self.image_type == 'RSD' and re.search('OLI', self.sensor, re.I) else False

    @property
    def coreg_needed(self):
264
        if self._coreg_needed is None:
265
            self._coreg_needed = not (self.dataset_ID == CFG.datasetid_spatial_ref)
266
        return self._coreg_needed
267
268
269
270
271

    @coreg_needed.setter
    def coreg_needed(self, value):
        self._coreg_needed = value

272
273
274
275
276
277
278
279
280
281
282
283
284
285
    @property
    def coreg_info(self):
        if not self._coreg_info:
            self._coreg_info = {
                'corrected_shifts_px': {'x': 0, 'y': 0},
                'corrected_shifts_map': {'x': 0, 'y': 0},
                'original map info': self.meta_odict['map info'],
                'updated map info': None,
                'reference scene ID': None,
                'reference entity ID': None,
                'reference geotransform': None,
                # reference projection must be the own projection in order to avoid overwriting with a wrong EPSG
                'reference projection': self.meta_odict['coordinate system string'],
                'reference extent': {'rows': None, 'cols': None},
286
287
                'reference grid': [list(CFG.spatial_ref_gridx),
                                   list(CFG.spatial_ref_gridy)],
288
289
290
291
292
293
294
295
296
                'success': False
            }

        return self._coreg_info

    @coreg_info.setter
    def coreg_info(self, val):
        self._coreg_info = val

297
298
299
300
    @property
    def resamp_needed(self):
        if self._resamp_needed is None:
            gt = mapinfo2geotransform(self.meta_odict['map info'])
301
302
            self._resamp_needed = not is_coord_grid_equal(gt, CFG.spatial_ref_gridx,
                                                          CFG.spatial_ref_gridy)
303
304
305
306
307
308
309
310
        return self._resamp_needed

    @resamp_needed.setter
    def resamp_needed(self, value):
        self._resamp_needed = value

    @property
    def masks(self):
311
        # if self.mask_nodata is not None and self.mask_clouds is not None and \
312
313
314
315
        #     self._masks is not None and self._masks.bands==1:

        #     self.build_combined_masks_array()

316
317
318
        return self._masks

    @masks.setter
319
    def masks(self, *geoArr_initArgs):
320
321
322
323
        """
        NOTE: This does not automatically update mask_nodata and mask_clouds BUT if mask_nodata and mask_clouds are
        None their getters will automatically synchronize!
        """
Daniel Scheffler's avatar
Daniel Scheffler committed
324

325
        if geoArr_initArgs[0] is not None:
326
            self._masks = GeoArray(*geoArr_initArgs)
327
            self._masks.nodata = 0
328
329
            self._masks.gt = self.arr.gt
            self._masks.prj = self.arr.prj
330
331
        else:
            del self.masks
332

333
334
335
336
    @masks.deleter
    def masks(self):
        self._masks = None

337
338
339
340
341
342
343
344
345
346
347
348
349
    @property
    def mask_clouds_confidence(self):
        return self._mask_clouds_confidence

    @mask_clouds_confidence.setter
    def mask_clouds_confidence(self, *geoArr_initArgs):
        if geoArr_initArgs[0] is not None:
            cnfArr = GeoArray(*geoArr_initArgs)

            assert cnfArr.shape == self.arr.shape[:2], \
                "The 'mask_clouds_confidence' GeoArray can only be instanced with an array of the same dimensions " \
                "like GMS_obj.arr. Got %s." % str(cnfArr.shape)

Daniel Scheffler's avatar
Daniel Scheffler committed
350
            # noinspection PyProtectedMember
351
352
353
354
            if cnfArr._nodata is None:
                cnfArr.nodata = DEF_D.get_outFillZeroSaturated(cnfArr.dtype)[0]
            cnfArr.gt = self.arr.gt
            cnfArr.prj = self.arr.prj
355
            cnfArr.bandnames = ['confidence']
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387

            self._mask_clouds_confidence = cnfArr
        else:
            del self._mask_clouds_confidence

    @mask_clouds_confidence.deleter
    def mask_clouds_confidence(self):
        self._mask_clouds_confidence = None

    @property
    def ac_errors(self):
        """Returns an instance of GeoArray containing error information calculated by the atmospheric correction.

        :return:
        """

        return self._ac_errors  # FIXME should give a warning if None

    @ac_errors.setter
    def ac_errors(self, *geoArr_initArgs):
        if geoArr_initArgs[0] is not None:
            errArr = GeoArray(*geoArr_initArgs)

            if CFG.ac_bandwise_accuracy:
                assert errArr.shape == self.arr.shape, \
                    "The 'ac_errors' GeoArray can only be instanced with an array of the same dimensions like " \
                    "GMS_obj.arr. Got %s." % str(errArr.shape)
            else:
                assert errArr.shape[:2] == self.arr.shape[:2], \
                    "The 'ac_errors' GeoArray can only be instanced with an array of the same X/Y dimensions like " \
                    "GMS_obj.arr. Got %s." % str(errArr.shape)

Daniel Scheffler's avatar
Daniel Scheffler committed
388
            # noinspection PyProtectedMember
389
390
391
392
            if errArr._nodata is None:
                errArr.nodata = DEF_D.get_outFillZeroSaturated(errArr.dtype)[0]
            errArr.gt = self.arr.gt
            errArr.prj = self.arr.prj
393
            errArr.bandnames = self.LBA2bandnames(self.LayerBandsAssignment) if errArr.ndim == 3 else ['median']
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425

            self._ac_errors = errArr
        else:
            del self.ac_errors

    @ac_errors.deleter
    def ac_errors(self):
        self._ac_errors = None

    @property
    def spec_homo_errors(self):
        """Returns an instance of GeoArray containing error information calculated during spectral homogenization.

        :return:
        """

        return self._spec_homo_errors  # FIXME should give a warning if None

    @spec_homo_errors.setter
    def spec_homo_errors(self, *geoArr_initArgs):
        if geoArr_initArgs[0] is not None:
            errArr = GeoArray(*geoArr_initArgs)

            if CFG.spechomo_bandwise_accuracy:
                assert errArr.shape == self.arr.shape, \
                    "The 'spec_homo_errors' GeoArray can only be instanced with an array of the same dimensions like " \
                    "GMS_obj.arr. Got %s." % str(errArr.shape)
            else:
                assert errArr.shape[:2] == self.arr.shape[:2], \
                    "The 'spec_homo_errors' GeoArray can only be instanced with an array of the same X/Y dimensions " \
                    "like GMS_obj.arr. Got %s." % str(errArr.shape)

Daniel Scheffler's avatar
Daniel Scheffler committed
426
            # noinspection PyProtectedMember
427
428
429
430
            if errArr._nodata is None:
                errArr.nodata = DEF_D.get_outFillZeroSaturated(errArr.dtype)[0]
            errArr.gt = self.arr.gt
            errArr.prj = self.arr.prj
431
            errArr.bandnames = self.LBA2bandnames(self.LayerBandsAssignment) if errArr.ndim == 3 else ['median']
432
433
434
435
436
437
438
439
440
441
442

            self._spec_homo_errors = errArr
        else:
            del self.spec_homo_errors

    @spec_homo_errors.deleter
    def spec_homo_errors(self):
        self._spec_homo_errors = None

    @property
    def accuracy_layers(self):
443
444
445
446
        if not self._accuracy_layers:
            if not self.proc_level.startswith('L2'):
                self.logger.warning('Attempt to get %s accuracy layers failed - they are a Level 2 feature only.'
                                    % self.proc_level)
447

448
449
450
451
            self.logger.info('Generating combined accuracy layers array..')
            try:
                from ..algorithms.L2C_P import AccuracyCube
                self._accuracy_layers = AccuracyCube(self)
452
453
454
455

            except ValueError as e:
                if str(e) == 'The given GMS_object contains no accuracy layers for combination.':
                    if CFG.ac_estimate_accuracy or CFG.spechomo_estimate_accuracy:
456
457
                        self.logger.warning('The given GMS_object contains no accuracy layers although computation '
                                            'of accurracy layers was enabled in job configuration.')
458
459
460
461
462
                    else:
                        pass  # self._accuracy_layers keeps None
                else:
                    raise

463
464
            except Exception as e:
                raise RuntimeError('Failed to generate AccuracyCube!', e)
465

466
        return self._accuracy_layers
467
468
469
470
471
472
473
474
475

    @accuracy_layers.setter
    def accuracy_layers(self, geoArr_initArgs):
        if geoArr_initArgs[0] is not None:
            acc_lay = GeoArray(geoArr_initArgs)
            assert acc_lay.shape[:2] == self.arr.shape[:2],\
                "The 'accuracy_layers' GeoArray can only be instanced with an array of the same dimensions like " \
                "GMS_obj.arr. Got %s." % str(acc_lay.shape)

Daniel Scheffler's avatar
Daniel Scheffler committed
476
            # noinspection PyProtectedMember
477
478
479
480
481
482
483
484
485
486
487
488
            if acc_lay._nodata is None:
                acc_lay.nodata = DEF_D.get_outFillZeroSaturated(acc_lay.dtype)[0]
            acc_lay.gt = self.arr.gt
            acc_lay.prj = self.arr.prj

            if not acc_lay.bandnames:
                raise ValueError

            self._accuracy_layers = acc_lay
        else:
            del self._accuracy_layers

489
490
491
492
    @accuracy_layers.deleter
    def accuracy_layers(self):
        self._accuracy_layers = None

493
494
495
496
497
498
499
500
501
502
503
    @property
    def accuracy_layers_meta(self):
        if self._accuracy_layers is not None:
            return {'map info': geotransform2mapinfo(self._accuracy_layers.gt, self._accuracy_layers.projection),
                    'coordinate system string': self._accuracy_layers.projection,
                    'bands': self._accuracy_layers.bands,
                    'band names': list(self._accuracy_layers.bandnames),
                    'data ignore value': self._accuracy_layers.nodata}
        else:
            return None

504
505
506
    @property
    def cloud_masking_algorithm(self):
        if not self._cloud_masking_algorithm:
507
            self._cloud_masking_algorithm = CFG.cloud_masking_algorithm[self.satellite]
508
509
        return self._cloud_masking_algorithm

510
511
    @property
    def ac_options(self):
512
        # type: () -> dict
513
        """
514
515
        Returns the options dictionary needed as input for atmospheric correction. If an empty dictionary is returned,
        atmospheric correction is not yet available for the current sensor and will later be skipped.
516
        """
Daniel Scheffler's avatar
Daniel Scheffler committed
517

518
        if not self._ac_options:
519
            path_ac_options = CFG.path_custom_sicor_options or PG.get_path_ac_options(self.GMS_identifier)
520

521
            if path_ac_options and os.path.exists(path_ac_options):
522
523
                # don't validate because options contain pathes that do not exist on another server:
                opt_dict = get_ac_options(path_ac_options, validation=False)
524

Daniel Scheffler's avatar
Daniel Scheffler committed
525
                # update some file paths depending on the current environment
526
527
                opt_dict['DEM']['fn'] = CFG.path_dem_proc_srtm_90m
                opt_dict['ECMWF']['path_db'] = CFG.path_ECMWF_db
528
529
530
                opt_dict['S2Image'][
                    'S2_MSI_granule_path'] = None  # only a placeholder -> will always be None for GMS usage
                opt_dict['output'] = []  # outputs are not needed for GMS -> so
531
                opt_dict['report']['report_path'] = os.path.join(self.pathGen.get_path_procdata(), '[TYPE]')
532
                if 'uncertainties' in opt_dict:
533
534
535
536
                    if CFG.ac_estimate_accuracy:
                        opt_dict['uncertainties']['snr_model'] = PG.get_path_snr_model(self.GMS_identifier)
                    else:
                        del opt_dict['uncertainties']  # SICOR will not compute uncertainties if that key is missing
537

538
539
540
541
542
543
                # apply custom configuration
                opt_dict["logger"]['level'] = CFG.log_level
                opt_dict["ram"]['upper_limit'] = CFG.ac_max_ram_gb
                opt_dict["ram"]['unit'] = 'GB'
                opt_dict["AC"]['fill_nonclear_areas'] = CFG.ac_fillnonclear_areas
                opt_dict["AC"]['clear_area_labels'] = CFG.ac_clear_area_labels
544
                # opt_dict['AC']['n_cores'] = CFG.CPUs if CFG.allow_subMultiprocessing else 1
545

546
                self._ac_options = opt_dict
547
548
549
            else:
                self.logger.warning('There is no options file available for atmospheric correction. '
                                    'Atmospheric correction must be skipped.')
550

551
552
        return self._ac_options

553
    def get_copied_dict_and_props(self, remove_privates=False):
554
        # type: (bool) -> dict
555
        """Returns a copy of the current object dictionary including the current values of all object properties."""
556
557
558

        # loggers must be closed
        self.close_GMS_loggers()
559
560
        # this disables automatic recreation of loggers (otherwise loggers are created by using getattr()):
        self._loggers_disabled = True
561
562
563
564
565

        out_dict = self.__dict__.copy()

        # add properties
        property_names = [p for p in dir(self.__class__) if isinstance(getattr(self.__class__, p), property)]
566
        [out_dict.update({propK: copy.copy(getattr(self, propK))}) for propK in property_names]
567
568
569
570
571
572
573
574
575

        # remove private attributes
        if remove_privates:
            out_dict = {k: v for k, v in out_dict.items() if not k.startswith('_')}

        self._loggers_disabled = False  # enables automatic recreation of loggers

        return out_dict

576
577
    def attributes2dict(self, remove_privates=False):
        # type: (bool) -> dict
578
        """Returns a copy of the current object dictionary including the current values of all object properties."""
579
580
581

        # loggers must be closed
        self.close_GMS_loggers()
582
583
        # this disables automatic recreation of loggers (otherwise loggers are created by using getattr()):
        self._loggers_disabled = True
584
585
586
587

        out_dict = self.__dict__.copy()

        # add some selected property values
588
589
        for i in ['GMS_identifier', 'LayerBandsAssignment', 'coreg_needed', 'coreg_info', 'resamp_needed',
                  'dict_LayerOptTherm', 'georef', 'meta_odict']:
590
            out_dict[i] = getattr(self, i)
591
592
593
594
595

        # remove private attributes
        if remove_privates:
            out_dict = {k: v for k, v in out_dict.items() if not k.startswith('_')}

596
        self._loggers_disabled = False  # enables automatic recreation of loggers
597
598
        return out_dict

599
    def _data_downloader(self, sensor, entity_ID):
600
601
602
603
        self.logger.info('Data downloader started.')
        success = False
        " > download source code for Landsat here < "
        if not success:
604
605
            self.logger.critical(
                "Download for %s dataset '%s' failed. No further processing possible." % (sensor, entity_ID))
606
            raise RuntimeError('Archive download failed.')
607
608
        return success

609
610
611
612
613
    def from_disk(self, tuple_GMS_subset):
        """Fills an already instanced GMS object with data from disk. Excludes array attributes in Python mode.

        :param tuple_GMS_subset:    <tuple> e.g. ('/path/gms_file.gms', ['cube', None])
        """
614

615
        path_GMS_file = tuple_GMS_subset[0]
616
        GMSfileDict = INP_R.GMSfile2dict(path_GMS_file)
617
618

        # copy all attributes from GMS file (private attributes are not touched since they are not included in GMS file)
619
        self.meta_odict = GMSfileDict['meta_odict']  # set that first in order to make some getters and setters work
620
621
        for key, value in GMSfileDict.items():
            if key in ['GMS_identifier', 'georef', 'dict_LayerOptTherm']:
622
                continue  # properties that should better be created on the fly
623
624
            try:
                setattr(self, key, value)
625
626
            except Exception:
                raise AttributeError("Can't set attribute %s." % key)
627
628
629

        self.arr_shape, self.arr_pos = tuple_GMS_subset[1]

630
631
632
        self.arr = self.pathGen.get_path_imagedata()
        # self.mask_nodata and self.mask_clouds are auto-synchronized via self.masks (see their getters):
        self.masks = self.pathGen.get_path_maskdata()
633

634
635
        return copy.copy(self)

636
    def from_sensor_subsystems(self, list_GMS_objs):
637
638
        # type: (List[GMS_object]) -> GMS_object
        # TODO convert to classmethod
639
640
641
642
643
644
        """Merge separate GMS objects belonging to the same scene-ID into ONE GMS object.

        :param list_GMS_objs:   <list> of GMS objects covering the same geographic area but representing different
                                sensor subsystems (e.g. 3 GMS_objects for Sentinel-2 10m/20m/60m bands)
        """

645
        # assertions
646
647
        assert len(list_GMS_objs) > 1, "'GMS_object.from_sensor_subsystems()' expects multiple input GMS objects. " \
                                       "Got %d." % len(list_GMS_objs)
648
        assert all([is_coord_grid_equal(list_GMS_objs[0].arr.gt, *obj.arr.xygrid_specs) for obj in list_GMS_objs[1:]]),\
649
650
651
            "The input GMS objects must have the same pixel grid. Received: %s" \
            % np.array([obj.arr.xygrid_specs for obj in list_GMS_objs])
        assert len(list(set([GMS_obj.proc_level for GMS_obj in list_GMS_objs]))) == 1, \
652
653
654
            "The input GMS objects for GMS_object.from_sensor_subsystems() must have the same processing level."
        subsystems = [GMS_obj.subsystem for GMS_obj in list_GMS_objs]
        assert len(subsystems) == len(list(set(subsystems))), \
655
            "The input 'list_GMS_objs' contains duplicates: %s" % subsystems
656

657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
        ##################
        # merge logfiles #
        ##################

        # read all logs into DataFrame, sort it by the first column
        [GMS_obj.close_GMS_loggers() for GMS_obj in list_GMS_objs]  # close the loggers of the input objects
        paths_inLogs = [GMS_obj.pathGen.get_path_logfile() for GMS_obj in list_GMS_objs]
        allLogs_df = DataFrame()
        for log in paths_inLogs:
            df = read_csv(log, sep='\n', delimiter=':   ', header=None,
                          engine='python')  # engine suppresses a pandas warning
            allLogs_df = allLogs_df.append(
                df)  # FIXME this will log e.g. atm. corr 3 times for S2A -> use captured streams instead?

        allLogs_df = allLogs_df.sort_values(0)

        # set common metadata, needed for logfile
        self.baseN = list_GMS_objs[0].pathGen.get_baseN(merged_subsystems=True)
        self.path_logfile = list_GMS_objs[0].pathGen.get_path_logfile(merged_subsystems=True)
        self.scene_ID = list_GMS_objs[0].scene_ID

        # write the merged logfile and flush previous logger
        np.savetxt(self.path_logfile, np.array(allLogs_df), delimiter=':   ', fmt="%s")
        self.close_GMS_loggers()

682
        # log
683
684
        self.logger.info('Merging the subsystems %s to a single GMS object...'
                         % ', '.join([GMS_obj.subsystem for GMS_obj in list_GMS_objs]))
685
686

        # find the common extent. NOTE: boundsMap is expected in the order [xmin,xmax,ymin,ymax]
687
688
        geoExtents = np.array([GMS_obj.arr.box.boundsMap for GMS_obj in list_GMS_objs])
        common_extent = (min(geoExtents[:, 0]), max(geoExtents[:, 1]), min(geoExtents[:, 2]), max(geoExtents[:, 3]))
689

690
691
692
693
        ##################
        # MERGE METADATA #
        ##################

694
695
696
        # copy all attributes from the first input GMS file (private attributes are not touched)
        for key, value in list_GMS_objs[0].__dict__.copy().items():
            if key in ['GMS_identifier', 'georef', 'dict_LayerOptTherm']:
697
                continue  # properties that should better be created on the fly
698
699
            elif key in ['baseN', 'path_logfile', 'scene_ID', 'subsystem']:
                continue  # either previously set with common values or not needed for merged GMS_object
700
701
            try:
                setattr(self, key, value)
702
703
            except Exception:
                raise AttributeError("Can't set attribute %s." % key)
704

705
        # update LayerBandsAssignment and get full list of output bandnames
706
        from .metadata import get_LayerBandsAssignment
707
708
        # use identifier of first input GMS object for getting LBA (respects current proc_level):
        gms_idf = list_GMS_objs[0].GMS_identifier
709
        self.LayerBandsAssignment = get_LayerBandsAssignment(gms_idf, return_fullLBA=True)
710
        bandnames = ['B%s' % i if len(i) == 2 else 'B0%s' % i for i in self.LayerBandsAssignment]
711
712
713

        # update layer-dependent metadata with respect to remaining input GMS objects
        self.meta_odict.update({
714
715
716
717
            'band names': [('Band %s' % i) for i in self.LayerBandsAssignment],
            'LayerBandsAssignment': self.LayerBandsAssignment,
            'Subsystem': '',
            'PhysUnit': self.meta_odict['PhysUnit'],  # TODO can contain val_optical / val_thermal
718
719
        })
        self.subsystem = ''
720
721
        del self.pathGen  # must be refreshed because subsystem is now ''
        self.close_GMS_loggers()  # must also be refreshed because it depends on pathGen
722

723
724
        for attrN in ['SolIrradiance', 'CWL', 'FWHM', 'Offsets', 'OffsetsRef', 'Gains', 'GainsRef',
                      'ThermalConstK1', 'ThermalConstK2', 'ViewingAngle_arrProv', 'IncidenceAngle_arrProv']:
725
726
727
728
729
730
731

            # combine values from separate subsystems to a single value
            attrDic_fullLBA = {}
            for GMS_obj in list_GMS_objs:
                attr_val = getattr(GMS_obj.MetaObj, attrN)
                if isinstance(attr_val, list):
                    attrDic_fullLBA.update(dict(zip(GMS_obj.LayerBandsAssignment, attr_val)))
732
                elif isinstance(attr_val, (dict, collections.OrderedDict)):
733
734
735
736
737
738
739
                    attrDic_fullLBA.update(attr_val)
                else:
                    raise ValueError(attrN)

            # update the attribute in self.MetaObj
            if attrDic_fullLBA:
                val2set = [attrDic_fullLBA[bN] for bN in self.LayerBandsAssignment] \
740
                    if isinstance(getattr(list_GMS_objs[0].MetaObj, attrN), list) else attrDic_fullLBA
741
742
                setattr(self.MetaObj, attrN, val2set)

743
744
745
        ####################
        # MERGE ARRAY DATA #
        ####################
746

747
        # overwrite array data with merged arrays, clipped to common_extent and reordered according to FullLayerBandsAss
748
749
750
        for attrname in ['arr', 'ac_errors', 'dem', 'mask_nodata', 'mask_clouds', 'mask_clouds_confidence', 'masks']:

            # get current attribute of each subsystem without running property getters
751
            all_arrays = [getattr(GMS_obj, '_%s' % attrname) for GMS_obj in list_GMS_objs]
752
753
754
755
756
757
758
759
760

            # get the same geographical extent for each input GMS object
            if len(set(tuple(ext) for ext in geoExtents.tolist())) > 1:
                # in case of different extents
                geoArrs_same_extent = []

                for geoArr in all_arrays:

                    if geoArr is not None:
761
                        # FIXME mask_clouds_confidence is no GeoArray until here
762
                        # FIXME -> has no nodata value -> calculation throughs warning
763
764
                        geoArr_same_extent = \
                            GeoArray(*geoArr.get_mapPos(
765
766
767
768
                                mapBounds=np.array(common_extent)[[0, 2, 1, 3]],  # pass (xmin, ymin, xmax, ymax)
                                mapBounds_prj=geoArr.prj),
                                     bandnames=list(geoArr.bandnames.keys()),
                                     nodata=geoArr.nodata)
769
770
                        geoArrs_same_extent.append(geoArr_same_extent)
                    else:
771
772
                        # e.g. in case of cloud mask that is only appended to the GMS object with the same
                        # spatial resolution)
773
774
775
776
777
778
                        geoArrs_same_extent.append(None)

            else:
                # skip get_mapPos() if all input GMS objects have the same extent
                geoArrs_same_extent = all_arrays

779
780
            # validate output GeoArrays #
            #############################
781

782
783
            if len([gA for gA in geoArrs_same_extent if gA is not None]) > 1:
                equal_bounds = all([geoArrs_same_extent[0].box.boundsMap == gA.box.boundsMap
784
                                    for gA in geoArrs_same_extent[1:]])
785
786
                equal_epsg = all([geoArrs_same_extent[0].epsg == gA.epsg for gA in geoArrs_same_extent[1:]])
                equal_xydims = all([geoArrs_same_extent[0].shape[:2] == gA.shape[:2] for gA in geoArrs_same_extent[1:]])
787
788
789
790
791
                if not all([equal_bounds, equal_epsg, equal_xydims]):
                    raise RuntimeError('Something went wrong during getting the same geographical extent for all the '
                                       'input GMS objects. The extents, projections or pixel dimensions of the '
                                       'calculated input GMS objects are not equal.')

792
793
            # set output arrays #
            #####################
794

795
796
            # handle those arrays where bands have to be reordered according to FullLayerBandsAssignment
            if attrname in ['arr', 'ac_errors'] and list(set(geoArrs_same_extent)) != [None]:
797
798
                # check that each desired band name for the current attribute is provided by one of the input
                # GMS objects
799
800
                available_bandNs = list(chain.from_iterable([list(gA.bandnames) for gA in geoArrs_same_extent]))
                for bN in bandnames:
801
                    if bN not in available_bandNs:
802
                        raise ValueError("The given input GMS objects (subsystems) do not provide a bandname '%s' for "
803
804
                                         "the attribute '%s'. Available band names amongst all input GMS objects are: "
                                         "%s" % (bN, attrname, str(available_bandNs)))
805
806

                # merge arrays
807
808
                def get_band(bandN):
                    return [gA[bandN] for gA in geoArrs_same_extent if gA and bandN in gA.bandnames][0]
809
810
                full_geoArr = GeoArray(np.dstack((get_band(bandN) for bandN in bandnames)),
                                       geoArrs_same_extent[0].gt, geoArrs_same_extent[0].prj,
811
812
                                       bandnames=bandnames,
                                       nodata=geoArrs_same_extent[0].nodata)
813
814
                setattr(self, attrname, full_geoArr)

815
            # handle the remaining arrays
816
817
818
            else:
                # masks, dem, mask_nodata, mask_clouds, mask_clouds_confidence
                if attrname == 'dem':
819
820
                    # use the DEM of the first input object
                    # (if the grid is the same, the DEMs should be the same anyway)
821
                    self.dem = geoArrs_same_extent[0]
822

823
824
825
                elif attrname == 'mask_nodata':
                    # must not be merged -> self.arr is already merged, so just recalculate it (np.all)
                    self.mask_nodata = self.calc_mask_nodata(overwrite=True)
826

827
828
829
                elif attrname == 'mask_clouds':
                    # possibly only present in ONE subsystem (set by atm. Corr.)
                    mask_clouds = [msk for msk in geoArrs_same_extent if msk is not None]
830
831
                    if len(mask_clouds) > 1:
                        raise ValueError('Expected mask clouds in only one subsystem. Got %s.' % len(mask_clouds))
832
                    self.mask_clouds = mask_clouds[0] if mask_clouds else None
833

834
835
836
                elif attrname == 'mask_clouds_confidence':
                    # possibly only present in ONE subsystem (set by atm. Corr.)
                    mask_clouds_conf = [msk for msk in geoArrs_same_extent if msk is not None]
837
838
839
                    if len(mask_clouds_conf) > 1:
                        raise ValueError(
                            'Expected mask_clouds_conf in only one subsystem. Got %s.' % len(mask_clouds_conf))
840
                    self.mask_clouds_confidence = mask_clouds_conf[0] if mask_clouds_conf else None
841

842
                elif attrname == 'masks':
843
844
845
846
847
848
849
850
                    # self.mask_nodata and self.mask_clouds will already be set here -> so just recreate it from there
                    self.masks = None

        # recreate self.masks
        self.build_combined_masks_array()

        # update array-dependent metadata
        self.meta_odict.update({
851
852
            'samples': self.arr.cols, 'lines': self.arr.rows, 'bands': self.arr.bands,
            'map info': geotransform2mapinfo(self.arr.gt, self.arr.prj), 'coordinate system string': self.arr.prj, })
853
854

        # set shape of full array
855
        self.shape_fullArr = self.arr.shape
856
857
858

        return copy.copy(self)

859
860
861
862
863
864
    def from_tiles(self, list_GMS_tiles):
        # type: (list) -> self
        """Merge separate GMS objects with different spatial coverage but belonging to the same scene-ID to ONE GMS object.

        :param list_GMS_tiles: <list> of GMS objects that have been created by cut_GMS_obj_into_blocks()
        """
Daniel Scheffler's avatar
Daniel Scheffler committed
865

866
867
868
869
870
        if 'IMapUnorderedIterator' in str(type(list_GMS_tiles)):
            list_GMS_tiles = list(list_GMS_tiles)

        # copy all attributes except of array attributes
        tile1 = list_GMS_tiles[0]
871
872
        [setattr(self, i, getattr(tile1, i)) for i in tile1.__dict__
         if not callable(getattr(tile1, i)) and not isinstance(getattr(tile1, i), (np.ndarray, GeoArray))]
873
874

        # MERGE ARRAY-ATTRIBUTES
875
        list_arraynames = [i for i in tile1.__dict__ if not callable(getattr(tile1, i)) and
876
                           isinstance(getattr(tile1, i), (np.ndarray, GeoArray))]
877
878
        list_arraynames = ['_arr'] + [i for i in list_arraynames if
                                      i != '_arr']  # list must start with _arr, otherwise setters will not work
879
880
881
882

        for arrname in list_arraynames:
            samplearray = getattr(tile1, arrname)
            assert isinstance(samplearray, (np.ndarray, GeoArray)), \
883
                'Received a %s object for attribute %s. Expected a numpy array or an instance of GeoArray.' \
884
                % (type(samplearray), arrname)
885
886
            is_3d = samplearray.ndim == 3
            bands = (samplearray.shape[2],) if is_3d else ()  # dynamic -> works for arr, cld_arr,...
887
888
889
890
            target_shape = tuple(self.shape_fullArr[:2]) + bands
            target_dtype = samplearray.dtype
            merged_array = self._numba_array_merger(list_GMS_tiles, arrname, target_shape, target_dtype)

891
892
            setattr(self, arrname if not arrname.startswith('_') else arrname[1:],
                    merged_array)  # use setters if possible
893
894
895
896
            # NOTE: this asserts that each attribute starting with '_' has also a property with a setter!

        # UPDATE ARRAY-DEPENDENT ATTRIBUTES
        self.arr_shape = 'cube'
897
        self.arr_pos = None
898
899
900

        # update MetaObj attributes
        self.meta_odict.update({
901
902
            'samples': self.arr.cols, 'lines': self.arr.rows, 'bands': self.arr.bands,
            'map info': geotransform2mapinfo(self.arr.gt, self.arr.prj), 'coordinate system string': self.arr.prj, })
903
904
905
906

        # calculate data_corners_imXY (mask_nodata is always an array here because get_mapPos always returns an array)
        corners_imYX = calc_FullDataset_corner_positions(
            self.mask_nodata, assert_four_corners=False, algorithm='shapely')
907
        self.trueDataCornerPos = [(YX[1], YX[0]) for YX in corners_imYX]  # [UL, UR, LL, LR]
908
909
910
911
912
913

        # calculate trueDataCornerLonLat
        data_corners_LatLon = pixelToLatLon(self.trueDataCornerPos, geotransform=self.arr.gt, projection=self.arr.prj)
        self.trueDataCornerLonLat = [(YX[1], YX[0]) for YX in data_corners_LatLon]

        # calculate trueDataCornerUTM
914
915
        data_corners_utmYX = pixelToMapYX(self.trueDataCornerPos, geotransform=self.arr.gt,
                                          projection=self.arr.prj)  # FIXME asserts gt in UTM coordinates
916
917
918
919
        self.trueDataCornerUTM = [(YX[1], YX[0]) for YX in data_corners_utmYX]

        return copy.copy(self)

920
921
922
    @staticmethod
    @jit
    def _numba_array_merger(list_GMS_tiles, arrname2merge, target_shape, target_dtype):
Daniel Scheffler's avatar
Daniel Scheffler committed
923
        # type: (list, str, tuple, np.dtype) -> np.ndarray
924
925
926
927
928
929
930
931
932
        """
        private function, e.g. called by merge_GMS_tiles_to_GMS_obj() in order to fasten array merging

        :param list_GMS_tiles:
        :param arrname2merge:
        :param target_shape:
        :param target_dtype:
        :return:
        """
Daniel Scheffler's avatar
Daniel Scheffler committed
933

934
935
936
937
938
939
940
        out_arr = np.empty(target_shape, dtype=target_dtype)
        for idx, tile in enumerate(list_GMS_tiles):
            rowStart, rowEnd = tile.arr_pos[0]
            colStart, colEnd = tile.arr_pos[1]
            out_arr[rowStart:rowEnd + 1, colStart:colEnd + 1] = getattr(tile, arrname2merge)
        return out_arr

Daniel Scheffler's avatar
Daniel Scheffler committed
941
    def log_for_fullArr_or_firstTile(self, log_msg, subset=None):
942
943
944
945
946
947
948
        """Send a message to the logger only if full array or the first tile is currently processed.
        This function can be called when processing any tile but log message will only be sent from first tile.

        :param log_msg:  the log message to be logged
        :param subset:   subset argument as sent to e.g. DN2TOARadRefTemp that indicates which tile is to be processed.
                         Not needed if self.arr_pos is not None.
        """
Daniel Scheffler's avatar
Daniel Scheffler committed
949

950
951
952
953
        if subset is None and \
            (self.arr_shape == 'cube' or self.arr_pos is None or [self.arr_pos[0][0], self.arr_pos[1][0]] == [0, 0]) or\
                subset == ['cube', None] or (subset and [subset[1][0][0], subset[1][1][0]] == [0, 0]) or \
                hasattr(self, 'logAtThisTile') and getattr(self, 'logAtThisTile'):  # cube or 1st tile
Daniel Scheffler's avatar
Daniel Scheffler committed
954
            self.logger.info(log_msg)
955
956
957
958
        else:
            pass

    def apply_nodata_mask_to_ObjAttr(self, attrname, out_nodata_val=None):
959
        # type: (str,int) -> None
960
        """Applies self.mask_nodata to the specified array attribute by setting all values where mask_nodata is 0 to the
961
962
963
964
965
966
967
        given nodata value.

        :param attrname:         The attribute to apply the nodata mask to. Must be an array attribute or
                                 a string path to a previously saved ENVI-file.
        :param out_nodata_val:   set the values of the given attribute to this value.
        """

968
        assert hasattr(self, attrname)
969

970
        if getattr(self, attrname) is not None:
971

972
973
974
            if isinstance(getattr(self, attrname), str):
                update_spec_vals = True if attrname == 'arr' else False
                self.apply_nodata_mask_to_saved_ENVIfile(getattr(self, attrname), out_nodata_val, update_spec_vals)
975
            else:
976
                assert isinstance(getattr(self, attrname), (np.ndarray, GeoArray)), \
977
                    'L1A_obj.%s must be a numpy array or an instance of GeoArray. Got type %s.' \
978
979
                    % (attrname, type(getattr(self, attrname)))
                assert hasattr(self, 'mask_nodata') and self.mask_nodata is not None
980

981
                self.log_for_fullArr_or_firstTile('Applying nodata mask to L1A_object.%s...' % attrname)
982

983
                nodata_val = out_nodata_val if out_nodata_val else \
984
                    DEF_D.get_outFillZeroSaturated(getattr(self, attrname).dtype)[0]
985
                getattr(self, attrname)[self.mask_nodata.astype(np.int8) == 0] = nodata_val
986

987
988
                if attrname == 'arr':
                    self.MetaObj.spec_vals['fill'] = nodata_val
989
990
991
992
993
994

    def build_combined_masks_array(self):
        # type: () -> dict
        """Generates self.masks attribute (unsigned integer 8bit) from by concatenating all masks included in GMS obj.
        The corresponding metadata is assigned to L1A_obj.masks_meta. Empty mask attributes are skipped."""

995
        arrays2combine = [aN for aN in ['mask_nodata', 'mask_clouds']
996
                          if hasattr(self, aN) and isinstance(getattr(self, aN), (GeoArray, np.ndarray))]
997
998
        if arrays2combine:
            self.log_for_fullArr_or_firstTile('Combining masks...')
999
1000

            def get_data(arrName): return getattr(self, arrName).astype(np.uint8)[:, :, None]
1001
1002

            for aN in arrays2combine:
1003
                if False in np.equal(getattr(self, aN), getattr(self, aN).astype(np.uint8)):
1004
1005
1006
                    warnings.warn('Some pixel values of attribute %s changed during data type '
                                  'conversion within build_combined_masks_array().')

1007
            # set self.masks
1008
1009
1010
            self.masks = get_data(arrays2combine[0]) if len(arrays2combine) == 1 else \
                np.concatenate([get_data(aN) for aN in arrays2combine], axis=2)
            self.masks.bandnames = arrays2combine  # set band names of GeoArray (allows later indexing by band name)
1011

1012
            # set self.masks_meta
1013
            nodataVal = DEF_D.get_outFillZeroSaturated(self.masks.dtype)[0]
1014
            self.masks_meta = {'map info': self.MetaObj.map_info, 'coordinate system string': self.MetaObj.projection,
1015
1016
                               'bands': len(arrays2combine), 'band names': arrays2combine,
                               'data ignore value': nodataVal}
1017
1018

            return {'desc': 'masks', 'row_start': 0, 'row_end': self.shape_fullArr[0],
1019
                    'col_start': 0, 'col_end': self.shape_fullArr[1], 'data': self.masks}  # usually not needed
1020
1021

    def apply_nodata_mask_to_saved_ENVIfile(self, path_saved_ENVIhdr, custom_nodata_val=None, update_spec_vals=False):
1022
        # type: (str,int,bool) -> None
1023
1024
        """Applies self.mask_nodata to a saved ENVI file with the same X/Y dimensions like self.mask_nodata by setting all
         values where mask_nodata is 0 to the given nodata value.
1025
1026
1027
1028
1029
1030
1031
1032

        :param path_saved_ENVIhdr:  <str> The path of the ENVI file to apply the nodata mask to.
        :param custom_nodata_val:   <int> set the values of the given attribute to this value.
        :param update_spec_vals:    <bool> whether to update self.MetaObj.spec_vals['fill']
        """

        self.log_for_fullArr_or_firstTile('Applying nodata mask to saved ENVI file...')
        assert os.path.isfile(path_saved_ENVIhdr)
Daniel Scheffler's avatar