CPFParser.java

  1. /* Copyright 2002-2025 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.ilrs;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.Reader;
  21. import java.util.Arrays;
  22. import java.util.Collections;
  23. import java.util.regex.Pattern;

  24. import org.hipparchus.exception.LocalizedCoreFormats;
  25. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  26. import org.orekit.annotation.DefaultDataContext;
  27. import org.orekit.data.DataContext;
  28. import org.orekit.data.DataSource;
  29. import org.orekit.errors.OrekitException;
  30. import org.orekit.errors.OrekitMessages;
  31. import org.orekit.files.general.EphemerisFileParser;
  32. import org.orekit.frames.Frame;
  33. import org.orekit.frames.Frames;
  34. import org.orekit.time.AbsoluteDate;
  35. import org.orekit.time.DateComponents;
  36. import org.orekit.time.TimeScale;
  37. import org.orekit.utils.CartesianDerivativesFilter;
  38. import org.orekit.utils.Constants;
  39. import org.orekit.utils.IERSConventions;

  40. /**
  41.  * A parser for the CPF orbit file format.
  42.  * <p>
  43.  * It supports both 1.0 and 2.0 versions
  44.  * <p>
  45.  * <b>Note:</b> Only required header keys are read. Furthermore, only position data are read.
  46.  * Other keys are simply ignored
  47.  * Contributions are welcome to support more fields in the format.
  48.  * </p>
  49.  * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2006/cpf_1.01.pdf">1.0 file format</a>
  50.  * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2018/cpf_2.00h-1.pdf">2.0 file format</a>
  51.  * @author Bryan Cazabonne
  52.  * @since 10.3
  53.  */
  54. public class CPFParser implements EphemerisFileParser<CPF> {

  55.     /** Default number of sample for interpolating data (See: reference documents). */
  56.     public static final int DEFAULT_INTERPOLATION_SAMPLE = 10;

  57.     /** File format. */
  58.     private static final String FILE_FORMAT = "CPF";

  59.     /** Miscroseconds to seconds converter. */
  60.     private static final double MS_TO_S = 1.0e-6;

  61.     /** Pattern for delimiting regular expressions. */
  62.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  63.     /** Standard gravitational parameter in m^3 / s^2. */
  64.     private final double mu;

  65.     /** Time scale used to define epochs in CPF file. */
  66.     private final TimeScale timeScale;

  67.     /** Set of frames. */
  68.     private final Frames frames;

  69.     /** Interpolation sample for data interpolating. */
  70.     private final int interpolationSample;

  71.     /** IERS convention for frames. */
  72.     private final IERSConventions iersConvention;

  73.     /**
  74.      * Default constructor.
  75.      * <p>
  76.      * This constructor uses the {@link DataContext#getDefault() default data context}.
  77.      */
  78.     @DefaultDataContext
  79.     public CPFParser() {
  80.         this(Constants.EIGEN5C_EARTH_MU, DEFAULT_INTERPOLATION_SAMPLE,
  81.              IERSConventions.IERS_2010, DataContext.getDefault().getTimeScales().getUTC(),
  82.              DataContext.getDefault().getFrames());
  83.     }

  84.     /**
  85.      * Constructor.
  86.      * @param mu standard gravitational parameter to use for
  87.      *           creating {@link org.orekit.orbits.Orbit Orbits} from
  88.      *           the ephemeris data.
  89.      * @param interpolationSamples number of samples to use when interpolating
  90.      * @param iersConventions IERS convention for frames definition
  91.      * @param utc time scale used to define epochs in CPF files (UTC)
  92.      * @param frames set of frames for satellite coordinates
  93.      */
  94.     public CPFParser(final double mu,
  95.                      final int interpolationSamples,
  96.                      final IERSConventions iersConventions,
  97.                      final TimeScale utc,
  98.                      final Frames frames) {
  99.         this.mu                  = mu;
  100.         this.interpolationSample = interpolationSamples;
  101.         this.iersConvention      = iersConventions;
  102.         this.timeScale           = utc;
  103.         this.frames              = frames;
  104.     }

  105.     /** {@inheritDoc} */
  106.     @Override
  107.     public CPF parse(final DataSource source) {

  108.         try (Reader reader = source.getOpener().openReaderOnce();
  109.              BufferedReader br = (reader == null) ? null : new BufferedReader(reader)) {

  110.             if (br == null) {
  111.                 throw new OrekitException(OrekitMessages.UNABLE_TO_FIND_FILE, source.getName());
  112.             }

  113.             // initialize internal data structures
  114.             final ParseInfo pi = new ParseInfo();

  115.             int lineNumber = 0;
  116.             Iterable<LineParser> candidateParsers = Collections.singleton(LineParser.H1);
  117.             nextLine:
  118.                 for (String line = br.readLine(); line != null; line = br.readLine()) {
  119.                     ++lineNumber;
  120.                     for (final LineParser candidate : candidateParsers) {
  121.                         if (candidate.canHandle(line)) {
  122.                             try {

  123.                                 candidate.parse(line, pi);

  124.                                 if (pi.done) {
  125.                                     pi.file.setFilter(pi.hasVelocityEntries ?
  126.                                                       CartesianDerivativesFilter.USE_PV :
  127.                                                       CartesianDerivativesFilter.USE_P);
  128.                                     // Return file
  129.                                     return pi.file;
  130.                                 }

  131.                                 candidateParsers = candidate.allowedNext();
  132.                                 continue nextLine;

  133.                             } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  134.                                 throw new OrekitException(e,
  135.                                                           OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  136.                                                           lineNumber, source.getName(), line);
  137.                             }
  138.                         }
  139.                     }

  140.                 }

  141.             // We never reached the EOF marker
  142.             throw new OrekitException(OrekitMessages.CPF_UNEXPECTED_END_OF_FILE, lineNumber);

  143.         } catch (IOException ioe) {
  144.             throw new OrekitException(ioe, LocalizedCoreFormats.SIMPLE_MESSAGE, ioe.getLocalizedMessage());
  145.         }

  146.     }

  147.     /** Transient data used for parsing a CPF file. The data is kept in a
  148.      * separate data structure to make the parser thread-safe.
  149.      * <p><b>Note</b>: The class intentionally does not provide accessor
  150.      * methods, as it is only used internally for parsing a CPF file.</p>
  151.      */
  152.     private class ParseInfo {

  153.         /** The corresponding CPF file. */
  154.         private CPF file;

  155.         /** IERS convention. */
  156.         private IERSConventions convention;

  157.         /** Set of frames. */
  158.         private Frames frames;

  159.         /** Frame for the ephemeris data. */
  160.         private Frame frame;

  161.         /** Time scale. */
  162.         private TimeScale timeScale;

  163.         /** Indicates if the SP3 file has velocity entries. */
  164.         private boolean hasVelocityEntries;

  165.         /** End Of File reached indicator. */
  166.         private boolean done;

  167.         /**
  168.          * Constructor.
  169.          */
  170.         protected ParseInfo() {

  171.             // Initialise file
  172.             file = new CPF();

  173.             // Time scale
  174.             this.timeScale = CPFParser.this.timeScale;

  175.             // Initialise fields
  176.             file.setMu(mu);
  177.             file.setInterpolationSample(interpolationSample);
  178.             file.setTimeScale(timeScale);

  179.             // Default values
  180.             this.done               = false;
  181.             this.hasVelocityEntries = false;

  182.             // Default value for reference frame
  183.             this.convention = CPFParser.this.iersConvention;
  184.             this.frames     = CPFParser.this.frames;
  185.             frame           = frames.getITRF(convention, false);

  186.         }

  187.     }

  188.     /** Parsers for specific lines. */
  189.     private enum LineParser {

  190.         /** Header first line. */
  191.         H1("H1") {

  192.             /** {@inheritDoc} */
  193.             @Override
  194.             public void parse(final String line, final ParseInfo pi) {

  195.                 // Data contained in the line
  196.                 final String[] values = SEPARATOR.split(line);

  197.                 // Index for reading data.
  198.                 // Allow taking into consideration difference between 1.0 and 2.0 formats
  199.                 int index = 1;

  200.                 // Format
  201.                 final String format = values[index++];

  202.                 // Throw an exception if format is not equal to "CPF"
  203.                 if (!FILE_FORMAT.equals(format)) {
  204.                     throw new OrekitException(OrekitMessages.UNEXPECTED_FORMAT_FOR_ILRS_FILE, FILE_FORMAT, format);
  205.                 }

  206.                 // Fill first elements
  207.                 pi.file.getHeader().setFormat(format);
  208.                 pi.file.getHeader().setVersion(Integer.parseInt(values[index++]));
  209.                 pi.file.getHeader().setSource(values[index++]);

  210.                 // Epoch of ephemeris production
  211.                 final int year  = Integer.parseInt(values[index++]);
  212.                 final int month = Integer.parseInt(values[index++]);
  213.                 final int day   = Integer.parseInt(values[index++]);
  214.                 pi.file.getHeader().setProductionEpoch(new DateComponents(year, month, day));

  215.                 // Hour of ephemeris production
  216.                 pi.file.getHeader().setProductionHour(Integer.parseInt(values[index++]));

  217.                 // Ephemeris sequence number
  218.                 pi.file.getHeader().setSequenceNumber(Integer.parseInt(values[index++]));

  219.                 // Difference between version 1.0 and 2.0: sub-daily ephemeris sequence number
  220.                 if (pi.file.getHeader().getVersion() == 2) {
  221.                     pi.file.getHeader().setSubDailySequenceNumber(Integer.parseInt(values[index++]));
  222.                 }

  223.                 // Target Name
  224.                 pi.file.getHeader().setName(values[index]);

  225.             }

  226.             /** {@inheritDoc} */
  227.             @Override
  228.             public Iterable<LineParser> allowedNext() {
  229.                 return Arrays.asList(H2, ZERO);
  230.             }

  231.         },

  232.         /** Header second line. */
  233.         H2("H2") {

  234.             /** {@inheritDoc} */
  235.             @Override
  236.             public void parse(final String line, final ParseInfo pi) {

  237.                 // Data contained in the line
  238.                 final String[] values = SEPARATOR.split(line);

  239.                 // Identifiers
  240.                 pi.file.getHeader().setIlrsSatelliteId(values[1]);
  241.                 pi.file.getHeader().setSic(values[2]);
  242.                 pi.file.getHeader().setNoradId(values[3]);

  243.                 // Start epoch
  244.                 final int    yearS   = Integer.parseInt(values[4]);
  245.                 final int    monthS  = Integer.parseInt(values[5]);
  246.                 final int    dayS    = Integer.parseInt(values[6]);
  247.                 final int    hourS   = Integer.parseInt(values[7]);
  248.                 final int    minuteS = Integer.parseInt(values[8]);
  249.                 final double secondS = Integer.parseInt(values[9]);

  250.                 pi.file.getHeader().setStartEpoch(new AbsoluteDate(yearS, monthS, dayS,
  251.                                                                    hourS, minuteS, secondS,
  252.                                                                    pi.file.getTimeScale()));

  253.                 // End epoch
  254.                 final int    yearE   = Integer.parseInt(values[10]);
  255.                 final int    monthE  = Integer.parseInt(values[11]);
  256.                 final int    dayE    = Integer.parseInt(values[12]);
  257.                 final int    hourE   = Integer.parseInt(values[13]);
  258.                 final int    minuteE = Integer.parseInt(values[14]);
  259.                 final double secondE = Integer.parseInt(values[15]);

  260.                 pi.file.getHeader().setEndEpoch(new AbsoluteDate(yearE, monthE, dayE,
  261.                                                                  hourE, minuteE, secondE,
  262.                                                                  pi.file.getTimeScale()));

  263.                 // Time between table entries
  264.                 pi.file.getHeader().setStep(Integer.parseInt(values[16]));

  265.                 // Compatibility with TIVs
  266.                 pi.file.getHeader().setIsCompatibleWithTIVs(Integer.parseInt(values[17]) == 1);

  267.                 // Target class
  268.                 pi.file.getHeader().setTargetClass(Integer.parseInt(values[18]));

  269.                 // Reference frame
  270.                 final int frameId = Integer.parseInt(values[19]);
  271.                 switch (frameId) {
  272.                     case 0:
  273.                         pi.frame = pi.frames.getITRF(pi.convention, false);
  274.                         break;
  275.                     case 1:
  276.                         pi.frame = pi.frames.getTOD(true);
  277.                         break;
  278.                     case 2:
  279.                         pi.frame = pi.frames.getMOD(pi.convention);
  280.                         break;
  281.                     default:
  282.                         pi.frame = pi.frames.getITRF(pi.convention, false);
  283.                         break;
  284.                 }
  285.                 pi.file.getHeader().setRefFrame(pi.frame);
  286.                 pi.file.getHeader().setRefFrameId(frameId);

  287.                 // Last fields
  288.                 pi.file.getHeader().setRotationalAngleType(Integer.parseInt(values[20]));
  289.                 pi.file.getHeader().setIsCenterOfMassCorrectionApplied(Integer.parseInt(values[21]) == 1);
  290.                 if (pi.file.getHeader().getVersion() == 2) {
  291.                     pi.file.getHeader().setTargetLocation(Integer.parseInt(values[22]));
  292.                 }

  293.             }

  294.             /** {@inheritDoc} */
  295.             @Override
  296.             public Iterable<LineParser> allowedNext() {
  297.                 return Arrays.asList(H3, H4, H5, H9, ZERO);
  298.             }

  299.         },

  300.         /** Header third line. */
  301.         H3("H3") {

  302.             /** {@inheritDoc} */
  303.             @Override
  304.             public void parse(final String line, final ParseInfo pi) {
  305.                 // Not implemented yet
  306.             }

  307.             /** {@inheritDoc} */
  308.             @Override
  309.             public Iterable<LineParser> allowedNext() {
  310.                 return Arrays.asList(H4, H5, H9, ZERO);
  311.             }

  312.         },

  313.         /** Header fourth line. */
  314.         H4("H4") {

  315.             /** {@inheritDoc} */
  316.             @Override
  317.             public void parse(final String line, final ParseInfo pi) {

  318.                 // Data contained in the line
  319.                 final String[] values = SEPARATOR.split(line);

  320.                 // Pulse Repetition Frequency (PRF)
  321.                 pi.file.getHeader().setPrf(Double.parseDouble(values[1]));

  322.                 // Transponder information
  323.                 pi.file.getHeader().setTranspTransmitDelay(Double.parseDouble(values[2]) * MS_TO_S);
  324.                 pi.file.getHeader().setTranspUtcOffset(Double.parseDouble(values[3]) * MS_TO_S);
  325.                 pi.file.getHeader().setTranspOscDrift(Double.parseDouble(values[4]));
  326.                 if (pi.file.getHeader().getVersion() == 2) {
  327.                     pi.file.getHeader().setTranspClkRef(Double.parseDouble(values[5]));
  328.                 }

  329.             }

  330.             /** {@inheritDoc} */
  331.             @Override
  332.             public Iterable<LineParser> allowedNext() {
  333.                 return Arrays.asList(H5, H9, ZERO);
  334.             }

  335.         },

  336.         /** Header fifth line. */
  337.         H5("H5") {

  338.             /** {@inheritDoc} */
  339.             @Override
  340.             public void parse(final String line, final ParseInfo pi) {

  341.                 // Approximate center of mass to reflector offset in meters
  342.                 final double offset = Double.parseDouble(SEPARATOR.split(line)[1]);
  343.                 pi.file.getHeader().setCenterOfMassOffset(offset);

  344.             }

  345.             /** {@inheritDoc} */
  346.             @Override
  347.             public Iterable<LineParser> allowedNext() {
  348.                 return Arrays.asList(H9, ZERO);
  349.             }

  350.         },

  351.         /** Header last line. */
  352.         H9("H9") {

  353.             /** {@inheritDoc} */
  354.             @Override
  355.             public void parse(final String line, final ParseInfo pi) {
  356.                 // End of header. Nothing to do
  357.             }

  358.             /** {@inheritDoc} */
  359.             @Override
  360.             public Iterable<LineParser> allowedNext() {
  361.                 return Arrays.asList(TEN, ZERO);
  362.             }

  363.         },

  364.         /** Position values. */
  365.         TEN("10") {

  366.             /** {@inheritDoc} */
  367.             @Override
  368.             public void parse(final String line, final ParseInfo pi) {

  369.                 // Data contained in the line
  370.                 final String[] values = SEPARATOR.split(line);

  371.                 // Epoch
  372.                 final int mjd           = Integer.parseInt(values[2]);
  373.                 final double secInDay   = Double.parseDouble(values[3]);
  374.                 final AbsoluteDate date = AbsoluteDate.createMJDDate(mjd, secInDay, pi.timeScale);

  375.                 // Leap second flag
  376.                 final int leap = Integer.parseInt(values[4]);

  377.                 // Coordinates
  378.                 final double x = Double.parseDouble(values[5]);
  379.                 final double y = Double.parseDouble(values[6]);
  380.                 final double z = Double.parseDouble(values[7]);
  381.                 final Vector3D position = new Vector3D(x, y, z);

  382.                 // CPF coordinate
  383.                 final CPF.CPFCoordinate coordinate = new CPF.CPFCoordinate(date, position, leap);
  384.                 pi.file.addSatelliteCoordinate(pi.file.getHeader().getIlrsSatelliteId(), coordinate);

  385.             }

  386.             /** {@inheritDoc} */
  387.             @Override
  388.             public Iterable<LineParser> allowedNext() {
  389.                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  390.             }

  391.         },

  392.         /** Velocity values. */
  393.         TWENTY("20") {

  394.             /** {@inheritDoc} */
  395.             @Override
  396.             public void parse(final String line, final ParseInfo pi) {

  397.                 // Data contained in the line
  398.                 final String[] values = SEPARATOR.split(line);

  399.                 // Coordinates
  400.                 final double x = Double.parseDouble(values[2]);
  401.                 final double y = Double.parseDouble(values[3]);
  402.                 final double z = Double.parseDouble(values[4]);
  403.                 final Vector3D velocity = new Vector3D(x, y, z);

  404.                 // CPF coordinate
  405.                 pi.file.addSatelliteVelocityToCPFCoordinate(pi.file.getHeader().getIlrsSatelliteId(), velocity);
  406.             }

  407.             /** {@inheritDoc} */
  408.             @Override
  409.             public Iterable<LineParser> allowedNext() {
  410.                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  411.             }

  412.         },

  413.         /** Corrections. */
  414.         THIRTY("30") {

  415.             /** {@inheritDoc} */
  416.             @Override
  417.             public void parse(final String line, final ParseInfo pi) {
  418.                 // Not implemented yet
  419.             }

  420.             /** {@inheritDoc} */
  421.             @Override
  422.             public Iterable<LineParser> allowedNext() {
  423.                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  424.             }

  425.         },

  426.         /** Transponder specific. */
  427.         FORTY("40") {

  428.             /** {@inheritDoc} */
  429.             @Override
  430.             public void parse(final String line, final ParseInfo pi) {
  431.                 // Not implemented yet
  432.             }

  433.             /** {@inheritDoc} */
  434.             @Override
  435.             public Iterable<LineParser> allowedNext() {
  436.                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  437.             }

  438.         },

  439.         /** Offset from center of main body. */
  440.         FIFTY("50") {

  441.             /** {@inheritDoc} */
  442.             @Override
  443.             public void parse(final String line, final ParseInfo pi) {
  444.                 // Not implemented yet
  445.             }

  446.             /** {@inheritDoc} */
  447.             @Override
  448.             public Iterable<LineParser> allowedNext() {
  449.                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  450.             }

  451.         },

  452.         /** Rotation angle of offset. */
  453.         SIXTY("60") {

  454.             /** {@inheritDoc} */
  455.             @Override
  456.             public void parse(final String line, final ParseInfo pi) {
  457.                 // Not implemented yet
  458.             }

  459.             /** {@inheritDoc} */
  460.             @Override
  461.             public Iterable<LineParser> allowedNext() {
  462.                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  463.             }

  464.         },

  465.         /** Earth orientation. */
  466.         SEVENTY("70") {

  467.             /** {@inheritDoc} */
  468.             @Override
  469.             public void parse(final String line, final ParseInfo pi) {
  470.                 // Not implemented yet
  471.             }

  472.             /** {@inheritDoc} */
  473.             @Override
  474.             public Iterable<LineParser> allowedNext() {
  475.                 return Arrays.asList(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  476.             }

  477.         },

  478.         /** Comments. */
  479.         ZERO("00") {

  480.             /** {@inheritDoc} */
  481.             @Override
  482.             public void parse(final String line, final ParseInfo pi) {

  483.                 // Comment
  484.                 final String comment = line.split(getIdentifier())[1].trim();
  485.                 pi.file.getComments().add(comment);

  486.             }

  487.             /** {@inheritDoc} */
  488.             @Override
  489.             public Iterable<LineParser> allowedNext() {
  490.                 return Arrays.asList(H1, H2, H3, H4, H5, H9,
  491.                                      TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  492.             }

  493.         },

  494.         /** Last record in ephemeris. */
  495.         EOF("99") {

  496.             @Override
  497.             public void parse(final String line, final ParseInfo pi) {
  498.                 pi.done = true;
  499.             }

  500.             /** {@inheritDoc} */
  501.             @Override
  502.             public Iterable<LineParser> allowedNext() {
  503.                 return Collections.singleton(EOF);
  504.             }

  505.         };

  506.         /** Pattern for identifying line. */
  507.         private final Pattern pattern;

  508.         /** Identifier. */
  509.         private final String identifier;

  510.         /** Simple constructor.
  511.          * @param identifier regular expression for identifying line (i.e. first element)
  512.          */
  513.         LineParser(final String identifier) {
  514.             this.identifier = identifier;
  515.             pattern = Pattern.compile(identifier);
  516.         }

  517.         /**
  518.          * Get the regular expression for identifying line.
  519.          * @return the regular expression for identifying line
  520.          */
  521.         public String getIdentifier() {
  522.             return identifier;
  523.         }

  524.         /** Parse a line.
  525.          * @param line line to parse
  526.          * @param pi holder for transient data
  527.          */
  528.         public abstract void parse(String line, ParseInfo pi);

  529.         /** Get the allowed parsers for next line.
  530.          * @return allowed parsers for next line
  531.          */
  532.         public abstract Iterable<LineParser> allowedNext();

  533.         /** Check if parser can handle line.
  534.          * @param line line to parse
  535.          * @return true if parser can handle the specified line
  536.          */
  537.         public boolean canHandle(final String line) {
  538.             return pattern.matcher(SEPARATOR.split(line)[0]).matches();
  539.         }

  540.     }

  541. }