/**
   * Appends elevation model parameters as elements to a specified context. If a parameter key
   * exists, that parameter is appended to the context. Supported key and element paths are:
   *
   * <table> <th><td>Key</td><td>Name</td><td>Type</td></th>
   * <tr><td>{@link AVKey#DISPLAY_NAME}</td><td>DisplayName</td><td>String</td></tr> <tr><td>{@link
   * AVKey#NETWORK_RETRIEVAL_ENABLED}</td><td>NetworkRetrievalEnabled</td><td>Boolean</td></tr> <tr><td>{@link
   * AVKey#MISSING_DATA_SIGNAL}</td><td>MissingData/@signal</td><td>Double</td></tr> <tr><td>{@link
   * AVKey#MISSING_DATA_REPLACEMENT}</td><td>MissingData/@replacement</td><td>Double</td></tr> <tr><td>{@link
   * AVKey#DETAIL_HINT}</td><td>DataDetailHint</td><td>Double</td></tr> </table>
   *
   * @param params the key-value pairs which define the elevation model parameters.
   * @param context the XML document root on which to append parameter elements.
   * @return a reference to context.
   * @throws IllegalArgumentException if either the parameters or the context are null.
   */
  public static Element createElevationModelElements(AVList params, Element context) {
    if (params == null) {
      String message = Logging.getMessage("nullValue.ParametersIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    if (context == null) {
      String message = Logging.getMessage("nullValue.ContextIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    WWXML.checkAndAppendTextElement(params, AVKey.DISPLAY_NAME, context, "DisplayName");
    WWXML.checkAndAppendBooleanElement(
        params, AVKey.NETWORK_RETRIEVAL_ENABLED, context, "NetworkRetrievalEnabled");

    if (params.getValue(AVKey.MISSING_DATA_SIGNAL) != null
        || params.getValue(AVKey.MISSING_DATA_REPLACEMENT) != null) {
      Element el = WWXML.getElement(context, "MissingData", null);
      if (el == null) el = WWXML.appendElementPath(context, "MissingData");

      Double d = AVListImpl.getDoubleValue(params, AVKey.MISSING_DATA_SIGNAL);
      if (d != null) el.setAttribute("signal", Double.toString(d));

      d = AVListImpl.getDoubleValue(params, AVKey.MISSING_DATA_REPLACEMENT);
      if (d != null) el.setAttribute("replacement", Double.toString(d));
    }

    WWXML.checkAndAppendDoubleElement(params, AVKey.DETAIL_HINT, context, "DataDetailHint");

    return context;
  }
  @Override
  protected Layer doCreateFromCapabilities(OGCCapabilities caps, AVList params) {
    String serviceName = caps.getServiceInformation().getServiceName();
    if (serviceName == null
        || !(serviceName.equalsIgnoreCase(OGCConstants.WMS_SERVICE_NAME)
            || serviceName.equalsIgnoreCase("WMS"))) {
      String message =
          Logging.getMessage("WMS.NotWMSService", serviceName != null ? serviceName : "null");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    if (params == null) params = new AVListImpl();

    if (params.getStringValue(AVKey.LAYER_NAMES) == null) {
      // Use the first named layer since no other guidance given
      List<WMSLayerCapabilities> namedLayers = ((WMSCapabilities) caps).getNamedLayers();

      if (namedLayers == null || namedLayers.size() == 0 || namedLayers.get(0) == null) {
        String message = Logging.getMessage("WMS.NoLayersFound");
        Logging.logger().severe(message);
        throw new IllegalStateException(message);
      }

      params.setValue(AVKey.LAYER_NAMES, namedLayers.get(0).getName());
    }

    return new WMSTiledImageLayer((WMSCapabilities) caps, params);
  }
  /**
   * Parses basic elevation model parameters from a specified DOM document. This also parses
   * LevelSet parameters by invoking {@link
   * gov.nasa.worldwind.util.DataConfigurationUtils#getLevelSetParams(org.w3c.dom.Element,
   * gov.nasa.worldwind.avlist.AVList)}. This writes output as key-value pairs to params. If a
   * parameter from the XML document already exists in params, that parameter is ignored. Supported
   * key and parameter names are:
   *
   * <table>
   * <th><td>Key</td><td>Name</td><td>Type</td></th> <tr><td>{@link AVKey#SERVICE_NAME}</td><td>Service/@serviceName</td><td>String</td></tr>
   * <tr><td>{@link AVKey#PIXEL_TYPE}</td><td>DataType</td><td>String</td></tr> <tr><td>{@link
   * AVKey#BYTE_ORDER}</td><td>DataType/@byteOrder</td><td>String</td></tr> <tr><td>{@link
   * AVKey#ELEVATION_EXTREMES_FILE}</td><td>ExtremeElevations/FileName</td><td>String</td></tr> <tr><td>{@link
   * AVKey#ELEVATION_MAX}</td><td>ExtremeElevations/@max</td><td>Double</td></tr> <tr><td>{@link
   * AVKey#ELEVATION_MIN}</td><td>ExtremeElevations/@min</td><td>Double</td></tr> </table>
   *
   * @param domElement the XML document root to parse for basic elevation model parameters.
   * @param params the output key-value pairs which recieve the basic elevation model parameters. A
   *     null reference is permitted.
   * @return a reference to params, or a new AVList if params is null.
   * @throws IllegalArgumentException if the document is null.
   */
  public static AVList getBasicElevationModelParams(Element domElement, AVList params) {
    if (domElement == null) {
      String message = Logging.getMessage("nullValue.DocumentIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    if (params == null) params = new AVListImpl();

    XPath xpath = WWXML.makeXPath();

    // LevelSet properties.
    DataConfigurationUtils.getLevelSetParams(domElement, params);

    // Service properties.
    WWXML.checkAndSetStringParam(
        domElement, params, AVKey.SERVICE_NAME, "Service/@serviceName", xpath);
    WWXML.checkAndSetBooleanParam(
        domElement,
        params,
        AVKey.RETRIEVE_PROPERTIES_FROM_SERVICE,
        "RetrievePropertiesFromService",
        xpath);

    // Image format properties.
    if (params.getValue(AVKey.PIXEL_TYPE) == null) {
      String s = WWXML.getText(domElement, "DataType/@type", xpath);
      if (s != null && s.length() > 0) {
        s = WWXML.parseDataType(s);
        if (s != null && s.length() > 0) params.setValue(AVKey.PIXEL_TYPE, s);
      }
    }

    if (params.getValue(AVKey.BYTE_ORDER) == null) {
      String s = WWXML.getText(domElement, "DataType/@byteOrder", xpath);
      if (s != null && s.length() > 0) {
        s = WWXML.parseByteOrder(s);
        if (s != null && s.length() > 0) params.setValue(AVKey.BYTE_ORDER, s);
      }
    }

    // Elevation data properties.
    WWXML.checkAndSetStringParam(
        domElement, params, AVKey.ELEVATION_EXTREMES_FILE, "ExtremeElevations/FileName", xpath);
    WWXML.checkAndSetDoubleParam(
        domElement, params, AVKey.ELEVATION_MAX, "ExtremeElevations/@max", xpath);
    WWXML.checkAndSetDoubleParam(
        domElement, params, AVKey.ELEVATION_MIN, "ExtremeElevations/@min", xpath);

    return params;
  }
  /**
   * Create a layer described by an XML layer description.
   *
   * @param domElement the XML element describing the layer to create.
   * @param params any parameters to apply when creating the layer.
   * @return a new layer
   * @throws WWUnrecognizedException if the layer type or service type given in the describing
   *     element is unrecognized.
   * @see #createTiledImageLayer(org.w3c.dom.Element, gov.nasa.worldwind.avlist.AVList).
   */
  protected Layer createFromLayerDocument(Element domElement, AVList params) {
    String className = WWXML.getText(domElement, "@className");
    if (className != null && className.length() > 0) {
      Layer layer = (Layer) WorldWind.createComponent(className);
      String actuate = WWXML.getText(domElement, "@actuate");
      layer.setEnabled(WWUtil.isEmpty(actuate) || actuate.equals("onLoad"));
      WWXML.invokePropertySetters(layer, domElement);
      return layer;
    }

    AVList props = WWXML.copyProperties(domElement, null);
    if (props != null) { // Copy params and add any properties for this layer to the copy
      if (params != null) props.setValues(params);
      params = props;
    }

    Layer layer;
    String href = WWXML.getText(domElement, "@href");
    if (href != null && href.length() > 0) {
      Object o = this.createFromConfigSource(href, params);
      if (o == null) return null;

      if (!(o instanceof Layer)) {
        String msg =
            Logging.getMessage("LayerFactory.UnexpectedTypeForLayer", o.getClass().getName());
        throw new WWRuntimeException(msg);
      }

      layer = (Layer) o;
    } else {
      String layerType = WWXML.getText(domElement, "@layerType");
      if (layerType != null && layerType.equals("TiledImageLayer")) {
        layer = this.createTiledImageLayer(domElement, params);
      } else {
        String msg = Logging.getMessage("generic.UnrecognizedLayerType", layerType);
        throw new WWUnrecognizedException(msg);
      }
    }

    if (layer != null) {
      String actuate = WWXML.getText(domElement, "@actuate");
      layer.setEnabled(actuate != null && actuate.equals("onLoad"));
      WWXML.invokePropertySetters(layer, domElement);
    }

    return layer;
  }
Example #5
0
  public static DataRaster wrap(BufferedImage image, AVList params) {
    if (null == image) {
      String message = Logging.getMessage("nullValue.ImageIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    if (null == params) {
      String msg = Logging.getMessage("nullValue.AVListIsNull");
      Logging.logger().finest(msg);
      throw new IllegalArgumentException(msg);
    }

    if (params.hasKey(AVKey.WIDTH)) {
      int width = (Integer) params.getValue(AVKey.WIDTH);
      if (width != image.getWidth()) {
        String msg =
            Logging.getMessage("generic.InvalidWidth", "" + width + "!=" + image.getWidth());
        Logging.logger().finest(msg);
        throw new IllegalArgumentException(msg);
      }
    } else {
      params.setValue(AVKey.WIDTH, image.getWidth());
    }

    if (params.hasKey(AVKey.HEIGHT)) {
      int height = (Integer) params.getValue(AVKey.HEIGHT);
      if (height != image.getHeight()) {
        String msg =
            Logging.getMessage("generic.InvalidHeight", "" + height + "!=" + image.getHeight());
        Logging.logger().finest(msg);
        throw new IllegalArgumentException(msg);
      }
    } else {
      params.setValue(AVKey.HEIGHT, image.getHeight());
    }

    Sector sector = null;
    if (params.hasKey(AVKey.SECTOR)) {
      Object o = params.getValue(AVKey.SECTOR);
      if (o instanceof Sector) {
        sector = (Sector) o;
      }
    }

    return new BufferedImageRaster(sector, image, params);
  }
Example #6
0
  public static DataRaster wrapAsGeoreferencedRaster(BufferedImage image, AVList params) {
    if (null == image) {
      String message = Logging.getMessage("nullValue.ImageIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    if (null == params) {
      String msg = Logging.getMessage("nullValue.AVListIsNull");
      Logging.logger().finest(msg);
      throw new IllegalArgumentException(msg);
    }

    if (params.hasKey(AVKey.WIDTH)) {
      int width = (Integer) params.getValue(AVKey.WIDTH);
      if (width != image.getWidth()) {
        String msg =
            Logging.getMessage("generic.InvalidWidth", "" + width + "!=" + image.getWidth());
        Logging.logger().finest(msg);
        throw new IllegalArgumentException(msg);
      }
    }

    if (params.hasKey(AVKey.HEIGHT)) {
      int height = (Integer) params.getValue(AVKey.HEIGHT);
      if (height != image.getHeight()) {
        String msg =
            Logging.getMessage("generic.InvalidHeight", "" + height + "!=" + image.getHeight());
        Logging.logger().finest(msg);
        throw new IllegalArgumentException(msg);
      }
    }

    if (!params.hasKey(AVKey.SECTOR)) {
      String msg = Logging.getMessage("generic.MissingRequiredParameter", AVKey.SECTOR);
      Logging.logger().finest(msg);
      throw new IllegalArgumentException(msg);
    }

    Sector sector = (Sector) params.getValue(AVKey.SECTOR);
    if (null == sector) {
      String msg = Logging.getMessage("nullValue.SectorIsNull");
      Logging.logger().severe(msg);
      throw new IllegalArgumentException(msg);
    }

    if (!params.hasKey(AVKey.COORDINATE_SYSTEM)) {
      // assume Geodetic Coordinate System
      params.setValue(AVKey.COORDINATE_SYSTEM, AVKey.COORDINATE_SYSTEM_GEOGRAPHIC);
    }

    String cs = params.getStringValue(AVKey.COORDINATE_SYSTEM);
    if (!params.hasKey(AVKey.PROJECTION_EPSG_CODE)) {
      if (AVKey.COORDINATE_SYSTEM_GEOGRAPHIC.equals(cs)) {
        // assume WGS84
        params.setValue(AVKey.PROJECTION_EPSG_CODE, GeoTiff.GCS.WGS_84);
      } else {
        String msg =
            Logging.getMessage("generic.MissingRequiredParameter", AVKey.PROJECTION_EPSG_CODE);
        Logging.logger().finest(msg);
        throw new IllegalArgumentException(msg);
      }
    }

    // if PIXEL_WIDTH is specified, we are not overriding it because UTM images
    // will have different pixel size
    if (!params.hasKey(AVKey.PIXEL_WIDTH)) {
      if (AVKey.COORDINATE_SYSTEM_GEOGRAPHIC.equals(cs)) {
        double pixelWidth = sector.getDeltaLonDegrees() / (double) image.getWidth();
        params.setValue(AVKey.PIXEL_WIDTH, pixelWidth);
      } else {
        String msg = Logging.getMessage("generic.MissingRequiredParameter", AVKey.PIXEL_WIDTH);
        Logging.logger().finest(msg);
        throw new IllegalArgumentException(msg);
      }
    }

    // if PIXEL_HEIGHT is specified, we are not overriding it
    // because UTM images will have different pixel size
    if (!params.hasKey(AVKey.PIXEL_HEIGHT)) {
      if (AVKey.COORDINATE_SYSTEM_GEOGRAPHIC.equals(cs)) {
        double pixelHeight = sector.getDeltaLatDegrees() / (double) image.getHeight();
        params.setValue(AVKey.PIXEL_HEIGHT, pixelHeight);
      } else {
        String msg = Logging.getMessage("generic.MissingRequiredParameter", AVKey.PIXEL_HEIGHT);
        Logging.logger().finest(msg);
        throw new IllegalArgumentException(msg);
      }
    }

    if (!params.hasKey(AVKey.PIXEL_FORMAT)) {
      params.setValue(AVKey.PIXEL_FORMAT, AVKey.IMAGE);
    } else if (!AVKey.IMAGE.equals(params.getStringValue(AVKey.PIXEL_FORMAT))) {
      String msg =
          Logging.getMessage(
              "generic.UnknownValueForKey",
              params.getStringValue(AVKey.PIXEL_FORMAT),
              AVKey.PIXEL_FORMAT);
      Logging.logger().severe(msg);
      throw new IllegalArgumentException(msg);
    }

    if (!params.hasKey(AVKey.ORIGIN) && AVKey.COORDINATE_SYSTEM_GEOGRAPHIC.equals(cs)) {
      // set UpperLeft corner as the origin, if not specified
      LatLon origin = new LatLon(sector.getMaxLatitude(), sector.getMinLongitude());
      params.setValue(AVKey.ORIGIN, origin);
    }

    if (!params.hasKey(AVKey.DATE_TIME)) {
      // add NUL (\0) termination as required by TIFF v6 spec (20 bytes length)
      String timestamp = String.format("%1$tY:%1$tm:%1$td %tT\0", Calendar.getInstance());
      params.setValue(AVKey.DATE_TIME, timestamp);
    }

    if (!params.hasKey(AVKey.VERSION)) {
      params.setValue(AVKey.VERSION, Version.getVersion());
    }

    boolean hasAlpha = (null != image.getColorModel() && image.getColorModel().hasAlpha());
    params.setValue(AVKey.RASTER_HAS_ALPHA, hasAlpha);

    return new BufferedImageRaster(sector, image, params);
  }
  /**
   * Appends basic elevation model parameters as elements to a specified context. If a parameter key
   * exists, that parameter is appended to the context. This also writes LevelSet parameters by
   * invoking {@link DataConfigurationUtils#createLevelSetElements(gov.nasa.worldwind.avlist.AVList,
   * org.w3c.dom.Element)}. Supported key and element paths are:
   *
   * <table> <th><td>Key</td><td>Name</td><td>Type</td></th> <tr><td>{@link
   * AVKey#SERVICE_NAME}</td><td>Service/@serviceName</td><td>String</td></tr> <tr><td>{@link
   * AVKey#PIXEL_TYPE}</td><td>PixelType</td><td>String</td></tr> <tr><td>{@link AVKey#BYTE_ORDER}</td><td>ByteOrder</td><td>String</td></tr>
   * <tr><td>{@link AVKey#ELEVATION_EXTREMES_FILE}</td><td>ExtremeElevations/FileName</td><td>String</td></tr>
   * <tr><td>{@link AVKey#ELEVATION_MAX}</td><td>ExtremeElevations/@max</td><td>Double</td></tr> <tr><td>{@link
   * AVKey#ELEVATION_MIN}</td><td>ExtremeElevations/@min</td><td>Double</td></tr> </table>
   *
   * @param params the key-value pairs which define the basic elevation model parameters.
   * @param context the XML document root on which to append parameter elements.
   * @return a reference to context.
   * @throws IllegalArgumentException if either the parameters or the context are null.
   */
  public static Element createBasicElevationModelElements(AVList params, Element context) {
    if (params == null) {
      String message = Logging.getMessage("nullValue.ParametersIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    if (context == null) {
      String message = Logging.getMessage("nullValue.ContextIsNull");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    XPath xpath = WWXML.makeXPath();

    // LevelSet properties.
    DataConfigurationUtils.createLevelSetElements(params, context);

    // Service properties.
    // Try to get the SERVICE_NAME property, but default to "WWTileService".
    String s = AVListImpl.getStringValue(params, AVKey.SERVICE_NAME, "WWTileService");
    if (s != null && s.length() > 0) {
      // The service element may already exist, in which case we want to append to it.
      Element el = WWXML.getElement(context, "Service", xpath);
      if (el == null) el = WWXML.appendElementPath(context, "Service");
      el.setAttribute("serviceName", s);
    }

    WWXML.checkAndAppendBooleanElement(
        params, AVKey.RETRIEVE_PROPERTIES_FROM_SERVICE, context, "RetrievePropertiesFromService");

    // Image format properties.
    if (params.getValue(AVKey.PIXEL_TYPE) != null || params.getValue(AVKey.BYTE_ORDER) != null) {
      Element el = WWXML.getElement(context, "DataType", null);
      if (el == null) el = WWXML.appendElementPath(context, "DataType");

      s = params.getStringValue(AVKey.PIXEL_TYPE);
      if (s != null && s.length() > 0) {
        s = WWXML.dataTypeAsText(s);
        if (s != null && s.length() > 0) el.setAttribute("type", s);
      }

      s = params.getStringValue(AVKey.BYTE_ORDER);
      if (s != null && s.length() > 0) {
        s = WWXML.byteOrderAsText(s);
        if (s != null && s.length() > 0) el.setAttribute("byteOrder", s);
      }
    }

    // Elevation data properties.
    Element el = WWXML.appendElementPath(context, "ExtremeElevations");
    WWXML.checkAndAppendTextElement(params, AVKey.ELEVATION_EXTREMES_FILE, el, "FileName");

    Double d = AVListImpl.getDoubleValue(params, AVKey.ELEVATION_MAX);
    if (d != null) el.setAttribute("max", Double.toString(d));

    d = AVListImpl.getDoubleValue(params, AVKey.ELEVATION_MIN);
    if (d != null) el.setAttribute("min", Double.toString(d));

    return context;
  }
  /**
   * Parses WMS elevation model parameters from a specified WMS {@link Capabilities} document. This
   * also parses common WMS layer parameters by invoking {@link
   * DataConfigurationUtils#getWMSLayerParams(gov.nasa.worldwind.wms.Capabilities, String[],
   * gov.nasa.worldwind.avlist.AVList)}. This writes output as key-value pairs to params. Supported
   * key and parameter names are:
   *
   * <table> <th><td>Key</td><td>Value</td><td>Type</td></th> <tr><td>{@link
   * AVKey#ELEVATION_MAX}</td><td>WMS layer's maximum extreme elevation</td><td>Double</td></tr> <tr><td>{@link
   * AVKey#ELEVATION_MIN}</td><td>WMS layer's minimum extreme elevation</td><td>Double</td></tr> <tr><td>{@link
   * AVKey#PIXEL_TYPE}</td><td>Translate WMS layer's image format to a matching pixel type</td><td>String</td></tr>
   * </table>
   *
   * @param caps the WMS Capabilities document to parse for WMS layer parameters.
   * @param formatOrderPreference an ordered array of preferred image formats, or null to use the
   *     default format.
   * @param params the output key-value pairs which recieve the WMS layer parameters.
   * @return a reference to params.
   * @throws IllegalArgumentException if either the document or params are null, or if params does
   *     not contain the required key-value pairs.
   * @throws gov.nasa.worldwind.exception.WWRuntimeException if the Capabilities document does not
   *     contain any of the required information.
   */
  public static AVList getWMSElevationModelParams(
      Capabilities caps, String[] formatOrderPreference, AVList params) {
    if (caps == null) {
      String message = Logging.getMessage("nullValue.WMSCapabilities");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    if (params == null) {
      String message = Logging.getMessage("nullValue.ElevationModelConfigParams");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    DataConfigurationUtils.getWMSLayerParams(caps, formatOrderPreference, params);

    // Attempt to extract the WMS layer names from the specified parameters.
    String layerNames = params.getStringValue(AVKey.LAYER_NAMES);
    if (layerNames == null || layerNames.length() == 0) {
      String message = Logging.getMessage("nullValue.WMSLayerNames");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    String[] names = layerNames.split(",");
    if (names == null || names.length == 0) {
      String message = Logging.getMessage("nullValue.WMSLayerNames");
      Logging.logger().severe(message);
      throw new IllegalArgumentException(message);
    }

    // Get the layer's extreme elevations.
    Double[] extremes = caps.getLayerExtremeElevations(caps, names);

    Double d = (Double) params.getValue(AVKey.ELEVATION_MIN);
    if (d == null && extremes != null && extremes[0] != null)
      params.setValue(AVKey.ELEVATION_MIN, extremes[0]);

    d = (Double) params.getValue(AVKey.ELEVATION_MAX);
    if (d == null && extremes != null && extremes[1] != null)
      params.setValue(AVKey.ELEVATION_MAX, extremes[1]);

    // Determine the internal pixel type from the image format.
    if (params.getValue(AVKey.PIXEL_TYPE) == null) {
      String imageFormat = params.getStringValue(AVKey.IMAGE_FORMAT);
      if (imageFormat != null) {
        if (imageFormat.equals("application/bil32"))
          params.setValue(AVKey.PIXEL_TYPE, AVKey.FLOAT32);
        else if (imageFormat.equals("application/bil16"))
          params.setValue(AVKey.PIXEL_TYPE, AVKey.INT16);
        else if (imageFormat.equals("application/bil"))
          params.setValue(AVKey.PIXEL_TYPE, AVKey.INT16);
        else if (imageFormat.equals("image/bil")) params.setValue(AVKey.PIXEL_TYPE, AVKey.INT16);
      }
    }

    // Use the default pixel type.
    if (params.getValue(AVKey.PIXEL_TYPE) == null) params.setValue(AVKey.PIXEL_TYPE, AVKey.INT16);

    // Use the default byte order.
    if (params.getValue(AVKey.BYTE_ORDER) == null)
      params.setValue(AVKey.BYTE_ORDER, AVKey.LITTLE_ENDIAN);

    return params;
  }