StreamingAemWriter.java

  1. /* Copyright 2002-2020 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.files.ccsds;

  18. import java.io.IOException;
  19. import java.text.DecimalFormat;
  20. import java.text.DecimalFormatSymbols;
  21. import java.time.ZoneOffset;
  22. import java.time.ZonedDateTime;
  23. import java.time.format.DateTimeFormatter;
  24. import java.util.Date;
  25. import java.util.LinkedHashMap;
  26. import java.util.Locale;
  27. import java.util.Map;

  28. import org.hipparchus.exception.LocalizedCoreFormats;
  29. import org.hipparchus.geometry.euclidean.threed.RotationOrder;
  30. import org.orekit.errors.OrekitException;
  31. import org.orekit.files.ccsds.AEMParser.AEMRotationOrder;
  32. import org.orekit.propagation.SpacecraftState;
  33. import org.orekit.propagation.sampling.OrekitFixedStepHandler;
  34. import org.orekit.time.AbsoluteDate;
  35. import org.orekit.time.DateTimeComponents;
  36. import org.orekit.time.TimeComponents;
  37. import org.orekit.time.TimeScale;
  38. import org.orekit.utils.TimeStampedAngularCoordinates;

  39. /**
  40.  * A writer for AEM files.
  41.  *
  42.  * <p> Each instance corresponds to a single AEM file.
  43.  *
  44.  * <h2> Metadata </h2>
  45.  *
  46.  * <p> The AEM metadata used by this writer is described in the following table. Many
  47.  * metadata items are optional or have default values so they do not need to be specified.
  48.  * At a minimum the user must supply those values that are required and for which no
  49.  * default exits: {@link Keyword#OBJECT_NAME}, and {@link Keyword#OBJECT_ID}. The usage
  50.  * column in the table indicates where the metadata item is used, either in the AEM header
  51.  * or in the metadata section at the start of an AEM attitude segment.
  52.  *
  53.  * <p> The AEM metadata for the whole AEM file is set in the {@link
  54.  * #StreamingAemWriter(Appendable, TimeScale, Map) constructor}.
  55.  *
  56.  * <table>
  57.  * <caption>AEM metadata</caption>
  58.  *     <thead>
  59.  *         <tr>
  60.  *             <th>Keyword
  61.  *             <th>Usage
  62.  *             <th>Obligatory
  63.  *             <th>Default
  64.  *             <th>Reference
  65.  *    </thead>
  66.  *    <tbody>
  67.  *        <tr>
  68.  *            <td>{@link Keyword#CCSDS_AEM_VERS}
  69.  *            <td>Header
  70.  *            <td>Yes
  71.  *            <td>{@link #CCSDS_AEM_VERS}
  72.  *            <td>Table 4-2
  73.  *        <tr>
  74.  *            <td>{@link Keyword#COMMENT}
  75.  *            <td>Header
  76.  *            <td>No
  77.  *            <td>
  78.  *            <td>Table 4-2
  79.  *        <tr>
  80.  *            <td>{@link Keyword#CREATION_DATE}
  81.  *            <td>Header
  82.  *            <td>Yes
  83.  *            <td>{@link Date#Date() Now}
  84.  *            <td>Table 4-2
  85.  *        <tr>
  86.  *            <td>{@link Keyword#ORIGINATOR}
  87.  *            <td>Header
  88.  *            <td>Yes
  89.  *            <td>{@link #DEFAULT_ORIGINATOR}
  90.  *            <td>Table 4-2
  91.  *        <tr>
  92.  *            <td>{@link Keyword#OBJECT_NAME}
  93.  *            <td>Segment
  94.  *            <td>Yes
  95.  *            <td>
  96.  *            <td>Table 4-3
  97.  *        <tr>
  98.  *            <td>{@link Keyword#OBJECT_ID}
  99.  *            <td>Segment
  100.  *            <td>Yes
  101.  *            <td>
  102.  *            <td>Table 4-3
  103.  *        <tr>
  104.  *            <td>{@link Keyword#CENTER_NAME}
  105.  *            <td>Segment
  106.  *            <td>No
  107.  *            <td>
  108.  *            <td>Table 4-3
  109.  *        <tr>
  110.  *            <td>{@link Keyword#REF_FRAME_A}
  111.  *            <td>Segment
  112.  *            <td>Yes
  113.  *            <td>
  114.  *            <td>Table 4-3
  115.  *        <tr>
  116.  *            <td>{@link Keyword#REF_FRAME_B}
  117.  *            <td>Segment
  118.  *            <td>Yes
  119.  *            <td>
  120.  *            <td>Table 4-3
  121.  *        <tr>
  122.  *            <td>{@link Keyword#ATTITUDE_DIR}
  123.  *            <td>Segment
  124.  *            <td>Yes
  125.  *            <td>
  126.  *            <td>Table 4-3
  127.  *        <tr>
  128.  *            <td>{@link Keyword#TIME_SYSTEM}
  129.  *            <td>Segment
  130.  *            <td>Yes
  131.  *            <td>
  132.  *            <td>Table 4-3, Annex A
  133.  *        <tr>
  134.  *            <td>{@link Keyword#START_TIME}
  135.  *            <td>Segment
  136.  *            <td>Yes
  137.  *            <td>
  138.  *            <td>Table 4-3
  139.  *        <tr>
  140.  *            <td>{@link Keyword#USEABLE_START_TIME}
  141.  *            <td>Segment
  142.  *            <td>No
  143.  *            <td>
  144.  *            <td>Table 4-3
  145.  *        <tr>
  146.  *            <td>{@link Keyword#STOP_TIME}
  147.  *            <td>Segment
  148.  *            <td>Yes
  149.  *            <td>
  150.  *            <td>Table 4-3
  151.  *        <tr>
  152.  *            <td>{@link Keyword#USEABLE_STOP_TIME}
  153.  *            <td>Segment
  154.  *            <td>No
  155.  *            <td>
  156.  *            <td>Table 4-3
  157.  *        <tr>
  158.  *            <td>{@link Keyword#ATTITUDE_TYPE}
  159.  *            <td>Segment
  160.  *            <td>Yes
  161.  *            <td>
  162.  *            <td>Table 4-3, 4-4
  163.  *        <tr>
  164.  *            <td>{@link Keyword#QUATERNION_TYPE}
  165.  *            <td>Segment
  166.  *            <td>No
  167.  *            <td>
  168.  *            <td>Table 4-3, 4-4
  169.  *        <tr>
  170.  *            <td>{@link Keyword#EULER_ROT_SEQ}
  171.  *            <td>Segment
  172.  *            <td>No
  173.  *            <td>
  174.  *            <td>Table 4-3
  175.  *        <tr>
  176.  *            <td>{@link Keyword#RATE_FRAME}
  177.  *            <td>Segment
  178.  *            <td>No
  179.  *            <td>
  180.  *            <td>Table 4-3
  181.  *        <tr>
  182.  *            <td>{@link Keyword#INTERPOLATION_METHOD}
  183.  *            <td>Segment
  184.  *            <td>No
  185.  *            <td>
  186.  *            <td>Table 4-3
  187.  *        <tr>
  188.  *            <td>{@link Keyword#INTERPOLATION_DEGREE}
  189.  *            <td>Segment
  190.  *            <td>No
  191.  *            <td>
  192.  *            <td>Table 4-3
  193.  *    </tbody>
  194.  *</table>
  195.  *
  196.  * <p> The {@link Keyword#TIME_SYSTEM} must be constant for the whole file and is used
  197.  * to interpret all dates except {@link Keyword#CREATION_DATE}. The guessing algorithm
  198.  * is not guaranteed to work so it is recommended to provide values for {@link
  199.  * Keyword#CENTER_NAME} and {@link Keyword#TIME_SYSTEM} to avoid any bugs associated with
  200.  * incorrect guesses.
  201.  *
  202.  * <p> Standardized values for {@link Keyword#TIME_SYSTEM} are GMST, GPS, MET, MRT, SCLK,
  203.  * TAI, TCB, TDB, TT, UT1, and UTC. Standardized values for reference frames
  204.  * are EME2000, GTOD, ICRF, ITRF2000, ITRF-93, ITRF-97, LVLH, RTN, QSW, TOD, TNW, NTW and RSW.
  205.  * Additionally ITRF followed by a four digit year may be used.
  206.  *
  207.  * @author Bryan Cazabonne
  208.  * @see <a href="https://public.ccsds.org/Pubs/504x0b1c1.pdf">CCSDS 504.0-B-1 Attitude Data Messages</a>
  209.  * @see AEMWriter
  210.  * @since 10.2
  211.  */
  212. public class StreamingAemWriter {

  213.     /** Version number implemented. **/
  214.     public static final String CCSDS_AEM_VERS = "1.0";

  215.     /** Default value for {@link Keyword#ORIGINATOR}. */
  216.     public static final String DEFAULT_ORIGINATOR = "OREKIT";

  217.     /**
  218.      * Default format used for attitude ephemeris data output: 5 digits
  219.      * after the decimal point and leading space for positive values.
  220.      */
  221.     public static final String DEFAULT_ATTITUDE_FORMAT = "% .5f";

  222.     /** New line separator for output file. See 5.4.5. */
  223.     private static final String NEW_LINE = "\n";

  224.     /**
  225.      * Standardized locale to use, to ensure files can be exchanged without
  226.      * internationalization issues.
  227.      */
  228.     private static final Locale STANDARDIZED_LOCALE = Locale.US;

  229.     /** String format used for all key/value pair lines. **/
  230.     private static final String KV_FORMAT = "%s = %s%n";

  231.     /** Output stream. */
  232.     private final Appendable writer;

  233.     /** Metadata for this AEM file. */
  234.     private final Map<Keyword, String> metadata;

  235.     /** Time scale for all dates except {@link Keyword#CREATION_DATE}. */
  236.     private final TimeScale timeScale;

  237.     /** Format for attitude ephemeris data output. */
  238.     private final String attitudeFormat;

  239.     /**
  240.      * Create an AEM writer that streams data to the given output stream.
  241.      * {@link #DEFAULT_ATTITUDE_FORMAT Default formatting} will be used for attitude ephemeris data.
  242.      *
  243.      * @param writer    The output stream for the AEM file. Most methods will append data
  244.      *                  to this {@code writer}.
  245.      * @param timeScale for all times in the AEM except {@link Keyword#CREATION_DATE}. See
  246.      *                  Section 4.2.5.4.2 and Annex A.
  247.      * @param metadata  for the satellite.
  248.      */
  249.     public StreamingAemWriter(final Appendable writer,
  250.                               final TimeScale timeScale,
  251.                               final Map<Keyword, String> metadata) {
  252.         this(writer, timeScale, metadata, DEFAULT_ATTITUDE_FORMAT);
  253.     }

  254.     /**
  255.      * Create an AEM writer than streams data to the given output stream as
  256.      * {@link #StreamingAemWriter(Appendable, TimeScale, Map)} with
  257.      * {@link java.util.Formatter format parameters} for attitude ephemeris data.
  258.      *
  259.      * @param writer    The output stream for the AEM file. Most methods will append data
  260.      *                  to this {@code writer}.
  261.      * @param timeScale for all times in the AEM except {@link Keyword#CREATION_DATE}. See
  262.      *                  Section 4.2.5.4.2 and Annex A.
  263.      * @param metadata  for the satellite.
  264.      * @param attitudeFormat format parameters for attitude ephemeris data output.
  265.      * @since 10.3
  266.      */
  267.     public StreamingAemWriter(final Appendable writer,
  268.                               final TimeScale timeScale,
  269.                               final Map<Keyword, String> metadata,
  270.                               final String attitudeFormat) {
  271.         this.writer    = writer;
  272.         this.timeScale = timeScale;
  273.         this.metadata  = new LinkedHashMap<>(metadata);

  274.         // Set default metadata
  275.         this.metadata.putIfAbsent(Keyword.CCSDS_AEM_VERS, CCSDS_AEM_VERS);

  276.         // creation date is informational only
  277.         this.metadata.putIfAbsent(Keyword.CREATION_DATE,
  278.                 ZonedDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_INSTANT));
  279.         this.metadata.putIfAbsent(Keyword.ORIGINATOR, DEFAULT_ORIGINATOR);
  280.         this.metadata.putIfAbsent(Keyword.TIME_SYSTEM, timeScale.getName());
  281.         this.attitudeFormat = attitudeFormat;
  282.     }

  283.     /**
  284.      * Write a single key and value to the stream using Key Value Notation (KVN).
  285.      * @param key   the keyword to write
  286.      * @param value the value to write
  287.      * @throws IOException if an I/O error occurs.
  288.      */
  289.     private void writeKeyValue(final Keyword key, final String value) throws IOException {
  290.         writer.append(String.format(STANDARDIZED_LOCALE, KV_FORMAT, key.toString(), value));
  291.     }

  292.     /**
  293.      * Writes the standard AEM header for the file.
  294.      * @throws IOException if the stream cannot write to stream
  295.      */
  296.     public void writeHeader() throws IOException {
  297.         writeKeyValue(Keyword.CCSDS_AEM_VERS, this.metadata.get(Keyword.CCSDS_AEM_VERS));
  298.         final String comment = this.metadata.get(Keyword.COMMENT);
  299.         if (comment != null) {
  300.             writeKeyValue(Keyword.COMMENT, comment);
  301.         }
  302.         writeKeyValue(Keyword.CREATION_DATE, this.metadata.get(Keyword.CREATION_DATE));
  303.         writeKeyValue(Keyword.ORIGINATOR, this.metadata.get(Keyword.ORIGINATOR));
  304.         writer.append(NEW_LINE);
  305.     }

  306.     /**
  307.      * Create a writer for a new AEM attitude ephemeris segment.
  308.      * <p> The returned writer can only write a single attitude ephemeris segment in an AEM.
  309.      * This method must be called to create a writer for each attitude ephemeris segment.
  310.      * @param segmentMetadata the metadata to use for the segment. Overrides for this
  311.      *                        segment any other source of meta data values. See {@link
  312.      *                        #StreamingAemWriter} for a description of which metadata are
  313.      *                        required and how they are determined.
  314.      * @return a new AEM segment, ready for writing.
  315.      */
  316.     public AEMSegment newSegment(final Map<Keyword, String> segmentMetadata) {
  317.         final Map<Keyword, String> meta = new LinkedHashMap<>(this.metadata);
  318.         meta.putAll(segmentMetadata);
  319.         return new AEMSegment(meta);
  320.     }

  321.     /**
  322.      * Convert a date to a string with more precision.
  323.      *
  324.      * @param components to convert to a String.
  325.      * @return the String form of {@code date} with at least 9 digits of precision.
  326.      */
  327.     static String dateToString(final DateTimeComponents components) {
  328.         final TimeComponents time = components.getTime();
  329.         final int hour = time.getHour();
  330.         final int minute = time.getMinute();
  331.         final double second = time.getSecond();
  332.         // Decimal formatting classes could be static final if they were thread safe.
  333.         final DecimalFormatSymbols locale = new DecimalFormatSymbols(STANDARDIZED_LOCALE);
  334.         final DecimalFormat twoDigits = new DecimalFormat("00", locale);
  335.         final DecimalFormat precise = new DecimalFormat("00.0########", locale);
  336.         return components.getDate().toString() + "T" + twoDigits.format(hour) + ":" +
  337.                 twoDigits.format(minute) + ":" + precise.format(second);
  338.     }

  339.     /** A writer for a segment of an AEM. */
  340.     public class AEMSegment implements OrekitFixedStepHandler {

  341.         /** Metadata for this AEM Segment. */
  342.         private final Map<Keyword, String> metadata;

  343.         /**
  344.          * Create a new segment writer.
  345.          * @param metadata to use when writing this segment.
  346.          */
  347.         private AEMSegment(final Map<Keyword, String> metadata) {
  348.             this.metadata = metadata;
  349.         }

  350.         /**
  351.          * Write the ephemeris segment metadata.
  352.          *
  353.          * <p> See {@link StreamingAemWriter} for a description of how the metadata is
  354.          * set.
  355.          *
  356.          * @throws IOException if the output stream throws one while writing.
  357.          */
  358.         public void writeMetadata() throws IOException {
  359.             // Start metadata
  360.             writer.append("META_START").append(NEW_LINE);

  361.             // Table 4.3
  362.             writeKeyValue(Keyword.OBJECT_NAME,  this.metadata.get(Keyword.OBJECT_NAME));
  363.             writeKeyValue(Keyword.OBJECT_ID,    this.metadata.get(Keyword.OBJECT_ID));
  364.             writeKeyValue(Keyword.CENTER_NAME,  this.metadata.get(Keyword.CENTER_NAME));
  365.             writeKeyValue(Keyword.REF_FRAME_A,  this.metadata.get(Keyword.REF_FRAME_A));
  366.             writeKeyValue(Keyword.REF_FRAME_B,  this.metadata.get(Keyword.REF_FRAME_B));
  367.             writeKeyValue(Keyword.ATTITUDE_DIR, this.metadata.get(Keyword.ATTITUDE_DIR));
  368.             writeKeyValue(Keyword.TIME_SYSTEM,  this.metadata.get(Keyword.TIME_SYSTEM));
  369.             writeKeyValue(Keyword.START_TIME,   this.metadata.get(Keyword.START_TIME));

  370.             // Optional values: USEABLE_START_TIME & USEABLE_STOP_TIME
  371.             final String usableStartTime = this.metadata.get(Keyword.USEABLE_START_TIME);
  372.             if (usableStartTime != null) {
  373.                 writeKeyValue(Keyword.USEABLE_START_TIME, usableStartTime);
  374.             }
  375.             final String usableStopTime = this.metadata.get(Keyword.USEABLE_STOP_TIME);
  376.             if (usableStopTime != null) {
  377.                 writeKeyValue(Keyword.USEABLE_STOP_TIME, usableStopTime);
  378.             }

  379.             // Table 4.3
  380.             writeKeyValue(Keyword.STOP_TIME,     this.metadata.get(Keyword.STOP_TIME));
  381.             writeKeyValue(Keyword.ATTITUDE_TYPE, this.metadata.get(Keyword.ATTITUDE_TYPE));

  382.             // Optional values: QUATERNION_ TYPE; EULER_ROT_SEQ; RATE_FRAME; INTERPOLATION_METHOD and INTERPOLATION_DEGREE
  383.             final String quaternionType = this.metadata.get(Keyword.QUATERNION_TYPE);
  384.             if (quaternionType != null) {
  385.                 writeKeyValue(Keyword.QUATERNION_TYPE, quaternionType);
  386.             }
  387.             final String eulerRotSeq = this.metadata.get(Keyword.EULER_ROT_SEQ);
  388.             if (eulerRotSeq != null) {
  389.                 writeKeyValue(Keyword.EULER_ROT_SEQ, eulerRotSeq);
  390.             }
  391.             final String rateFrame = this.metadata.get(Keyword.RATE_FRAME);
  392.             if (rateFrame != null) {
  393.                 writeKeyValue(Keyword.RATE_FRAME, rateFrame);
  394.             }
  395.             final String interpolationMethod = this.metadata.get(Keyword.INTERPOLATION_METHOD);
  396.             if (interpolationMethod != null) {
  397.                 writeKeyValue(Keyword.INTERPOLATION_METHOD, interpolationMethod);
  398.             }
  399.             final String interpolationDegree = this.metadata.get(Keyword.INTERPOLATION_DEGREE);
  400.             if (interpolationDegree != null) {
  401.                 writeKeyValue(Keyword.INTERPOLATION_DEGREE, interpolationDegree);
  402.             }

  403.             // Stop metadata
  404.             writer.append("META_STOP").append(NEW_LINE).append(NEW_LINE);
  405.         }

  406.         /**
  407.          * Write a single attitude ephemeris line according to section 4.2.4 and Table 4-4.
  408.          * @param attitude the attitude information for a given date.
  409.          * @param isFirst true if QC is the first element in the attitude data
  410.          * @param attitudeName name of the attitude type
  411.          * @param rotationOrder rotation order
  412.          * @throws IOException if the output stream throws one while writing.
  413.          */
  414.         public void writeAttitudeEphemerisLine(final TimeStampedAngularCoordinates attitude,
  415.                                                final boolean isFirst,
  416.                                                final String attitudeName,
  417.                                                final RotationOrder rotationOrder)
  418.             throws IOException {
  419.             // Epoch
  420.             final String epoch = dateToString(attitude.getDate().getComponents(timeScale));
  421.             writer.append(epoch).append(" ");
  422.             // Attitude data in degrees
  423.             final AEMAttitudeType type = AEMAttitudeType.getAttitudeType(attitudeName);
  424.             final double[]        data = type.getAttitudeData(attitude, isFirst, rotationOrder);
  425.             final int             size = data.length;
  426.             for (int index = 0; index < size; index++) {
  427.                 writer.append(String.format(STANDARDIZED_LOCALE, attitudeFormat, data[index]));
  428.                 final String space = (index == size - 1) ? "" : " ";
  429.                 writer.append(space);
  430.             }
  431.             // end the line
  432.             writer.append(NEW_LINE);
  433.         }

  434.         /**
  435.          * {@inheritDoc}
  436.          *
  437.          * <p> Sets the {@link Keyword#START_TIME} and {@link Keyword#STOP_TIME} in this
  438.          * segment's metadata if not already set by the user. Then calls {@link
  439.          * #writeMetadata()} to start the segment.
  440.          */
  441.         @Override
  442.         public void init(final SpacecraftState s0,
  443.                          final AbsoluteDate t,
  444.                          final double step) {
  445.             try {
  446.                 final String start = dateToString(s0.getDate().getComponents(timeScale));
  447.                 final String stop = dateToString(t.getComponents(timeScale));
  448.                 this.metadata.putIfAbsent(Keyword.START_TIME, start);
  449.                 this.metadata.putIfAbsent(Keyword.STOP_TIME, stop);
  450.                 this.writeMetadata();
  451.             } catch (IOException e) {
  452.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  453.                         e.getLocalizedMessage());
  454.             }
  455.         }

  456.         /** {@inheritDoc}. */
  457.         @Override
  458.         public void handleStep(final SpacecraftState currentState, final boolean isLast) {
  459.             try {

  460.                 // Quaternion type
  461.                 final String quaternionType = this.metadata.get(Keyword.QUATERNION_TYPE);
  462.                 // If the QUATERNION_TYPE keyword is not present in the file, this means that
  463.                 // the attitude data are not given using quaternion. Therefore, the computation
  464.                 // of the attitude data will not be sensitive to this parameter. A default value
  465.                 // can be set
  466.                 boolean isFirst = false;
  467.                 if (quaternionType != null) {
  468.                     isFirst = (quaternionType.equals("FIRST")) ? true : false;
  469.                 }

  470.                 // Attitude type
  471.                 final String attitudeType = this.metadata.get(Keyword.ATTITUDE_TYPE);

  472.                 // Rotation order
  473.                 final String eulerRotSeq = this.metadata.get(Keyword.EULER_ROT_SEQ);
  474.                 final RotationOrder order = (eulerRotSeq == null) ? null : AEMRotationOrder.getRotationOrder(eulerRotSeq);

  475.                 // Write attitude ephemeris data
  476.                 writeAttitudeEphemerisLine(currentState.getAttitude().getOrientation(), isFirst,
  477.                                            attitudeType, order);

  478.             } catch (IOException e) {
  479.                 throw new OrekitException(e, LocalizedCoreFormats.SIMPLE_MESSAGE,
  480.                         e.getLocalizedMessage());
  481.             }

  482.         }

  483.         /**
  484.          * Start of an attitude block.
  485.          * @throws IOException if the output stream throws one while writing.
  486.          */
  487.         void startAttitudeBlock() throws IOException {
  488.             writer.append("DATA_START").append(NEW_LINE);
  489.         }

  490.         /**
  491.          * End of an attitude block.
  492.          * @throws IOException if the output stream throws one while writing.
  493.          */
  494.         void endAttitudeBlock() throws IOException {
  495.             writer.append("DATA_STOP").append(NEW_LINE);
  496.         }

  497.     }

  498. }