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 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/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/components/DepthWidgetFlex.jsx b/viewer/components/DepthWidgetFlex.jsx index d255203f7..a11737a12 100644 --- a/viewer/components/DepthWidgetFlex.jsx +++ b/viewer/components/DepthWidgetFlex.jsx @@ -36,7 +36,7 @@ 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); } if (iprops.offset && iprops.value != null){ iprops.value+=parseFloat(iprops.offset ); @@ -85,18 +85,19 @@ DepthDisplayFlex.predefined={ 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', @@ -155,4 +156,4 @@ DepthBelowWater.predefined={ value: keys.nav.gps.depthBelowWaterline }, caption:'DBW' -} \ No newline at end of file +} diff --git a/viewer/components/DirectWidget.jsx b/viewer/components/DirectWidget.jsx index 1831a781b..7494fbcc5 100644 --- a/viewer/components/DirectWidget.jsx +++ b/viewer/components/DirectWidget.jsx @@ -1,4 +1,4 @@ -/** + /** * Created by andreas on 23.02.16. */ @@ -7,6 +7,7 @@ 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)=>{ let props; @@ -16,22 +17,31 @@ const DirectWidget=(wprops)=>{ props={...wprops,value:'Error: '+e} } let val; - let vdef=props.default||'0'; - if (props.value !== undefined) { - if (props.minValue != null && parseFloat(props.value) < props.minValue)val=vdef; - else if(props.maxValue != null && parseFloat(props.value) > props.maxValue)val=vdef; - else val=props.formatter?props.formatter(props.value):vdef+""; - } - else{ - if (! isNaN(vdef) && props.formatter) val=props.formatter(vdef); - else val=vdef+""; + let vdef=props.default||'---'; + try { + if (props.value != null) { + 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; } const display={ value:val }; const resizeSequence=useStringsChanged(display,wprops.mode==='gps') + if(props.addClass) props.addClass=concatsp(props.addClass,'DirectWidget'); return ( - +
@@ -59,4 +69,4 @@ DirectWidget.editableParameters={ value: true }; -export default DirectWidget; \ No newline at end of file +export default DirectWidget; 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; diff --git a/viewer/components/WidgetList.js b/viewer/components/WidgetList.js index 46a8d2d02..2b902f167 100644 --- a/viewer/components/WidgetList.js +++ b/viewer/components/WidgetList.js @@ -22,10 +22,10 @@ import {SKPitchWidget, SKRollWidget} from "./SKWidgets"; import {CombinedWidget} from "./CombinedWidget"; import Formatter from "../util/formatter"; import {DepthBelowKeel, DepthBelowTransducer, DepthBelowWater} from "./DepthWidgetFlex"; +const degrees='\u00b0'; let widgetList=[ { name: 'SOG', - default: "---", caption: 'SOG', storeKeys: { value: keys.nav.gps.speed, @@ -38,8 +38,7 @@ let widgetList=[ }, { name: 'COG', - default: "---", - unit: "\u00b0", + unit: degrees, caption: 'COG', storeKeys:{ value: keys.nav.gps.course, @@ -47,70 +46,128 @@ 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, } }, { name: 'Position', - default: "-------------", 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', 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', - 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', - default: "---", 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 } }, @@ -122,20 +179,27 @@ 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, } }, { name: 'VMG', - default: "---", caption: 'VMG', storeKeys: { value: keys.nav.wp.vmg @@ -148,7 +212,6 @@ let widgetList=[ }, { name: 'STW', - default: '---', caption: 'STW', storeKeys:{ value: keys.nav.gps.waterSpeed @@ -160,8 +223,7 @@ let widgetList=[ }, { name: 'WindAngle', - default: "---", - unit: "\u00b0", + unit: degrees, caption: 'Wind Angle', storeKeys:WindStoreKeys, formatter: 'formatString', @@ -170,6 +232,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)'} @@ -177,8 +240,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), @@ -195,7 +258,6 @@ let widgetList=[ }, { name: 'WindSpeed', - default: "---", caption: 'Wind Speed', storeKeys:WindStoreKeys, formatter: 'formatSpeed', @@ -220,31 +282,34 @@ let widgetList=[ }, { name: 'WaterTemp', - default: '---', - unit: '°', caption: 'Water Temp', storeKeys: { 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 } }, { name: 'AnchorDistance', - default: "---", caption: 'ACHR-DST', storeKeys:{ value:keys.nav.anchor.distance @@ -257,7 +322,6 @@ let widgetList=[ }, { name: 'AnchorWatchDistance', - default: "---", caption: 'ACHR-WATCH', storeKeys:{ value:keys.nav.anchor.watchDistance @@ -271,51 +335,81 @@ 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: 'formatClock' + formatter: 'formatTime', + translateFunction: (props)=>{ + return {...props, + unit: props.gpsValid?'OK':'ERROR', + addClass: (props.gpsValid?'ok':'error')+((!props.formatterParameters||props.formatterParameters?.[0])?' med':''), + } + }, + editableParameters: { + unit: false, + } }, { name: 'WpPosition', - default: "-------------", 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', @@ -342,23 +436,26 @@ let widgetList=[ name: 'WindDisplay', wclass: WindWidget, }, + { + name: 'WindGraphics', + wclass: WindGraphics + }, { name: 'DepthDisplay', - default: "---", - caption: 'DPT', - unit: 'm', + caption: 'DBT', storeKeys:{ value:keys.nav.gps.depthBelowTransducer }, - formatter: 'formatDecimal', - formatterParameters: [3,1,true], + formatter: 'formatDistance', + formatterParameters: ['m',3,1], editableParameters: { - maxValue: {type:'NUMBER',default:12000,description:'consider any value above this (in meters) as invalid'} + unit: false, + maxValue: {type:'NUMBER',default:999,description:'consider any value above this (in meters) as invalid'} } }, { - name: 'DepthBelowTransducer', - wclass: DepthBelowTransducer + name: 'DepthBelowTransducer', + wclass: DepthBelowTransducer }, { name: 'DepthBelowKeel', @@ -372,13 +469,13 @@ let widgetList=[ 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', @@ -414,7 +511,6 @@ let widgetList=[ }, { name: 'Default', //a way to access the default widget providing all parameters in the layout - default: "---", }, { name: 'RadialGauge', @@ -430,7 +526,6 @@ let widgetList=[ }, { name: 'signalKPressureHpa', - default: "---", formatter: 'skPressure', editableParameters: { unit:false @@ -438,9 +533,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/WindGraphics.jsx b/viewer/components/WindGraphics.jsx index 48fa46879..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 @@ -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}
); @@ -172,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 }, @@ -180,4 +178,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; 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; diff --git a/viewer/style/avnav_viewer_new.less b/viewer/style/avnav_viewer_new.less index 239a905f3..c98d0dedb 100644 --- a/viewer/style/avnav_viewer_new.less +++ b/viewer/style/avnav_viewer_new.less @@ -1204,6 +1204,9 @@ span.valuePrefix{ .mdText2(); width: 6em; } + .unit { + font-weight: normal; + } .aisData { display: inline-block; text-align: left; 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 09980f161..120b97bc4 100644 --- a/viewer/style/widgets.less +++ b/viewer/style/widgets.less @@ -12,26 +12,37 @@ font-size: @infoFontSize; opacity: 0.7; } +#navpage { + .widgetContainer { + padding: var(--avnav-widget-border-width); + gap: var(--avnav-widget-border-width); + } + .widgetContainer:empty { display: none; } + .widgetContainer.vertical { padding-bottom: 0; } + .widgetContainer.bottomLeft { padding-right: 0; } +} .widget{ position: relative; z-index: 100; - margin: 0.1em; + margin: 0; overflow: hidden; pointer-events: all; + font-variant-numeric: tabular-nums; background: var(--avnav-back-color); color:var(--avnav-fore-color); .flex-shrink(0); .flex-grow(1); + .flex-basis(auto); .flex-display(); .flex-direction(column); .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 +51,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{ color:var(--avnav-attention-color); } @@ -57,6 +70,7 @@ } .editing &{ .flex-grow(0); + min-width: 3em; } #gpspage.editing &{ .flex-grow(1); @@ -86,7 +100,7 @@ .widgetData{ padding-top: 0; display: block; - white-space: pre; + white-space: pre-wrap; } .centeredWidget { .widgetData{ @@ -106,7 +120,7 @@ } .widgetContainer.horizontal{ .flex-wrap(wrap); - .flex-align-items(center); + .flex-align-items(top); max-height: @horizontalContainerHeight; .twoRows &{ max-height: @horizontalContainerDoubleHeight; @@ -131,11 +145,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: 100%; } .editing &{ .widget{ @@ -159,7 +171,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; @@ -168,101 +180,96 @@ @size1: 7em; @size15: 9em; @size2: 11em; + +.blink(@period:1s) { + animation: blinker @period linear infinite; +} + +@keyframes blinker { + 50% { opacity: 0; } +} + .widget{ + width: min-content; // allow dynamic scaling to size of content background-color: var(--avnav-widget-color); color: var(--avnav-widget-fore-color); - .bigWidget(@size){ + + .widgetHead { + background: var(--avnav-widget-head-color); + } + + .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 +289,7 @@ } } &.etaWidget{ - .smallWidget(@size1); + .smallWidget(); .widgetData{ text-align: center; margin-left: auto; @@ -294,14 +301,21 @@ } } &.aisTargetWidget{ - .smallWidget(@size1); + .smallWidget(); .aisFront{ display: inline-block; font-size: 1.5em; + line-height: 1.2em; } .label{ width: 2em; } + .widgetData{ + display: flex; + span{ + flex: 1 0 1em; + } + } .widgetData ~ .widgetData{ padding-top: 0; } @@ -342,15 +356,17 @@ margin: 0; } } + .widgetData{ + display: block; + } } - } &.activeRouteWidget{ &.approach{ background-color: var(--avnav-attention-color); } - .smallWidget(@size1); + .smallWidget(); .routeName{ margin-right: 0.2em; .mdText2(); @@ -403,7 +419,7 @@ border: @borderAttentionLarge; border-color: var(--avnav-attention-color); } -.smallWidget(@size1); +.smallWidget(); .widgetData{ .routeInfo { width: 4.5em; @@ -437,7 +453,7 @@ } &.centerDisplayWidget{ -.smallWidget(@size1); + .smallWidget(); .widgetData ~ .widgetData{ margin-top: 0; } @@ -464,28 +480,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; @@ -506,8 +527,8 @@ canvas{ } } canvas{ - height: 90%; - width: 90%; +// width: 85%; + height: 80%; } .vertical &{ height: 11em; diff --git a/viewer/util/formatter.js b/viewer/util/formatter.js index e764f1410..50b04d722 100644 --- a/viewer/util/formatter.js +++ b/viewer/util/formatter.js @@ -15,37 +15,49 @@ function pad(num, size, pad='0') { * @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?'_'+str:str+'_'; } - 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; }; /** @@ -53,114 +65,100 @@ const formatLonLatsDecimal=function(coordinate,axis){ * @param {Point} lonlat * @returns {string} */ -const formatLonLats=function(lonlat){ - if (! lonlat || isNaN(lonlat.lat) || isNaN(lonlat.lon)){ - return "-----"; - } - let ns=this.formatLonLatsDecimal(lonlat.lat, 'lat'); - let ew=this.formatLonLatsDecimal(lonlat.lon, 'lon'); - return ns + ', ' + ew; +const formatLonLats=function(lonlat,format='DDM',hemFirst=false){ + 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'],default:'DDM'}, + {name:'hemFirst',type:'BOOLEAN',default:false} +]; + /** * 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 - * @returns {string} + * @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 number as string, always with decimal point */ const formatDecimal=function(number,fix,fract,addSpace,prefixZero){ - let sign=""; number=parseFloat(number); - if (isNaN(number)){ - let rt=""; - while (fix > 0) { - rt+="-"; - fix--; - } - return rt; - } - if (addSpace !== undefined && addSpace) sign=" "; - if (number < 0) { - number=-number; - 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; + if (!isFinite(number)) return '-'.repeat(fix)+(fract?'.'+'-'.repeat(fract):''); + let sign = addSpace ? ' ' : ''; + 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) { + 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'}, - {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); - 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=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 @@ -170,75 +168,57 @@ 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); - if (isNaN(number)) return " -"; //4 spaces let factor=unitToFactor(opt_unit||'nm'); - number=number/factor; - let fract=0; - let fixed=undefined; - if (number < 1) { - fract = 2; - fixed = 1; - } - else if (number < 10){ - fract=1; - fixed=1; - } - else if (number < 100){ - fract=1; - fixed=2; - } - else{ - fixed=1+Math.floor(Math.log10(Math.abs(number))); - } - if (opt_fixed == null || opt_fixed < (fixed+fract)){ - fixed=undefined; - } - if (fixed != null){ - if (opt_fillRight){ - fract+=opt_fixed-(fixed+fract); - } - else{ - fixed+=opt_fixed-(fixed+fract); - } - } - return formatDecimal(number,fixed,fract,false,true); + return formatFloat(number/factor,digits,maxFrac); }; formatDistance.parameters=[ {name:'unit',type:'SELECT',list:DEPTH_UNITS,default:'nm'}, - {name:'numDigits', type: 'NUMBER',default: 0, description:'Always show at least this number of digits. Leave at 0 to have this flexible.'}, - {name:'fillRight', type: 'BOOLEAN',default: false, description:'let the fractional part extend to have the requested number of digits (only if numDigits > 0)'} + {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)"}, ]; /** * * @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){ 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}, @@ -247,7 +227,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)'} @@ -258,49 +238,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=[]; @@ -312,13 +290,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=[ @@ -328,17 +306,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; diff --git a/viewer/util/helper.js b/viewer/util/helper.js index 22499315b..0b39f3d63 100644 --- a/viewer/util/helper.js +++ b/viewer/util/helper.js @@ -145,9 +145,7 @@ Helper.getParam=(key)=>{ }; Helper.to360=(a)=>{ - while (a < 360) { - a += 360; - } + while (a < 0) { a += 360; } return a % 360; }; @@ -195,13 +193,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); } diff --git a/viewer/util/keys.jsx b/viewer/util/keys.jsx index fd07a1871..faa5bf75e 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,