From fd55a0860d5e852a6bc073ceaf57b440fa6efcfc Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 18:26:24 +0200 Subject: [PATCH 01/26] tuned widget layout - centered values - more compact cells - tuned spacing - distinct header - monospace coordinates --- viewer/components/CenterDisplayWidget.jsx | 4 +- viewer/style/avnav_viewer_new.less | 3 + viewer/style/widgets.less | 207 +++++++++++----------- 3 files changed, 109 insertions(+), 105 deletions(-) diff --git a/viewer/components/CenterDisplayWidget.jsx b/viewer/components/CenterDisplayWidget.jsx index e9b76596a..02b8875e4 100644 --- a/viewer/components/CenterDisplayWidget.jsx +++ b/viewer/components/CenterDisplayWidget.jsx @@ -19,7 +19,7 @@ const CenterDisplayWidget = (props) => { } return ( - {!small &&
{Formatter.formatLonLats(props.centerPosition)}
} + {!small &&
{Formatter.formatLonLats(props.centerPosition,props.positionFmt)}
} {(measurePosition !== undefined) &&
@@ -99,4 +99,4 @@ CenterDisplayWidget.propTypes={ style: PropTypes.object, mode: PropTypes.string }; -export default CenterDisplayWidget; \ No newline at end of file +export default CenterDisplayWidget; diff --git a/viewer/style/avnav_viewer_new.less b/viewer/style/avnav_viewer_new.less index b78b5bba6..091e2d8f8 100644 --- a/viewer/style/avnav_viewer_new.less +++ b/viewer/style/avnav_viewer_new.less @@ -1209,6 +1209,9 @@ span.valuePrefix{ .mdText2(); width: 6em; } + .unit { + font-weight: normal; + } .aisData { display: inline-block; text-align: left; diff --git a/viewer/style/widgets.less b/viewer/style/widgets.less index 84d370cf3..94c72a720 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -4,7 +4,7 @@ @x: rgba(255, 20, 7, 0.54); @horizontalContainerHeight: 4.2em; -@horizontalContainerDoubleHeight: 7.9em; +@horizontalContainerDoubleHeight: 8.2em; @horizontalWidgetHeight: 4em; @infoFontSize: 0.71em; @@ -15,7 +15,7 @@ .widget{ position: relative; z-index: 100; - margin: 0.1em; + margin: 1px; overflow: hidden; pointer-events: all; background: white; @@ -27,11 +27,11 @@ .flex-justify-content(flex-start); .widgetHead { margin: 0; - padding: 0.1em; + padding: 0 0.2em; display: flex; .flex-direction(row); .flex-justify-content(space-between); - height: 0.7em; + white-space: nowrap; .infoLeft{ .widgetInfo(); } @@ -40,14 +40,16 @@ } } .widgetData{ - text-align: right; - max-width: calc(100% - 0.2em); - margin-left: auto; - margin-right: 0.1em; + text-align: center; + padding: 0 0.1em; + width: 100%; max-height: 100%; min-height: 0; min-width: 0; } + canvas.widgetData{ + padding: 0; + } &.average .infoLeft{ .nightForeColor(red); } @@ -87,7 +89,7 @@ .widgetData{ padding-top: 0; display: block; - white-space: pre; + white-space: pre-wrap; } .centeredWidget { .widgetData{ @@ -108,7 +110,7 @@ } .widgetContainer.horizontal{ .flex-wrap(wrap); - .flex-align-items(center); + .flex-align-items(top); max-height: @horizontalContainerHeight; .twoRows &{ max-height: @horizontalContainerDoubleHeight; @@ -133,11 +135,9 @@ .widget{ min-width: 0; .flex-shrink(0); - margin-left: 0.1em; - margin-right: 0.1em; - padding-left: 0.1em; - padding-right: 0.1em; - width: calc(100% - 0.2em); + margin-left: 0; + margin-right: 2px; + width: calc(100% - 2px); } .editing &{ .widget{ @@ -161,7 +161,7 @@ } //------------------ dedicated widgets ------------------------- -//widgets have their name from the widget list and maybe some additonial fixed name as classes +//widgets have their name from the widget list and maybe some additional fixed name as classes @bigFont: 3em; @bigFontVertical: 2em; @smallFont: 1em; @@ -170,99 +170,94 @@ @size1: 7em; @size15: 9em; @size2: 11em; + +.blink(@period:1s) { + animation: blinker @period linear infinite; +} + +@keyframes blinker { + 50% { opacity: 0; } +} + .widget{ - .bigWidget(@size){ + width: min-content; // allow dynamic scaling to size of content + + .widgetHead { + .nightBackColor(#eee); + } + + .widgetData { + line-height: 1em; +// background: yellow; + } + + .bigWidget(){ .widgetData{ font-size: @bigFont; + white-space: nowrap; } - width: @size; - .vertical &{ - .widgetData{ + .vertical & .widgetData { font-size: @bigFontVertical; } } - } - .smallWidget(@size){ + + .medWidget(){ .widgetData{ - font-size: @smallFont; + text-align: center; + font-size: @timeFont; + white-space: normal; } - width: @size; - .vertical &{ - .widgetData{ - font-size: @smallFont; - } + .horizontal & .widgetData { + padding-top: 0.5em; } } - .timeWidget(@font){ - .widgetData{ - font-size: @font; - } - width: 7em; - .vertical &{ + + .smallWidget(){ .widgetData{ - font-size: @font; - } - } - } - &.SOG{ - .bigWidget(@size2); - } - &.VMG{ - .bigWidget(@size2); - } - &.COG{ - .bigWidget(@size1); - } - &.BRG{ - .bigWidget(@size1); - } - &.DST{ - .bigWidget(@size2); - } - &.WindAngle,&.WindSpeed{ - .bigWidget(@size1); - } - &.AnchorBearing{ - .bigWidget(@size1); - } - &.AnchorDistance{ - .bigWidget(@size2); - } - &.AnchorWatchDistance{ - .bigWidget(@size15); - } - &.RteDistance{ - .bigWidget(@size2); - } - &.RteDistance{ - .timeWidget(@timeFont); - } - &.LargeTime{ - .timeWidget(@clockFont); - } - &.zoomWidget{ - .smallWidget(@size1); - .widgetData{ - text-align: center; - font-size: @timeFont; + font-size: @smallFont; + line-height: 1.2em; + white-space: normal; } - .vertical &{ - .widgetData{ - font-size: @timeFont; + .horizontal & .widgetData { + padding-top: 0.2em; } } - .rzoom{ - display: inline-block; - } + + .bigWidget(); // big is the default + + // shortcuts for manual use + &.big { .bigWidget(); } + &.med { .medWidget(); } + &.small { .smallWidget(); } + + &.DateTime { .smallWidget(); } +// &.ETA { .medWidget(); } +// &.RteEta { .medWidget(); } +// &.TimeStatus { .medWidget(); } + &.GNSSStatus { .smallWidget(); } + &.error .infoRight { + .blink(); + color: red; + font-weight: bold; + } + &.warning .infoRight { + .blink(2s); + color: orange; + font-weight: bold; + } + &.ok .infoRight { + opacity: 1; + color: green; } &.Position,&.WpPosition{ - .smallWidget(@size1); + .smallWidget(); .widgetData{ text-align: center; + font-family: monospace; } } &.timeStatusWidget{ - .smallWidget(@size1); + .smallWidget(); .status{ width: 1.5em; height: 1.5em; @@ -282,7 +277,7 @@ } } &.etaWidget{ - .smallWidget(@size1); + .smallWidget(); .widgetData{ text-align: center; margin-left: auto; @@ -294,11 +289,12 @@ } } &.aisTargetWidget{ - .smallWidget(@size1); + .smallWidget(); .nightBackColor(@colorSecond); .aisFront{ display: inline-block; font-size: 1.5em; + line-height: 1.2em; } .label{ width: 2em; @@ -322,7 +318,7 @@ } &.activeRouteWidget{ - .smallWidget(@size1); + .smallWidget(); .routeName{ margin-top: 0.8em; margin-right: 0.2em; @@ -343,7 +339,7 @@ &.activeRoute{ .nightBorderFade(@colorRed); } - .smallWidget(@size1); + .smallWidget(); .widgetData{ .routeInfo { width: 4.5em; @@ -377,7 +373,7 @@ } &.centerDisplayWidget{ - .smallWidget(@size1); + .smallWidget(); .widgetData ~ .widgetData{ margin-top: 0; } @@ -404,28 +400,33 @@ .horizontal &{ min-width: 10em; } + .Position { + font-family: monospace; + } } &.windWidget{ - .smallWidget(@size15); - padding-top: 0; - padding-left: 0; - padding-right: 0; + .smallDisplay & { + .medWidget(); + .resize { + flex-direction: column; + } .widgetData{ - font-size: @timeFont; + padding-top: 0.5em; + } } } &.DepthDisplay{ - .bigWidget(@size2); + .bigWidget(); } &.xteWidget{ - .smallWidget(@size1); + .smallWidget(); canvas{ margin-right: auto; margin-left: auto; } } &.windGraphics{ - .smallWidget(@size2); + .smallWidget(); .windSpeed { text-align: right; font-size: @timeFont; @@ -446,8 +447,8 @@ } } canvas{ - height: 90%; - width: 90%; +// width: 85%; + height: 80%; } .vertical &{ height: 11em; @@ -506,7 +507,7 @@ .flex-shrink(1); margin: 0; min-width: 0; - border-right: 1px solid + border-right: 1px solid; } .widget:last-of-type { From 1311764f2892eac2af275ad4c03720ca868b753e Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 18:32:46 +0200 Subject: [PATCH 02/26] revised wind graphics and wind display wind display more compact with direct divs --- viewer/components/WindGraphics.jsx | 23 +++++------ viewer/components/WindWidget.jsx | 65 +++++++++++++----------------- 2 files changed, 38 insertions(+), 50 deletions(-) diff --git a/viewer/components/WindGraphics.jsx b/viewer/components/WindGraphics.jsx index 48fa46879..56f812e6e 100644 --- a/viewer/components/WindGraphics.jsx +++ b/viewer/components/WindGraphics.jsx @@ -115,12 +115,8 @@ const WindGraphics = (props) => { // Move the pointer from 0,0 to center position ctx.translate(width / 2, height / 2); ctx.font = fontSize + "px "+globalstore.getData(keys.properties.fontBase); - let show180=false; - if (!props.show360 && current.suffix !== 'TD') { - if (winddirection > 180) winddirection -= 360; - show180=true; - } - let txt = Formatter.formatDirection(winddirection,undefined,show180,true); + let a180 = !(props.show360 || current.suffix.endsWith('D')); + let txt = Formatter.formatDirection(winddirection,false,a180,true); let xFactor = -1.0; if (winddirection < 0) xFactor = -1.0; ctx.fillStyle = colors.text; @@ -141,13 +137,16 @@ const WindGraphics = (props) => { setTimeout(drawWind, 0); } setTimeout(drawWind, 0); - let current = getWindData(props); - let windSpeed = props.formatter(current.windSpeed); + let wind = getWindData(props); + let a180 = !(props.show360 || wind.suffix.endsWith('D')); + let angle = Formatter.formatDirection(wind.windAngle,false,a180); + let unit = ((props.formatterParameters instanceof Array) && props.formatterParameters.length > 0) ? props.formatterParameters[0] : 'kn'; + let speed = Formatter.formatSpeed(wind.windSpeed,unit); return ( - + -
{windSpeed}
-
{current.suffix}
+
{speed}
+
{wind.suffix}
); @@ -180,4 +179,4 @@ WindGraphics.predefined= { caption: 'Wind' } -export default WindGraphics; \ No newline at end of file +export default WindGraphics; diff --git a/viewer/components/WindWidget.jsx b/viewer/components/WindWidget.jsx index 82069e1dc..01d923827 100644 --- a/viewer/components/WindWidget.jsx +++ b/viewer/components/WindWidget.jsx @@ -16,21 +16,13 @@ export const getWindData=(props)=>{ if (kind !== 'true' && kind !== 'apparent' && kind !== 'trueAngle' && kind !== 'trueDirection') kind='auto'; if (kind === 'auto'){ if (props.windAngle !== undefined && props.windSpeed !== undefined){ - windAngle=props.windAngle; - windSpeed=props.windSpeed; - suffix='A'; - } - else{ - if (props.windAngleTrue !== undefined){ - windAngle=props.windAngleTrue; - windSpeed=props.windSpeedTrue; - suffix="TA"; - } - else{ - windAngle=props.windDirectionTrue; - windSpeed=props.windSpeedTrue; - suffix="TD"; - } + kind = 'apparent'; + } else if (props.windAngleTrue !== undefined && props.windSpeedTrue !== undefined){ + kind = 'trueAngle'; + } else if (props.windDirectionTrue !== undefined && props.windSpeedTrue !== undefined){ + kind = 'trueDirection'; + } else { + kind = 'apparent'; } } if (kind === 'apparent'){ @@ -71,7 +63,6 @@ export const WindProps={ } const WindWidget = (props) => { - let wind = getWindData(props); const names = { A: { speed: 'AWS', @@ -86,32 +77,30 @@ const WindWidget = (props) => { angle: 'TWA' } } - let windSpeedStr = props.formatter(wind.windSpeed); - let show180=false; - if (!props.show360 && wind.suffix !== 'TD') { - show180=true; - if (wind.windAngle > 180) wind.windAngle -= 360; - } + let wind = getWindData(props); + let a180 = !(props.show360 || wind.suffix.endsWith('D')); + let angle = Formatter.formatDirection(wind.windAngle,false,a180,true); + let unit = ((props.formatterParameters instanceof Array) && props.formatterParameters.length > 0) ? props.formatterParameters[0] : 'kn'; + let speed = Formatter.formatSpeed(wind.windSpeed,unit); return ( - + {(props.mode === 'horizontal') ? - +
- {Formatter.formatDirection(wind.windAngle,undefined,show180)} - ° - /{windSpeedStr} - {props.unit} + {angle}/{speed}
: - -
{Formatter.formatDirection(wind.windAngle,undefined,show180)}
-
- -
{windSpeedStr}
-
+
+ +
{angle}
+
+
+ +
{speed}
+
}
@@ -128,6 +117,7 @@ WindWidget.propTypes={ WindWidget.predefined= { storeKeys: WindStoreKeys, + formatter: 'formatSpeed', editableParameters: { show360: {type: 'BOOLEAN', default: false}, kind: { @@ -136,10 +126,9 @@ WindWidget.predefined= { default: 'auto', description: 'which wind data to be shown\nauto will try apparent, trueAngle, trueDirection and display the first found data' }, - formatter: true, + formatter: false, formatterParameters: true }, - formatter :'formatSpeed' -} +}; -export default WindWidget; \ No newline at end of file +export default WindWidget; From a82ff92b940cf9efcb95ae70bceeedb7d4431850 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 18:44:16 +0200 Subject: [PATCH 03/26] tuned widget configuration - show ERROR/OK in top right on position and time - added TTG mode to ETA - put WP name to top right to make more space for value - disable config of unit if it is determined by formatter - fixed unit of temperature - added options select kind of depth - extended range of max xte --- viewer/components/WidgetList.js | 202 +++++++++++++++++++++++++------- viewer/components/XteWidget.jsx | 4 +- 2 files changed, 159 insertions(+), 47 deletions(-) diff --git a/viewer/components/WidgetList.js b/viewer/components/WidgetList.js index 74f2026c2..d8d8e515a 100644 --- a/viewer/components/WidgetList.js +++ b/viewer/components/WidgetList.js @@ -21,6 +21,7 @@ import UndefinedWidget from './UndefinedWidget.jsx'; import {SKPitchWidget, SKRollWidget} from "./SKWidgets"; import {CombinedWidget} from "./CombinedWidget"; import Formatter from "../util/formatter"; +const degrees='\u00b0'; let widgetList=[ { name: 'SOG', @@ -37,8 +38,7 @@ let widgetList=[ }, { name: 'COG', - default: "---", - unit: "\u00b0", + unit: degrees, caption: 'COG', storeKeys:{ value: keys.nav.gps.course, @@ -46,34 +46,32 @@ let widgetList=[ }, formatter: 'formatDirection360', editableParameters: { - formatterParameters: true + unit: false, } }, { name: 'HDM', - default: "---", - unit: "\u00b0", + unit: degrees, caption: 'HDM', storeKeys:{ value: keys.nav.gps.headingMag }, formatter: 'formatDirection360', editableParameters: { - formatterParameters: true + unit: false, } }, { name: 'HDT', - default: "---", - unit: "\u00b0", + unit: degrees, caption: 'HDT', storeKeys:{ value: keys.nav.gps.headingTrue }, formatter: 'formatDirection360', editableParameters: { - formatterParameters: true + unit: false, } }, { @@ -82,10 +80,19 @@ let widgetList=[ caption: 'BOAT', storeKeys:{ value: keys.nav.gps.position, - isAverage: keys.nav.gps.positionAverageOn + isAverage: keys.nav.gps.positionAverageOn, + gpsValid: keys.nav.gps.valid, + }, + formatter: 'formatLonLats', + editableParameters: { + unit: false, + }, + translateFunction: (props)=>{ + return {...props, + unit: props.gpsValid?'OK':'ERROR', + addClass: props.gpsValid?'ok':'error', + } }, - formatter: 'formatLonLats' - }, { name: 'TimeStatus', @@ -95,9 +102,26 @@ let widgetList=[ }, { name: 'ETA', - caption: 'ETA', - wclass: EtaWidget, - storeKeys: EtaWidget.storeKeys + storeKeys:{ + value: keys.nav.wp.eta, + time:keys.nav.gps.rtime, + name: keys.nav.wp.name, + server: keys.nav.wp.server + }, + formatter: 'formatTime', + translateFunction: (props)=>{ + return {...props, + value: !props.value?null:props.kind=='TTG'?new Date(props.value-props.time):props.value, + unit: props.name, + caption: (props.caption||' ')+props.kind, + disconnect: props.server === false, + addClass: (!props.formatterParameters||props.formatterParameters?.[0])?'med':null, + } + }, + editableParameters: { + unit: false, + kind: {type:'SELECT',list:['ETA','TTG'],default:'ETA'}, + } }, { name: 'DST', @@ -105,11 +129,13 @@ let widgetList=[ caption: 'DST', storeKeys:{ value: keys.nav.wp.distance, + name: keys.nav.wp.name, server: keys.nav.wp.server }, - updateFunction: (state)=>{ + translateFunction: (state)=>{ return { value: state.value, + caption: state.caption+' '+state.name, disconnect: state.server === false } }, @@ -121,15 +147,23 @@ let widgetList=[ }, { name: 'BRG', - default: "---", - unit: "\u00b0", + unit: degrees, caption: 'BRG', storeKeys:{ - value: keys.nav.wp.course + value: keys.nav.wp.course, + name: keys.nav.wp.name, + server: keys.nav.wp.server + }, + translateFunction: (state)=>{ + return { + value: state.value, + caption: state.caption+' '+state.name, + disconnect: state.server === false + } }, formatter: 'formatDirection360', editableParameters: { - formatterParameters: true + unit: false, } }, { @@ -160,7 +194,7 @@ let widgetList=[ { name: 'WindAngle', default: "---", - unit: "\u00b0", + unit: degrees, caption: 'Wind Angle', storeKeys:WindStoreKeys, formatter: 'formatString', @@ -169,6 +203,7 @@ let widgetList=[ formatter: false, value: false, caption: false, + unit: false, kind: {type:'SELECT',list:['auto','trueAngle','trueDirection','apparent'],default:'auto'}, show360: {type:'BOOLEAN',default: false,description:'always show 360°'}, leadingZero:{type:'BOOLEAN',default: false,description:'show leading zeroes (012)'} @@ -176,8 +211,8 @@ let widgetList=[ translateFunction: (props)=>{ const captions={ A:'AWA', + TA: 'TWA', TD: 'TWD', - TA: 'TWA' }; const formatter={ A: (v)=>Formatter.formatDirection(v,undefined,!props.show360,props.leadingZero), @@ -226,18 +261,25 @@ let widgetList=[ value: keys.nav.gps.waterTemp }, formatter: 'formatTemperature', - formatterParameters: 'celsius' + editableParameters: { + unit: false, + }, + translateFunction: (props)=>{ + let u=(props?.formatterParameters?.[0]||'').toUpperCase()[0]||''; + return {...props, unit: '°'+u } + } }, { name: 'AnchorBearing', default: "---", - unit: "\u00b0", + unit: degrees, caption: 'ACHR-BRG', storeKeys:{ value:keys.nav.anchor.direction }, formatter: 'formatDirection360', editableParameters: { + unit: false, formatterParameters: true } }, @@ -271,33 +313,62 @@ let widgetList=[ { name: 'RteDistance', default: "---", - caption: 'RTE-Dst', + caption: 'RTE-DST', storeKeys:{ - value:keys.nav.route.remain + value:keys.nav.route.remain, + server: keys.nav.wp.server, }, editableParameters: { unit:false }, - formatter: 'formatDistance' + formatter: 'formatDistance', + translateFunction: (props)=>{ + return {...props, + disconnect: props.server === false, + } + }, }, { name: 'RteEta', default: " --:--:-- ", - unit: "h", - caption: 'RTE-ETA', storeKeys:{ - value:keys.nav.route.eta + value:keys.nav.route.eta, + time:keys.nav.gps.rtime, + server: keys.nav.wp.server, }, - formatter: 'formatTime' + formatter: 'formatTime', + translateFunction: (props)=>{ + return {...props, + value: !props.value?null:props.kind=='TTG'?new Date(props.value-props.time):props.value, + caption: (props.caption||'RTE-')+props.kind, + disconnect: props.server === false, + addClass: props.formatterParameters?.[0]?'med':null, + } + }, + editableParameters: { + unit: false, + kind: {type:'SELECT',list:['ETA','TTG'],default:'ETA'}, + } }, { name: 'LargeTime', default: "--:--", caption: 'Time', storeKeys:{ - value:keys.nav.gps.rtime + value:keys.nav.gps.rtime, + gpsValid: keys.nav.gps.valid, + visible: keys.properties.showClock + }, + formatter: 'formatTime', + translateFunction: (props)=>{ + return {...props, + unit: props.gpsValid?'OK':'ERROR', + addClass: (props.gpsValid?'ok':'error')+((!props.formatterParameters||props.formatterParameters?.[0])?' med':''), + } }, - formatter: 'formatClock' + editableParameters: { + unit: false, + } }, { name: 'WpPosition', @@ -305,16 +376,21 @@ let widgetList=[ caption: 'MRK', storeKeys:{ value:keys.nav.wp.position, - server: keys.nav.wp.server + server: keys.nav.wp.server, + name: keys.nav.wp.name }, updateFunction: (state)=>{ return { value: state.value, + unit: state.name, disconnect: state.server === false } }, - formatter: 'formatLonLats' + formatter: 'formatLonLats', + editableParameters: { + unit: false, + }, }, { name: 'Zoom', @@ -341,28 +417,58 @@ let widgetList=[ name: 'WindDisplay', wclass: WindWidget, }, + { + name: 'WindGraphics', + wclass: WindGraphics + }, { name: 'DepthDisplay', default: "---", caption: 'DPT', unit: 'm', storeKeys:{ - value:keys.nav.gps.depthBelowTransducer + DBK: keys.nav.gps.depthBelowKeel, + DBS: keys.nav.gps.depthBelowWaterline, + DBT: keys.nav.gps.depthBelowTransducer, + visible: keys.properties.showDepth, + }, + formatter: 'formatDistance', + formatterParameters: ['m'], + translateFunction: (props)=>{ + let kind=props.kind; + if(kind=='auto') { + kind='DBT'; + if(props.DBK !== undefined) kind='DBK'; + if(props.DBS !== undefined) kind='DBS'; + } + let depth=undefined; + if(kind=='DBT') depth=props.DBT; + if(kind=='DBK') depth=props.DBK; + if(kind=='DBS') depth=props.DBS; + return {...props, + value: depth, + caption: kind, + unit: ((props.formatterParameters instanceof Array) && props.formatterParameters.length > 0) ? props.formatterParameters[0] : props.unit, + } + }, + editableParameters:{ + unit: false, + value: false, + caption: false, + kind: {type:'SELECT',list:['auto','DBT','DBK','DBS'],default:'auto'} }, - formatter: 'formatDecimal', - formatterParameters: [3,1,true] }, { name: 'XteDisplay', wclass: XteWidget, }, - { - name: 'WindGraphics', - wclass: WindGraphics - }, { name: "DateTime", - wclass: DateTimeWidget + caption: 'Date/Time', + storeKeys:{ + value:keys.nav.gps.rtime + }, + formatter: 'formatDateTime' }, { name: 'Empty', @@ -423,8 +529,14 @@ let widgetList=[ { name:'signalKCelsius', default: "---", - unit:'°', - formatter: 'skTemperature' + formatter: 'skTemperature', + editableParameters: { + unit: false, + }, + translateFunction: (props)=>{ + let u=(props?.formatterParameters?.[0]||'').toUpperCase()[0]||''; + return {...props, unit: '°'+u } + } }, { name: 'signalKRoll', diff --git a/viewer/components/XteWidget.jsx b/viewer/components/XteWidget.jsx index 981078976..5af8d3de2 100644 --- a/viewer/components/XteWidget.jsx +++ b/viewer/components/XteWidget.jsx @@ -105,10 +105,10 @@ XteWidget.predefined={ markerXte: keys.nav.wp.xte, }, editableParameters:{ - xteMax:{type:'FLOAT',displayName:"XTE max",default:1,list:[0.1,10], description:'The end points of the XTE graph (1.0).\nAlways provide this in the unit you choose for the formatter'}, + xteMax:{type:'FLOAT',displayName:"XTE max",default:1,list:[0.01,100], description:'The end points of the XTE graph (1.0).\nAlways provide this in the unit you choose for the formatter'}, formatterParameters:true }, formatter: 'formatDistance' }; -export default XteWidget; \ No newline at end of file +export default XteWidget; From 2f2c7d3f9482f014cdc174e18115c71b6e8814dc Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:46:10 +0200 Subject: [PATCH 04/26] added options to position formatter - decimal degrees - degrees, decimal minutes - degrees, minutes, seconds - allow hemisphere first --- viewer/util/formatter.js | 89 +++++++++++++++++++++++++--------------- viewer/util/helper.js | 12 +----- 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index 8d763746a..51e482fb1 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -5,43 +5,59 @@ import navcompute from '../nav/navcompute.js'; import Helper from "./helper.js"; +function pad(num, size, pad='0') { + return (''+num).trim().padStart(size,pad); +} + /** * * @param {number} coordinate * @param axis * @returns {string} */ -const formatLonLatsDecimal=function(coordinate,axis){ - coordinate = Helper.to180(coordinate); // normalize to ±180° - - let abscoordinate = Math.abs(coordinate); - let coordinatedegrees = Math.floor(abscoordinate); - - let coordinateminutes = (abscoordinate - coordinatedegrees)/(1/60); - let numdecimal=2; - //correctly handle the toFixed(x) - will do math rounding - if (coordinateminutes.toFixed(numdecimal) == 60){ - coordinatedegrees+=1; - coordinateminutes=0; - } - if( coordinatedegrees < 10 ) { - coordinatedegrees = "0" + coordinatedegrees; - } - if (coordinatedegrees < 100 && axis == 'lon'){ - coordinatedegrees = "0" + coordinatedegrees; +const formatLonLatsDecimal=function(coordinate,axis,format='DDM',hemFirst=false){ + if(coordinate==null) { + let str="____\u00B0__.___'"; + if(format=='DD') str="____._____\u00B0"; // use _ to prevent line breaks + if(format=='DMS') str="____\u00B0__'__._\""; + return hemFirst?hem+str:str+hem; } - let str = coordinatedegrees + "\u00B0"; - - if( coordinateminutes < 10 ) { - str +="0"; - } - str += coordinateminutes.toFixed(numdecimal) + "'"; + coordinate = Helper.to180(coordinate); // normalize to ±180° + let deg = Math.abs(coordinate); + let padding = 2; + let str = '\u00A0'; + let hem = coordinate < 0 ? "S" :"N"; if (axis == "lon") { - str += coordinate < 0 ? "W" :"E"; + padding = 3; + str = ''; + hem = coordinate < 0 ? "W" :"E"; + } + if(format=='DD') { + str += pad(deg.toFixed(5),padding+6) + "\u00B0"; + } else if(format=='DMS') { + let DEG = Math.floor(deg); + let min = 60*(deg-DEG); + let MIN = Math.floor(min); + let sec = 60*(min-MIN); + if (sec.toFixed(1).startsWith('60.')){ + MIN+=1; + sec=0; + if(MIN==60){ + MIN=0; + DEG+=1; + } + } + str += pad(DEG,padding) + "\u00B0" + pad(MIN,2) + "'" + pad(sec.toFixed(1),4) + '"'; } else { - str += coordinate < 0 ? "S" :"N"; + let DEG = Math.floor(deg); + let min = 60*(deg-DEG); + if (min.toFixed(3).startsWith('60.')){ + DEG+=1; + min=0; + } + str += pad(DEG,padding) + "\u00B0" + pad(min.toFixed(3),6) + "'"; } - return str; + return hemFirst?hem+str:str+hem; }; /** @@ -49,15 +65,20 @@ const formatLonLatsDecimal=function(coordinate,axis){ * @param {Point} lonlat * @returns {string} */ -const formatLonLats=function(lonlat){ - if (! lonlat || isNaN(lonlat.lat) || isNaN(lonlat.lon)){ - return "-----"; +const formatLonLats=function(lonlat,format='DDM',hemFirst=false){ + if(format=='OLC') { + if(!lonlat||lonlat.lat==null||lonlat.lon==null) return "________+__"; + return new OpenLocationCode().encode(lonlat.lat,lonlat.lon); } - let ns=this.formatLonLatsDecimal(lonlat.lat, 'lat'); - let ew=this.formatLonLatsDecimal(lonlat.lon, 'lon'); - return ns + ', ' + ew; + let lat=this.formatLonLatsDecimal(lonlat?.lat, 'lat', format, hemFirst); + let lon=this.formatLonLatsDecimal(lonlat?.lon, 'lon', format, hemFirst); + return lat + ' ' + lon; }; -formatLonLats.parameters=[]; +formatLonLats.parameters=[ + {name:'format',type:'SELECT',list:['DD','DDM','DMS','OLC'],default:'DDM'}, + {name:'hemFirst',type:'BOOLEAN',default:false} +]; + /** * format a number with a fixed number of fractions diff --git a/viewer/util/helper.js b/viewer/util/helper.js index 47903a22a..be6646061 100644 --- a/viewer/util/helper.js +++ b/viewer/util/helper.js @@ -130,9 +130,7 @@ Helper.getParam=(key)=>{ }; Helper.to360=(a)=>{ - while (a < 360) { - a += 360; - } + while (a < 0) { a += 360; } return a % 360; }; @@ -180,13 +178,7 @@ export const concat=(...args)=>{ }); return rt; } -export const concatsp=(...args)=>{ - let rt=""; - args.forEach((a)=>{ - if (a !== undefined) rt+=" "+a; - }); - return rt; -} +export const concatsp=(...args)=>args.filter(i=>i!=null).join(' '); export const unsetOrTrue=(item)=>{ return !!(item === undefined || item); } From 8100d169b76fcb069e7ff244024f386e071794b4 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:47:44 +0200 Subject: [PATCH 05/26] added float formatter, revised decimal formatter --- viewer/util/formatter.js | 83 +++++++++++++++++++++++++--------------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index 51e482fb1..7c1698385 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -80,44 +80,68 @@ formatLonLats.parameters=[ ]; +/** + * format number with N digits + * at max N-1 digits after decimal point + * there are at least N digits and a decimal point at a variable position + * like the display of a multimeter in auto-range mode + * bigger numbers: more digits are appended to the right if necessary + * smaller numbers: up to maxPlaces decimal places are added or they get rounded to zero + * negative numbers: minus sign is added if necessary + * @param digits = number of (significant) digits in total, negative: padding space is added for sign + * @param maxPlaces = max. number of decimal places (after the decimal point, default = digits-1) + * @param leadingZeroes = use leading zeroes instead of spaces + * returns string with at least digits(+1 if digits<0) characters + */ +const formatFloat=function(number, digits, maxPlaces, leadingZeroes=false) { + let signed = digits<0; + digits = Math.abs(digits); + if(maxPlaces==null) maxPlaces=digits-1; + if(isNaN(number)) return '-'.repeat(digits+(signed?1:0)-maxPlaces)+(maxPlaces?'.'+'-'.repeat(maxPlaces):''); + if(digits==0) return number.toFixed(0); + if(number<0 && !signed) digits-=1; + let sign = number<0 ? '-' : signed ? ' ' : ''; + number = Math.abs(number); + let decPlaces = digits-1-Math.floor(Math.log10(Math.abs(number))); + decPlaces = Math.max(0,Math.min(decPlaces,Math.max(0,maxPlaces))); + let str = number.toFixed(decPlaces); + let n = digits+(str.includes('.')?1:0); // expected length of string w/o sign + if(leadingZeroes) { + return sign+'0'.repeat(Math.max(0,n-str.length))+str; // add sign and padding zeroes + } else { + return ' '.repeat(Math.max(0,n-str.length))+sign+str; // add padding spaces and sign + } +}; +formatFloat.parameters=[ + {name:'digits',type:'NUMBER'}, + {name:'maxPlaces',type:'NUMBER'}, +]; + /** * format a number with a fixed number of fractions * @param number - * @param fix - * @param fract - * @param addSpace if set - add a space for positive numbers - * @param prefixZero if set - use 0 instead of space to fill the fixed digits + * @param fix total # digits + * @param fract # of fractional digits + * @param addSpace if set - add a padding space for sign + * @param prefixZero if set - print leading zeroes, not space * @returns {string} */ const formatDecimal=function(number,fix,fract,addSpace,prefixZero){ - let sign=""; number=parseFloat(number); - if (isNaN(number)){ - rt=""; - while (fix > 0) { - rt+="-"; - fix--; - } - return rt; - } - if (addSpace !== undefined && addSpace) sign=" "; + if (isNaN(number)) return '-'.repeat(fix-fract)+(fract?'.'+'-'.repeat(fract):''); + let sign = addSpace ? ' ' : ''; if (number < 0) { number=-number; - sign="-"; + sign='-'; } - let rt=(prefixZero?"":sign)+number.toFixed(fract); - let v=10; - fix-=1; - while (fix > 0){ - if (number < v){ - if (prefixZero) rt="0"+rt; - else rt=" "+rt; - } - v=v*10; - fix-=1; + let str = number.toFixed(fract); // formatted number w/o sign + let n = fix+fract+(fract?1:0); // expected length of string w/o sign + if(prefixZero || fix<0) { + return sign+'0'.repeat(Math.max(0,n-str.length))+str; // add sign and padding zeroes + } else { + return ' '.repeat(Math.max(0,n-str.length))+sign+str; // add padding spaces and sign } - return prefixZero?(sign+rt):rt; }; formatDecimal.parameters=[ {name:'fix',type:'NUMBER'}, @@ -127,11 +151,8 @@ formatDecimal.parameters=[ ]; const formatDecimalOpt=function(number,fix,fract,addSpace,prefixZero){ number=parseFloat(number); - if (isNaN(number)) return formatDecimal(number,fix,fract,addSpace,prefixZero); - if (Math.floor(number) == number){ - return formatDecimal(number,fix,0,addSpace,prefixZero); - } - return formatDecimal(number,fix,fract,addSpace,prefixZero); + let isint = Math.floor(number) == number; + return formatDecimal(number,fix,isint?0:fract,addSpace,prefixZero); }; formatDecimalOpt.parameters=[ From bf770cad7345b41836622aac412aee8fb6f2811e Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:48:33 +0200 Subject: [PATCH 06/26] removed OLC --- viewer/util/formatter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index 7c1698385..283745383 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -75,7 +75,7 @@ const formatLonLats=function(lonlat,format='DDM',hemFirst=false){ return lat + ' ' + lon; }; formatLonLats.parameters=[ - {name:'format',type:'SELECT',list:['DD','DDM','DMS','OLC'],default:'DDM'}, + {name:'format',type:'SELECT',list:['DD','DDM','DMS'],default:'DDM'}, {name:'hemFirst',type:'BOOLEAN',default:false} ]; @@ -170,7 +170,6 @@ formatDecimalOpt.parameters=[ */ const formatDistance=function(distance,opt_unit){ let number=parseFloat(distance); - if (isNaN(number)) return " -"; //4 spaces let factor=navcompute.NM; if (opt_unit == 'm') factor=1; if (opt_unit == 'km') factor=1000; From 4f4bd6251fd77765c7e5d38af3242672ff4c0e0b Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:49:14 +0200 Subject: [PATCH 07/26] added distance and speed units --- viewer/util/formatter.js | 46 +++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index 283745383..ea3d471b9 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -171,43 +171,51 @@ formatDecimalOpt.parameters=[ const formatDistance=function(distance,opt_unit){ let number=parseFloat(distance); let factor=navcompute.NM; + if (opt_unit == 'ft') factor=1/3.280839895; // feet + if (opt_unit == 'yd') factor=3/3.280839895; // yards if (opt_unit == 'm') factor=1; if (opt_unit == 'km') factor=1000; - number=number/factor; - if (number < 1){ - return formatDecimal(number,undefined,2,false); - } - if (number < 100){ - return formatDecimal(number,undefined,1,false); - } - return formatDecimal(number,undefined,0,false); + number/=factor; + return formatFloat(number,3); }; formatDistance.parameters=[ - {name:'unit',type:'SELECT',list:['nm','m','km'],default:'nm'} + {name:'unit',type:'SELECT',list:['nm','m','km','ft','yd'],default:'nm'} ]; /** * * @param speed in m/s - * @param opt_unit one of kn,ms,kmh + * @param opt_unit one of kn,ms,kmh,bft * @returns {*} */ const formatSpeed=function(speed,opt_unit){ let number=parseFloat(speed); - if (isNaN(number)) return " -"; //2 spaces - let factor=3600/navcompute.NM; - if (opt_unit == 'ms') factor=1; - if (opt_unit == 'kmh') factor=3.6; - number=number*factor; - if (number < 100){ - return formatDecimal(number,undefined,1,false); + if (opt_unit == 'bft') { + let v=number*3600/navcompute.NM; + if(v<=1) return ' 0'; + if(v<=3) return ' 1'; + if(v<=6) return ' 2'; + if(v<=10) return ' 3'; + if(v<=16) return ' 4'; + if(v<=21) return ' 5'; + if(v<=27) return ' 6'; + if(v<=33) return ' 7'; + if(v<=40) return ' 8'; + if(v<=47) return ' 9'; + if(v<=55) return '10'; + if(v<=63) return '11'; + return '12'; } - return formatDecimal(number,undefined,0,false); + let factor=3600/navcompute.NM; + if (opt_unit == 'ms' || opt_unit == 'm/s') factor=1; + if (opt_unit == 'kmh' || opt_unit == 'km/h') factor=3.6; + number*=factor; + return formatFloat(number,3,1); }; formatSpeed.parameters=[ - {name:'unit',type:'SELECT',list:['kn','ms','kmh'],default:'kn'} + {name:'unit',type:'SELECT',list:['kn','ms','kmh','bft','m/s','km/h'],default:'kn'} ]; const formatDirection=function(dir,opt_rad,opt_180,opt_lz){ From 5c66f0bf2d42a20ff4a639af1f9b36650b8b4a55 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:50:28 +0200 Subject: [PATCH 08/26] revised formatters, added fahrenheit --- viewer/util/formatter.js | 58 +++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index ea3d471b9..e24b49b4e 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -221,7 +221,7 @@ formatSpeed.parameters=[ const formatDirection=function(dir,opt_rad,opt_180,opt_lz){ dir=opt_rad ? Helper.degrees(dir) : dir; dir=opt_180 ? Helper.to180(dir) : Helper.to360(dir); - return formatDecimal(dir,3,0,(!!opt_lz && !!opt_180),!!opt_lz); + return formatDecimal(dir,3,0,opt_180,opt_lz); }; formatDirection.parameters=[ {name:'inputRadian',type:'BOOLEAN',default:false}, @@ -230,7 +230,7 @@ formatDirection.parameters=[ ]; const formatDirection360=function(dir,opt_lz){ - return formatDecimal(dir,3,0,false,!!opt_lz); + return formatDecimal(dir,3,0,false,opt_lz); }; formatDirection360.parameters=[ {name:'leadingZero',type:'BOOLEAN',default: false,description:'show leading zeroes (012)'} @@ -241,49 +241,47 @@ formatDirection360.parameters=[ * @param {Date} curDate * @returns {string} */ -const formatTime=function(curDate){ - if (! curDate || ! (curDate instanceof Date)) return "--:--:--"; - let datestr=this.formatDecimal(curDate.getHours(),2,0).replace(" ","0")+":"+ - this.formatDecimal(curDate.getMinutes(),2,0).replace(" ","0")+":"+ - this.formatDecimal(curDate.getSeconds(),2,0).replace(" ","0"); - return datestr; +const formatTime=function(curDate, seconds=true){ + if (!(curDate instanceof Date)) return "--:--"+(seconds?':--':''); + return this.formatDecimal(curDate.getHours(),2,0,false,true)+":"+ + this.formatDecimal(curDate.getMinutes(),2,0,false,true)+(seconds?":"+ + this.formatDecimal(curDate.getSeconds(),2,0,false,true):''); }; -formatTime.parameters=[] +formatTime.parameters=[ + {name:'seconds',type:'BOOLEAN',default:true} +]; /** * * @param {Date} curDate * @returns {string} hh:mm */ const formatClock=function(curDate){ - if (! curDate || ! (curDate instanceof Date)) return "--:--"; - let datestr=this.formatDecimal(curDate.getHours(),2,0).replace(" ","0")+":"+ - this.formatDecimal(curDate.getMinutes(),2,0).replace(" ","0"); - return datestr; + if (!(curDate instanceof Date)) return "--:--"; + return this.formatDecimal(curDate.getHours(),2,0,false,true)+":"+ + this.formatDecimal(curDate.getMinutes(),2,0,false,true); }; -formatClock.parameters=[] +formatClock.parameters=[]; /** * format date and time * @param {Date} curDate * @returns {string} */ const formatDateTime=function(curDate){ - if (! curDate || ! (curDate instanceof Date)) return "----/--/-- --:--:--"; - let datestr=this.formatDecimal(curDate.getFullYear(),4,0)+"/"+ + if (!(curDate instanceof Date)) return "----/--/-- --:--:--"; + return this.formatDecimal(curDate.getFullYear(),4,0,false,true)+"/"+ this.formatDecimal(curDate.getMonth()+1,2,0,false,true)+"/"+ this.formatDecimal(curDate.getDate(),2,0,false,true)+" "+ this.formatDecimal(curDate.getHours(),2,0,false,true)+":"+ this.formatDecimal(curDate.getMinutes(),2,0,false,true)+":"+ this.formatDecimal(curDate.getSeconds(),2,0,false,true); - return datestr; }; formatDateTime.parameters=[]; const formatDate=function(curDate){ - if (! curDate || ! (curDate instanceof Date)) return "----/--/--"; - let datestr=this.formatDecimal(curDate.getFullYear(),4,0)+"/"+ - this.formatDecimal(curDate.getMonth()+1,2,0)+"/"+ - this.formatDecimal(curDate.getDate(),2,0); - return datestr; + if (!(curDate instanceof Date)) return "----/--/--"; + return this.formatDecimal(curDate.getFullYear(),4,0,false,true)+"/"+ + this.formatDecimal(curDate.getMonth()+1,2,0,false,true)+"/"+ + this.formatDecimal(curDate.getDate(),2,0,false,true); }; formatDate.parameters=[]; @@ -295,13 +293,13 @@ const formatPressure=function(data,opt_unit){ try { if (!opt_unit || opt_unit.toLowerCase() === 'pa') return formatDecimal(data); if (opt_unit.toLowerCase() === 'hpa') { - return (parseFloat(data)/100).toFixed(2) + return (parseFloat(data)/100).toFixed(2); } if (opt_unit.toLowerCase() === 'bar') { return formatDecimal(parseFloat(data)/100000,2,4,false); } }catch(e){ - return "-----"; + return "---"; } } formatPressure.parameters=[ @@ -311,17 +309,20 @@ formatPressure.parameters=[ const formatTemperature=function(data,opt_unit){ try{ if (! opt_unit || opt_unit.toLowerCase().match(/^k/)){ - return formatDecimal(data,3,1); + return formatFloat(data,3,1); } if (opt_unit.toLowerCase().match(/^c/)){ - return formatDecimal(parseFloat(data)-273.15,3,1) + return formatFloat(parseFloat(data)-273.15,3,1) + } + if (opt_unit.toLowerCase().match(/^f/)){ + return formatFloat(parseFloat(data)*9/5+32,3,1) } }catch(e){ - return "-----" + return "---" } } formatTemperature.parameters=[ - {name:'unit',type:'SELECT',list:['celsius','kelvin'],default:'kelvin'} + {name:'unit',type:'SELECT',list:['celsius','kelvin','fahrenheit'],default:'kelvin'} ] const skTemperature=formatTemperature; @@ -332,6 +333,7 @@ export default { formatTime, formatDecimalOpt, formatDecimal, + formatFloat, formatLonLats, formatLonLatsDecimal, formatDistance, From 09ddfe1979c676fa85bc5db98ca00ffcc25325f7 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:52:45 +0200 Subject: [PATCH 09/26] wind graphics as angle when ending in A --- viewer/components/WindGraphics.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/viewer/components/WindGraphics.jsx b/viewer/components/WindGraphics.jsx index 56f812e6e..93fb9a98f 100644 --- a/viewer/components/WindGraphics.jsx +++ b/viewer/components/WindGraphics.jsx @@ -75,7 +75,7 @@ const WindGraphics = (props) => { ctx.arc(width / 2, height / 2, radius * 0.97, 0, 2 * Math.PI); ctx.stroke(); let start, end; - if (current.suffix === 'A') { + if (current.suffix.endsWith('A')) { // Write left partial circle ctx.beginPath(); ctx.strokeStyle = colors.red; // red @@ -171,7 +171,6 @@ WindGraphics.predefined= { default: 'auto', description:'which wind data to be shown\nauto will try apparent, trueAngle, trueDirection and display the first found data' }, - formatter: true, formatterParameters: true, caption: true }, From dc55da22cc8179676379935743e14bba3cbd6255 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:54:32 +0200 Subject: [PATCH 10/26] let formatters handle default value --- viewer/components/DirectWidget.jsx | 19 ++++++++++--------- viewer/components/WidgetList.js | 20 -------------------- 2 files changed, 10 insertions(+), 29 deletions(-) diff --git a/viewer/components/DirectWidget.jsx b/viewer/components/DirectWidget.jsx index e728c0d0f..72025ea86 100644 --- a/viewer/components/DirectWidget.jsx +++ b/viewer/components/DirectWidget.jsx @@ -7,24 +7,25 @@ import PropTypes from 'prop-types'; import Value from './Value.jsx'; import {WidgetFrame, WidgetProps} from "./WidgetBase"; import {useStringsChanged} from "../hoc/Resizable"; +import {concatsp} from "../util/helper"; const DirectWidget=(wprops)=>{ const props=wprops.translateFunction?{...wprops,...wprops.translateFunction({...wprops})}:wprops; let val; - let vdef=props.default||'0'; - if (props.value !== undefined) { - val=props.formatter?props.formatter(props.value):vdef+""; - } - else{ - if (! isNaN(vdef) && props.formatter) val=props.formatter(vdef); - else val=vdef+""; + try { + val=props.formatter?props.formatter(props.value):props.value; + val=(val==null?'':''+val)||props.default||'---'; + }catch(error){ + val=props.default||'---'; } + if(!/^-\d/.test(val)) val=val.replaceAll('-','\u2012'); // replace - by digit wide hyphen if not a neg. number, _ would also work well const display={ value:val }; const resizeSequence=useStringsChanged(display,wprops.mode==='gps') + if(props.addClass) props.addClass=concatsp(props.addClass,'DirectWidget'); return ( - +
@@ -50,4 +51,4 @@ DirectWidget.editableParameters={ value: true }; -export default DirectWidget; \ No newline at end of file +export default DirectWidget; diff --git a/viewer/components/WidgetList.js b/viewer/components/WidgetList.js index d8d8e515a..5e7d45a38 100644 --- a/viewer/components/WidgetList.js +++ b/viewer/components/WidgetList.js @@ -25,7 +25,6 @@ const degrees='\u00b0'; let widgetList=[ { name: 'SOG', - default: "---", caption: 'SOG', storeKeys: { value: keys.nav.gps.speed, @@ -76,7 +75,6 @@ let widgetList=[ }, { name: 'Position', - default: "-------------", caption: 'BOAT', storeKeys:{ value: keys.nav.gps.position, @@ -125,7 +123,6 @@ let widgetList=[ }, { name: 'DST', - default: "---", caption: 'DST', storeKeys:{ value: keys.nav.wp.distance, @@ -168,7 +165,6 @@ let widgetList=[ }, { name: 'VMG', - default: "---", caption: 'VMG', storeKeys: { value: keys.nav.wp.vmg @@ -181,7 +177,6 @@ let widgetList=[ }, { name: 'STW', - default: '---', caption: 'STW', storeKeys:{ value: keys.nav.gps.waterSpeed @@ -193,7 +188,6 @@ let widgetList=[ }, { name: 'WindAngle', - default: "---", unit: degrees, caption: 'Wind Angle', storeKeys:WindStoreKeys, @@ -229,7 +223,6 @@ let widgetList=[ }, { name: 'WindSpeed', - default: "---", caption: 'Wind Speed', storeKeys:WindStoreKeys, formatter: 'formatSpeed', @@ -254,8 +247,6 @@ let widgetList=[ }, { name: 'WaterTemp', - default: '---', - unit: '°', caption: 'Water Temp', storeKeys: { value: keys.nav.gps.waterTemp @@ -271,7 +262,6 @@ let widgetList=[ }, { name: 'AnchorBearing', - default: "---", unit: degrees, caption: 'ACHR-BRG', storeKeys:{ @@ -285,7 +275,6 @@ let widgetList=[ }, { name: 'AnchorDistance', - default: "---", caption: 'ACHR-DST', storeKeys:{ value:keys.nav.anchor.distance @@ -298,7 +287,6 @@ let widgetList=[ }, { name: 'AnchorWatchDistance', - default: "---", caption: 'ACHR-WATCH', storeKeys:{ value:keys.nav.anchor.watchDistance @@ -312,7 +300,6 @@ let widgetList=[ { name: 'RteDistance', - default: "---", caption: 'RTE-DST', storeKeys:{ value:keys.nav.route.remain, @@ -330,7 +317,6 @@ let widgetList=[ }, { name: 'RteEta', - default: " --:--:-- ", storeKeys:{ value:keys.nav.route.eta, time:keys.nav.gps.rtime, @@ -352,7 +338,6 @@ let widgetList=[ }, { name: 'LargeTime', - default: "--:--", caption: 'Time', storeKeys:{ value:keys.nav.gps.rtime, @@ -372,7 +357,6 @@ let widgetList=[ }, { name: 'WpPosition', - default: "-------------", caption: 'MRK', storeKeys:{ value:keys.nav.wp.position, @@ -423,7 +407,6 @@ let widgetList=[ }, { name: 'DepthDisplay', - default: "---", caption: 'DPT', unit: 'm', storeKeys:{ @@ -504,7 +487,6 @@ let widgetList=[ }, { name: 'Default', //a way to access the default widget providing all parameters in the layout - default: "---", }, { name: 'RadialGauge', @@ -520,7 +502,6 @@ let widgetList=[ }, { name: 'signalKPressureHpa', - default: "---", formatter: 'skPressure', editableParameters: { unit:false @@ -528,7 +509,6 @@ let widgetList=[ }, { name:'signalKCelsius', - default: "---", formatter: 'skTemperature', editableParameters: { unit: false, From 2deacb35f94c26442a354d47faa77b5583ba8c29 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 20:57:00 +0200 Subject: [PATCH 11/26] added GNSS status widget NMEA parse xDOP, fix type and quality --- server/avnav_nmea.py | 24 +++++++++++++++++++++- viewer/components/WidgetList.js | 36 ++++++++++++++++++++++++++++++++- viewer/util/keys.jsx | 8 ++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/server/avnav_nmea.py b/server/avnav_nmea.py index 7fbd664e5..7bc8bed54 100755 --- a/server/avnav_nmea.py +++ b/server/avnav_nmea.py @@ -143,6 +143,11 @@ class NMEAParser(object): K_TIME=Key('time','the received GPS time',signalK='navigation.datetime') K_SATUSED=Key('satUsed', 'number of Sats in use',signalK='navigation.gnss.satellites') K_SATVIEW=Key('satInview', 'number of Sats in view',signalK='navigation.gnss.satellitesInView.count') + K_FIX_TYPE=Key('fixType', 'GNSS fix type (1=none, 2=2D, 3=3D)') + K_FIX_QUALITY=Key('fixQuality', 'GNSS fix quality (0=invalid, 1=nonRTK, 2=SBAS/diff, 4=RTK fixed, 5=RTK float, 6=dead reckoning)') + K_PDOP=Key('PDOP', 'Position Dilution of Precision') + K_HDOP=Key('HDOP', 'Horizontal Dilution of Precision') + K_VDOP=Key('VDOP', 'Vertical Dilution of Precision') #we will add the GPS base to all entries GPS_DATA=[ K_LAT, @@ -162,6 +167,11 @@ class NMEAParser(object): K_TIME, K_SATVIEW, K_SATUSED, + K_FIX_TYPE, + K_FIX_QUALITY, + K_PDOP, + K_HDOP, + K_VDOP, Key('transducers.*','transducer data from xdr'), K_HDGC, K_HDGM, @@ -382,9 +392,14 @@ def parseData(self,data,source='internal',sourcePriority=DEFAULT_SOURCE_PRIORITY try: if tag=='GGA': mode=int(darray[6] or 0) #quality + rt[self.K_FIX_QUALITY.key]=mode if mode >= 1 and all(darray[i] for i in (2,3,4,5)): rt[self.K_LAT.key]=self.nmeaPosToFloat(darray[2],darray[3]) rt[self.K_LON.key]=self.nmeaPosToFloat(darray[4],darray[5]) + if darray[7]: + rt[self.K_SATUSED.key]=int(darray[7]) + if darray[8]: + rt[self.K_HDOP.key]=float(darray[8]) self.addToNavData(rt,source=source,record=tag,timestamp=timestamp,priority=basePriority) return True if tag == 'GSA': @@ -393,6 +408,13 @@ def parseData(self,data,source='internal',sourcePriority=DEFAULT_SOURCE_PRIORITY used=store.getUsed() AVNLog.debug("GSA: added %d used %d",an,used) rt[self.K_SATUSED.key]=used + if darray[2]: + fix=int(darray[2]) + if fix>1: rt[self.K_FIX_TYPE.key]=fix + for i,k in enumerate((self.K_PDOP,self.K_HDOP,self.K_VDOP)): + if not k: continue + dop=float(darray[15+i]) + rt[k.key]=dop self.addToNavData(rt,source=source,record=tag,timestamp=timestamp,priority=basePriority) return True if tag=='GSV': @@ -930,4 +952,4 @@ def setValue(self,key, data, source='test', priority=1, record=None, timestamp=N continue print(line) parser.parseData(line,source='test') - navdata.print_stats() \ No newline at end of file + navdata.print_stats() diff --git a/viewer/components/WidgetList.js b/viewer/components/WidgetList.js index 5e7d45a38..d34cd5efc 100644 --- a/viewer/components/WidgetList.js +++ b/viewer/components/WidgetList.js @@ -96,7 +96,41 @@ let widgetList=[ name: 'TimeStatus', caption: 'GPS', wclass: TimeStatusWidget, - storeKeys: TimeStatusWidget.storeKeys + storeKeys: TimeStatusWidget.storeKeys, + }, + { + name: 'GNSSStatus', + caption: 'GNSS Status', + storeKeys:{ + fix: keys.nav.gps.fixType, + qual: keys.nav.gps.fixQuality, + sats: keys.nav.gps.satInview, + used: keys.nav.gps.satUsed, + hdop: keys.nav.gps.HDOP, + valid: keys.nav.gps.valid, + }, + formatter: 'formatString', + editableParameters: { + unit: false, + value: false, + }, + translateFunction: (props)=>{ + const ok = props.valid && (props.fix??3)>1 && (props.qual??1)>0; + const warn = (props.hdop??0)>5; + let q = props.qual??''; + if(q==0) q=''; + if(q==1) q=''; + if(q==2) q='SBAS'; + if(q==4) q='fixed RTK'; + if(q==5) q='floating RTK'; + if(q==6) q='dead reckoning'; + if(q==8) q='simulated'; + return {...props, + unit: warn?'HDOP':ok?'OK':'ERR', + addClass: warn?'warning':ok?'ok':'error', + value: `${props.fix??'-'}D ${props.used??'--'}/${props.sats??'--'} H${props.hdop??'--.-'} ${q}` + } + }, }, { name: 'ETA', diff --git a/viewer/util/keys.jsx b/viewer/util/keys.jsx index 6f1ef9637..c914e10cf 100644 --- a/viewer/util/keys.jsx +++ b/viewer/util/keys.jsx @@ -121,6 +121,14 @@ let keys={ speed: V, rtime: V, valid: K, + fixType: K, + satUsed: K, + satInview: K, + fixType: K, + fixQuality: K, + PDOP: K, + HDOP: K, + VDOP: K, windAngle: V, windSpeed: V, trueWindAngle: V, From a505e7b0ae744d8b32f1cdf04750a01156a9e172 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 13 Oct 2025 21:10:15 +0200 Subject: [PATCH 12/26] fixed position formatter --- viewer/util/formatter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index e24b49b4e..841e8aba6 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -20,7 +20,7 @@ const formatLonLatsDecimal=function(coordinate,axis,format='DDM',hemFirst=false) let str="____\u00B0__.___'"; if(format=='DD') str="____._____\u00B0"; // use _ to prevent line breaks if(format=='DMS') str="____\u00B0__'__._\""; - return hemFirst?hem+str:str+hem; + return hemFirst?'_'+str:str+'_'; } coordinate = Helper.to180(coordinate); // normalize to ±180° let deg = Math.abs(coordinate); From bd355b4706472c80152499a9553a700e1ab4ff81 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 01:07:21 +0200 Subject: [PATCH 13/26] make kind of depth selectable but set DBT as default removed auto select to keep value stable --- viewer/components/WidgetList.js | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/viewer/components/WidgetList.js b/viewer/components/WidgetList.js index 3481f2271..b7256d194 100644 --- a/viewer/components/WidgetList.js +++ b/viewer/components/WidgetList.js @@ -442,20 +442,36 @@ let widgetList=[ }, { name: 'DepthDisplay', - caption: 'DPT', storeKeys:{ - value: keys.nav.gps.depthBelowTransducer, + DBK: keys.nav.gps.depthBelowKeel, + DBS: keys.nav.gps.depthBelowWaterline, + DBT: keys.nav.gps.depthBelowTransducer, visible: keys.properties.showDepth, }, formatter: 'formatDistance', - formatterParameters: ['m'], + formatterParameters: ['m',3,1], + translateFunction: (props)=>{ + let kind=props.kind; + let depth=null; + depth=props.DBT; + if(kind=='DBK') depth=props.DBK; + if(kind=='DBS') depth=props.DBS; + return {...props, + value: depth, + caption: props.caption||kind, + unit: ((props.formatterParameters instanceof Array) && props.formatterParameters.length > 0) ? props.formatterParameters[0] : props.unit, + } + }, editableParameters: { + unit: false, + value: false, + kind: {type:'SELECT',list:['DBT','DBK','DBS'],default:'DBT',description:'kind of depth value, DBT=below transducer, DBK=below keel, DBS=below surface/waterline'}, maxValue: {type:'NUMBER',default:12000,description:'consider any value above this (in meters) as invalid'} } }, { - name: 'DepthDisplayFlex', - wclass: DepthDisplayFlex + name: 'DepthDisplayFlex', + wclass: DepthDisplayFlex }, { name: 'XteDisplay', From ddee41bde0a1ff9dc09b340ff36efd97307da979 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 01:08:01 +0200 Subject: [PATCH 14/26] replace : by raised colon for nicer time format --- viewer/components/DirectWidget.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/viewer/components/DirectWidget.jsx b/viewer/components/DirectWidget.jsx index 44a499f01..f56775275 100644 --- a/viewer/components/DirectWidget.jsx +++ b/viewer/components/DirectWidget.jsx @@ -23,7 +23,8 @@ const DirectWidget=(wprops)=>{ }catch(error){ val=vdef; } - if(!/^-\d/.test(val)) val=val.replaceAll('-','\u2012'); // replace - by digit wide hyphen if not a neg. number, _ would also work well + if(!/^-\d/.test(val)) val=val.replaceAll('-','\u2012'); // replace - by digit wide hyphen (figure dash) if not a neg. number, _ would also work well + val=val.replaceAll(':','\uA789'); // replace : with raised colon, looks better in time format 00:00 const display={ value:val }; From 9f3b6149237d0d458ff54bc793ab3d0612a49292 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 01:10:14 +0200 Subject: [PATCH 15/26] fixed decimal formatter and added parameter descriptions --- viewer/util/formatter.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index 716347b78..5114e2b40 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -116,21 +116,18 @@ formatFloat.parameters=[ /** * format a number with a fixed number of fractions * @param number - * @param fix total # digits - * @param fract # of fractional digits + * @param fix number of integer digits (before .) + * @param fract number of fractional digits (after .) * @param addSpace if set - add a padding space for sign * @param prefixZero if set - print leading zeroes, not space - * @returns {string} + * @returns number as string, always with decimal point */ const formatDecimal=function(number,fix,fract,addSpace,prefixZero){ number=parseFloat(number); - if (isNaN(number)) return '-'.repeat(fix-fract)+(fract?'.'+'-'.repeat(fract):''); + if (!isFinite(number)) return '-'.repeat(fix)+(fract?'.'+'-'.repeat(fract):''); let sign = addSpace ? ' ' : ''; - if (number < 0) { - number=-number; - sign='-'; - } + if (number < 0) { number=-number; sign='-'; } let str = number.toFixed(fract); // formatted number w/o sign let n = fix+fract+(fract?1:0); // expected length of string w/o sign if(prefixZero || fix<0) { @@ -140,16 +137,19 @@ const formatDecimal=function(number,fix,fract,addSpace,prefixZero){ } }; formatDecimal.parameters=[ - {name:'fix',type:'NUMBER'}, - {name: 'fract',type:'NUMBER'}, - {name: 'addSpace',type:'BOOLEAN'}, - {name: 'prefixZero',type:'BOOLEAN'} + {name:'fix',type:'NUMBER',description:'number of integer digits (before .)'}, + {name:'fract',type:'NUMBER',description:'number of fractional digits (after .)'}, + {name:'addSpace',type:'BOOLEAN',description:'add single padding space for sign'}, + {name:'prefixZero',type:'BOOLEAN',description:'add leading zeroes'} ]; + +// like formatDecimal, but with OPTional decimal point if number is integer const formatDecimalOpt=function(number,fix,fract,addSpace,prefixZero){ number=parseFloat(number); let isint = Math.floor(number) == number; return formatDecimal(number,fix,isint?0:fract,addSpace,prefixZero); }; +formatDecimalOpt.parameters=formatDecimal.parameters; formatDecimalOpt.parameters=[ {name:'fix',type:'NUMBER'}, From e8d300c6ad684a743a04beed34ab70a816def6e0 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 01:11:28 +0200 Subject: [PATCH 16/26] reworked float formatter --- viewer/util/formatter.js | 90 ++++++++++++---------------------------- 1 file changed, 27 insertions(+), 63 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index 5114e2b40..76dc0bd63 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -76,43 +76,6 @@ formatLonLats.parameters=[ ]; -/** - * format number with N digits - * at max N-1 digits after decimal point - * there are at least N digits and a decimal point at a variable position - * like the display of a multimeter in auto-range mode - * bigger numbers: more digits are appended to the right if necessary - * smaller numbers: up to maxPlaces decimal places are added or they get rounded to zero - * negative numbers: minus sign is added if necessary - * @param digits = number of (significant) digits in total, negative: padding space is added for sign - * @param maxPlaces = max. number of decimal places (after the decimal point, default = digits-1) - * @param leadingZeroes = use leading zeroes instead of spaces - * returns string with at least digits(+1 if digits<0) characters - */ -const formatFloat=function(number, digits, maxPlaces, leadingZeroes=false) { - let signed = digits<0; - digits = Math.abs(digits); - if(maxPlaces==null) maxPlaces=digits-1; - if(isNaN(number)) return '-'.repeat(digits+(signed?1:0)-maxPlaces)+(maxPlaces?'.'+'-'.repeat(maxPlaces):''); - if(digits==0) return number.toFixed(0); - if(number<0 && !signed) digits-=1; - let sign = number<0 ? '-' : signed ? ' ' : ''; - number = Math.abs(number); - let decPlaces = digits-1-Math.floor(Math.log10(Math.abs(number))); - decPlaces = Math.max(0,Math.min(decPlaces,Math.max(0,maxPlaces))); - let str = number.toFixed(decPlaces); - let n = digits+(str.includes('.')?1:0); // expected length of string w/o sign - if(leadingZeroes) { - return sign+'0'.repeat(Math.max(0,n-str.length))+str; // add sign and padding zeroes - } else { - return ' '.repeat(Math.max(0,n-str.length))+sign+str; // add padding spaces and sign - } -}; -formatFloat.parameters=[ - {name:'digits',type:'NUMBER'}, - {name:'maxPlaces',type:'NUMBER'}, -]; - /** * format a number with a fixed number of fractions * @param number @@ -151,50 +114,51 @@ const formatDecimalOpt=function(number,fix,fract,addSpace,prefixZero){ }; formatDecimalOpt.parameters=formatDecimal.parameters; -formatDecimalOpt.parameters=[ - {name:'fix',type:'NUMBER'}, - {name: 'fract',type:'NUMBER'}, - {name: 'addSpace',type:'BOOLEAN'}, - {name: 'prefixZero',type:'BOOLEAN'} -]; +// clamp x to a<=x<=b +function clamp(a,x,b) { + return Math.max(a,Math.min(x,b)); +} /** - * format number with N digits + * format number with N significant digits + * naming: the number 12.345 has 5 TOTAL digits, 2 INTEGER digits, 3 FRACTIONAL digits * at max N-1 digits after decimal point - * there are at least N digits and a decimal point at a variable position - * like the display of a multimeter in auto-range mode - * bigger numbers: more digits are appended to the right if necessary - * smaller numbers: up to maxPlaces decimal places are added or they get rounded to zero + * there are at least N total digits and the decimal point at a variable position and and optional sign + * it's like the display of a multimeter in auto-range mode + * bigger numbers: more integer digits are appended to the left if necessary, fractional digits are removed + * smaller numbers: up to maxFrac fractional digits are added (can get rounded to zero) * negative numbers: minus sign is added if necessary - * @param digits = number of (significant) digits in total, negative: padding space is added for sign - * @param maxPlaces = max. number of decimal places (after the decimal point, default = digits-1) + * @param digits = number of total digits, negative: single padding space is added for sign + * @param maxFrac = max. number of fractional digits (default = digits-1), negative: fixed value of fractional digits * @param leadingZeroes = use leading zeroes instead of spaces - * returns string with at least digits(+1 if digits<0) characters + * returns string with at least digits (+1 if digits<0) (+1 if maxFrac!=0) characters */ -const formatFloat=function(number, digits, maxPlaces, leadingZeroes=false) { - if (digits == null) digits=3; +const formatFloat=function(number, digits, maxFrac, leadingZeroes=false) { + if (!digits) digits=3; let signed = digits<0; digits = Math.abs(digits); - if(maxPlaces==null) maxPlaces=digits-1; - if(isNaN(number)) return '-'.repeat(digits+(signed?1:0)-maxPlaces)+(maxPlaces?'.'+'-'.repeat(maxPlaces):''); + if(maxFrac==null) maxFrac=digits-1; + maxFrac=clamp(0,maxFrac,digits-1); + number=parseFloat(number); // null-->NaN + if(!isFinite(number)) return '-'.repeat(digits+(signed?1:0)-maxFrac)+(maxFrac?'.'+'-'.repeat(maxFrac):''); if(digits==0) return number.toFixed(0); - if(number<0 && !signed) digits-=1; + if(number<0 && !signed) digits-=1; // make room for unexpected sign let sign = number<0 ? '-' : signed ? ' ' : ''; number = Math.abs(number); - let decPlaces = digits-1-Math.floor(Math.log10(Math.abs(number))); - decPlaces = Math.max(0,Math.min(decPlaces,Math.max(0,maxPlaces))); + let decPlaces = digits-1-Math.floor(Math.log10(number)); + decPlaces = clamp(0,decPlaces,maxFrac); let str = number.toFixed(decPlaces); let n = digits+(str.includes('.')?1:0); // expected length of string w/o sign if(leadingZeroes) { - return sign+'0'.repeat(Math.max(0,n-str.length))+str; // add sign and padding zeroes + return sign+'0'.repeat(Math.max(0,n-str.length))+str; // -001.23 } else { - return ' '.repeat(Math.max(0,n-str.length))+sign+str; // add padding spaces and sign + return ' '.repeat(Math.max(0,n-str.length))+sign+str; // __-1.23 } }; formatFloat.parameters=[ - {name:'digits',type:'NUMBER',default: 3,description:"number of (significant) digits in total, negative: padding space is added for sign"}, - {name:'maxPlaces',type:'NUMBER',default:2,description:"max. number of decimal places (after the decimal point, default = digits-1)"}, - {name: 'leadingZeroes', type: 'BOOLEAN',description: "use leading zeroes instead of spaces"} + {name:'digits',type:'NUMBER',default:3,description:"number of (significant) digits in total, negative: padding space is added for sign"}, + {name:'maxFrac',type:'NUMBER',default:2,list:[0,20],description:"max. number of decimal places (after the decimal point, default = digits-1)"}, + {name:'leadingZeroes',type:'BOOLEAN',description: "use leading zeroes instead of spaces"} ]; /** * format a distance From 04ac96a950af45709a375aa0acb84616bbc43162 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 01:11:47 +0200 Subject: [PATCH 17/26] reworked distance formatter --- viewer/util/formatter.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index 76dc0bd63..50b04d722 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -168,14 +168,15 @@ formatFloat.parameters=[ * @param opt_fixed if > 0 set this much digits at min * @param opt_fillRight if set - extend the fractional part */ -const formatDistance=function(distance,opt_unit,opt_fixed,opt_fillRight){ +const formatDistance=function(distance,opt_unit,digits,maxFrac){ let number=parseFloat(distance); let factor=unitToFactor(opt_unit||'nm'); - number/=factor; - return formatFloat(number,3); + return formatFloat(number/factor,digits,maxFrac); }; formatDistance.parameters=[ {name:'unit',type:'SELECT',list:DEPTH_UNITS,default:'nm'}, + {name:'digits',type:'NUMBER',default:3,description:"number of (significant) digits in total, negative: padding space is added for sign"}, + {name:'maxFrac',type:'NUMBER',default:1,description:"max. number of decimal places (after the decimal point, default = digits-1)"}, ]; /** From eb4ec4701c1eb97f0e9a7e7f488b2a533210dd1d Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 01:13:32 +0200 Subject: [PATCH 18/26] adjusted flex depth widget added kind to select kind of depth to display adjusted for depth formatter params --- viewer/components/DepthWidgetFlex.jsx | 36 +++++++++++++++++---------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/viewer/components/DepthWidgetFlex.jsx b/viewer/components/DepthWidgetFlex.jsx index 36adf75bc..f3ba42656 100644 --- a/viewer/components/DepthWidgetFlex.jsx +++ b/viewer/components/DepthWidgetFlex.jsx @@ -36,8 +36,12 @@ export const DepthDisplayFlex=(props)=>{ const iprops={...props}; iprops.unit=props.dunit; iprops.formatter=(v)=>{ - return formatter.formatDistance(v,props.dunit,props.digits,props.fillRight); + return formatter.formatDistance(v,props.dunit,props.digits,props.maxFrac); } + iprops.value=props.DBT; + if(props.kind=='DBK') iprops.value=props.DBK; + if(props.kind=='DBS') iprops.value=props.DBS; + iprops.caption=props.caption||props.kind; if (iprops.offset && iprops.value != null){ iprops.value+=parseFloat(iprops.offset ); } @@ -73,38 +77,46 @@ const unitConverter={ } DepthDisplayFlex.predefined={ storeKeys:{ - value: keys.nav.gps.depthBelowTransducer + DBK: keys.nav.gps.depthBelowKeel, + DBS: keys.nav.gps.depthBelowWaterline, + DBT: keys.nav.gps.depthBelowTransducer, }, editableParameters:{ formatter: false, formatterParams:false, unit: false, caption: true, + kind: { + type:'SELECT', + list:['DBT','DBK','DBS'], + default:'DBT', + description:'kind of depth value, DBT=below transducer, DBK=below keel, DBS=below surface/waterline' + }, dunit:{ type:'SELECT', displayName:"unit", list:DEPTH_UNITS, default:'m', - description:'Select the unit for the depth display'}, + description:'Select the unit for the depth display' + }, digits:{ type:'NUMBER', default:0, - description:'minimal number of digits for the depth display, set to 0 to let the system choose', + description:'minimal number of digits for the depth display, 0=global default', list:[0,10] }, - fillRight:{ - type:'BOOLEAN', - default: false, - description: 'let the fractional part extend to have the requested number of digits', - condition: {digits:(all,dv)=>dv>0} + maxFrac:{ + type:'NUMBER', + default:1, + description: 'max. number of decimal places', + list:[0,10] }, offset: new EditableFloatParameterUI({ name:'offset', displayName:'offset', default:0, description:'Add this offset to the measured value from depthBelowTransducer', - },unitConverter - ), + },unitConverter), warningd:new EditableFloatParameterUI({ name:'warningd', displayName:'warning', @@ -117,6 +129,4 @@ DepthDisplayFlex.predefined={ description:'Any value above this is considered to be invalid', },unitConverter), }, - caption: 'DBT' - } From 5095e47a639b72e95e1037c0c8e45c1e5a20fcf8 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 01:51:10 +0200 Subject: [PATCH 19/26] added tabular-nums for widgets is often already the case --- viewer/style/widgets.less | 1 + 1 file changed, 1 insertion(+) diff --git a/viewer/style/widgets.less b/viewer/style/widgets.less index 85ac56df2..4a30d5f30 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -18,6 +18,7 @@ margin: 1px; overflow: hidden; pointer-events: all; + font-variant-numeric: tabular-nums; background: white; .nightColors(); .flex-shrink(0); From 845a4fd291013b7b2e1d014ca39e614179bd8839 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 20:24:12 +0100 Subject: [PATCH 20/26] moved replacements into value fragment overflow/underflow indicators of right width --- viewer/components/DirectWidget.jsx | 15 ++++++++++----- viewer/components/Value.jsx | 9 ++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/viewer/components/DirectWidget.jsx b/viewer/components/DirectWidget.jsx index f56775275..36fab5213 100644 --- a/viewer/components/DirectWidget.jsx +++ b/viewer/components/DirectWidget.jsx @@ -1,4 +1,4 @@ -/** + /** * Created by andreas on 23.02.16. */ @@ -15,16 +15,21 @@ const DirectWidget=(wprops)=>{ let vdef=props.default||'---'; try { if (props.value != null) { - if(props.minValue != null && parseFloat(props.value) < props.minValue) { vdef='<<<'; throw new Error(); } - if(props.maxValue != null && parseFloat(props.value) > props.maxValue) { vdef='>>>'; throw new Error(); } + let outOfRange=0; + if(parseFloat(props.value) < props.minValue) outOfRange=-1; + if(parseFloat(props.value) > props.maxValue) outOfRange=+1; + if(outOfRange) { + vdef=props.formatter?props.formatter(null):vdef; // placeholder with correct with + if (outOfRange<0) vdef=vdef.replace(/./g,'<'); // underflow + if (outOfRange>0) vdef=vdef.replace(/./g,'>'); // overflow + throw new Error(); + } } val=props.formatter?props.formatter(props.value):props.value; val=(val==null?'':''+val)||vdef; }catch(error){ val=vdef; } - if(!/^-\d/.test(val)) val=val.replaceAll('-','\u2012'); // replace - by digit wide hyphen (figure dash) if not a neg. number, _ would also work well - val=val.replaceAll(':','\uA789'); // replace : with raised colon, looks better in time format 00:00 const display={ value:val }; diff --git a/viewer/components/Value.jsx b/viewer/components/Value.jsx index 822d78aec..88660df4f 100644 --- a/viewer/components/Value.jsx +++ b/viewer/components/Value.jsx @@ -8,8 +8,11 @@ import PropTypes from 'prop-types'; */ const Value=function(props){ if (! props.value) return null; - let prefix=(props.value+"").replace(/[^ ].*/,''); - let remain=(props.value+"").replace(/^ */,''); + let val=''+props.value; + val=val.replaceAll('-','\u2012'); // replace - by digit wide hyphen (figure dash) + val=val.replaceAll(':','\uA789'); // replace : with raised colon, looks better in time format 00:00 + let prefix=val.replace(/[^ ].*/,''); + let remain=val.replace(/^ */,''); return( {prefix.replace(/ /g,'0')} @@ -21,4 +24,4 @@ const Value=function(props){ Value.propTypes={ value: PropTypes.string } -export default Value; \ No newline at end of file +export default Value; From 4e08d48b5abfc2eeaf1622ec71efcb144f14b7bf Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Sun, 26 Oct 2025 22:51:44 +0100 Subject: [PATCH 21/26] added flex-basis auto --- viewer/style/widgets.less | 1 + 1 file changed, 1 insertion(+) diff --git a/viewer/style/widgets.less b/viewer/style/widgets.less index 4a30d5f30..a7bddb6d7 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -23,6 +23,7 @@ .nightColors(); .flex-shrink(0); .flex-grow(1); + .flex-basis(auto); .flex-display(); .flex-direction(column); .flex-justify-content(flex-start); From 58458a1a48ee91ce1c4bfd06a7b528ec1268ab10 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Wed, 29 Oct 2025 12:03:59 +0100 Subject: [PATCH 22/26] prevent widgets from collapsing during editing --- viewer/style/widgets.less | 1 + 1 file changed, 1 insertion(+) diff --git a/viewer/style/widgets.less b/viewer/style/widgets.less index a7bddb6d7..78ea8198d 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -61,6 +61,7 @@ } .editing &{ .flex-grow(0); + min-width: 3em; } #gpspage.editing &{ .flex-grow(1); From d602b9c50b3f543b86c9d64072c103ddda901bcd Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Wed, 29 Oct 2025 12:07:10 +0100 Subject: [PATCH 23/26] adjusted borders on nav page to 2px - calc container height with fixed border width - hide vertical widgets if empty --- viewer/style/widgets.less | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/viewer/style/widgets.less b/viewer/style/widgets.less index 78ea8198d..d35d24670 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -3,19 +3,29 @@ @x: rgba(255, 20, 7, 0.54); -@horizontalContainerHeight: 4.2em; -@horizontalContainerDoubleHeight: 8.2em; +@widgetBorderWidth: 2px; @horizontalWidgetHeight: 4em; +@horizontalContainerHeight: calc(@horizontalWidgetHeight + 2*@widgetBorderWidth); +@horizontalContainerDoubleHeight: calc(2*@horizontalWidgetHeight + 3*@widgetBorderWidth); @infoFontSize: 0.71em; .widgetInfo(){ font-size: @infoFontSize; opacity: 0.7; } +#navpage { + .widgetContainer { + padding: @widgetBorderWidth; + gap: @widgetBorderWidth; + } + .widgetContainer:empty { display: none; } + .widgetContainer.vertical { padding-bottom: 0; } + .widgetContainer.bottomLeft { padding-right: 0; } +} .widget{ position: relative; z-index: 100; - margin: 1px; + margin: 0; overflow: hidden; pointer-events: all; font-variant-numeric: tabular-nums; @@ -140,7 +150,7 @@ .flex-shrink(0); margin-left: 0; margin-right: 2px; - width: calc(100% - 2px); + width: 100%; } .editing &{ .widget{ From 8629162a742d9d3a2f836d2af75140a39cac9800 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Wed, 29 Oct 2025 12:58:08 +0100 Subject: [PATCH 24/26] added/adjusted formatter docs --- docs/hints/en_layouts.html | 32 ++++++++++++++++-------- docs/hints/layouts.html | 50 +++++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/docs/hints/en_layouts.html b/docs/hints/en_layouts.html index fe33d0e0b..85d6ad263 100644 --- a/docs/hints/en_layouts.html +++ b/docs/hints/en_layouts.html @@ -117,21 +117,33 @@

Formatter

fractional digits are only shown if the value has a fractional part same as formatDecimal + + formatFloat + format as floating-point number + digits: number of digits in total, negative values indicate signed numbers inserting a placeholder for the sign if necessary
+ maxFrac: max. number of fractional digits
+ leadingZeroes: show leading zeroes
+ + formatDistance - distance in nm/m/km - unit:
- nm - distance in nm
- m- distance in m instead of nm
- km - distance  in km instead of nm + distance in selected unit + unit: distance unit
+ nm - nautical miles
+ m - meters
+ km - Kilometers
+ ft - feet
+ yd - Yards
+ digits: number of digits in total (see formatFloat)
+ maxFrac: max. number of fractional digits (see formatFloat) formatSpeed - speed in kn|m/s|km/h - unit:
- ms - m/s instead of kn
- kmh - km/h instead of kn
- kn - kn + speed in selected unit + <>unit: speed unit
+ kn - Knots
+ ms or m/s - meters per second
+ kmh or km/h - kilometers per hour formatDirection diff --git a/docs/hints/layouts.html b/docs/hints/layouts.html index 3a9633bb9..894429d23 100644 --- a/docs/hints/layouts.html +++ b/docs/hints/layouts.html @@ -121,63 +121,75 @@

Formatierer (formatter)

nicht ganzzahlige Werte dargestellt. wie bei formatDecimal + + formatFloat + Formatierung als Gleitkommazahl + digits: Anzahl der Stellen insgesamt, negative Werte zeigen Zahlen mit potenziell negativem Vorzeichen an (signed, fügt Vorzeichenplatzhalter ein)
+ maxFrac: maximale Anzahl Nachkommastellen
+ leadingZeroes: führende Nullen anzeigen
+ + formatDistance - Entfernung in nm|m|km - unit:
- nm - Enterfnung in nm
- m - Entfernung in m statt nm
- km - Entfernung in km statt nm + Entfernung in der gewählten Einheit + unit: Entfernungseinheit
+ nm - Seemeilen (nautical miles)
+ m - Meter
+ km - Kilometer
+ ft - Fuß (foot)
+ yd - Yards
+ digits: Anzahl der Stellen (siehe formatFloat)
+ maxFrac: maximale Anzahl Nachkommastellen (siehe formatFloat) formatSpeed - Geschwindigkeit in kn|m/s|km/h + Geschwindigkeit in der gewählten Einheit unit:
- kn - knoten
- ms - m/s statt kn
- kmh - km/h statt kn + kn - Knoten
+ ms oder m/s - Meter pro Sekunde
+ kmh oder km/h - Kilometer pro Stunde formatDirection - Formatiere einen Gradwert - inputRadian: - Input in rad statt Grad
+ Richtung als Gradwert + inputRadian: Input in rad statt Grad
range180: zeige +/- 180° statt 0...360°
leadingZero: zeige immer 3 Stellen formatDirection360 - Formatiere einen Gradwert + Richtung als Gradwert immer im Bereich 0...360° leadingZero: zeige immer 3 Stellen formatTime - Formatiere einen Zeitwert (Wert muss intern ein Date Wert sein) + Formatiere einen Zeitwert (nur auf Zeit/Datumswerte anwendbar) (hh:mm:ss) -
+ seconds: Sekunden anzeigen
formatClock - Formatiere einen Zeitwert (Wert muss intern ein Date Wert sein) + Formatiere einen Zeitwert (nur auf Zeit/Datumswerte anwendbar) (hh:mm)
formatDateTime - Formatiere Datum und Uhrzeit (Wert muss intern ein Date Wert sein) + Formatiere Datum und Uhrzeit (nur auf Zeit/Datumswerte anwendbar)
formatDate - Formatiere Datum (Wert muss intern ein Date Wert sein) + Formatiere Datum (nur auf Zeit/Datumswerte anwendbar)
formatString - gibt den Input unverändert weiter + direkte Umwandlung in einen String durch JavaScript
@@ -185,7 +197,7 @@

Formatierer (formatter)

formatTemperature Formatiere eine Temperatur (seit 20210106), Input in Kelvin unit:
- celsius, kelvin + celsius, kelvin, fahrenheit formatPressure From d063e0042128a9a701c5c07bf480f996de9b438b Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 23 Feb 2026 16:44:20 +0100 Subject: [PATCH 25/26] switched widget styles to use CSS variables for improved consistency --- viewer/style/properties.less | 3 ++- viewer/style/widgets.less | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/viewer/style/properties.less b/viewer/style/properties.less index 45945dac0..ed6ad1d5d 100644 --- a/viewer/style/properties.less +++ b/viewer/style/properties.less @@ -33,6 +33,7 @@ --avnav-night-opacity: 1; --avnav-headline-height: 4em; //widgets + --avnav-widget-border-width: 2px; --avnav-left-widgets-width: 8.8em; --avnav-horizontal-widgets-height: 4em; --avnav-widget-head-color: @_widgetHeadColor; //header background @@ -59,4 +60,4 @@ --avnav-widget-head-color: rgba(50,47,47,0.6); --avnav-widget-color: var(--avnav-back-color); //widget background --avnav-widget-fore-color: var(--avnav-fore-color); -} \ No newline at end of file +} diff --git a/viewer/style/widgets.less b/viewer/style/widgets.less index 442490f63..57f2d0167 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -14,8 +14,8 @@ } #navpage { .widgetContainer { - padding: @widgetBorderWidth; - gap: @widgetBorderWidth; + padding: var(--avnav-widget-border-width); + gap: var(--avnav-widget-border-width); } .widgetContainer:empty { display: none; } .widgetContainer.vertical { padding-bottom: 0; } @@ -195,7 +195,7 @@ color: var(--avnav-widget-fore-color); .widgetHead { - .nightBackColor(#eee); + background: var(--avnav-widget-head-color); } .widgetData { @@ -411,7 +411,7 @@ border: @borderAttentionLarge; border-color: var(--avnav-attention-color); } - .smallWidget(); +.smallWidget(); .widgetData{ .routeInfo { width: 4.5em; From 146720d884aeba6abf56e359ea15429ac1b177a4 Mon Sep 17 00:00:00 2001 From: quantenschaum Date: Mon, 23 Feb 2026 17:04:07 +0100 Subject: [PATCH 26/26] AIS widget styling to use flexbox for improved layout --- viewer/style/widgets.less | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/viewer/style/widgets.less b/viewer/style/widgets.less index 57f2d0167..120b97bc4 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -310,6 +310,12 @@ .label{ width: 2em; } + .widgetData{ + display: flex; + span{ + flex: 1 0 1em; + } + } .widgetData ~ .widgetData{ padding-top: 0; } @@ -350,8 +356,10 @@ margin: 0; } } + .widgetData{ + display: block; + } } - } &.activeRouteWidget{