Task 469: .OBS File Format
Task 469: .OBS File Format
1. List of Properties Intrinsic to the .OBS File Format
The .OBS file format refers to the RINEX (Receiver Independent Exchange) observation file format, commonly used for storing Global Navigation Satellite System (GNSS) data. This format is ASCII-based and consists of a header section with metadata and a data section with epoch-based observations. The properties listed below represent all intrinsic fields defined in the RINEX 4.01 specification, including header records and data record elements. These encompass metadata, configuration details, and observational data structures. Header fields are labeled in columns 61-80 of each line, and data records follow a structured format for epochs and observations.
Header Properties
These are metadata fields that describe the file, receiver, antenna, and observation setup. Mandatory fields are noted; others are optional.
- RINEX VERSION / TYPE (Mandatory): Specifies the format version (e.g., 4.01), file type (O for observation), and satellite system(s) (e.g., M for mixed GNSS).
- PGM / RUN BY / DATE (Mandatory): Indicates the program name, running agency, and file creation date/time.
- COMMENT: Free-text comments for additional notes.
- MARKER NAME (Mandatory): Name of the antenna marker or station (e.g., 4- or 9-character ID).
- MARKER NUMBER: Unique identifier for the marker (e.g., IERS DOMES number).
- MARKER TYPE: Type of marker (e.g., GEODETIC, SPACEBORNE, AIRBORNE).
- OBSERVER / AGENCY: Name of the observer and affiliated agency.
- REC # / TYPE / VERS: Receiver serial number, type/model, and firmware version.
- ANT # / TYPE: Antenna serial number and type/model.
- APPROX POSITION XYZ: Approximate geocentric position of the marker (X, Y, Z coordinates in meters, typically in ITRF).
- ANTENNA: DELTA H/E/N: Antenna reference point offsets (height, east, north in meters).
- ANTENNA: DELTA X/Y/Z: Antenna offsets in body-fixed coordinates (X, Y, Z in meters, for vehicles).
- ANTENNA: PHASECENTER: Phase center offsets relative to the antenna reference point (system-dependent).
- ANTENNA: B.SIGHT XYZ: Unit vector for the antenna boresight (vertical axis).
- ANTENNA: ZERODIR AZI: Azimuth of the antenna zero-direction (degrees from north).
- ANTENNA: ZERODIR XYZ: Unit vector for the antenna zero-direction.
- CENTER OF MASS: XYZ: Center of mass coordinates for the vehicle (body-fixed, in meters).
- DOI: Digital Object Identifier for the dataset.
- LICENSE OF USE: License details for data usage (e.g., CC BY 4.0 with URL).
- STATION INFORMATION: Links to additional station metadata (e.g., site logs).
- SYS / # / OBS TYPES (Mandatory): System code (e.g., G for GPS), number of observation types, and list of 3-character codes (e.g., C1C for pseudorange on L1 C/A).
- SIGNAL STRENGTH UNIT: Units for signal strength observations (e.g., DBHZ for dBHz).
- INTERVAL: Nominal observation interval (in seconds).
- TIME OF FIRST OBS: Timestamp of the first observation (year, month, day, hour, minute, second; system time identifier).
- TIME OF LAST OBS: Timestamp of the last observation (similar format).
- RCV CLOCK OFFS APPL: Indicator for whether receiver clock offsets are applied (0 or 1).
- SYS / DCBS APPLIED: Differential code bias corrections applied (system-specific, with program and source).
- SYS / PCVS APPLIED: Phase center variation corrections applied (system-specific, with program and source).
- SYS / SCALE FACTOR: Scale factor for observations (system-specific).
- SYS / PHASE SHIFT (Deprecated): Phase shift corrections (in cycles).
- GLONASS SLOT / FRQ #: GLONASS satellite slots and frequency channel numbers.
- GLONASS COD/PHS/BIS (Deprecated): GLONASS code-phase biases (in meters).
- LEAP SECONDS: Number of leap seconds, with optional future/past values and week/day details.
- # OF SATELLITES: Total number of unique satellites observed.
- PRN / # OF OBS: Per-satellite pseudorandom noise (PRN) code and count of observations per type.
- END OF HEADER (Mandatory): Marks the end of the header section.
Data Section Properties
These describe the observational data structure per epoch.
- Epoch Record: Starts with ">", includes timestamp (year, month, day, hour, minute, second), event flag (0-6, e.g., 0 for normal, 1 for power failure), number of satellites or special records, and optional receiver clock offset.
- Observation Records (Per Satellite): Satellite identifier (e.g., G01 for GPS PRN 1), followed by observations in the order defined in SYS / # / OBS TYPES. Each observation includes:
- Value (F14.3 format; e.g., pseudorange in meters, phase in cycles, Doppler in Hz).
- Loss of Lock Indicator (LLI): 1-digit flag for phase observations (e.g., cycle slip or half-cycle ambiguity).
- Signal Strength Indicator (SSI): 1-digit value (0-9, based on C/N0 in dBHz; deprecated in favor of Sna codes).
- Observation Codes: 3-character descriptors (type/band/attribute, e.g., C1C for pseudorange on L1 C/A code). Types include C (pseudorange), L (phase), D (Doppler), S (signal strength), X (channel number). System-specific bands and attributes vary (e.g., GPS: L1, L2, L5; attributes like C, P, I).
Additional intrinsic characteristics include units (meters for pseudoranges, cycles for phases, Hz for Doppler), time system (e.g., GPS time without leap seconds), satellite ordering (arbitrary per epoch), and handling of missing data (blanks or 0.0).
2. Direct Download Links for .OBS Files
The following are two direct download links to example RINEX observation files (commonly using .o or .obs extensions, but conforming to the .OBS format). These are uncompressed or compressed examples suitable for testing.
- https://raw.githubusercontent.com/geospace-code/georinex/main/tests/demo.10o (Uncompressed RINEX 2 observation file from the georinex repository).
- https://cddis.nasa.gov/archive/gnss/data/daily/2024/001/24o/abmf0010.24o.gz (Compressed RINEX observation file from NASA's CDDIS archive; unzip to access the .24o file).
3. Ghost Blog Embedded HTML JavaScript for .OBS File Drag-and-Drop
The following is an HTML snippet with embedded JavaScript that can be inserted into a Ghost blog post. It creates a drag-and-drop area for a .OBS (RINEX observation) file. Upon dropping, the script reads the file, parses the header and data sections, extracts all properties listed in section 1, and displays them on the screen in a structured format.
This script parses the file by splitting lines, extracting header labels and values, and summarizing data epochs. It handles basic validation for the .obs extension and displays results in an ordered list.
4. Python Class for .OBS File Handling
The following Python class can open, decode (parse), read, write (modify or create), and print all properties from a .OBS (RINEX observation) file to the console.
import os
class ObsFileHandler:
def __init__(self, filepath=None):
self.filepath = filepath
self.header = {}
self.data = []
if filepath and os.path.exists(filepath):
self.read()
def read(self):
with open(self.filepath, 'r') as f:
content = f.readlines()
in_header = True
current_system = ''
for line in content:
if 'END OF HEADER' in line:
in_header = False
continue
if in_header:
label = line[60:].strip()
value = line[:60].strip()
if label == 'SYS / # / OBS TYPES':
current_system = value[0]
key = f'{label} ({current_system})'
else:
key = label
if key:
self.header[key] = self.header.get(key, '') + value + ' '
else:
if line.startswith('>'):
epoch = {'timestamp': line[2:31].strip(), 'flag': line[32:33], 'num_sats': line[35:38].strip()}
self.data.append(epoch)
elif line.strip():
self.data[-1]['observations'] = self.data[-1].get('observations', []) + [line.strip()]
def print_properties(self):
print("Header Properties:")
for key, value in self.header.items():
print(f"{key}: {value.strip()}")
print("\nData Properties (Epoch Summaries):")
for i, epoch in enumerate(self.data, 1):
print(f"Epoch {i}: Timestamp = {epoch['timestamp']}, Flag = {epoch['flag']}, Satellites = {epoch['num_sats']}")
if 'observations' in epoch:
print(" Observations:", epoch['observations'])
def write(self, new_filepath=None):
filepath = new_filepath or self.filepath
with open(filepath, 'w') as f:
for key, value in self.header.items():
f.write(f"{value:<60}{key}\n")
f.write(f"{'':<60}END OF HEADER\n")
for epoch in self.data:
f.write(f"> {epoch['timestamp']:<28} {epoch['flag']} {epoch['num_sats']:<3} 0.000000000\n")
if 'observations' in epoch:
for obs in epoch['observations']:
f.write(f"{obs}\n")
# Example usage:
# handler = ObsFileHandler('example.obs')
# handler.print_properties()
# handler.write('modified.obs')
This class parses the header into a dictionary and data into a list of epochs. The print_properties method outputs all extracted properties to the console. The write method allows creating or modifying files based on the internal state.
5. Java Class for .OBS File Handling
The following Java class can open, decode (parse), read, write (modify or create), and print all properties from a .OBS (RINEX observation) file to the console.
import java.io.*;
import java.util.*;
public class ObsFileHandler {
private String filepath;
private Map<String, String> header = new LinkedHashMap<>();
private List<Map<String, Object>> data = new ArrayList<>();
public ObsFileHandler(String filepath) {
this.filepath = filepath;
if (new File(filepath).exists()) {
read();
}
}
public void read() {
try (BufferedReader reader = new BufferedReader(new FileReader(filepath))) {
String line;
boolean inHeader = true;
String currentSystem = "";
while ((line = reader.readLine()) != null) {
if (line.contains("END OF HEADER")) {
inHeader = false;
continue;
}
if (inHeader) {
String label = line.substring(Math.min(60, line.length())).trim();
String value = line.length() > 60 ? line.substring(0, 60).trim() : line.trim();
if (label.equals("SYS / # / OBS TYPES")) {
currentSystem = value.substring(0, 1);
label = label + " (" + currentSystem + ")";
}
if (!label.isEmpty()) {
header.put(label, header.getOrDefault(label, "") + value + " ");
}
} else {
if (line.startsWith(">")) {
Map<String, Object> epoch = new HashMap<>();
epoch.put("timestamp", line.substring(2, 31).trim());
epoch.put("flag", line.substring(32, 33));
epoch.put("num_sats", line.substring(35, 38).trim());
data.add(epoch);
} else if (!line.trim().isEmpty()) {
Map<String, Object> lastEpoch = data.get(data.size() - 1);
List<String> observations = (List<String>) lastEpoch.getOrDefault("observations", new ArrayList<>());
observations.add(line.trim());
lastEpoch.put("observations", observations);
}
}
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}
public void printProperties() {
System.out.println("Header Properties:");
for (Map.Entry<String, String> entry : header.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue().trim());
}
System.out.println("\nData Properties (Epoch Summaries):");
for (int i = 0; i < data.size(); i++) {
Map<String, Object> epoch = data.get(i);
System.out.println("Epoch " + (i + 1) + ": Timestamp = " + epoch.get("timestamp") + ", Flag = " + epoch.get("flag") + ", Satellites = " + epoch.get("num_sats"));
if (epoch.containsKey("observations")) {
System.out.println(" Observations: " + epoch.get("observations"));
}
}
}
public void write(String newFilepath) {
try (BufferedWriter writer = new BufferedWriter(new FileWriter(newFilepath == null ? filepath : newFilepath))) {
for (Map.Entry<String, String> entry : header.entrySet()) {
writer.write(String.format("%-60s%s\n", entry.getValue().trim(), entry.getKey()));
}
writer.write(String.format("%-60sEND OF HEADER\n", ""));
for (Map<String, Object> epoch : data) {
writer.write(String.format("> %-28s %s %-3s 0.000000000\n", epoch.get("timestamp"), epoch.get("flag"), epoch.get("num_sats")));
if (epoch.containsKey("observations")) {
for (String obs : (List<String>) epoch.get("observations")) {
writer.write(obs + "\n");
}
}
}
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
}
// Example usage:
// public static void main(String[] args) {
// ObsFileHandler handler = new ObsFileHandler("example.obs");
// handler.printProperties();
// handler.write("modified.obs");
// }
}
This class uses maps for header and data storage. The printProperties method outputs all properties to the console, and write enables file creation or modification.
6. JavaScript Class for .OBS File Handling
The following JavaScript class can open (via FileReader), decode (parse), read, write (via Blob download), and print all properties from a .OBS (RINEX observation) file to the console. It is suitable for browser or Node.js environments (with fs for Node).
class ObsFileHandler {
constructor(filepath = null) {
this.filepath = filepath;
this.header = {};
this.data = [];
if (filepath) {
this.read(); // Assumes Node.js; for browser, use async with FileReader
}
}
read() {
// For Node.js
const fs = require('fs');
const content = fs.readFileSync(this.filepath, 'utf8').split('\n');
let inHeader = true;
let currentSystem = '';
content.forEach(line => {
if (line.includes('END OF HEADER')) {
inHeader = false;
return;
}
if (inHeader) {
const label = line.substring(60).trim();
const value = line.substring(0, 60).trim();
if (label === 'SYS / # / OBS TYPES') {
currentSystem = value.charAt(0);
const key = `${label} (${currentSystem})`;
this.header[key] = (this.header[key] || '') + value + ' ';
} else if (label) {
this.header[label] = (this.header[label] || '') + value + ' ';
}
} else {
if (line.startsWith('>')) {
const epoch = {
timestamp: line.substring(2, 31).trim(),
flag: line.substring(32, 33),
numSats: line.substring(35, 38).trim()
};
this.data.push(epoch);
} else if (line.trim()) {
const lastEpoch = this.data[this.data.length - 1];
lastEpoch.observations = lastEpoch.observations || [];
lastEpoch.observations.push(line.trim());
}
}
});
}
printProperties() {
console.log('Header Properties:');
for (let key in this.header) {
console.log(`${key}: ${this.header[key].trim()}`);
}
console.log('\nData Properties (Epoch Summaries):');
this.data.forEach((epoch, index) => {
console.log(`Epoch ${index + 1}: Timestamp = ${epoch.timestamp}, Flag = ${epoch.flag}, Satellites = ${epoch.numSats}`);
if (epoch.observations) {
console.log(' Observations:', epoch.observations);
}
});
}
write(newFilepath) {
let content = '';
for (let key in this.header) {
content += `${this.header[key].trim().padEnd(60)}${key}\n`;
}
content += `${''.padEnd(60)}END OF HEADER\n`;
this.data.forEach(epoch => {
content += `> ${epoch.timestamp.padEnd(28)} ${epoch.flag} ${epoch.numSats.padEnd(3)} 0.000000000\n`;
if (epoch.observations) {
epoch.observations.forEach(obs => {
content += `${obs}\n`;
});
}
});
// For Node.js
const fs = require('fs');
fs.writeFileSync(newFilepath || this.filepath, content);
}
}
// Example usage (Node.js):
// const handler = new ObsFileHandler('example.obs');
// handler.printProperties();
// handler.write('modified.obs');
This class mirrors the functionality of the others, with console output for properties. For browser use, adapt read to use FileReader and write to create a downloadable Blob.
7. C++ Class for .OBS File Handling
The following C++ class can open, decode (parse), read, write (modify or create), and print all properties from a .OBS (RINEX observation) file to the console.
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <map>
#include <sstream>
class ObsFileHandler {
private:
std::string filepath;
std::map<std::string, std::string> header;
std::vector<std::map<std::string, std::string>> data; // Simplified; use structs for complex data
public:
ObsFileHandler(const std::string& fp) : filepath(fp) {
if (!fp.empty()) {
read();
}
}
void read() {
std::ifstream file(filepath);
if (!file.is_open()) {
std::cerr << "Error opening file." << std::endl;
return;
}
std::string line;
bool inHeader = true;
std::string currentSystem;
while (std::getline(file, line)) {
if (line.find("END OF HEADER") != std::string::npos) {
inHeader = false;
continue;
}
if (inHeader) {
std::string label = line.substr(60, line.size() - 60);
label.erase(0, label.find_first_not_of(" \t"));
label.erase(label.find_last_not_of(" \t") + 1);
std::string value = line.substr(0, 60);
value.erase(0, value.find_first_not_of(" \t"));
value.erase(value.find_last_not_of(" \t") + 1);
if (label == "SYS / # / OBS TYPES") {
currentSystem = value[0];
label = label + " (" + currentSystem + ")";
}
if (!label.empty()) {
header[label] = header[label] + value + " ";
}
} else {
if (line[0] == '>') {
std::map<std::string, std::string> epoch;
epoch["timestamp"] = line.substr(2, 29);
epoch["timestamp"].erase(epoch["timestamp"].find_last_not_of(" \t") + 1);
epoch["flag"] = line.substr(32, 1);
epoch["num_sats"] = line.substr(35, 3);
epoch["num_sats"].erase(epoch["num_sats"].find_last_not_of(" \t") + 1);
data.push_back(epoch);
} else if (!line.empty()) {
auto& lastEpoch = data.back();
lastEpoch["observations"] = lastEpoch["observations"] + line + "\n";
}
}
}
file.close();
}
void printProperties() const {
std::cout << "Header Properties:" << std::endl;
for (const auto& pair : header) {
std::string value = pair.second;
value.erase(value.find_last_not_of(" \t") + 1);
std::cout << pair.first << ": " << value << std::endl;
}
std::cout << "\nData Properties (Epoch Summaries):" << std::endl;
for (size_t i = 0; i < data.size(); ++i) {
const auto& epoch = data[i];
std::cout << "Epoch " << (i + 1) << ": Timestamp = " << epoch.at("timestamp") << ", Flag = " << epoch.at("flag") << ", Satellites = " << epoch.at("num_sats") << std::endl;
if (epoch.count("observations")) {
std::cout << " Observations: " << epoch.at("observations") << std::endl;
}
}
}
void write(const std::string& newFilepath) const {
std::ofstream file(newFilepath.empty() ? filepath : newFilepath);
if (!file.is_open()) {
std::cerr << "Error writing file." << std::endl;
return;
}
for (const auto& pair : header) {
std::string value = pair.second;
value.erase(value.find_last_not_of(" \t") + 1);
file << std::left << std::setw(60) << value << pair.first << std::endl;
}
file << std::left << std::setw(60) << "" << "END OF HEADER" << std::endl;
for (const auto& epoch : data) {
file << "> " << std::left << std::setw(28) << epoch.at("timestamp") << " " << epoch.at("flag") << " " << std::left << std::setw(3) << epoch.at("num_sats") << " 0.000000000" << std::endl;
if (epoch.count("observations")) {
file << epoch.at("observations");
}
}
file.close();
}
};
// Example usage:
// int main() {
// ObsFileHandler handler("example.obs");
// handler.printProperties();
// handler.write("modified.obs");
// return 0;
// }
This class uses maps for storage and provides methods for reading, printing to console, and writing files. It handles basic parsing and output in a professional manner.