11package portrayal
22
33import (
4+ "fmt"
45 "io/fs"
56 "math"
67 "os"
@@ -289,6 +290,17 @@ func hasAdditionalInfo(attrs map[string]any) bool {
289290 return false
290291}
291292
293+ // quaposSolidClass: man-made structures drawn with a definite (solid) line regardless
294+ // of QUAPOS. The S-52 approximate-position dashing (DEPCNT03 and friends) is for natural
295+ // features whose position is uncertain — depth contours, coastline, rivers — not
296+ // engineered structures whose charted extent is definite. Without this, a bridge or road
297+ // whose edges carry a low-accuracy QUAPOS (often inherited from a shared coastline edge)
298+ // is wrongly dashed.
299+ var quaposSolidClass = map [string ]bool {
300+ "BRIDGE" : true , "ROADWY" : true , "RAILWY" : true ,
301+ "CAUSWY" : true , "DAMCON" : true , "GATCON" : true ,
302+ }
303+
292304// buildFeatureBody turns one feature's emitted instruction stream into its FeatureBuild.
293305func (b * S101Builder ) buildFeatureBody (f * s57.Feature , stream string ) FeatureBuild {
294306 // NEWOBJ with a SYMINS attribute: portray the producer's explicit symbol
@@ -299,6 +311,15 @@ func (b *S101Builder) buildFeatureBody(f *s57.Feature, stream string) FeatureBui
299311 if fb , ok := parseSYMINS (f ); ok {
300312 return fb
301313 }
314+ // No producer SYMINS, and the V-AIS alias would emit only the generic untyped
315+ // "default V-AIS" (VATON00) — almost always a plain new object, not a real
316+ // virtual AIS aid (those carry a type → VATON01-12). Portray the S-52 NEWOBJ
317+ // "!" instead; typed V-AIS still go through the rule below.
318+ if strings .Contains (stream , "VATON00" ) {
319+ if nb := newObjectBuild (f ); len (nb .Primitives ) > 0 {
320+ return nb
321+ }
322+ }
302323 }
303324 // M_NSYS (navigational system of marks): the S-101 NavigationalSystemOfMarks
304325 // rule is an UNOFFICIAL stub (NullInstruction only), so draw the S-52 boundary
@@ -409,7 +430,7 @@ func (b *S101Builder) buildFeatureBody(f *s57.Feature, stream string) FeatureBui
409430 // from a per-edge spatial-quality association we don't model, so apply it here
410431 // from the parsed per-feature QUAPOS aggregate: switch the feature's solid simple
411432 // strokes to dashed. Complex line styles and point symbols keep their look.
412- if q := f .Geometry ().Quapos ; q != 0 && q != 1 && q != 10 && q != 11 {
433+ if q := f .Geometry ().Quapos ; q != 0 && q != 1 && q != 10 && q != 11 && ! quaposSolidClass [ f . ObjectClass ()] {
413434 for i , p := range prims {
414435 if sl , ok := p .(StrokeLine ); ok && sl .Dash == DashSolid {
415436 sl .Dash = DashDashed
@@ -436,6 +457,13 @@ func (b *S101Builder) buildFeatureBody(f *s57.Feature, stream string) FeatureBui
436457 if cat == 0 {
437458 cat = displayStandard // no display-category band emitted (e.g. text-only)
438459 }
460+ if f .ObjectClass () == "BRIDGE" {
461+ prims = bridgePostProcess (f , prims )
462+ }
463+ switch f .ObjectClass () {
464+ case "OBSTRN" , "WRECKS" , "UWTROC" :
465+ prims = obstructionPostProcess (f , prims )
466+ }
439467 return FeatureBuild {
440468 Primitives : prims ,
441469 DisplayPriority : priority ,
@@ -446,6 +474,42 @@ func (b *S101Builder) buildFeatureBody(f *s57.Feature, stream string) FeatureBui
446474 }
447475}
448476
477+ // bridgePostProcess fixes two S-101-model gaps the Bridge rule can't, because the
478+ // S-101 Bridge feature type binds neither a verticalClearance* attribute (the model
479+ // puts clearance on the related SpanFixed feature) nor a true openingBridge for S-57
480+ // CATBRG:1 (the framework still resolves openingBridge → true, so the rule stamps the
481+ // opening-bridge symbol BRIDGE01 on fixed bridges):
482+ // - drop BRIDGE01 unless CATBRG is an opening category (2–8);
483+ // - emit the "clr <value>" vertical-clearance label from the S-57 VERCLR directly.
484+ func bridgePostProcess (f * s57.Feature , prims []Primitive ) []Primitive {
485+ catbrg , _ := floatAttr (f .Attributes (), "CATBRG" )
486+ opening := catbrg >= 2 && catbrg <= 8
487+ if ! opening {
488+ out := prims [:0 ]
489+ for _ , p := range prims {
490+ if sc , ok := p .(SymbolCall ); ok && sc .SymbolName == "BRIDGE01" {
491+ continue
492+ }
493+ out = append (out , p )
494+ }
495+ prims = out
496+ }
497+ if v , ok := floatAttr (f .Attributes (), "VERCLR" ); ok && v > 0 {
498+ if anchor , ok := representativePoint (f ); ok {
499+ prims = append (prims , DrawText {
500+ Anchor : anchor ,
501+ Text : fmt .Sprintf ("clr %.1f" , v ),
502+ FontSizePx : 12 ,
503+ ColorToken : "CHBLK" ,
504+ Halo : & TextHalo {ColorToken : "CHWHT" , WidthPx : 1 },
505+ Group : 11 , // S-52 text group 11: clearances / important
506+ OffsetYPx : 11 ,
507+ })
508+ }
509+ }
510+ return prims
511+ }
512+
449513// commandsNeedAnchor reports whether any reduced draw command consumes the
450514// feature anchor — the anchored ops emitPrimitives reads geom.Anchor for: point
451515// symbols, text, and sector/augmented figures. Fills and boundary lines don't,
@@ -582,6 +646,53 @@ func attachSoundingDepths(prims []Primitive, pts [][3]float64) {
582646 }
583647}
584648
649+ // attachDangerDepth tags the isolated-danger symbol (ISODGR01) on an under/awash hazard
650+ // with the hazard's sounding depth (S-57 VALSOU), which the baker bakes as danger_depth.
651+ // The client then hides / swaps the mark when the hazard is DEEPER than the mariner's
652+ // safety contour (S-52 UDWHAZ05: only a sub-safety-contour danger is an isolated danger).
653+ // Without it DangerDepthM stays NaN and every obstruction shows ISODGR01 regardless of
654+ // depth (a line obstruction deeper than the safety contour is not an isolated danger).
655+ func obstructionPostProcess (f * s57.Feature , prims []Primitive ) []Primitive {
656+ d , ok := floatAttr (f .Attributes (), "VALSOU" )
657+ if ! ok {
658+ return prims
659+ }
660+ // Tag the isolated-danger mark (ISODGR01) with the sounding so the client shows the
661+ // ⊗ only when the hazard is shallower than the live safety contour (S-52 UDWHAZ05);
662+ // a deeper-than-safety obstruction is portrayed by its depth label, not the mark.
663+ for i := range prims {
664+ if sc , ok := prims [i ].(SymbolCall ); ok && sc .SymbolName == "ISODGR01" {
665+ sc .DangerDepthM = float32 (d )
666+ prims [i ] = sc
667+ }
668+ }
669+ // The obstruction carries its VALSOU as a depth (sounding) label. A low-accuracy
670+ // sounding (unreliable QUAPOS) is parenthesised — the S-52 approximate convention.
671+ if anchor , ok := representativePoint (f ); ok {
672+ txt := formatSounding (d )
673+ if q := f .Geometry ().Quapos ; q != 0 && q != 1 && q != 10 && q != 11 {
674+ txt = "(" + txt + ")"
675+ }
676+ prims = append (prims , DrawText {
677+ Anchor : anchor ,
678+ Text : txt ,
679+ FontSizePx : 10 ,
680+ ColorToken : "CHBLK" ,
681+ Halo : & TextHalo {ColorToken : "CHWHT" , WidthPx : 1 },
682+ Group : 11 ,
683+ })
684+ }
685+ return prims
686+ }
687+
688+ // formatSounding renders a sounding depth: an integer for whole metres, else one decimal.
689+ func formatSounding (d float64 ) string {
690+ if d == math .Trunc (d ) {
691+ return strconv .FormatFloat (d , 'f' , 0 , 64 )
692+ }
693+ return strconv .FormatFloat (d , 'f' , 1 , 64 )
694+ }
695+
585696func primitiveName (t s57.GeometryType ) string {
586697 switch t {
587698 case s57 .GeometryTypeLineString :
0 commit comments