Adds the five v1 inter-product rules from docs/design/s98-interoperability.md §3
on top of PR-L1's plane-assignment plumbing:
* R-101-102-A — S-102 sits between S-101 area fills and line work
(plane-order property, no mutation).
* R-101-102-B — when an S-102 dataset is loaded and active, suppress
S-101 DepthArea and DepthContour features (cites Annex A §8.4.1 +
Part B §B-3.1.2). The MSC.232(82) §5.8 safety-contour exception
retains any DepthContour whose VALDCO matches the active mariner
safety contour (tolerance 1e-6).
* R-101-124-A — S-124 on CautionsAndWarnings (plane-order property).
* R-104-A — S-104 on OnDemandSurface (plane-order property).
* R-111-A — S-111 colour band on OnDemandSurface, arrows on
DynamicArrows (plane-order property).
Implementation notes
--------------------
* New types in src/EncDotNet.S100.Datasets.Pipelines/Interoperability/:
LoadedDatasetInfo, S98RuleContext, S98InteroperabilityRule (record),
FeatureTagKeys, S98DefaultRules (static, declaration-ordered).
* IInteroperabilityAuthority gains ApplyRules(stack, loaded, mariner?,
rules?). Default impl runs rules in declaration order; load-order
authority is a no-op.
* DatasetLoaderService.FlattenLayerOrder now calls ApplyRules after
LayerStackBuilder.Build, passing the current mariner settings.
Suppression strategy: option (b) — feature-tag filtering. S101DatasetProcessor
tags every Mapsui IFeature with its S-100 feature-type code (and VALDCO
for DepthContour) using the lazy _featureIndex it already maintains.
The R-101-102-B effect builds a new MemoryLayer per affected S-101 entry
with the offending features filtered out. No portrayal re-run; no per-feature
plane metadata (TBD-5 stays out of scope per briefing).
LoadedDatasetInfo.Active maps 1:1 to entry.IsVisible && layers.Count > 0
for this PR (briefing decision: 'loaded == active'; real active/inactive
arrives with Layer Controls UI in PR-L3).
Every IC element name introduction carries a // TODO PR-L2-RESYNC:
confirm against S-100 Part 16 XSD marker (briefing TBD-1).
Tests
-----
* tests/EncDotNet.S100.Pipelines.Tests/S98DefaultRulesTests.cs — 11
tests covering: plane-order property for all four anchor rules,
R-101-102-B suppression on/off, safety-contour exception, Active
flag honoured, rule composition declaration order, empty rule
set is a no-op, Default ordering pin.
* tests/EncDotNet.S100.Viewer.Tests/PickServiceTests.cs — adds
HandlePick_AfterR_101_102_B_Suppression_ReturnsS102Hit verifying
picks over a suppressed DepthArea resolve to the S-102 hit.
Snapshots
---------
No visual-regression snapshots changed — no existing fixture loads
S-101 and S-102 together.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Implements the five v1 inter-product rules from
docs/design/s98-interoperability.md§3 on top of PR-L1's plane-assignment plumbing.Rules delivered
DepthArea+DepthContourwhen S-102 loaded and active (with safety-contour exception)CautionsAndWarningsOnDemandSurfaceOnDemandSurface, arrows onDynamicArrowsFour of the five are pure plane-order properties satisfied by PR-L1's default plane table; this PR pins them via tests. R-101-102-B is the only mutating rule.
Suppression strategy: option (b) — feature-tag filtering
I chose option (b) (filter
IFeatures by tag) over option (a) (re-run the portrayal pipeline with a suppression set) because:S101DatasetProcessoralready maintains a lazy_featureIndexmappingFeatureRef→Feature. Tagging the emittedIFeatures with their feature-type code is one dictionary lookup per feature; no pipeline change.MemoryLayerper affected S-101 entry, copying only surviving features. No per-feature plane metadata is introduced (TBD-5 stays out of scope).The R-101-102-B effect honours the MSC.232(82) §5.8 safety-contour exception: any
DepthContourwhosevalueOfDepthContourmatches the current marinerSafetyContour(tolerance 1e-6) is retained even when others are suppressed.Wiring
S98InteroperabilityRuleis a record(RuleId, SpecCitation, Condition, Effect)— no DSL, no Part-16 dependency. Predicates and effects are plain delegates.S98DefaultRules.Defaultholds the five rules in declaration order (rules run in that order; pinned byDefault_rule_set_is_in_documented_ordertest).IInteroperabilityAuthority.ApplyRules(stack, loadedDatasets, mariner?, rules?)added;LoadOrderInteroperabilityAuthorityreturns the stack untouched.DatasetLoaderService.FlattenLayerOrdercallsApplyRulesafterLayerStackBuilder.Build.LoadedDatasetInfo.Activemaps 1:1 toentry.IsVisible && layers.Count > 0per briefing — real active/inactive lands with PR-L3 (Layer Controls UI).TODO PR-L2-RESYNCmarkersEvery IC element name introduced in this PR carries the resync marker so a future S-100 Part 16 sync can find them by
grep -r "TODO PR-L2-RESYNC":S98DefaultRules.cs(rule IDs)SuppressS101DepthFeaturesTests
S98DefaultRulesTests.cs(Pipelines, 11 tests): plane-order property for all four anchor rules; R-101-102-B suppression on/off; safety-contour exception;Activeflag honoured; rule composition declaration order; empty rule set is a no-op;Defaultordering pin.PickServiceTests.cs(Viewer, +1 test):HandlePick_AfterR_101_102_B_Suppression_ReturnsS102Hitverifies picks over a suppressedDepthArearesolve to the S-102 hit.Visual regression
No snapshots rebaselined. Verified there are no existing fixtures that load S-101 + S-102 together; the new rule's first opportunity to affect rendered output is when such a fixture is added.
Out of scope (per briefing)
content/S98/(lands when Part 16 is in hand — PR-L2a).Refs: #117 (PR-L0 design note), #118 (PR-L1 plumbing).