diff --git a/satpy/formatting_html.py b/satpy/formatting_html.py new file mode 100644 index 0000000000..e8953d2232 --- /dev/null +++ b/satpy/formatting_html.py @@ -0,0 +1,640 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (c) 2010-2022 Satpy developers +# +# This file is part of satpy. +# +# satpy is free software: you can redistribute it and/or modify it under the +# terms of the GNU General Public License as published by the Free Software +# Foundation, either version 3 of the License, or (at your option) any later +# version. +# +# satpy is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +# A PARTICULAR PURPOSE. See the GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along with +# satpy. If not, see . +"""Html formatting function for Scene representation in notebooks.""" + +import uuid +from functools import lru_cache +from html import escape +from importlib.resources import read_binary + +import toolz +import xarray as xr + +try: + from pyresample._formatting_html import _icon, plot_area_def +except ModuleNotFoundError: + cartopy = False + +from xarray.core.formatting_html import _mapping_section, summarize_vars # , datavar_section + +STATIC_FILES = {"html": [("pyresample.static.html", "icons_svg_inline.html")], + "css": [("pyresample.static.css", "style.css")] + } + +css = """ +:root { + --xr-font-color0: var(--jp-content-font-color0, rgba(0, 0, 0, 1)); + --xr-font-color2: var(--jp-content-font-color2, rgba(0, 0, 0, 0.54)); + --xr-font-color3: var(--jp-content-font-color3, rgba(0, 0, 0, 0.38)); + --xr-border-color: var(--jp-border-color2, #e0e0e0); + --xr-disabled-color: var(--jp-layout-color3, #bdbdbd); + --xr-background-color: var(--jp-layout-color0, white); + --xr-background-color-row-even: var(--jp-layout-color1, white); + --xr-background-color-row-odd: var(--jp-layout-color2, #eeeeee); +} + +html[theme=dark], +body[data-theme=dark], +body.vscode-dark { + --xr-font-color0: rgba(255, 255, 255, 1); + --xr-font-color2: rgba(255, 255, 255, 0.54); + --xr-font-color3: rgba(255, 255, 255, 0.38); + --xr-border-color: #1F1F1F; + --xr-disabled-color: #515151; + --xr-background-color: #111111; + --xr-background-color-row-even: #111111; + --xr-background-color-row-odd: #313131; +} + +.satpy-scene-sections { + padding: 0 !important; + display: grid; + grid-template-columns: 20px 20px 150px auto 20px 20px; + width: 1000px; + margin-top: 0px; +} + +.satpy-section-name { + grid-column: 2 / 3; +} + +.satpy-scene-section-item { + display: contents; +} + +.satpy-scene-section-item input { + display: none; +} + +.satpy-scene-section-item input:enabled + label { + cursor: pointer; +} + +.satpy-scene-section-summary { + grid-column: 1 / 4; + padding-top: 4px; + padding-bottom: 0px; +} + +.satpy-scene-section-summary > span { + float: right; +} + +.satpy-scene-section-in:checked + label > span { + display: none; +} + +.satpy-scene-section-inline-preview { + grid-column: 4 / -1; + padding-left: 3px; + padding-top: 4px; + padding-bottom: 0px; +} + +.satpy-scene-section-in:checked ~ .satpy-scene-section-inline-preview { + display: none; +} + +.satpy-scene-section-details, +.satpy-scene-section-in:checked ~ .satpy-scene-section-preview { + display: none; +} + +.satpy-scene-section-in:checked ~ .satpy-scene-section-details, +.satpy-scene-section-preview { + display: contents; + padding: 0; + grid-column: 3 / -1; +} + +.satpy-scene-section-area { + display: contents; +} + +.satpy-area-name { + grid-column: 3; + font-weight: bold; +} + +.satpy-area-details { + grid-column: 4; +} + +/*show hide css for area def section */ +.satpy-area-attrs { + margin-bottom: 5px; +} + +.satpy-area-attrs, +.satpy-area-map { + display: none; +} + +.satpy-area-attrs-in:checked ~ .satpy-area-attrs, +.satpy-area-map-in:checked ~ .satpy-area-map { + display: block; + grid-column: 3 / -1; +} + +.satpy-area-attrs dt { + width: 190px; + float: left; + font-weight: bold; + margin-right: 0.5em; +} + +.satpy-area-attrs dt:after { + content: ": "; +} + +.satpy-area-attrs dd { + all: initial; +} + +.satpy-area-attrs dd:after { + clear: left; + content: " "; + display: block; +} + +.satpy-scene-section-datasets { + grid-column: 3 / -1; +} + +.xr-wrap { + all: initial; +} + +.xr-wrap { + display: block !important; + min-width: 300px; + max-width: 1000px; +} + .xr-sections { + padding-left: 0 !important; + display: grid; + grid-template-columns: 150px auto auto 1fr 20px 20px; +} + +.xr-section-item { + display: contents; +} + +.xr-section-item input { + display: none; +} + +.xr-section-item input + label { + color: var(--xr-disabled-color); +} + +.xr-section-item input:enabled + label { + cursor: pointer; + color: var(--xr-font-color2); +} + +.xr-section-item input:enabled + label:hover { + color: var(--xr-font-color0); +} + +.xr-section-summary { + grid-column: 1; + color: var(--xr-font-color2); + font-weight: 500; +} + +.xr-section-summary > span { + display: inline-block; + padding-left: 0.5em; +} + +.xr-section-summary-in:disabled + label { + color: var(--xr-font-color2); +} + +.xr-section-summary-in + label:before { + display: inline-block; + content: '►'; + font-size: 11px; + width: 15px; + text-align: center; +} + +.xr-section-summary-in:disabled + label:before { + color: var(--xr-disabled-color); +} + +.xr-section-summary-in:checked + label:before { + content: '▼'; +} + +.xr-section-summary-in:checked + label > span { + display: none; +} + +.xr-section-summary, +.xr-section-inline-details { + padding-top: 4px; + padding-bottom: 4px; +} + +.xr-section-inline-details { + grid-column: 2 / -1; +} + +.xr-section-details { + display: none; + grid-column: 1 / -1; + margin-bottom: 5px; +} + +.xr-section-summary-in:checked ~ .xr-section-details { + display: contents; +} + +.xr-sections { + padding-left: 0 !important; + display: grid; + grid-template-columns: 250px auto auto 1fr 20px 20px; +} + +.xr-section-item { + display: contents; +} + +.xr-section-item input:enabled + label { + cursor: pointer; + color: var(--xr-font-color2); +} + +.xr-section-summary, +.xr-section-inline-details { + padding-top: 4px; + padding-bottom: 4px; +} + +.xr-section-inline-details { + margin-bottom: 5px; +} + +.xr-var-list, +.xr-var-item { + display: contents; +} + +.xr-var-item > div, +.xr-var-item label, +.xr-var-item > .xr-var-name span { + background-color: var(--xr-background-color-row-even); + margin-bottom: 0; +} + +.xr-var-item > .xr-var-name:hover span { + padding-right: 5px; +} + +.xr-var-list > li:nth-child(odd) > div, +.xr-var-list > li:nth-child(odd) > label, +.xr-var-list > li:nth-child(odd) > .xr-var-name span { + background-color: var(--xr-background-color-row-odd); +} + +.xr-var-name { + grid-column: 1; +} + +.xr-var-dims { + grid-column: 2; +} + +.xr-var-dtype { + grid-column: 3; + text-align: right; + color: var(--xr-font-color2); +} + +.xr-var-preview { + grid-column: 4; +} + +.xr-var-name, +.xr-var-dims, +.xr-var-dtype, +.xr-preview, +.xr-attrs dt { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-right: 10px; +} + +.xr-var-name:hover, +.xr-var-dims:hover, +.xr-var-dtype:hover, +.xr-attrs dt:hover { + overflow: visible; + width: auto; + z-index: 1; +} + +.xr-var-attrs, +.xr-var-data { + display: none; + background-color: var(--xr-background-color) !important; + padding-bottom: 5px !important; +} + +.xr-var-attrs-in:checked ~ .xr-var-attrs, +.xr-var-data-in:checked ~ .xr-var-data { + display: block; +} + +.xr-var-data > table { + float: right; +} + +.xr-var-name span, +.xr-var-data, +.xr-attrs { + padding-left: 25px !important; +} + +.xr-attrs, +.xr-var-attrs, +.xr-var-data { + grid-column: 1 / -1; +} + +dl.xr-attrs { + padding: 0; + margin: 0; + display: grid; + grid-template-columns: 125px auto; +} + +.xr-attrs dt, +.xr-attrs dd { + padding: 0; + margin: 0; + float: left; + padding-right: 10px; + width: auto; +} + +.xr-attrs dt { + font-weight: normal; + grid-column: 1; +} + +.xr-attrs dt:hover span { + display: inline-block; + background: var(--xr-background-color); + padding-right: 10px; +} + +.xr-attrs dd { + grid-column: 2; + white-space: pre-wrap; + word-break: break-all; +} + +.xr-icon-database, +.xr-icon-file-text2 { + display: inline-block; + vertical-align: middle; + width: 1em; + height: 1.5em !important; + stroke-width: 0; + stroke: currentColor; + fill: currentColor; +} +""" + + +@lru_cache(None) +def _load_static_files(): + """Lazily load the resource files into memory the first time they are needed.""" + css = "\n".join([read_binary(package, resource).decode("utf-8") for package, resource in STATIC_FILES["css"]]) + + html = "\n".join([read_binary(package, resource).decode("utf-8") for package, resource in STATIC_FILES["html"]]) + + return [html, css] + + +@toolz.curry +def attr(attr_name, ds): + """Get attribute.""" + return ds.attrs.get(attr_name) + + +def collapsible_section_satpy(name, inline_details="", details="", n_items=None, enabled=True, collapsed=False, + icon=None): + """Create a collapsible section. + + Args: + name (str): Name of the section + inline_details (str): Information to show when section is collapsed. Default nothing. + details (str): Details to show when section is expanded. + n_items (int): Number of items in this section. + enabled (boolean): Is collapsing enabled. Default True. + collapsed (boolean): Is the section collapsed on first show. Default False. + icon (str): Name of the icon to use for the section. + + Returns: + str: Html div structure for collapsible section. + + """ + # "unique" id to expand/collapse the section + data_id = "section-" + str(uuid.uuid4()) + + has_items = n_items is not None and n_items + n_items_span = "" if n_items is None else f" {n_items}" + enabled = "" if enabled and has_items else "disabled" + collapsed = "" if collapsed or not has_items else "checked" + tip = " title='Expand/collapse section'" if enabled else "" + + if icon is None: + icon = _icon("icon-database") + + return ("
" + f"" + f"" + f"
{inline_details}
" + f"
{details}
" + "
" + ) + + +def sensor_section(platform_name, sensor_name, datasets): + """Generate sensor section.""" + by_area = toolz.groupby(lambda x: x.attrs.get("area").proj_dict.get("proj"), datasets) + n_areas = len(list(by_area.keys())) + n_datasets = len(datasets) + inline_details = f"Area(s) with {n_datasets} channels" + + by_area_type = toolz.groupby(lambda x: type(x.attrs.get("area")).__name__, datasets) + + sensor_name = sensor_name.upper() + icon = _icon("icon-satellite") + + section_name = (f"{platform_name} / " + f"{sensor_name}" + ) + + if "AreaDefinition" in by_area_type.keys(): + by_area = toolz.groupby(lambda x: x.attrs.get("area").proj_dict.get("proj"), by_area_type["AreaDefinition"]) + section_details = "" + for proj, ds in by_area.items(): + section_details += resolution_section(proj, ds) + + # html = collapsible_section_satpy(section_name, details=section_details, + # inline_details=inline_details, n_items=n_areas, icon=icon) + html = collapsible_section_satpy(section_name, details=section_details, n_items=number_of_platforms, icon=icon) + + if "SwathDefinition" in by_area_type.keys(): + from pyresample._formatting_html import swath_area_attrs_section + + from satpy import Scene + + swathlist = Scene._compare_swath_defs(max, [ds.area for ds in by_area_type["SwathDefinition"]]) + + # by_area = toolz.groupby(lambda x: x.attrs.get("area").proj_dict.get("proj"), by_area_type["AreaDefinition"]) + + html = collapsible_section_satpy(section_name, details=swath_area_attrs_section(swathlist), + n_items=number_of_platforms, icon=icon) + + return html + + +def resolution_section(projection, datasets): + """Generate resolution section.""" + def resolution(dataset): + area = dataset.attrs.get("area") + resolution_str = "/".join([str(round(x, 1)) for x in area.resolution]) + return resolution_str + + by_resolution = toolz.groupby(resolution, datasets) + + areadefinition = datasets[0].attrs.get("area") + proj_dict = areadefinition.proj_dict + proj_str = "{{{}}}".format(", ".join(["'%s': '%s'" % (str(k), str(proj_dict[k])) for k in + sorted(proj_dict.keys())])) + + area_attrs = ("
" + f"
Description
{areadefinition.description}
" + f"
Projection
{proj_str}
" + f"
Extent (ll_x, ll_y, ur_x, ur_y)
" + f"
{tuple(round(x, 4) for x in areadefinition.area_extent)}
" + "
" + ) + + area_map = plot_area_def(areadefinition, fmt="svg") + + attrs_id = "attrs-" + str(uuid.uuid4()) + map_id = "map-" + str(uuid.uuid4()) + attrs_icon = _icon("icon-file-text2") + map_icon = _icon("icon-globe") + + html = ("
" + f"
{areadefinition.area_id}
" + f"
" + f"" + f"" + f"" + f"" + f"
{area_attrs}
" + f"
{area_map}
" + "
" + ) + + for res, ds in by_resolution.items(): + ds_dict = {i.attrs["name"]: i.rename(i.attrs["name"]) for i in ds if i.attrs.get("area") is not None} + dss = xr.merge(ds_dict.values(), compat="override") + html += xarray_dataset_repr(dss, "Resolution (x/y): {}".format(res)) + + return html + + +def scene_repr(scene): + """Html representation of Scene. + + Args: + scene (:class:`~satpy.scene.Scene`): Satpy scene. + + Returns: + str: Html str + + Todo: + - streamline loading and combining of css styles. Move _load_static_files function into pyresample + - display combined numer of datasets, area name, projection, extent, sensor, start/end time after object type? + - drop "unecessary" attributes from the datasets? + - only show resolution and dimensions (number of pixels) for each section if the area definition extent, + projection (and name) is the same? + - for the data variables list not only display channel (dataarray) name but also other DataId info + (like spectral range)? + - what about composites? + """ + icons_svg, css_style = _load_static_files() + css_style = "\n".join([css, css_style]) + + obj_type = f"satpy.scene.{type(scene).__name__}" + header = ("
" + "
" + f"{escape(obj_type)}" + "
" + "
" + ) + # insert number of different sensors, area defs (projections), resolutions, area_extents? after object type + + html = ( + f"{icons_svg}" + f"
{escape(repr(scene))}
" + "" + + return html + + +def xarray_dataset_repr(dataset, ds_name): + """Wrap xarray dataset representation html.""" + data_variables = _mapping_section(mapping=dataset, name=ds_name, details_func=summarize_vars, + max_items_collapse=15, expand_option_name="display_expand_data_vars") + + ds_list = ("
" + # "" + # "
" + ) + + return ds_list diff --git a/satpy/scene.py b/satpy/scene.py index e340cee372..fdb25136da 100644 --- a/satpy/scene.py +++ b/satpy/scene.py @@ -32,6 +32,7 @@ from satpy.composites.config_loader import load_compositor_configs_for_sensors from satpy.dataset import DataID, DataQuery, DatasetDict, combine_metadata, dataset_walker, replace_anc from satpy.dependency_tree import DependencyTree +from satpy.formatting_html import scene_repr from satpy.node import CompositorNode, MissingDependencies, ReaderNode from satpy.readers import load_readers from satpy.resample import get_area_def, prepare_resampler, resample_dataset @@ -548,6 +549,10 @@ def __str__(self): res = (str(proj) for proj in self._datasets.values()) return "\n".join(res) + def _repr_html_(self): + """Html representation of Scene for notebooks.""" + return scene_repr(self) + def __iter__(self): """Iterate over the datasets.""" for x in self._datasets.values():