RinexObservationWriter.java
/* Copyright 2022-2025 Thales Alenia Space
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.files.rinex.observation;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import org.hipparchus.geometry.euclidean.threed.Vector3D;
import org.hipparchus.util.FastMath;
import org.orekit.annotation.DefaultDataContext;
import org.orekit.data.DataContext;
import org.orekit.files.rinex.AppliedDCBS;
import org.orekit.files.rinex.AppliedPCVS;
import org.orekit.files.rinex.section.CommonLabel;
import org.orekit.files.rinex.section.Label;
import org.orekit.files.rinex.utils.BaseRinexWriter;
import org.orekit.gnss.PredefinedObservationType;
import org.orekit.gnss.SatInSystem;
import org.orekit.gnss.SatelliteSystem;
import org.orekit.time.AbsoluteDate;
import org.orekit.time.clocks.ClockModel;
import org.orekit.time.ClockTimeScale;
import org.orekit.time.DateTimeComponents;
import org.orekit.time.TimeScale;
import org.orekit.time.TimeScales;
import org.orekit.utils.formatting.FastDecimalFormatter;
import org.orekit.utils.formatting.FastDoubleFormatter;
import org.orekit.utils.formatting.FastLongFormatter;
/** Writer for Rinex observation file.
* <p>
* As RINEX file are organized in batches of observations at some dates,
* these observations are cached and a new batch is output only when
* a new date appears when calling {@link #writeObservationDataSet(ObservationDataSet)}
* or when the file is closed by calling the {@link #close() close} method.
* Failing to call {@link #close() close} would imply the last batch
* of measurements is not written. This is the reason why this class implements
* {@link AutoCloseable}, so the {@link #close() close} method can be called automatically in
* a {@code try-with-resources} statement.
* </p>
* @author Luc Maisonobe
* @since 12.0
*/
public class RinexObservationWriter extends BaseRinexWriter<RinexObservationHeader> implements AutoCloseable {
/** Format for one 1 digit integer field. */
private static final FastLongFormatter ONE_DIGIT_INTEGER = new FastLongFormatter(1, false);
/** Format for one 8.3 digits float field. */
private static final FastDoubleFormatter EIGHT_THREE_DIGITS_FLOAT = new FastDecimalFormatter(8, 3);
/** Format for one 8.5 digits float field. */
private static final FastDoubleFormatter EIGHT_FIVE_DIGITS_FLOAT = new FastDecimalFormatter(8, 5);
/** Format for one 9.4 digits float field. */
private static final FastDoubleFormatter NINE_FOUR_DIGITS_FLOAT = new FastDecimalFormatter(9, 4);
/** Format for one 10.3 digits float field. */
private static final FastDoubleFormatter TEN_THREE_DIGITS_FLOAT = new FastDecimalFormatter(10, 3);
/** Format for one 11.7 digits float field. */
private static final FastDoubleFormatter ELEVEN_SEVEN_DIGITS_FLOAT = new FastDecimalFormatter(11, 7);
/** Format for one 12.9 digits float field. */
private static final FastDoubleFormatter TWELVE_NINE_DIGITS_FLOAT = new FastDecimalFormatter(12, 9);
/** Format for one 13.7 digits float field. */
private static final FastDoubleFormatter THIRTEEN_SEVEN_DIGITS_FLOAT = new FastDecimalFormatter(13, 7);
/** Format for one 14.3 digits float field. */
private static final FastDoubleFormatter FOURTEEN_THREE_DIGITS_FLOAT = new FastDecimalFormatter(14, 3);
/** Format for one 14.4 digits float field. */
private static final FastDoubleFormatter FOURTEEN_FOUR_DIGITS_FLOAT = new FastDecimalFormatter(14, 4);
/** Format for one 15.12 digits float field. */
private static final FastDoubleFormatter FIFTEEN_TWELVE_DIGITS_FLOAT = new FastDecimalFormatter(15, 12);
/** Threshold for considering measurements that occur at the same time.
* (we know the RINEX files encode dates with a resolution of 0.1µs)
*/
private static final double EPS_DATE = 1.0e-8;
/** Receiver clock offset model. */
private ClockModel receiverClockModel;
/** Mapper from satellite system to time scales. */
private final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder;
/** Set of time scales. */
private final TimeScales timeScales;
/** Time scale for writing dates. */
private TimeScale timeScale;
/** Pending observations. */
private final List<ObservationDataSet> pending;
/** Simple constructor.
* <p>
* This constructor uses the {@link DataContext#getDefault() default data context}
* and recognizes only {@link PredefinedObservationType} and {@link SatelliteSystem}
* with non-null {@link SatelliteSystem#getObservationTimeScale() time scales}
* (i.e. neither user-defined, nor {@link SatelliteSystem#SBAS}, nor {@link SatelliteSystem#MIXED}).
* </p>
* @param output destination of generated output
* @param outputName output name for error messages
*/
@DefaultDataContext
public RinexObservationWriter(final Appendable output, final String outputName) {
this(output, outputName,
(system, ts) -> system.getObservationTimeScale() == null ?
null :
system.getObservationTimeScale().getTimeScale(ts),
DataContext.getDefault().getTimeScales());
}
/** Simple constructor.
* @param output destination of generated output
* @param outputName output name for error messages
* @param timeScaleBuilder mapper from satellite system to time scales (useful for user-defined satellite systems)
* @param timeScales the set of time scales to use when parsing dates
* @since 13.0
*/
public RinexObservationWriter(final Appendable output, final String outputName,
final BiFunction<SatelliteSystem, TimeScales, ? extends TimeScale> timeScaleBuilder,
final TimeScales timeScales) {
super(output, outputName);
this.timeScaleBuilder = timeScaleBuilder;
this.timeScales = timeScales;
this.pending = new ArrayList<>();
}
/** {@inheritDoc} */
@Override
public void close() throws IOException {
processPending();
super.close();
}
/** Set receiver clock model.
* @param receiverClockModel receiver clock model
* @since 12.1
*/
public void setReceiverClockModel(final ClockModel receiverClockModel) {
this.receiverClockModel = receiverClockModel;
}
/** Write a complete observation file.
* <p>
* This method calls {@link #prepareComments(List)} and
* {@link #writeHeader(RinexObservationHeader)} once and then loops on
* calling {@link #writeObservationDataSet(ObservationDataSet)}
* for all observation data sets in the file
* </p>
* @param rinexObservation Rinex observation file to write
* @see #writeHeader(RinexObservationHeader)
* @see #writeObservationDataSet(ObservationDataSet)
* @exception IOException if an I/O error occurs.
*/
@DefaultDataContext
public void writeCompleteFile(final RinexObservation rinexObservation) throws IOException {
prepareComments(rinexObservation.getComments());
writeHeader(rinexObservation.getHeader());
for (final ObservationDataSet observationDataSet : rinexObservation.getObservationDataSets()) {
writeObservationDataSet(observationDataSet);
}
}
/** Write header.
* <p>
* This method must be called exactly once at the beginning
* (directly or by {@link #writeCompleteFile(RinexObservation)})
* </p>
* @param header header to write
* @exception IOException if an I/O error occurs.
*/
@DefaultDataContext
public void writeHeader(final RinexObservationHeader header) throws IOException {
super.writeHeader(header, RinexObservationHeader.LABEL_INDEX);
final String timeScaleName;
final TimeScale built = timeScaleBuilder.apply(header.getSatelliteSystem(), timeScales);
if (built != null) {
timeScale = built;
timeScaleName = " ";
} else {
timeScale = timeScaleBuilder.apply(SatelliteSystem.GPS, timeScales);
timeScaleName = timeScale.getName();
}
if (!header.getClockOffsetApplied() && receiverClockModel != null) {
// getClockOffsetApplied returned false, which means the measurements
// should *NOT* be put in system time scale, and the receiver has a clock model
// we have to set up a time scale corresponding to this receiver clock
// (but we keep the name set earlier despite it is not really relevant anymore)
timeScale = new ClockTimeScale(timeScale.getName(), timeScale, receiverClockModel);
}
// RINEX VERSION / TYPE
outputField(NINE_TWO_DIGITS_FLOAT, header.getFormatVersion(), 9);
outputField("", 20, true);
outputField("OBSERVATION DATA", 40, true);
outputField(header.getSatelliteSystem().getKey(), 41);
finishHeaderLine(CommonLabel.VERSION);
// PGM / RUN BY / DATE
writeProgramRunByDate(header);
// MARKER NAME
outputField(header.getMarkerName(), 60, true);
finishHeaderLine(ObservationLabel.MARKER_NAME);
// MARKER NUMBER
if (header.getMarkerNumber() != null) {
outputField(header.getMarkerNumber(), 20, true);
finishHeaderLine(ObservationLabel.MARKER_NUMBER);
}
// MARKER TYPE
if (header.getFormatVersion() >= 2.20) {
outputField(header.getMarkerType(), 20, true);
finishHeaderLine(ObservationLabel.MARKER_TYPE);
}
// OBSERVER / AGENCY
outputField(header.getObserverName(), 20, true);
outputField(header.getAgencyName(), 60, true);
finishHeaderLine(ObservationLabel.OBSERVER_AGENCY);
// REC # / TYPE / VERS
outputField(header.getReceiverNumber(), 20, true);
outputField(header.getReceiverType(), 40, true);
outputField(header.getReceiverVersion(), 60, true);
finishHeaderLine(ObservationLabel.REC_NB_TYPE_VERS);
// ANT # / TYPE
outputField(header.getAntennaNumber(), 20, true);
outputField(header.getAntennaType(), 40, true);
finishHeaderLine(ObservationLabel.ANT_NB_TYPE);
// APPROX POSITION XYZ
writeHeaderLine(header.getApproxPos(), ObservationLabel.APPROX_POSITION_XYZ);
// ANTENNA: DELTA H/E/N
if (!Double.isNaN(header.getAntennaHeight())) {
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaHeight(), 14);
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getX(), 28);
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getEccentricities().getY(), 42);
finishHeaderLine(ObservationLabel.ANTENNA_DELTA_H_E_N);
}
// ANTENNA: DELTA X/Y/Z
writeHeaderLine(header.getAntennaReferencePoint(), ObservationLabel.ANTENNA_DELTA_X_Y_Z);
// ANTENNA: PHASECENTER
if (header.getAntennaPhaseCenter() != null) {
outputField(header.getPhaseCenterSystem().getKey(), 1);
outputField("", 2, true);
outputField(header.getObservationCode(), 5, true);
outputField(NINE_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getX(), 14);
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getY(), 28);
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, header.getAntennaPhaseCenter().getZ(), 42);
finishHeaderLine(ObservationLabel.ANTENNA_PHASE_CENTER);
}
// ANTENNA: B.SIGHT XY
writeHeaderLine(header.getAntennaBSight(), ObservationLabel.ANTENNA_B_SIGHT_XYZ);
// ANTENNA: ZERODIR AZI
if (!Double.isNaN(header.getAntennaAzimuth())) {
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, FastMath.toDegrees(header.getAntennaAzimuth()), 14);
finishHeaderLine(ObservationLabel.ANTENNA_ZERODIR_AZI);
}
// ANTENNA: ZERODIR XYZ
writeHeaderLine(header.getAntennaZeroDirection(), ObservationLabel.ANTENNA_ZERODIR_XYZ);
// OBS SCALE FACTOR
if (FastMath.abs(header.getFormatVersion() - 2.20) < 0.001) {
for (final SatelliteSystem system : SatelliteSystem.values()) {
for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
if (sfc != null) {
outputField(SIX_DIGITS_INTEGER, (int) FastMath.round(sfc.getCorrection()), 6);
outputField(SIX_DIGITS_INTEGER, sfc.getTypesObsScaled().size(), 12);
for (int i = 0; i < sfc.getTypesObsScaled().size(); ++i) {
outputField(sfc.getTypesObsScaled().get(i), 18 + 6 * i, false);
}
finishHeaderLine(ObservationLabel.OBS_SCALE_FACTOR);
}
}
}
}
// CENTER OF MASS: XYZ
writeHeaderLine(header.getCenterMass(), ObservationLabel.CENTER_OF_MASS_XYZ);
// DOI
writeHeaderLine(header.getDoi(), CommonLabel.DOI);
// LICENSE OF USE
writeHeaderLine(header.getLicense(), CommonLabel.LICENSE);
// STATION INFORMATION
writeHeaderLine(header.getStationInformation(), CommonLabel.STATION_INFORMATION);
// SYS / # / OBS TYPES
for (Map.Entry<SatelliteSystem, List<String>> entry : header.getTypeObs().entrySet()) {
if (header.getFormatVersion() < 3.0) {
outputField(SIX_DIGITS_INTEGER, entry.getValue().size(), 6);
} else {
outputField(entry.getKey().getKey(), 1);
outputField(THREE_DIGITS_INTEGER, entry.getValue().size(), 6);
}
for (final String observationType : entry.getValue()) {
int next = getColumn() + (header.getFormatVersion() < 3.0 ? 6 : 4);
if (exceedsHeaderLength(next)) {
// we need to set up a continuation line
finishHeaderLine(header.getFormatVersion() < 3.0 ?
ObservationLabel.NB_TYPES_OF_OBSERV :
CommonLabel.SYS_NB_TYPES_OF_OBSERV);
outputField("", 6, true);
next = getColumn() + (header.getFormatVersion() < 3.0 ? 6 : 4);
}
outputField(observationType, next, false);
}
finishHeaderLine(header.getFormatVersion() < 3.0 ?
ObservationLabel.NB_TYPES_OF_OBSERV :
CommonLabel.SYS_NB_TYPES_OF_OBSERV);
}
// SIGNAL STRENGTH UNIT
writeHeaderLine(header.getSignalStrengthUnit(), ObservationLabel.SIGNAL_STRENGTH_UNIT);
// INTERVAL
if (!Double.isNaN(header.getInterval())) {
outputField(TEN_THREE_DIGITS_FLOAT, header.getInterval(), 10);
finishHeaderLine(ObservationLabel.INTERVAL);
}
// TIME OF FIRST OBS
final DateTimeComponents dtcFirst = header.getTFirstObs().getComponents(timeScale).roundIfNeeded(60, 7);
outputField(SIX_DIGITS_INTEGER, dtcFirst.getDate().getYear(), 6);
outputField(SIX_DIGITS_INTEGER, dtcFirst.getDate().getMonth(), 12);
outputField(SIX_DIGITS_INTEGER, dtcFirst.getDate().getDay(), 18);
outputField(SIX_DIGITS_INTEGER, dtcFirst.getTime().getHour(), 24);
outputField(SIX_DIGITS_INTEGER, dtcFirst.getTime().getMinute(), 30);
outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcFirst.getTime().getSecond(), 43);
outputField(timeScaleName, 51, false);
finishHeaderLine(ObservationLabel.TIME_OF_FIRST_OBS);
// TIME OF LAST OBS
if (!header.getTLastObs().equals(AbsoluteDate.FUTURE_INFINITY)) {
final DateTimeComponents dtcLast = header.getTLastObs().getComponents(timeScale).roundIfNeeded(60, 7);
outputField(SIX_DIGITS_INTEGER, dtcLast.getDate().getYear(), 6);
outputField(SIX_DIGITS_INTEGER, dtcLast.getDate().getMonth(), 12);
outputField(SIX_DIGITS_INTEGER, dtcLast.getDate().getDay(), 18);
outputField(SIX_DIGITS_INTEGER, dtcLast.getTime().getHour(), 24);
outputField(SIX_DIGITS_INTEGER, dtcLast.getTime().getMinute(), 30);
outputField(THIRTEEN_SEVEN_DIGITS_FLOAT, dtcLast.getTime().getSecond(), 43);
outputField(timeScaleName, 51, false);
finishHeaderLine(ObservationLabel.TIME_OF_LAST_OBS);
}
// RCV CLOCK OFFS APPL
outputField(SIX_DIGITS_INTEGER, header.getClockOffsetApplied() ? 1 : 0, 6);
finishHeaderLine(ObservationLabel.RCV_CLOCK_OFFS_APPL);
// SYS / DCBS APPLIED
for (final AppliedDCBS appliedDCBS : header.getListAppliedDCBS()) {
outputField(appliedDCBS.getSatelliteSystem().getKey(), 1);
outputField("", 2, true);
outputField(appliedDCBS.getProgDCBS(), 20, true);
outputField(appliedDCBS.getSourceDCBS(), 60, true);
finishHeaderLine(CommonLabel.SYS_DCBS_APPLIED);
}
// SYS / PCVS APPLIED
for (final AppliedPCVS appliedPCVS : header.getListAppliedPCVS()) {
outputField(appliedPCVS.getSatelliteSystem().getKey(), 1);
outputField("", 2, true);
outputField(appliedPCVS.getProgPCVS(), 20, true);
outputField(appliedPCVS.getSourcePCVS(), 60, true);
finishHeaderLine(CommonLabel.SYS_PCVS_APPLIED);
}
// SYS / SCALE FACTOR
if (header.getFormatVersion() >= 3.0) {
for (final SatelliteSystem system : SatelliteSystem.values()) {
for (final ScaleFactorCorrection sfc : header.getScaleFactorCorrections(system)) {
if (sfc != null) {
outputField(system.getKey(), 1);
outputField("", 2, true);
outputField(FOUR_DIGITS_INTEGER, (int) FastMath.rint(sfc.getCorrection()), 6);
if (sfc.getTypesObsScaled().size() < header.getTypeObs().get(system).size()) {
outputField("", 8, true);
outputField(TWO_DIGITS_INTEGER, sfc.getTypesObsScaled().size(), 10);
for (String observationType : sfc.getTypesObsScaled()) {
int next = getColumn() + 4;
if (exceedsHeaderLength(next)) {
// we need to set up a continuation line
finishHeaderLine(ObservationLabel.SYS_SCALE_FACTOR);
outputField("", 10, true);
next = getColumn() + 4;
}
outputField("", next - 3, true);
outputField(observationType, next, true);
}
}
finishHeaderLine(ObservationLabel.SYS_SCALE_FACTOR);
}
}
}
}
// SYS / PHASE SHIFT
for (final PhaseShiftCorrection psc : header.getPhaseShiftCorrections()) {
outputField(psc.getSatelliteSystem().getKey(), 1);
outputField(psc.getTypeObs().getName(), 5, false);
outputField(EIGHT_FIVE_DIGITS_FLOAT, psc.getCorrection(), 14);
if (!psc.getSatsCorrected().isEmpty()) {
outputField(TWO_DIGITS_INTEGER, psc.getSatsCorrected().size(), 18);
for (final SatInSystem sis : psc.getSatsCorrected()) {
int next = getColumn() + 4;
if (exceedsHeaderLength(next)) {
// we need to set up a continuation line
finishHeaderLine(ObservationLabel.SYS_PHASE_SHIFT);
outputField("", 18, true);
next = getColumn() + 4;
}
outputField(sis.toString(), next, false);
}
}
finishHeaderLine(ObservationLabel.SYS_PHASE_SHIFT);
}
if (header.getFormatVersion() >= 3.01) {
if (!header.getGlonassChannels().isEmpty()) {
// GLONASS SLOT / FRQ #
outputField(THREE_DIGITS_INTEGER, header.getGlonassChannels().size(), 3);
outputField("", 4, true);
for (final GlonassSatelliteChannel channel : header.getGlonassChannels()) {
int next = getColumn() + 7;
if (exceedsHeaderLength(next)) {
// we need to set up a continuation line
finishHeaderLine(ObservationLabel.GLONASS_SLOT_FRQ_NB);
outputField("", 4, true);
next = getColumn() + 7;
}
outputField(channel.getSatellite().getSystem().getKey(), next - 6);
outputField(PADDED_TWO_DIGITS_INTEGER, channel.getSatellite().getPRN(), next - 4);
outputField(TWO_DIGITS_INTEGER, channel.getK(), next - 1);
outputField("", next, true);
}
}
finishHeaderLine(ObservationLabel.GLONASS_SLOT_FRQ_NB);
}
if (header.getFormatVersion() >= 3.0) {
// GLONASS COD/PHS/BIS
if (Double.isNaN(header.getC1cCodePhaseBias())) {
outputField("", 13, true);
} else {
outputField(PredefinedObservationType.C1C.getName(), 4, false);
outputField("", 5, true);
outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1cCodePhaseBias(), 13);
}
if (Double.isNaN(header.getC1pCodePhaseBias())) {
outputField("", 26, true);
} else {
outputField(PredefinedObservationType.C1P.getName(), 17, false);
outputField("", 18, true);
outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC1pCodePhaseBias(), 26);
}
if (Double.isNaN(header.getC2cCodePhaseBias())) {
outputField("", 39, true);
} else {
outputField(PredefinedObservationType.C2C.getName(), 30, false);
outputField("", 31, true);
outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2cCodePhaseBias(), 39);
}
if (Double.isNaN(header.getC2pCodePhaseBias())) {
outputField("", 52, true);
} else {
outputField(PredefinedObservationType.C2P.getName(), 43, false);
outputField("", 44, true);
outputField(EIGHT_THREE_DIGITS_FLOAT, header.getC2pCodePhaseBias(), 52);
}
finishHeaderLine(ObservationLabel.GLONASS_COD_PHS_BIS);
}
// LEAP SECONDS
if (header.getLeapSecondsGNSS() > 0) {
outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsGNSS(), 6);
if (header.getFormatVersion() > 3.0) {
// extra fields introduced in 3.01
outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsFuture(), 12);
outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsWeekNum(), 18);
outputField(SIX_DIGITS_INTEGER, header.getLeapSecondsDayNum(), 24);
}
finishHeaderLine(CommonLabel.LEAP_SECONDS);
}
// # OF SATELLITES
if (header.getNbSat() >= 0) {
outputField(SIX_DIGITS_INTEGER, header.getNbSat(), 6);
finishHeaderLine(ObservationLabel.NB_OF_SATELLITES);
}
// PRN / # OF OBS
for (final Map.Entry<SatInSystem, Map<String, Integer>> entry1 : header.getNbObsPerSat().entrySet()) {
final SatInSystem sis = entry1.getKey();
outputField(sis.toString(), 6, false);
// list the entries in the order specified in SYS / # / OBS TYPES
for (final String obsType : header.getTypeObs().get(sis.getSystem())) {
final Integer nbObs = entry1.getValue().get(obsType);
int next = getColumn() + 6;
if (exceedsHeaderLength(next)) {
// we need to set up a continuation line
finishHeaderLine(ObservationLabel.PRN_NB_OF_OBS);
outputField("", 6, true);
next = getColumn() + 6;
}
outputField(SIX_DIGITS_INTEGER, nbObs == null ? 0 : nbObs, next);
}
finishHeaderLine(ObservationLabel.PRN_NB_OF_OBS);
}
// END OF HEADER
writeHeaderLine("", CommonLabel.END);
}
/** Write one observation data set.
* <p>
* Note that this writer outputs only regular observations, so
* the event flag is always set to 0
* </p>
* @param observationDataSet observation data set to write
* @exception IOException if an I/O error occurs.
*/
public void writeObservationDataSet(final ObservationDataSet observationDataSet) throws IOException {
// check header has already been written
checkHeaderWritten();
if (!pending.isEmpty() && observationDataSet.durationFrom(pending.get(0).getDate()) > EPS_DATE) {
// the specified observation belongs to the next batch
// we must process the current batch of pending observations
processPending();
}
// add the observation to the pending list, so it is written later on
pending.add(observationDataSet);
}
/** Process all pending measurements.
* @exception IOException if an I/O error occurs.
*/
private void processPending() throws IOException {
if (!pending.isEmpty()) {
// write the batch of pending observations
if (getHeader().getFormatVersion() < 3.0) {
writePendingRinex2Observations();
} else {
writePendingRinex34Observations();
}
// prepare for next batch
pending.clear();
}
}
/** Write one observation data set in RINEX 2 format.
* @exception IOException if an I/O error occurs.
*/
public void writePendingRinex2Observations() throws IOException {
final ObservationDataSet first = pending.get(0);
// EPOCH/SAT
final DateTimeComponents dtc = first.getDate().getComponents(timeScale).roundIfNeeded(60, 7);
outputField("", 1, true);
outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getYear() % 100, 3);
outputField("", 4, true);
outputField(TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 6);
outputField("", 7, true);
outputField(TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 9);
outputField("", 10, true);
outputField(TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 12);
outputField("", 13, true);
outputField(TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 15);
outputField(ELEVEN_SEVEN_DIGITS_FLOAT, dtc.getTime().getSecond(), 26);
// event flag
outputField("", 28, true);
outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 29);
// list of satellites and receiver clock offset
outputField(THREE_DIGITS_INTEGER, pending.size(), 32);
boolean offsetWritten = false;
final double clockOffset = first.getRcvrClkOffset();
for (final ObservationDataSet ods : pending) {
int next = getColumn() + 3;
if (next > 68) {
// we need to set up a continuation line
if (clockOffset != 0.0) {
outputField(TWELVE_NINE_DIGITS_FLOAT, clockOffset, 80);
}
offsetWritten = true;
finishLine();
outputField("", 32, true);
next = getColumn() + 3;
}
outputField(ods.getSatellite().toString(), next, false);
}
if (!offsetWritten && clockOffset != 0.0) {
outputField("", 68, true);
outputField(TWELVE_NINE_DIGITS_FLOAT, first.getRcvrClkOffset(), 80);
}
finishLine();
// observations per se
for (final ObservationDataSet ods : pending) {
for (final ObservationData od : ods.getObservationData()) {
int next = getColumn() + 16;
if (next > 80) {
// we need to set up a continuation line
finishLine();
next = getColumn() + 16;
}
final double scaling = getScaling(od.getObservationType().getName(), ods.getSatellite().getSystem());
outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
if (od.getLossOfLockIndicator() == 0) {
outputField("", next - 1, true);
} else {
outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
}
if (od.getSignalStrength() == 0) {
outputField("", next, true);
} else {
outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
}
}
finishLine();
}
}
/** Write one observation data set in RINEX 3/4 format.
* @exception IOException if an I/O error occurs.
*/
public void writePendingRinex34Observations() throws IOException {
final ObservationDataSet first = pending.get(0);
// EPOCH/SAT
final DateTimeComponents dtc = first.getDate().getComponents(timeScale).roundIfNeeded(60, 7);
outputField(">", 2, true);
outputField(FOUR_DIGITS_INTEGER, dtc.getDate().getYear(), 6);
outputField("", 7, true);
outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getMonth(), 9);
outputField("", 10, true);
outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getDate().getDay(), 12);
outputField("", 13, true);
outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getHour(), 15);
outputField("", 16, true);
outputField(PADDED_TWO_DIGITS_INTEGER, dtc.getTime().getMinute(), 18);
outputField(ELEVEN_SEVEN_DIGITS_FLOAT, dtc.getTime().getSecond(), 29);
// event flag
outputField("", 31, true);
outputField(ONE_DIGIT_INTEGER, first.getEventFlag(), 32);
// number of satellites and receiver clock offset
outputField(THREE_DIGITS_INTEGER, pending.size(), 35);
if (first.getRcvrClkOffset() != 0.0) {
outputField("", 41, true);
outputField(FIFTEEN_TWELVE_DIGITS_FLOAT, first.getRcvrClkOffset(), 56);
}
finishLine();
// observations per se
for (final ObservationDataSet ods : pending) {
outputField(ods.getSatellite().toString(), 3, false);
for (final ObservationData od : ods.getObservationData()) {
final int next = getColumn() + 16;
final double scaling = getScaling(od.getObservationType().getName(), ods.getSatellite().getSystem());
outputField(FOURTEEN_THREE_DIGITS_FLOAT, scaling * od.getValue(), next - 2);
if (od.getLossOfLockIndicator() == 0) {
outputField("", next - 1, true);
} else {
outputField(ONE_DIGIT_INTEGER, od.getLossOfLockIndicator(), next - 1);
}
if (od.getSignalStrength() == 0) {
outputField("", next, true);
} else {
outputField(ONE_DIGIT_INTEGER, od.getSignalStrength(), next);
}
}
finishLine();
}
}
/** Write one header vector.
* @param vector vector data (may be null)
* @param label line label
* @throws IOException if an I/O error occurs.
*/
private void writeHeaderLine(final Vector3D vector, final Label label) throws IOException {
if (vector != null) {
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getX(), 14);
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getY(), 28);
outputField(FOURTEEN_FOUR_DIGITS_FLOAT, vector.getZ(), 42);
finishHeaderLine(label);
}
}
/** Get the scaling factor for an observation.
* @param type type of observation
* @param system satellite system for the observation
* @return scaling factor
*/
private double getScaling(final String type, final SatelliteSystem system) {
for (final ScaleFactorCorrection scaleFactorCorrection : getHeader().getScaleFactorCorrections(system)) {
// check if the next Observation Type to read needs to be scaled
if (scaleFactorCorrection.getTypesObsScaled().contains(type)) {
return scaleFactorCorrection.getCorrection();
}
}
// no scaling
return 1.0;
}
}