@neilberkman/sidereon 2.1.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +507 -0
- package/README.md +64 -8
- package/THIRD-PARTY-NOTICES.md +3 -3
- package/package.json +5 -5
- package/pkg/sidereon.d.ts +11022 -2685
- package/pkg/sidereon.js +8925 -620
- package/pkg/sidereon_bg.wasm +0 -0
- package/pkg/sidereon_bg.wasm.d.ts +2515 -1949
- package/pkg-node/sidereon.d.ts +8282 -511
- package/pkg-node/sidereon.js +9031 -620
- package/pkg-node/sidereon_bg.wasm +0 -0
- package/pkg-node/sidereon_bg.wasm.d.ts +2515 -1949
- package/third_party_source/{sidereon-core-0.36.3 → sidereon-core-3.0.0}/tides/mod.rs +508 -89
- package/third_party_source/{sidereon-core-0.36.3 → sidereon-core-3.0.0}/tides/ocean.rs +391 -86
- package/types/sidereon-extra.d.ts +312 -1
- /package/third_party_source/{sidereon-core-0.36.3 → sidereon-core-3.0.0}/tides/pole.rs +0 -0
|
@@ -75,7 +75,10 @@ use crate::astro::math::vec3::norm3_ref as norm;
|
|
|
75
75
|
use crate::validate;
|
|
76
76
|
use std::fmt::Write as _;
|
|
77
77
|
|
|
78
|
-
use super::{
|
|
78
|
+
use super::{
|
|
79
|
+
gregorian_to_two_part_julian_date, invalid_tide_input, BlqParseErrorKind, BlqWriteErrorKind,
|
|
80
|
+
TideError,
|
|
81
|
+
};
|
|
79
82
|
|
|
80
83
|
/// Number of BLQ tidal constituents (M2 S2 N2 K2 K1 O1 P1 Q1 Mf Mm Ssa).
|
|
81
84
|
pub const NUM_OCEAN_CONSTITUENTS: usize = 11;
|
|
@@ -86,16 +89,39 @@ const TWO_PI: f64 = 2.0 * std::f64::consts::PI;
|
|
|
86
89
|
/// BLQ tidal constituents supported by the ARG2 evaluator.
|
|
87
90
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
88
91
|
pub enum OceanTideConstituent {
|
|
92
|
+
/// First BLQ/ARG2 slot (index 0), labeled `M2`; its ARG2 speed is
|
|
93
|
+
/// `1.40519e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (2, -2, 0, 0)`.
|
|
89
94
|
M2,
|
|
95
|
+
/// Second BLQ/ARG2 slot (index 1), labeled `S2`; its ARG2 speed is
|
|
96
|
+
/// `1.45444e-4` rad/s and its multiplier row is all zero, so its argument
|
|
97
|
+
/// advances with the fractional hour alone.
|
|
90
98
|
S2,
|
|
99
|
+
/// Third BLQ/ARG2 slot (index 2), labeled `N2`; its ARG2 speed is
|
|
100
|
+
/// `1.37880e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (2, -3, 1, 0)`.
|
|
91
101
|
N2,
|
|
102
|
+
/// Fourth BLQ/ARG2 slot (index 3), labeled `K2`; its ARG2 speed is
|
|
103
|
+
/// `1.45842e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (2, 0, 0, 0)`.
|
|
92
104
|
K2,
|
|
105
|
+
/// Fifth BLQ/ARG2 slot (index 4), labeled `K1`; its ARG2 speed is
|
|
106
|
+
/// `0.72921e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (1, 0, 0, 0.25)`.
|
|
93
107
|
K1,
|
|
108
|
+
/// Sixth BLQ/ARG2 slot (index 5), labeled `O1`; its ARG2 speed is
|
|
109
|
+
/// `0.67598e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (1, -2, 0, -0.25)`.
|
|
94
110
|
O1,
|
|
111
|
+
/// Seventh BLQ/ARG2 slot (index 6), labeled `P1`; its ARG2 speed is
|
|
112
|
+
/// `0.72523e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (-1, 0, 0, -0.25)`.
|
|
95
113
|
P1,
|
|
114
|
+
/// Eighth BLQ/ARG2 slot (index 7), labeled `Q1`; its ARG2 speed is
|
|
115
|
+
/// `0.64959e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (1, -3, 1, -0.25)`.
|
|
96
116
|
Q1,
|
|
117
|
+
/// Ninth BLQ/ARG2 slot (index 8), labeled `Mf`; its ARG2 speed is
|
|
118
|
+
/// `0.053234e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (0, 2, 0, 0)`.
|
|
97
119
|
Mf,
|
|
120
|
+
/// Tenth BLQ/ARG2 slot (index 9), labeled `Mm`; its ARG2 speed is
|
|
121
|
+
/// `0.026392e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (0, 1, -1, 0)`.
|
|
98
122
|
Mm,
|
|
123
|
+
/// Eleventh BLQ/ARG2 slot (index 10), labeled `Ssa`; its ARG2 speed is
|
|
124
|
+
/// `0.003982e-4` rad/s with multipliers `(h0, s0, p0, 2pi) = (2, 0, 0, 0)`.
|
|
99
125
|
Ssa,
|
|
100
126
|
}
|
|
101
127
|
|
|
@@ -213,37 +239,238 @@ pub struct OceanLoadingBlq {
|
|
|
213
239
|
pub phase_deg: [[f64; NUM_OCEAN_CONSTITUENTS]; 3],
|
|
214
240
|
}
|
|
215
241
|
|
|
242
|
+
/// Position of a retained comment or header line within its station block.
|
|
243
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
244
|
+
pub enum OceanLoadingBlqCommentPlacement {
|
|
245
|
+
/// Before the station line. The parser gives a block every line after the
|
|
246
|
+
/// previous block's last coefficient row, so file header comments belong
|
|
247
|
+
/// to the first block.
|
|
248
|
+
BeforeStation,
|
|
249
|
+
/// Before the zero-based coefficient row, `0..=5`; `BeforeRow(0)` is
|
|
250
|
+
/// between the station line and the first row. A column-order header here
|
|
251
|
+
/// sets the order of this row and every later one.
|
|
252
|
+
BeforeRow(usize),
|
|
253
|
+
/// After the sixth coefficient row. The parser uses it only for lines
|
|
254
|
+
/// that follow the last block of the input, and the writer accepts it
|
|
255
|
+
/// only on the last block it writes.
|
|
256
|
+
AfterRows,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// One comment or header line retained from a BLQ block, as read.
|
|
260
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
261
|
+
pub struct OceanLoadingBlqComment {
|
|
262
|
+
/// Where the line sits in the block.
|
|
263
|
+
pub placement: OceanLoadingBlqCommentPlacement,
|
|
264
|
+
/// The line exactly as read, without its line terminator.
|
|
265
|
+
pub line: String,
|
|
266
|
+
}
|
|
267
|
+
|
|
216
268
|
/// One parsed standard BLQ station block.
|
|
217
269
|
#[derive(Debug, Clone, PartialEq)]
|
|
218
270
|
pub struct OceanLoadingBlqBlock {
|
|
219
|
-
/// Station identifier line from the BLQ block.
|
|
271
|
+
/// Station identifier line from the BLQ block, trimmed.
|
|
220
272
|
pub station: String,
|
|
221
273
|
/// Parsed and reordered BLQ coefficients.
|
|
222
274
|
pub coefficients: OceanLoadingBlq,
|
|
275
|
+
/// Comment and column-order header lines of the block, in input order.
|
|
276
|
+
/// They carry the model, frame, unit and row-direction declarations the
|
|
277
|
+
/// provider writes as comments; the writer restates them unchanged.
|
|
278
|
+
pub comments: Vec<OceanLoadingBlqComment>,
|
|
223
279
|
}
|
|
224
280
|
|
|
225
281
|
impl OceanLoadingBlqBlock {
|
|
226
|
-
/// Format as a standard six-row BLQ block
|
|
227
|
-
|
|
228
|
-
|
|
282
|
+
/// Format as a standard six-row BLQ block.
|
|
283
|
+
///
|
|
284
|
+
/// The retained comments are written back at their placements, the station
|
|
285
|
+
/// line starts in the third column (two leading spaces, as in the
|
|
286
|
+
/// provider's files and where RTKLIB `readblq` reads the name), and each
|
|
287
|
+
/// row is written in the column order its retained header declares, or the
|
|
288
|
+
/// standard order when none does. Output reads back to an equal block.
|
|
289
|
+
///
|
|
290
|
+
/// Returns [`TideError::BlqWrite`] for a station, comment or coefficient
|
|
291
|
+
/// that the parser would not read back unchanged, including comments not
|
|
292
|
+
/// grouped by placement in file order. To write several blocks as one
|
|
293
|
+
/// file, use [`write_ocean_loading_blq_blocks`], which carries a
|
|
294
|
+
/// column-order header across blocks the way the parser does.
|
|
295
|
+
pub fn to_blq_block(&self) -> Result<String, TideError> {
|
|
229
296
|
let mut out = String::new();
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
297
|
+
let mut column_order = OCEAN_LOADING_CONSTITUENTS;
|
|
298
|
+
self.write_blq(&mut out, &mut column_order, 0, true)?;
|
|
299
|
+
Ok(out)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn write_blq(
|
|
303
|
+
&self,
|
|
304
|
+
out: &mut String,
|
|
305
|
+
column_order: &mut [OceanTideConstituent; NUM_OCEAN_CONSTITUENTS],
|
|
306
|
+
block: usize,
|
|
307
|
+
is_last: bool,
|
|
308
|
+
) -> Result<(), TideError> {
|
|
309
|
+
let fail = |kind| TideError::BlqWrite { block, kind };
|
|
310
|
+
validate_blq_station(&self.station).map_err(fail)?;
|
|
311
|
+
let rows = self.coefficient_rows();
|
|
312
|
+
for (row_index, row) in rows.iter().enumerate() {
|
|
313
|
+
for constituent in OCEAN_LOADING_CONSTITUENTS {
|
|
314
|
+
if !row[constituent.index()].is_finite() {
|
|
315
|
+
return Err(fail(BlqWriteErrorKind::NonFiniteCoefficient {
|
|
316
|
+
row: row_index,
|
|
317
|
+
constituent,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
239
321
|
}
|
|
240
|
-
|
|
241
|
-
|
|
322
|
+
let mut headers = Vec::with_capacity(self.comments.len());
|
|
323
|
+
let mut previous_rank = 0;
|
|
324
|
+
for (index, comment) in self.comments.iter().enumerate() {
|
|
325
|
+
headers.push(validate_blq_comment(index, comment).map_err(fail)?);
|
|
326
|
+
let rank = placement_rank(comment.placement);
|
|
327
|
+
if rank < previous_rank {
|
|
328
|
+
return Err(fail(BlqWriteErrorKind::CommentsOutOfPlacementOrder {
|
|
329
|
+
index,
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
332
|
+
previous_rank = rank;
|
|
333
|
+
if !is_last && comment.placement == OceanLoadingBlqCommentPlacement::AfterRows {
|
|
334
|
+
return Err(fail(BlqWriteErrorKind::AfterRowsBeforeAnotherBlock {
|
|
335
|
+
index,
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
let placements = std::iter::once(OceanLoadingBlqCommentPlacement::BeforeStation);
|
|
341
|
+
self.write_comments(out, column_order, &headers, placements);
|
|
342
|
+
let _ = writeln!(out, " {}", self.station);
|
|
343
|
+
for (row_index, row) in rows.iter().enumerate() {
|
|
344
|
+
let placement = OceanLoadingBlqCommentPlacement::BeforeRow(row_index);
|
|
345
|
+
self.write_comments(out, column_order, &headers, std::iter::once(placement));
|
|
346
|
+
write_blq_row(out, row, column_order);
|
|
347
|
+
}
|
|
348
|
+
let placements = std::iter::once(OceanLoadingBlqCommentPlacement::AfterRows);
|
|
349
|
+
self.write_comments(out, column_order, &headers, placements);
|
|
350
|
+
Ok(())
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// Write the retained lines at `placements`, in retained order, applying
|
|
354
|
+
/// each header's column order as it is passed.
|
|
355
|
+
fn write_comments(
|
|
356
|
+
&self,
|
|
357
|
+
out: &mut String,
|
|
358
|
+
column_order: &mut [OceanTideConstituent; NUM_OCEAN_CONSTITUENTS],
|
|
359
|
+
headers: &[Option<[OceanTideConstituent; NUM_OCEAN_CONSTITUENTS]>],
|
|
360
|
+
placements: impl Iterator<Item = OceanLoadingBlqCommentPlacement>,
|
|
361
|
+
) {
|
|
362
|
+
for placement in placements {
|
|
363
|
+
for (comment, header) in self.comments.iter().zip(headers) {
|
|
364
|
+
if comment.placement == placement {
|
|
365
|
+
if let Some(order) = header {
|
|
366
|
+
*column_order = *order;
|
|
367
|
+
}
|
|
368
|
+
out.push_str(&comment.line);
|
|
369
|
+
out.push('\n');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
242
372
|
}
|
|
243
|
-
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn coefficient_rows(&self) -> [[f64; NUM_OCEAN_CONSTITUENTS]; 6] {
|
|
376
|
+
let amplitude = self.coefficients.amplitude_m;
|
|
377
|
+
let phase = self.coefficients.phase_deg;
|
|
378
|
+
[
|
|
379
|
+
amplitude[0],
|
|
380
|
+
amplitude[1],
|
|
381
|
+
amplitude[2],
|
|
382
|
+
phase[0],
|
|
383
|
+
phase[1],
|
|
384
|
+
phase[2],
|
|
385
|
+
]
|
|
244
386
|
}
|
|
245
387
|
}
|
|
246
388
|
|
|
389
|
+
/// Write station blocks as one BLQ file.
|
|
390
|
+
///
|
|
391
|
+
/// Blocks are written in order with [`OceanLoadingBlqBlock::to_blq_block`]'s
|
|
392
|
+
/// layout. A column-order header retained on one block stays in force for the
|
|
393
|
+
/// blocks after it, as it does when the parser reads the file, so parsing the
|
|
394
|
+
/// output of this function gives back equal blocks. A comment placed after the
|
|
395
|
+
/// rows of any block but the last is refused, because the parser reads it as
|
|
396
|
+
/// part of the next block.
|
|
397
|
+
pub fn write_ocean_loading_blq_blocks(
|
|
398
|
+
blocks: &[OceanLoadingBlqBlock],
|
|
399
|
+
) -> Result<String, TideError> {
|
|
400
|
+
let mut out = String::new();
|
|
401
|
+
let mut column_order = OCEAN_LOADING_CONSTITUENTS;
|
|
402
|
+
for (index, block) in blocks.iter().enumerate() {
|
|
403
|
+
block.write_blq(
|
|
404
|
+
&mut out,
|
|
405
|
+
&mut column_order,
|
|
406
|
+
index,
|
|
407
|
+
index + 1 == blocks.len(),
|
|
408
|
+
)?;
|
|
409
|
+
}
|
|
410
|
+
Ok(out)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/// Position of a placement in file order, which is the order the parser
|
|
414
|
+
/// retains comments in.
|
|
415
|
+
fn placement_rank(placement: OceanLoadingBlqCommentPlacement) -> usize {
|
|
416
|
+
match placement {
|
|
417
|
+
OceanLoadingBlqCommentPlacement::BeforeStation => 0,
|
|
418
|
+
OceanLoadingBlqCommentPlacement::BeforeRow(row) => 1 + row,
|
|
419
|
+
OceanLoadingBlqCommentPlacement::AfterRows => 7,
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/// Refuse a station name the parser would not read back as the same station.
|
|
424
|
+
fn validate_blq_station(station: &str) -> Result<(), BlqWriteErrorKind> {
|
|
425
|
+
if station.is_empty() {
|
|
426
|
+
return Err(BlqWriteErrorKind::EmptyStation);
|
|
427
|
+
}
|
|
428
|
+
if station.contains('\n') {
|
|
429
|
+
return Err(BlqWriteErrorKind::StationLineBreak);
|
|
430
|
+
}
|
|
431
|
+
if station.trim() != station {
|
|
432
|
+
return Err(BlqWriteErrorKind::StationSurroundingWhitespace);
|
|
433
|
+
}
|
|
434
|
+
if is_blq_comment(station) {
|
|
435
|
+
return Err(BlqWriteErrorKind::StationReadsAsComment);
|
|
436
|
+
}
|
|
437
|
+
if !matches!(parse_constituent_header(station, 0), Ok(None)) {
|
|
438
|
+
return Err(BlqWriteErrorKind::StationReadsAsHeader);
|
|
439
|
+
}
|
|
440
|
+
if looks_like_numeric_row(station) {
|
|
441
|
+
return Err(BlqWriteErrorKind::StationReadsAsCoefficientRow);
|
|
442
|
+
}
|
|
443
|
+
Ok(())
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/// Refuse a retained line the parser would not read back as the same comment
|
|
447
|
+
/// or header at the same placement; return the column order a header declares.
|
|
448
|
+
fn validate_blq_comment(
|
|
449
|
+
index: usize,
|
|
450
|
+
comment: &OceanLoadingBlqComment,
|
|
451
|
+
) -> Result<Option<[OceanTideConstituent; NUM_OCEAN_CONSTITUENTS]>, BlqWriteErrorKind> {
|
|
452
|
+
if comment.line.contains('\n') || comment.line.ends_with('\r') {
|
|
453
|
+
return Err(BlqWriteErrorKind::CommentLineBreak { index });
|
|
454
|
+
}
|
|
455
|
+
if let OceanLoadingBlqCommentPlacement::BeforeRow(row) = comment.placement {
|
|
456
|
+
if row > 5 {
|
|
457
|
+
return Err(BlqWriteErrorKind::CommentPlacementOutOfRange { index });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
let trimmed = comment.line.trim();
|
|
461
|
+
if trimmed.is_empty() {
|
|
462
|
+
return Err(BlqWriteErrorKind::NotACommentLine { index });
|
|
463
|
+
}
|
|
464
|
+
let header = parse_constituent_header(trimmed, 0).map_err(|error| match error {
|
|
465
|
+
TideError::BlqParse { kind, .. } => BlqWriteErrorKind::InvalidHeader { index, kind },
|
|
466
|
+
_ => BlqWriteErrorKind::NotACommentLine { index },
|
|
467
|
+
})?;
|
|
468
|
+
if header.is_none() && !is_blq_comment(trimmed) {
|
|
469
|
+
return Err(BlqWriteErrorKind::NotACommentLine { index });
|
|
470
|
+
}
|
|
471
|
+
Ok(header)
|
|
472
|
+
}
|
|
473
|
+
|
|
247
474
|
impl OceanLoadingBlq {
|
|
248
475
|
/// Parse a single standard BLQ station block.
|
|
249
476
|
pub fn from_blq_block(text: &str) -> Result<OceanLoadingBlqBlock, TideError> {
|
|
@@ -270,10 +497,23 @@ pub fn parse_ocean_loading_blq_block(text: &str) -> Result<OceanLoadingBlqBlock,
|
|
|
270
497
|
}
|
|
271
498
|
|
|
272
499
|
/// Parse all standard station blocks in a BLQ file.
|
|
500
|
+
///
|
|
501
|
+
/// Lines starting with `$`, `#` or `!` are comments. A column-order header is
|
|
502
|
+
/// a line whose text after any comment markers is `COLUMN ORDER` (any case,
|
|
503
|
+
/// optional colon) followed by the constituent labels, or consists only of
|
|
504
|
+
/// two or more constituent labels, or is a comment in which a word `ORDER` is
|
|
505
|
+
/// followed to the end of the line by two or more constituent labels, such as
|
|
506
|
+
/// `$$ Constituent order: S2 M2 ...`. Other comments are prose, even when they
|
|
507
|
+
/// list constituents. A header sets the column order for every later row until
|
|
508
|
+
/// the next header, including the remaining rows of a block it appears inside.
|
|
509
|
+
/// Every label of a header must be one of the eleven supported constituents,
|
|
510
|
+
/// each once; anything else is refused by name rather than dropped. Comment
|
|
511
|
+
/// and header lines are retained on the block they belong to.
|
|
273
512
|
pub fn parse_ocean_loading_blq_blocks(text: &str) -> Result<Vec<OceanLoadingBlqBlock>, TideError> {
|
|
274
|
-
let mut blocks = Vec::new();
|
|
513
|
+
let mut blocks: Vec<OceanLoadingBlqBlock> = Vec::new();
|
|
275
514
|
let mut station: Option<(usize, String)> = None;
|
|
276
515
|
let mut rows: Vec<[f64; NUM_OCEAN_CONSTITUENTS]> = Vec::new();
|
|
516
|
+
let mut comments: Vec<OceanLoadingBlqComment> = Vec::new();
|
|
277
517
|
let mut column_order = OCEAN_LOADING_CONSTITUENTS;
|
|
278
518
|
let mut saw_content = false;
|
|
279
519
|
|
|
@@ -285,11 +525,20 @@ pub fn parse_ocean_loading_blq_blocks(text: &str) -> Result<Vec<OceanLoadingBlqB
|
|
|
285
525
|
}
|
|
286
526
|
saw_content = true;
|
|
287
527
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
528
|
+
let header = parse_constituent_header(trimmed, line_no)?;
|
|
529
|
+
if header.is_some() || is_blq_comment(trimmed) {
|
|
530
|
+
let placement = if station.is_some() {
|
|
531
|
+
OceanLoadingBlqCommentPlacement::BeforeRow(rows.len())
|
|
532
|
+
} else {
|
|
533
|
+
OceanLoadingBlqCommentPlacement::BeforeStation
|
|
534
|
+
};
|
|
535
|
+
if let Some(order) = header {
|
|
536
|
+
column_order = order;
|
|
537
|
+
}
|
|
538
|
+
comments.push(OceanLoadingBlqComment {
|
|
539
|
+
placement,
|
|
540
|
+
line: raw_line.to_string(),
|
|
541
|
+
});
|
|
293
542
|
continue;
|
|
294
543
|
}
|
|
295
544
|
|
|
@@ -316,21 +565,15 @@ pub fn parse_ocean_loading_blq_blocks(text: &str) -> Result<Vec<OceanLoadingBlqB
|
|
|
316
565
|
|
|
317
566
|
let row = parse_blq_numeric_row(trimmed, line_no, column_order)?;
|
|
318
567
|
rows.push(row);
|
|
319
|
-
if rows.len() > 6 {
|
|
320
|
-
let station_name = station
|
|
321
|
-
.as_ref()
|
|
322
|
-
.map(|(_, name)| name.clone())
|
|
323
|
-
.unwrap_or_default();
|
|
324
|
-
return Err(TideError::BlqParse {
|
|
325
|
-
line: line_no,
|
|
326
|
-
kind: BlqParseErrorKind::TooManyCoefficientRows {
|
|
327
|
-
station: station_name,
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
568
|
if rows.len() == 6 {
|
|
332
|
-
let (_, station_name) = station.take()
|
|
333
|
-
|
|
569
|
+
let Some((_, station_name)) = station.take() else {
|
|
570
|
+
unreachable!("coefficient rows are only collected after a station line");
|
|
571
|
+
};
|
|
572
|
+
blocks.push(block_from_rows(
|
|
573
|
+
station_name,
|
|
574
|
+
&rows,
|
|
575
|
+
std::mem::take(&mut comments),
|
|
576
|
+
));
|
|
334
577
|
rows.clear();
|
|
335
578
|
}
|
|
336
579
|
}
|
|
@@ -351,6 +594,13 @@ pub fn parse_ocean_loading_blq_blocks(text: &str) -> Result<Vec<OceanLoadingBlqB
|
|
|
351
594
|
},
|
|
352
595
|
});
|
|
353
596
|
}
|
|
597
|
+
if let Some(last) = blocks.last_mut() {
|
|
598
|
+
last.comments
|
|
599
|
+
.extend(comments.into_iter().map(|comment| OceanLoadingBlqComment {
|
|
600
|
+
placement: OceanLoadingBlqCommentPlacement::AfterRows,
|
|
601
|
+
line: comment.line,
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
354
604
|
|
|
355
605
|
Ok(blocks)
|
|
356
606
|
}
|
|
@@ -358,6 +608,7 @@ pub fn parse_ocean_loading_blq_blocks(text: &str) -> Result<Vec<OceanLoadingBlqB
|
|
|
358
608
|
fn block_from_rows(
|
|
359
609
|
station: String,
|
|
360
610
|
rows: &[[f64; NUM_OCEAN_CONSTITUENTS]],
|
|
611
|
+
comments: Vec<OceanLoadingBlqComment>,
|
|
361
612
|
) -> OceanLoadingBlqBlock {
|
|
362
613
|
let mut amplitude_m = [[0.0_f64; NUM_OCEAN_CONSTITUENTS]; 3];
|
|
363
614
|
let mut phase_deg = [[0.0_f64; NUM_OCEAN_CONSTITUENTS]; 3];
|
|
@@ -369,11 +620,17 @@ fn block_from_rows(
|
|
|
369
620
|
amplitude_m,
|
|
370
621
|
phase_deg,
|
|
371
622
|
},
|
|
623
|
+
comments,
|
|
372
624
|
}
|
|
373
625
|
}
|
|
374
626
|
|
|
375
|
-
fn write_blq_row(
|
|
376
|
-
|
|
627
|
+
fn write_blq_row(
|
|
628
|
+
out: &mut String,
|
|
629
|
+
row: &[f64; NUM_OCEAN_CONSTITUENTS],
|
|
630
|
+
column_order: &[OceanTideConstituent; NUM_OCEAN_CONSTITUENTS],
|
|
631
|
+
) {
|
|
632
|
+
for constituent in column_order {
|
|
633
|
+
let value = row[constituent.index()];
|
|
377
634
|
let _ = write!(out, " {value:>16}");
|
|
378
635
|
}
|
|
379
636
|
out.push('\n');
|
|
@@ -435,40 +692,93 @@ fn parse_blq_float_token(token: &str) -> Result<f64, BlqParseErrorKind> {
|
|
|
435
692
|
Ok(value)
|
|
436
693
|
}
|
|
437
694
|
|
|
695
|
+
/// Recognize a column-order header and return the order it declares.
|
|
696
|
+
///
|
|
697
|
+
/// Three forms are headers:
|
|
698
|
+
///
|
|
699
|
+
/// 1. The declared form: `COLUMN ORDER` (any case, optionally followed by a
|
|
700
|
+
/// colon) after any comment markers, as in the provider's
|
|
701
|
+
/// `$$ COLUMN ORDER: M2 S2 ...`. Everything after it is the label list.
|
|
702
|
+
/// 2. The bare form: a line of two or more tokens that all look like tidal
|
|
703
|
+
/// constituent labels, at least one of them a supported one.
|
|
704
|
+
/// 3. The ordered comment: a comment line in which a word `ORDER` (any case,
|
|
705
|
+
/// optionally followed by a colon) is followed, to the end of the line, by
|
|
706
|
+
/// two or more tokens that all look like constituent labels, at least one
|
|
707
|
+
/// of them a supported one, such as
|
|
708
|
+
/// `$$ Constituent order: S2 M2 ...`. Surrounding punctuation `: ; ( ) [ ] .`
|
|
709
|
+
/// is not part of a token.
|
|
710
|
+
///
|
|
711
|
+
/// Any other line is not a header, including prose that lists constituents
|
|
712
|
+
/// without declaring an order, such as
|
|
713
|
+
/// `$$ diurnal K1 O1 P1 Q1, semidiurnal M2 S2 N2 K2, long-period MF MM SSA`.
|
|
714
|
+
/// The labels of every header form must be exactly the eleven supported
|
|
715
|
+
/// constituents, each once, or the header is refused by name.
|
|
438
716
|
fn parse_constituent_header(
|
|
439
717
|
line: &str,
|
|
440
718
|
line_no: usize,
|
|
441
719
|
) -> Result<Option<[OceanTideConstituent; NUM_OCEAN_CONSTITUENTS]>, TideError> {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
.
|
|
445
|
-
|
|
446
|
-
|
|
720
|
+
let body = line.trim_start_matches(['$', '#', '!']).trim_start();
|
|
721
|
+
let split_labels = |text: &str| {
|
|
722
|
+
text.split(|c: char| c.is_whitespace() || c == ',')
|
|
723
|
+
.filter(|token| !token.is_empty())
|
|
724
|
+
.map(str::to_ascii_uppercase)
|
|
725
|
+
.collect::<Vec<_>>()
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const DECLARATION: &str = "COLUMN ORDER";
|
|
729
|
+
let declared = body
|
|
730
|
+
.get(..DECLARATION.len())
|
|
731
|
+
.filter(|prefix| prefix.eq_ignore_ascii_case(DECLARATION))
|
|
732
|
+
.map(|_| &body[DECLARATION.len()..])
|
|
733
|
+
.filter(|rest| {
|
|
734
|
+
rest.is_empty() || rest.starts_with(':') || rest.starts_with(char::is_whitespace)
|
|
735
|
+
});
|
|
736
|
+
if let Some(rest) = declared {
|
|
737
|
+
let rest = rest.trim_start();
|
|
738
|
+
let rest = rest.strip_prefix(':').unwrap_or(rest);
|
|
739
|
+
return constituent_order(&split_labels(rest), line_no).map(Some);
|
|
447
740
|
}
|
|
448
741
|
|
|
449
|
-
let
|
|
450
|
-
let
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
return Ok(None);
|
|
458
|
-
}
|
|
459
|
-
if labels.len() != NUM_OCEAN_CONSTITUENTS && !header_hint {
|
|
460
|
-
return Ok(None);
|
|
742
|
+
let labels = split_labels(body);
|
|
743
|
+
let is_bare_header = labels.len() >= 2
|
|
744
|
+
&& labels.iter().all(|label| is_constituent_like(label))
|
|
745
|
+
&& labels
|
|
746
|
+
.iter()
|
|
747
|
+
.any(|label| OceanTideConstituent::from_label(label).is_some());
|
|
748
|
+
if is_bare_header {
|
|
749
|
+
return constituent_order(&labels, line_no).map(Some);
|
|
461
750
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
751
|
+
|
|
752
|
+
if is_blq_comment(line) {
|
|
753
|
+
let tokens = labels
|
|
754
|
+
.iter()
|
|
755
|
+
.map(|token| {
|
|
756
|
+
token
|
|
757
|
+
.trim_matches(|c: char| matches!(c, ':' | ';' | '(' | ')' | '[' | ']' | '.'))
|
|
758
|
+
.to_string()
|
|
759
|
+
})
|
|
760
|
+
.filter(|token| !token.is_empty())
|
|
761
|
+
.collect::<Vec<_>>();
|
|
762
|
+
for (index, token) in tokens.iter().enumerate() {
|
|
763
|
+
let list = &tokens[index + 1..];
|
|
764
|
+
if token == "ORDER"
|
|
765
|
+
&& list.len() >= 2
|
|
766
|
+
&& list.iter().all(|label| is_constituent_like(label))
|
|
767
|
+
&& list
|
|
768
|
+
.iter()
|
|
769
|
+
.any(|label| OceanTideConstituent::from_label(label).is_some())
|
|
770
|
+
{
|
|
771
|
+
return constituent_order(list, line_no).map(Some);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
470
774
|
}
|
|
775
|
+
Ok(None)
|
|
776
|
+
}
|
|
471
777
|
|
|
778
|
+
fn constituent_order(
|
|
779
|
+
labels: &[String],
|
|
780
|
+
line_no: usize,
|
|
781
|
+
) -> Result<[OceanTideConstituent; NUM_OCEAN_CONSTITUENTS], TideError> {
|
|
472
782
|
let mut order = [OceanTideConstituent::M2; NUM_OCEAN_CONSTITUENTS];
|
|
473
783
|
let mut seen = [false; NUM_OCEAN_CONSTITUENTS];
|
|
474
784
|
for (idx, label) in labels.iter().enumerate() {
|
|
@@ -490,26 +800,20 @@ fn parse_constituent_header(
|
|
|
490
800
|
});
|
|
491
801
|
}
|
|
492
802
|
seen[constituent_index] = true;
|
|
493
|
-
|
|
803
|
+
if idx < NUM_OCEAN_CONSTITUENTS {
|
|
804
|
+
order[idx] = constituent;
|
|
805
|
+
}
|
|
494
806
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|| c == ';'
|
|
506
|
-
|| c == ','
|
|
507
|
-
|| c == '('
|
|
508
|
-
|| c == ')'
|
|
509
|
-
|| c == '['
|
|
510
|
-
|| c == ']'
|
|
511
|
-
})
|
|
512
|
-
.to_ascii_uppercase()
|
|
807
|
+
if labels.len() != NUM_OCEAN_CONSTITUENTS {
|
|
808
|
+
return Err(TideError::BlqParse {
|
|
809
|
+
line: line_no,
|
|
810
|
+
kind: BlqParseErrorKind::WrongColumnCount {
|
|
811
|
+
expected: NUM_OCEAN_CONSTITUENTS,
|
|
812
|
+
found: labels.len(),
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
Ok(order)
|
|
513
817
|
}
|
|
514
818
|
|
|
515
819
|
fn is_constituent_like(token: &str) -> bool {
|
|
@@ -523,6 +827,7 @@ fn is_constituent_like(token: &str) -> bool {
|
|
|
523
827
|
)
|
|
524
828
|
|| (token.len() <= 4
|
|
525
829
|
&& token.chars().any(|c| c.is_ascii_digit())
|
|
830
|
+
&& token.chars().any(|c| c.is_ascii_alphabetic())
|
|
526
831
|
&& token.chars().all(|c| c.is_ascii_alphanumeric()))
|
|
527
832
|
}
|
|
528
833
|
|
|
@@ -611,7 +916,7 @@ fn ocean_tide_loading_unchecked(
|
|
|
611
916
|
{
|
|
612
917
|
let mut sum = 0.0;
|
|
613
918
|
for ((&litude, &phase_deg), &a) in amplitudes.iter().zip(phases).zip(&arg) {
|
|
614
|
-
sum += amplitude * (a - phase_deg * DEG_TO_RAD)
|
|
919
|
+
sum += amplitude * libm::cos(a - phase_deg * DEG_TO_RAD);
|
|
615
920
|
}
|
|
616
921
|
*slot = sum;
|
|
617
922
|
}
|
|
@@ -625,8 +930,8 @@ fn ocean_tide_loading_unchecked(
|
|
|
625
930
|
let (lat_deg, lon_deg, _height_km) =
|
|
626
931
|
itrs_to_geodetic_compute(xsta[0] / KM_TO_M, xsta[1] / KM_TO_M, xsta[2] / KM_TO_M)
|
|
627
932
|
.expect("validated station position yields geodetic coordinates");
|
|
628
|
-
let (sinlat, coslat) = (lat_deg * DEG_TO_RAD)
|
|
629
|
-
let (sinlon, coslon) = (lon_deg * DEG_TO_RAD)
|
|
933
|
+
let (sinlat, coslat) = libm::sincos(lat_deg * DEG_TO_RAD);
|
|
934
|
+
let (sinlon, coslon) = libm::sincos(lon_deg * DEG_TO_RAD);
|
|
630
935
|
|
|
631
936
|
// ENU basis vectors expressed in ECEF (geodetic topocentric frame):
|
|
632
937
|
// e = [-sinlon, coslon, 0]
|