コード例 #1
0
 def verticalunitsclicked(self, index):
     new_unit = self._suffixes[index]
     old_unit = self.sbPbot.suffix().strip()
     if new_unit == old_unit:
         return
     self.setBotTopLimits("maximum")
     for sb in (self.sbPbot, self.sbPtop):
         sb.setSuffix(" " + new_unit)
         if new_unit == "hPa":
             sb.setValue(thermolib.flightlevel2pressure(
                 convert_to(sb.value(), old_unit, "hft", 1)) / 100)
         elif old_unit == "hPa":
             sb.setValue(convert_to(
                 thermolib.pressure2flightlevel(sb.value() * 100), "hft", new_unit))
         else:
             sb.setValue(convert_to(sb.value(), old_unit, new_unit, 1))
     self.setBotTopLimits(self.cbVerticalAxis.currentText())
コード例 #2
0
 def _prepare_datafields(self):
     """
     Computes vertical velocity in cm/s.
     """
     self.data["upward_wind"] = convert_to(
         thermolib.omega_to_w(
             self.data["lagrangian_tendency_of_air_pressure"],
             self.data['air_pressure'], self.data["air_temperature"]),
         "m/s", "cm/s")
     self.variable = "upward_wind"
     self.y_values = self.data[self.variable]
     self.unit = "cm/s"
コード例 #3
0
ファイル: mss_chem_plots.py プロジェクト: aravindm711/MSS
 def _prepare_datafields(self):
     """Computes potential temperature from pressure and temperature if
     it has not been passed as a data field.
     """
     if self.name[-2:] == "al":
         # CAMS Regional Ensemble doesn't provide any pressure information,
         # but we want to plot vertical sections anyways, so we do a
         # poor-man's on-the-fly conversion here.
         if 'air_pressure' not in self.data:
             self.data["air_pressure"] = np.empty_like(self.data[self.dataname])
             self.data['air_pressure'][:] = thermolib.flightlevel2pressure_a(convert_to(
                 self.driver.vert_data[::-self.driver.vert_order, np.newaxis],
                 self.driver.vert_units, "hft"))
コード例 #4
0
 def verticalunitsclicked(self, index):
     units = {
         "pressure": "hPa",
         "pressure altitude": "km",
         "flight level": "hft"
     }
     _translate = QtCore.QCoreApplication.translate
     unit = units[self.cbVerticalAxis.model().itemFromIndex(index).text()]
     currentunit = units[self.cbVerticalAxis.currentText()]
     if unit == currentunit:
         return
     self.setBotTopLimits("maximum")
     self.sbPbot.setSuffix(_translate("SideViewOptionsDialog", " " + unit))
     self.sbPtop.setSuffix(_translate("SideViewOptionsDialog", " " + unit))
     if unit == "hPa":
         self.sbPtop.setValue(
             thermolib.flightlevel2pressure(
                 convert_to(self.sbPtop.value(), currentunit, "hft", 1)) /
             100)
         self.sbPbot.setValue(
             thermolib.flightlevel2pressure(
                 convert_to(self.sbPbot.value(), currentunit, "hft", 1)) /
             100)
     elif currentunit == "hPa":
         self.sbPtop.setValue(
             convert_to(
                 thermolib.pressure2flightlevel(self.sbPtop.value() * 100),
                 "hft", unit))
         self.sbPbot.setValue(
             convert_to(
                 thermolib.pressure2flightlevel(self.sbPbot.value() * 100),
                 "hft", unit))
     else:
         self.sbPtop.setValue(
             convert_to(self.sbPtop.value(), currentunit, unit, 1))
         self.sbPbot.setValue(
             convert_to(self.sbPbot.value(), currentunit, unit, 1))
     self.setBotTopLimits(
         self.cbVerticalAxis.model().itemFromIndex(index).text())
コード例 #5
0
ファイル: test_convert.py プロジェクト: nakul-shahdadpuri/MSS
def test_convert_to():
    assert convert_to(10, "km", "m", None) == 10000
    assert convert_to(1000, "m", "km", None) == 1

    assert convert_to(1000, "Pa", "hPa", None) == 10
    assert convert_to(1000, "hPa", "Pa", None) == 100000

    assert convert_to(10, "degC", "K", None) == 283.15

    assert convert_to(10 * 9.81, "m^2s^-2", "m", None) == pytest.approx(10)
    assert convert_to(10 * 9.81, "m**2s**-2", "m", None) == pytest.approx(10)

    with pytest.raises(TypeError):
        assert convert_to(10, "m", "m**2s**-2", None) == 10

    assert convert_to(10, "m", "m**2s**-2", 1) == 10
    assert convert_to(10, "m", "m**2s**-2", 9.81) == pytest.approx(98.1)

    assert convert_to(1000, "", "Pa", 999) == 999000
    assert convert_to(1000, "Pa", "", 999) == 999000
    assert convert_to(1000, "whattheheck", "Pa", 999) == 999000
    assert convert_to(1000, "hPa", "whattheheck", 999) == 999000
    assert convert_to(1000, "whattheheck", "whatthehock", 999) == 999000

    assert convert_to(10, "percent", "dimensionless", None) == 0.1
    assert convert_to(10, "permille", "dimensionless", None) == 0.01
    assert convert_to(10, "ppm", "dimensionless", None) == pytest.approx(10e-6)
    assert convert_to(10, "ppb", "dimensionless", None) == pytest.approx(10e-9)
    assert convert_to(10, "ppt", "dimensionless",
                      None) == pytest.approx(10e-12)
    assert convert_to(10, "ppm", "ppt", None) == pytest.approx(10e6)
    assert convert_to(10, "ppb", "ppm", None) == pytest.approx(10e-3)
コード例 #6
0
ファイル: mpl_hsec.py プロジェクト: iamapickle/MSS
    def plot_hsection(self,
                      data,
                      lats,
                      lons,
                      bbox=(-180, -90, 180, 90),
                      level=None,
                      figsize=(960, 640),
                      crs=None,
                      proj_params=None,
                      valid_time=None,
                      init_time=None,
                      style=None,
                      resolution=-1,
                      noframe=False,
                      show=False,
                      transparent=False):
        """
        EPSG overrides proj_params!
        """
        if proj_params is None:
            proj_params = {"projection": "cyl"}
            bbox_units = "latlon"
        # Projection parameters from EPSG code.
        if crs is not None:
            proj_params, bbox_units = [
                get_projection_params(crs)[_x] for _x in ("basemap", "bbox")
            ]

        logging.debug("plotting data..")

        # Check if required data is available.
        self.data_units = self.driver.data_units.copy()
        for datatype, dataitem, dataunit in self.required_datafields:
            if dataitem not in data:
                raise KeyError(f"required data field '{dataitem}' not found")
            origunit = self.driver.data_units[dataitem]
            if dataunit is not None:
                data[dataitem] = convert_to(data[dataitem], origunit, dataunit)
                self.data_units[dataitem] = dataunit
            else:
                logging.debug("Please add units to plot variables")

        # Copy parameters to properties.
        self.data = data
        self.lats = lats
        self.lons = lons
        self.level = level
        self.valid_time = valid_time
        self.init_time = init_time
        self.style = style
        self.resolution = resolution
        self.noframe = noframe
        self.crs = crs

        # Derive additional data fields and make the plot.
        logging.debug("preparing additional data fields..")
        self._prepare_datafields()

        logging.debug("creating figure..")
        dpi = 80
        figsize = (figsize[0] / dpi), (figsize[1] / dpi)
        facecolor = "white"
        fig = mpl.figure.Figure(figsize=figsize, dpi=dpi, facecolor=facecolor)
        logging.debug(
            "\twith frame and legends" if not noframe else "\twithout frame")
        if noframe:
            ax = fig.add_axes([0.0, 0.0, 1.0, 1.0])
        else:
            ax = fig.add_axes([0.05, 0.05, 0.9, 0.88])

        # The basemap instance is created with a fixed aspect ratio for framed
        # plots; the aspect ratio is not fixed for frameless plots (standard
        # WMS). This means that for WMS plots, the map will always fill the
        # entire image area, no matter of whether this stretches or shears the
        # map. This is the behaviour specified by the WMS standard (WMS Spec
        # 1.1.1, Section 7.2.3.8):
        # "The returned picture, regardless of its return format, shall have
        # exactly the specified width and height in pixels. In the case where
        # the aspect ratio of the BBOX and the ratio width/height are different,
        # the WMS shall stretch the returned map so that the resulting pixels
        # could themselves be rendered in the aspect ratio of the BBOX.  In
        # other words, it should be possible using this definition to request a
        # map for a device whose output pixels are themselves non-square, or to
        # stretch a map into an image area of a different aspect ratio."
        # NOTE: While the MSUI always requests image sizes that match the aspect
        # ratio, for instance the Metview 4 client does not (mr, 2011Dec16).

        # Some additional code to store the last 20 coastlines in memory for quicker
        # access.
        key = repr((proj_params, bbox, bbox_units))
        basemap_use_cache = getattr(mss_wms_settings, "basemap_use_cache",
                                    False)
        basemap_request_size = getattr(mss_wms_settings,
                                       "basemap_request_size ", 200)
        basemap_cache_size = getattr(mss_wms_settings, "basemap_cache_size",
                                     20)
        bm_params = {
            "area_thresh": 1000.,
            "ax": ax,
            "fix_aspect": (not noframe)
        }
        bm_params.update(proj_params)
        if bbox_units == "degree":
            bm_params.update({
                "llcrnrlon": bbox[0],
                "llcrnrlat": bbox[1],
                "urcrnrlon": bbox[2],
                "urcrnrlat": bbox[3]
            })
        elif bbox_units.startswith("meter"):
            # convert meters to degrees
            try:
                bm_p = basemap.Basemap(resolution=None, **bm_params)
            except ValueError:  # projection requires some extent
                bm_p = basemap.Basemap(resolution=None,
                                       width=1e7,
                                       height=1e7,
                                       **bm_params)
            bm_center = [float(_x) for _x in bbox_units[6:-1].split(",")]
            center_x, center_y = bm_p(*bm_center)
            bbox_0, bbox_1 = bm_p(bbox[0] + center_x,
                                  bbox[1] + center_y,
                                  inverse=True)
            bbox_2, bbox_3 = bm_p(bbox[2] + center_x,
                                  bbox[3] + center_y,
                                  inverse=True)
            bm_params.update({
                "llcrnrlon": bbox_0,
                "llcrnrlat": bbox_1,
                "urcrnrlon": bbox_2,
                "urcrnrlat": bbox_3
            })
        elif bbox_units == "no":
            pass
        else:
            raise ValueError(f"bbox_units '{bbox_units}' not known.")
        if basemap_use_cache and key in BASEMAP_CACHE:
            bm = basemap.Basemap(resolution=None, **bm_params)
            (bm.resolution, bm.coastsegs, bm.coastpolygontypes,
             bm.coastpolygons, bm.coastsegs, bm.landpolygons, bm.lakepolygons,
             bm.cntrysegs) = BASEMAP_CACHE[key]
            logging.debug("Loaded '%s' from basemap cache", key)
        else:
            bm = basemap.Basemap(resolution='l', **bm_params)
            # read in countries manually, as those are laoded only on demand
            bm.cntrysegs, _ = bm._readboundarydata("countries")
            if basemap_use_cache:
                BASEMAP_CACHE[key] = (bm.resolution, bm.coastsegs,
                                      bm.coastpolygontypes, bm.coastpolygons,
                                      bm.coastsegs, bm.landpolygons,
                                      bm.lakepolygons, bm.cntrysegs)
        if basemap_use_cache:
            BASEMAP_REQUESTS.append(key)
            BASEMAP_REQUESTS[:] = BASEMAP_REQUESTS[-basemap_request_size:]

            if len(BASEMAP_CACHE) > basemap_cache_size:
                useful = {}
                for idx, key in enumerate(BASEMAP_REQUESTS):
                    useful[key] = useful.get(key, 0) + idx
                least_useful = sorted([
                    (value, key) for key, value in useful.items()
                ])[:-basemap_cache_size]
                for _, key in least_useful:
                    del BASEMAP_CACHE[key]
                    BASEMAP_REQUESTS[:] = [
                        _x for _x in BASEMAP_REQUESTS if key != _x
                    ]

        # Set up the map appearance.
        bm.drawcoastlines(color='0.25')
        bm.drawcountries(color='0.5')
        bm.drawmapboundary(fill_color='white')

        # zorder = 0 is necessary to paint over the filled continents with
        # scatter() for drawing the flight tracks and trajectories.
        # Curiously, plot() works fine without this setting, but scatter()
        # doesn't.
        bm.fillcontinents(color='0.98', lake_color='white', zorder=0)
        self._draw_auto_graticule(bm)

        if noframe:
            ax.axis('off')

        self.bm = bm  # !! BETTER PASS EVERYTHING AS PARAMETERS?
        self.fig = fig
        self.shift_data()
        self.mask_data()
        self._plot_style()

        # Set transparency for the output image.
        if transparent:
            fig.patch.set_alpha(0.)

        # Return the image as png embedded in a StringIO stream.
        canvas = FigureCanvas(fig)
        output = io.BytesIO()
        canvas.print_png(output)

        if show:
            logging.debug("saving figure to mpl_hsec.png ..")
            canvas.print_png("mpl_hsec.png")

        # Convert the image to an 8bit palette image with a significantly
        # smaller file size (~factor 4, from RGBA to one 8bit value, plus the
        # space to store the palette colours).
        # NOTE: PIL at the current time can only create an adaptive palette for
        # RGB images, hence alpha values are lost here. If transparency is
        # requested, the figure face colour is stored as the "transparent"
        # colour in the image. This works in most cases, but might lead to
        # visible artefacts in some cases.
        logging.debug("converting image to indexed palette.")
        # Read the above stored png into a PIL image and create an adaptive
        # colour palette.
        output.seek(0)  # necessary for PIL.Image.open()
        palette_img = PIL.Image.open(output).convert(mode="RGB").convert(
            "P", palette=PIL.Image.ADAPTIVE)
        output = io.BytesIO()
        if not transparent:
            logging.debug("saving figure as non-transparent PNG.")
            palette_img.save(
                output,
                format="PNG")  # using optimize=True doesn't change much
        else:
            # If the image has a transparent background, we need to find the
            # index of the background colour in the palette. See the
            # documentation for PIL's ImagePalette module
            # (http://www.pythonware.com/library/pil/handbook/imagepalette.htm). The
            # idea is to create a 256 pixel image with the same colour palette
            # as the original image and use it as a lookup-table. Converting the
            # lut image back to RGB gives us a list of all colours in the
            # palette. (Why doesn't PIL provide a method to directly access the
            # colours in a palette??)
            lut = palette_img.resize((256, 1))
            lut.putdata(list(range(256)))
            lut = [c[1] for c in lut.convert("RGB").getcolors()]
            facecolor_rgb = list(
                mpl.colors.hex2color(mpl.colors.cnames[facecolor]))
            for i in [0, 1, 2]:
                facecolor_rgb[i] = int(facecolor_rgb[i] * 255)
            facecolor_index = lut.index(tuple(facecolor_rgb))

            logging.debug(
                "saving figure as transparent PNG with transparency index %s.",
                facecolor_index)
            palette_img.save(output,
                             format="PNG",
                             transparency=facecolor_index)

        logging.debug("returning figure..")
        return output.getvalue()
コード例 #7
0
    def plot_lsection(self, data, lats, lons, valid_time, init_time):
        """
        """
        # Check if required data is available.
        self.data_units = self.driver.data_units.copy()
        for datatype, dataitem, dataunit in self.required_datafields:
            if dataitem not in data:
                raise KeyError(f"required data field '{dataitem}' not found")
            origunit = self.driver.data_units[dataitem]
            if dataunit is not None:
                data[dataitem] = convert_to(data[dataitem], origunit, dataunit)
                self.data_units[dataitem] = dataunit
            else:
                logging.debug("Please add units to plot variables")

        # Copy parameters to properties.
        self.data = data
        self.lats = lats
        self.lons = lons
        self.valid_time = valid_time
        self.init_time = init_time

        # Derive additional data fields and make the plot.
        self._prepare_datafields()

        impl = getDOMImplementation()
        xmldoc = impl.createDocument(None, "MSS_LinearSection_Data", None)

        # Title of this section.
        node = xmldoc.createElement("Title")
        node.appendChild(xmldoc.createTextNode(self.title))
        xmldoc.documentElement.appendChild(node)

        # Time information of this section.
        node = xmldoc.createElement("ValidTime")
        node.appendChild(
            xmldoc.createTextNode(
                self.valid_time.strftime("%Y-%m-%dT%H:%M:%SZ")))
        xmldoc.documentElement.appendChild(node)

        node = xmldoc.createElement("InitTime")
        node.appendChild(
            xmldoc.createTextNode(
                self.init_time.strftime("%Y-%m-%dT%H:%M:%SZ")))
        xmldoc.documentElement.appendChild(node)

        # Longitude data.
        node = xmldoc.createElement("Longitude")
        node.setAttribute("num_waypoints", f"{len(self.lons)}")
        data_str = ",".join([str(lon) for lon in self.lons])

        node.appendChild(xmldoc.createTextNode(data_str))
        xmldoc.documentElement.appendChild(node)

        # Latitude data.
        node = xmldoc.createElement("Latitude")
        node.setAttribute("num_waypoints", f"{len(self.lats)}")
        data_str = ",".join([str(lat) for lat in self.lats])

        node.appendChild(xmldoc.createTextNode(data_str))
        xmldoc.documentElement.appendChild(node)

        # Variable data.
        node = xmldoc.createElement("Data")
        node.setAttribute("num_waypoints", f"{len(self.y_values)}")
        node.setAttribute("unit", self.unit)
        if isinstance(self.y_values[0], Quantity):
            data_str = ",".join([str(val.magnitude) for val in self.y_values])
        else:
            data_str = ",".join([str(val) for val in self.y_values])

        node.appendChild(xmldoc.createTextNode(data_str))
        xmldoc.documentElement.appendChild(node)

        # Return the XML document as formatted string.
        return xmldoc.toprettyxml(indent="  ")
コード例 #8
0
    def plot_vsection(self,
                      data,
                      lats,
                      lons,
                      valid_time,
                      init_time,
                      resolution=(-1, -1),
                      bbox=(-1, 1050, -1, 200),
                      style=None,
                      show=False,
                      highlight=None,
                      noframe=False,
                      figsize=(960, 480),
                      numlabels=10,
                      orography_color='k',
                      transparent=False,
                      return_format="image/png"):
        """
        """
        # Check if required data is available.
        self.data_units = self.driver.data_units.copy()
        for datatype, dataitem, dataunit in self.required_datafields:
            if dataitem not in data:
                raise KeyError(
                    "required data field '{}' not found".format(dataitem))
            origunit = self.driver.data_units[dataitem]
            if dataunit is not None:
                data[dataitem] = convert_to(data[dataitem], origunit, dataunit)
                self.data_units[dataitem] = dataunit
            else:
                logging.debug("Please add units to plot variables")

        # Copy parameters to properties.
        self.data = data
        self.lats = lats
        self.lat_inds = np.arange(len(lats))
        self.lons = lons
        self.valid_time = valid_time
        self.init_time = init_time
        self.resolution = resolution
        self.style = style
        self.highlight = highlight
        self.noframe = noframe
        self.p_bot = bbox[1] * 100
        self.p_top = bbox[3] * 100
        self.numlabels = numlabels
        self.orography_color = orography_color

        # Derive additional data fields and make the plot.
        self._prepare_datafields()

        # Code for producing a png image with Matplotlib.
        # ===============================================
        if return_format == "image/png":

            logging.debug("creating figure..")
            dpi = 80
            figsize = (figsize[0] / dpi), (figsize[1] / dpi)
            facecolor = "white"
            self.fig = mpl.figure.Figure(figsize=figsize,
                                         dpi=dpi,
                                         facecolor=facecolor)
            logging.debug("\twith frame and legends"
                          if not noframe else "\twithout frame")
            if noframe:
                self.ax = self.fig.add_axes([0.0, 0.0, 1.0, 1.0])
            else:
                self.ax = self.fig.add_axes([0.07, 0.17, 0.9, 0.72])

            self._plot_style()

            # Set transparency for the output image.
            if transparent:
                self.fig.patch.set_alpha(0.)

            # Return the image as png embedded in a StringIO stream.
            canvas = FigureCanvas(self.fig)
            output = io.BytesIO()
            canvas.print_png(output)

            if show:
                logging.debug("saving figure to mpl_vsec.png ..")
                canvas.print_png("mpl_vsec.png")

            # Convert the image to an 8bit palette image with a significantly
            # smaller file size (~factor 4, from RGBA to one 8bit value, plus the
            # space to store the palette colours).
            # NOTE: PIL at the current time can only create an adaptive palette for
            # RGB images, hence alpha values are lost here. If transparency is
            # requested, the figure face colour is stored as the "transparent"
            # colour in the image. This works in most cases, but might lead to
            # visible artefacts in some cases.
            logging.debug("converting image to indexed palette.")
            # Read the above stored png into a PIL image and create an adaptive
            # colour palette.
            output.seek(0)  # necessary for PIL.Image.open()
            palette_img = PIL.Image.open(output).convert(mode="RGB").convert(
                "P", palette=PIL.Image.ADAPTIVE)
            output = io.BytesIO()
            if not transparent:
                logging.debug("saving figure as non-transparent PNG.")
                palette_img.save(
                    output,
                    format="PNG")  # using optimize=True doesn't change much
            else:
                # If the image has a transparent background, we need to find the
                # index of the background colour in the palette. See the
                # documentation for PIL's ImagePalette module
                # (http://www.pythonware.com/library/pil/handbook/imagepalette.htm). The
                # idea is to create a 256 pixel image with the same colour palette
                # as the original image and use it as a lookup-table. Converting the
                # lut image back to RGB gives us a list of all colours in the
                # palette. (Why doesn't PIL provide a method to directly access the
                # colours in a palette??)
                lut = palette_img.resize((256, 1))
                lut.putdata(list(range(256)))
                lut = [c[1] for c in lut.convert("RGB").getcolors()]
                facecolor_rgb = list(
                    mpl.colors.hex2color(mpl.colors.cnames[facecolor]))
                for i in [0, 1, 2]:
                    facecolor_rgb[i] = int(facecolor_rgb[i] * 255)
                facecolor_index = lut.index(tuple(facecolor_rgb))

                logging.debug(
                    "saving figure as transparent PNG with transparency index %i.",
                    facecolor_index)
                palette_img.save(output,
                                 format="PNG",
                                 transparency=facecolor_index)

            logging.debug("returning figure..")
            return output.getvalue()

        # Code for generating an XML document with the data values in ASCII format.
        # =========================================================================
        elif return_format == "text/xml":

            impl = getDOMImplementation()
            xmldoc = impl.createDocument(None, "MSS_VerticalSection_Data",
                                         None)

            # Title of this section.
            node = xmldoc.createElement("Title")
            node.appendChild(xmldoc.createTextNode(self.title))
            xmldoc.documentElement.appendChild(node)

            # Time information of this section.
            node = xmldoc.createElement("ValidTime")
            node.appendChild(
                xmldoc.createTextNode(
                    self.valid_time.strftime("%Y-%m-%dT%H:%M:%SZ")))
            xmldoc.documentElement.appendChild(node)

            node = xmldoc.createElement("InitTime")
            node.appendChild(
                xmldoc.createTextNode(
                    self.init_time.strftime("%Y-%m-%dT%H:%M:%SZ")))
            xmldoc.documentElement.appendChild(node)

            # Longitude data.
            node = xmldoc.createElement("Longitude")
            node.setAttribute("num_waypoints", "{}".format(len(self.lons)))

            data_str = ""
            for value in self.lons:
                data_str += str(value) + ","
            data_str = data_str[:-1]

            node.appendChild(xmldoc.createTextNode(data_str))
            xmldoc.documentElement.appendChild(node)

            # Latitude data.
            node = xmldoc.createElement("Latitude")
            node.setAttribute("num_waypoints", "{}".format(len(self.lats)))

            data_str = ""
            for value in self.lats:
                data_str += str(value) + ","
            data_str = data_str[:-1]

            node.appendChild(xmldoc.createTextNode(data_str))
            xmldoc.documentElement.appendChild(node)

            # Variable data.
            data_node = xmldoc.createElement("Data")

            for var in self.data:
                node = xmldoc.createElement(var)
                data_shape = self.data[var].shape
                node.setAttribute("num_levels", "{}".format(data_shape[0]))
                node.setAttribute("num_waypoints", "{}".format(data_shape[1]))

                data_str = ""
                for data_row in self.data[var]:
                    for value in data_row:
                        data_str += str(value) + ","
                    data_str = data_str[:-1] + "\n"
                data_str = data_str[:-1]

                node.appendChild(xmldoc.createTextNode(data_str))
                data_node.appendChild(node)

            xmldoc.documentElement.appendChild(data_node)

            # Return the XML document as formatted string.
            return xmldoc.toprettyxml(indent="  ")