Skip to content

Commit

Permalink
Merge pull request #436 from AnaVukasinovic/gmv_issue_393
Browse files Browse the repository at this point in the history
Update and improve handling of projections and area definitions
  • Loading branch information
ameraner authored Feb 27, 2025
2 parents 8ec955c + 2657814 commit 9a24f47
Show file tree
Hide file tree
Showing 12 changed files with 417 additions and 72 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# PyCharm project settings
.idea


# Ignore IDE settings and configuration files
.vscode/

Expand Down Expand Up @@ -118,3 +119,6 @@ CMakeCache.txt
cmake_install.cmake
CMakeFiles/
Makefile

# Windows soft links
/*.lnk
65 changes: 46 additions & 19 deletions doc/source/configuration/area_definitions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,56 @@ Satpy. All or some of them must be "activated" for use by configuring
``area_definitions`` as follows::

area_definitions:
[DISPLAY_NAME_1]: [AREA_ID_1]
[DISPLAY_NAME_AREA_GROUP_1]:
{
[DISPLAY_RESOLUTION_IDENTIFIER_1_1] : [AREA_ID_1_1],
[DISPLAY_RESOLUTION_IDENTIFIER_1_2] : [AREA_ID_1_2],
...
}
...
[DISPLAY_NAME_N]: [AREA_ID_N]

were ``DISPLAY_NAME_i`` is the name to be used in the GUI to refer to the area
definition with the according ID ``AREA_ID_i`` which must be provided by
according Satpy configuration (unknown area ids are skipped with a warning
log message).

The area definitions appear in the *Projection:* picklist in the same order they
are listed in the ``area_definitions`` configuration, the first entry is
selected at application start.

One additional area definition is appended as fallback by SIFT if no area
[DISPLAY_NAME_AREA_GROUP_N]:
{
[DISPLAY_RESOLUTION_IDENTIFIER_N_1] : [AREA_ID_N_1],
[DISPLAY_RESOLUTION_IDENTIFIER_N_2] : [AREA_ID_N_2],
...
}

where ``DISPLAY_NAME_AREA_GROUP_i`` is the name to be used in the GUI to refer to the area group. Each
``DISPLAY_NAME_AREA_GROUP_i`` represents a group of areas that share the same projection but have
different resolutions. Therefore, within a single area group, there are one or more areas organized
in a key-value structure. The key denotes ``DISPLAY_RESOLUTION_IDENTIFIER_i_j``, which is used in the GUI
to represent resolutions for the selected area group. The value represents ``AREA_ID_i_j``, by which a
specific area definition is reached. Area ID ``AREA_ID_i_j`` must be provided by according Satpy
configuration (unknown area ids are skipped with a warning log message).

The area groups appear in the *Projection:* picklist in the same order they are listed in the
``area_definitions`` configuration, with the first entry selected at application start. The resolution
identifiers appear in the *Resolution:* picklist in the Open File Wizard window based on the selected
projection (area group). They are listed in the same order as within a single area group in the
``area_definitions`` configuration, and by default, the first resolution identifier entry is selected.
In this way, the user has the possibility to select the projection and the resolution and based on that the area
ID is determined.

One additional area definition is appended as a fallback by SIFT if no area
definition or none with a pseudo lat/lon projection (Plate Carree) is
found. This is to make sure there is always projection showing the whole world
selectable in the application, which is useful for examining data of yet unknown
found. This is to make sure there is always a projection showing the whole world
selectable in the application, which is useful for examining data of a yet unknown
area.

**Example**::

area_definitions:
MSG SEVIRI FES 3km: msg_seviri_fes_3km
MSG SEVIRI RSS 1km: msg_seviri_rss_1km
MSG SEVIRI IODC 3km: msg_seviri_iodc_3km
MTG FCI FDSS 2km: mtg_fci_fdss_2km
MTG FCI FDSS:
{
1km: mtg_fci_fdss_1km,
2km: mtg_fci_fdss_2km,
500m: mtg_fci_fdss_500m,
32km: mtg_fci_fdss_32km
}

MSG SEVIRI FES:
{
3km: msg_seviri_fes_3km,
1km: msg_seviri_fes_1km,
9km: msg_seviri_fes_9km
}
5 changes: 5 additions & 0 deletions uwsift/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,11 @@ def _open_wizard(self, *args, **kwargs):

if self._wizard_dialog.exec_():
LOG.info("Loading products from open wizard...")
# The selected projection in the main window is the same as the one chosen in the open file wizard
self.ui.projectionComboBox.setCurrentIndex(self._wizard_dialog.ui.projectionComboBox.currentIndex())
# setting resampling info in layer details pane for displaying the resolution of the Area
self.ui.layerDetailsPane.set_resampling_info(self._wizard_dialog.resampling_info)
#
scenes = self._wizard_dialog.scenes
reader = self._wizard_dialog.get_reader()

Expand Down
65 changes: 49 additions & 16 deletions uwsift/etc/SIFT/config/area_definitions.yaml
Original file line number Diff line number Diff line change
@@ -1,21 +1,54 @@
area_definitions:

MTG FCI FDSS 1km: mtg_fci_fdss_1km
MTG FCI FDSS 2km: mtg_fci_fdss_2km
MTG FCI FDSS 500m: mtg_fci_fdss_500m
MSG SEVIRI FES 3km: msg_seviri_fes_3km
MSG SEVIRI FES 1km: msg_seviri_fes_1km
MSG SEVIRI RSS 3km: msg_seviri_rss_3km
MSG SEVIRI RSS 1km: msg_seviri_rss_1km
MSG SEVIRI IODC 3km: msg_seviri_iodc_3km
MSG SEVIRI IODC 1km: msg_seviri_iodc_1km
GOES-East ABI F 500m: goes_east_abi_f_500m
GOES-East ABI F 1km: goes_east_abi_f_1km
GOES-East ABI F 2km: goes_east_abi_f_2km
GOES-West ABI F 500m: goes_west_abi_f_500m
GOES-West ABI F 1km: goes_west_abi_f_1km
GOES-West ABI F 2km: goes_west_abi_f_2km
EUROL: eurol
MTG FCI FDSS:
{
1km: mtg_fci_fdss_1km,
2km: mtg_fci_fdss_2km,
500m: mtg_fci_fdss_500m,
32km: mtg_fci_fdss_32km
}

MSG SEVIRI FES:
{
3km: msg_seviri_fes_3km,
1km: msg_seviri_fes_1km,
9km: msg_seviri_fes_9km
}
MSG SEVIRI RSS:
{
3km: msg_seviri_rss_3km,
1km: msg_seviri_rss_1km
}
MSG SEVIRI IODC:
{
3km: msg_seviri_iodc_3km,
1km: msg_seviri_iodc_1km
}
GOES-East ABI F:
{
500m: goes_east_abi_f_500m,
1km: goes_east_abi_f_1km,
2km: goes_east_abi_f_2km
}

GOES-West ABI F:
{
500m: goes_west_abi_f_500m,
1km: goes_west_abi_f_1km,
2km: goes_west_abi_f_2km
}

EUROL:
{
eurol: eurol
}








#---- SIFT doesn't implement proj=eqc correctly/completely ----
# Plate Carree 3km: worldeqc3km
99 changes: 77 additions & 22 deletions uwsift/model/area_definitions_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
# plane chart) should be available always. It will be only added though,
# if there is no other area definition found in configuration which uses the
# 'latlong' projection (or its PROJ.4 aliases)
"Plate Carree": "plate_carree"
"Plate Carree": {"plate_carree": "plate_carree"}
}

DEFAULT_AREA_DEFINITIONS_YAML = """
Expand Down Expand Up @@ -54,27 +54,34 @@ class AreaDefinitionsManager:
"""

_available_area_defs_by_id: None | dict[str, AreaDefinition] = None
_available_area_defs_id_by_name: None | dict[str, str] = None
_available_area_defs_group_by_group_name: None | dict[str, dict[str, str]] = (
None # changed | group_name : { name : id name : id}
)

@classmethod
def init_available_area_defs(cls) -> None:
cls._available_area_defs_by_id = {}
cls._available_area_defs_id_by_name = {}
cls._available_area_defs_group_by_group_name = {}

desired_area_defs = config.get("area_definitions", {})

for area_def_name, area_id in desired_area_defs.items():
try:
area_def = get_area_def(area_id)
except AreaNotFound as e:
LOG.warning(
f"Area definition configured for display name" f" '{area_def_name}' unknown: {e}. Skipping..."
)
continue
for area_def_group_name, area_def_group in desired_area_defs.items():

LOG.info(f"Adding area definition: {area_def_name} -> {area_id}")
cls._available_area_defs_by_id[area_id] = area_def
cls._available_area_defs_id_by_name[area_def_name] = area_id
for area_resolution, area_id in area_def_group.items():
try:
area_def = get_area_def(area_id)
except AreaNotFound as e:
LOG.warning(
f"Area definition configured for display name" f" '{area_id}' unknown: {e}. Skipping..."
)
continue
# adding ar
LOG.info(f"Adding area definition: {area_def_group_name} , {area_resolution} -> {area_id}")
cls._available_area_defs_by_id[area_id] = area_def

cls._available_area_defs_group_by_group_name[area_def_group_name] = area_def_group

# cls._available_area_defs_group_by_group_name and cls._available_area_defs_by_id are initialised

# Check for existence of at least one 'latlong' projection
# (https://proj.org/operations/conversions/latlon.html)
Expand All @@ -83,30 +90,78 @@ def init_available_area_defs(cls) -> None:
return

# Add default area definition(s)?
for area_def_name, area_id in DEFAULT_AREAS.items():
area_def = load_area_from_string(DEFAULT_AREA_DEFINITIONS_YAML, area_id)
for area_def_group_name, area_def_group in DEFAULT_AREAS.items():
for area_resolution, area_id in area_def_group.items():
area_def = load_area_from_string(DEFAULT_AREA_DEFINITIONS_YAML, area_id)

LOG.info(f"Adding area definition: {area_def_group_name} , {area_resolution} -> {area_id}")
cls._available_area_defs_by_id[area_id] = area_def

LOG.info(f"Adding default area definition:" f" {area_def_name} -> {area_id}")
cls._available_area_defs_by_id[area_id] = area_def
cls._available_area_defs_id_by_name[area_def_name] = area_id
cls._available_area_defs_group_by_group_name[area_def_group_name] = area_def_group
#

# returns all area projections
@classmethod
def available_area_def_names(cls):
return cls._available_area_defs_id_by_name.keys()
return cls._available_area_defs_group_by_group_name.keys()

@classmethod
def area_def_by_id(cls, id):
return cls._available_area_defs_by_id.get(id)

# returns the first area definition from the group
@classmethod
def area_def_by_name(cls, name):
return cls.area_def_by_id(cls._available_area_defs_id_by_name.get(name))
area_group = cls._available_area_defs_group_by_group_name.get(name)
first_key = next(iter(area_group))
area_id = area_group[first_key]
return cls.area_def_by_id(area_id)

@classmethod
def default_area_def_name(cls): # TODO: take from configuration, make robust
# Since nothing has been configured, take the first key (i.e. the
# display_name) of the known area definitions
return next(iter(cls._available_area_defs_id_by_name))
return next(iter(cls._available_area_defs_group_by_group_name))

# returns all possible resolutions for one area projection
@classmethod
def available_area_def_group_resolutions(cls, group_name):
return cls._available_area_defs_group_by_group_name.get(group_name).keys()

# returns area group by its name --- for example if a group_name = MSG SEVIRI FES, a return value will be
# {3 km: msg_seviri_fes_3km, 1 km: msg_seviri_fes_1km}
@classmethod
def area_group_by_group_name(cls, group_name):
return cls._available_area_defs_group_by_group_name.get(group_name)

# returns area definition by its name and resolution --- for example if a group_name = MSG SEVIRI FES and
# resolution = 3km , a return value will be area definition for id = msg_seviri_fes_3km
@classmethod
def area_def_by_group_name_and_resolution(cls, group_name, resolution):
return cls.area_def_by_id(cls._available_area_defs_group_by_group_name.get(group_name).get(resolution))

# prepares area def for resampling
@classmethod
def prepare_area_def_for_resampling(cls, area_def, width, height):
# when the shape is changed, it also affects other parameters in the area definition
area_def.width = width
area_def.height = height
area_def.pixel_size_x = (area_def.area_extent[2] - area_def.area_extent[0]) / float(area_def.width)
area_def.pixel_size_y = (area_def.area_extent[3] - area_def.area_extent[1]) / float(area_def.height)
area_def.pixel_upper_left = (
float(area_def.area_extent[0]) + float(area_def.pixel_size_x) / 2,
float(area_def.area_extent[3]) - float(area_def.pixel_size_y) / 2,
)
area_def.pixel_offset_x = -area_def.area_extent[0] / area_def.pixel_size_x
area_def.pixel_offset_y = area_def.area_extent[3] / area_def.pixel_size_y

# calculates pixel_size_x and pixel_size_y for an area definition with custom resolution values
@classmethod
def area_def_custom_resolution_values(cls, area_def, width, height):
if width and height:
pixel_size_x = (area_def.area_extent[2] - area_def.area_extent[0]) / float(width)
pixel_size_y = (area_def.area_extent[3] - area_def.area_extent[1]) / float(height)
return pixel_size_x, pixel_size_y


# TODO: Why does this need to be a class being updated instead of an instance of a class?
Expand Down
Loading

0 comments on commit 9a24f47

Please sign in to comment.