diff --git a/helper/autopilot/pypilot.ts b/helper/autopilot/pypilot.ts index f84445eb..64aea72a 100644 --- a/helper/autopilot/pypilot.ts +++ b/helper/autopilot/pypilot.ts @@ -158,7 +158,7 @@ const initApiRoutes = () => { return; } - let deg = req.body.value * (180 / Math.PI); + let deg = req.body.value; if (deg > 359) { deg = 359; } else if (deg < -179) { @@ -181,8 +181,7 @@ const initApiRoutes = () => { return; } - const v = req.body.value * (180 / Math.PI); - let deg = apData.target + v; + let deg = apData.target + req.body.value; if (deg > 360) { deg = 360; } else if (deg < -180) { diff --git a/src/app/app.component.css b/src/app/app.component.css index e620d083..7bcf4a76 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -121,7 +121,7 @@ mat-nav-list { position: absolute; z-index: 4901; bottom: 30px; - left: 50px; + left: 10px; max-width: calc(100% - 55px); max-height: 100px; text-align: center; @@ -193,3 +193,9 @@ mat-nav-list { display: none; } } + +@media only screen and (max-height: 600px) { + .navdataPanel { + left: 50px; + } +} diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 0f44878c..fb1d57dc 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -587,6 +587,20 @@ export class AppComponent { }) => { // detect apis this.app.data.weather.hasApi = res.apis.includes('weather'); + this.app.data.autopilot.hasApi = res.apis.includes('autopilot'); + + // prefer FB Autopilot API if enabled + this.signalk.api + .get(this.app.skApiVersion, 'vessels/self/steering/autopilot') + .subscribe( + () => { + this.app.data.autopilot.hasApi = true; + this.app.data.autopilot.isLocal = true; + }, + () => { + this.app.debug('No local AP API found.'); + } + ); // detect plugins const hasPlugin = { @@ -627,18 +641,6 @@ export class AppComponent { this.app.data.anchor.hasApi = true; } ); - - // check for Autopilot API - this.signalk.api - .get(this.app.skApiVersion, 'vessels/self/steering/autopilot') - .subscribe( - () => { - this.app.data.autopilot.hasApi = true; - }, - () => { - this.app.data.autopilot.hasApi = false; - } - ); } // ** start trail / AIS timers diff --git a/src/app/app.info.ts b/src/app/app.info.ts index 8a44ca69..a1d4583e 100644 --- a/src/app/app.info.ts +++ b/src/app/app.info.ts @@ -167,7 +167,7 @@ export class AppInfo extends Info { this.name = 'Freeboard-SK'; this.shortName = 'Freeboard'; this.description = `Signal K Chart Plotter.`; - this.version = '2.12.2'; + this.version = '2.12.3'; this.url = 'https://github.com/signalk/freeboard-sk'; this.logo = './assets/img/app_logo.png'; @@ -269,7 +269,8 @@ export class AppInfo extends Info { }, autopilot: { console: false, // display Autopilot console - hasApi: false // Server implements Autopilot API + hasApi: false, // Server implements Autopilot API + isLocal: false // FB AP API }, buildRoute: { show: false diff --git a/src/app/app.settings.ts b/src/app/app.settings.ts index 6d027846..9a60a462 100644 --- a/src/app/app.settings.ts +++ b/src/app/app.settings.ts @@ -1,4 +1,5 @@ import { Position } from './types'; +import { Convert } from './lib/convert'; // validate supplied settings against base config export function validateConfig(settings: IAppConfig): boolean { @@ -211,6 +212,9 @@ export function cleanConfig( if (typeof settings.resources === 'undefined') { settings.resources = { + fetchFilter: + '?position=[%map:longitude%,%map:latitude%]&distance=%fetch:radius%', + fetchRadius: 0, notes: { rootFilter: '?position=[%map:longitude%,%map:latitude%]&distance=%note:radius%', @@ -231,16 +235,13 @@ export function cleanConfig( if (typeof settings.resources.paths === 'undefined') { settings.resources.paths = []; } - } - // update rootFilter params - if (typeof settings.resources.notes.rootFilter !== 'undefined') { - settings.resources.notes.rootFilter = - settings.resources.notes.rootFilter.replace('dist=', 'distance='); - settings.resources.notes.rootFilter = - settings.resources.notes.rootFilter.replace( - `position=%map:latitude%,%map:longitude%`, - `position=[%map:longitude%,%map:latitude%]` - ); + if (typeof settings.resources.fetchFilter === 'undefined') { + settings.resources.fetchFilter = + '?position=[%map:longitude%,%map:latitude%]&distance=%fetch:radius%'; + } + if (typeof settings.resources.fetchRadius === 'undefined') { + settings.resources.fetchRadius = 0; + } } // apply url params @@ -325,7 +326,7 @@ export const DefaultConfig: IAppConfig = { windVectors: true, // display vessel TWD, AWD vectors laylines: false, cogLine: 10, // self COG line time (mins) - aisCogLine: 10, // self COG line time (mins) + aisCogLine: 10, // ais COG line time (mins) headingLineSize: -1 // mode for display of heading line -1 = default }, positionFormat: 'XY', @@ -378,8 +379,12 @@ export const DefaultConfig: IAppConfig = { }, resources: { // ** resource options + fetchFilter: + '?position=[%map:longitude%,%map:latitude%]&distance=%fetch:radius%', + fetchRadius: 0, // radius (NM/km) within which to return resources notes: { - rootFilter: '?position=%map:latitude%,%map:longitude%&dist=%note:radius%', // param string to provide record filtering + rootFilter: + '?position=[%map:longitude%,%map:latitude%]&distance=%note:radius%', // param string to provide record filtering getRadius: 20, // radius (NM/km) within which to return notes groupNameEdit: false, groupRequiresPosition: true @@ -506,6 +511,8 @@ export interface IAppConfig { }; resources: { // ** resource options + fetchFilter: string; // param string to provide record filtering + fetchRadius: number; // radius (NM/km) within which to return resources notes: { rootFilter: string; // param string to provide record filtering getRadius: number; // radius (NM/km) within which to return notes @@ -519,3 +526,40 @@ export interface IAppConfig { paths: string[]; }; } + +// ** process url tokens +export const processUrlTokens = (s: string, config: IAppConfig): string => { + if (!s) { + return s; + } + const ts = s.split('%'); + if (ts.length > 1) { + const uts = ts.map((i) => { + if (i === 'map:latitude') { + return config.map.center[1]; + } else if (i === 'map:longitude') { + return config.map.center[0]; + } else if (i === 'note:radius') { + const dist = + config.units.distance === 'm' + ? config.resources.notes.getRadius + : Math.floor( + Convert.nauticalMilesToKm(config.resources.notes.getRadius) + ); + return dist * 1000; + } else if (i === 'fetch:radius') { + const dist = + config.units.distance === 'm' + ? config.resources.notes.getRadius + : Math.floor( + Convert.nauticalMilesToKm(config.resources.fetchRadius) + ); + return dist * 1000; + } else { + return i; + } + }); + s = uts.join(''); + } + return s; +}; diff --git a/src/app/modules/autopilot/autopilot.component.ts b/src/app/modules/autopilot/autopilot.component.ts index 77b2cd78..b527ef4d 100644 --- a/src/app/modules/autopilot/autopilot.component.ts +++ b/src/app/modules/autopilot/autopilot.component.ts @@ -146,14 +146,18 @@ import { SKStreamFacade } from 'src/app/modules'; export class AutopilotComponent { protected autopilotOptions = { modes: [], states: [] }; private deltaSub: Subscription; - private autopilotApiPath = 'vessels/self/steering/autopilot/default'; + private autopilotApiPath: string; constructor( protected app: AppInfo, private signalk: SignalKClient, private stream: SKStreamFacade, private cdr: ChangeDetectorRef - ) {} + ) { + this.autopilotApiPath = this.app.data.autopilot.isLocal + ? 'vessels/self/steering/autopilot/default' + : 'vessels/self/autopilots/_default'; + } ngOnInit() { this.deltaSub = this.stream.delta$().subscribe((e: UpdateMessage) => { @@ -192,16 +196,15 @@ export class AutopilotComponent { } targetAdjust(value: number) { - const rad = value ? Convert.degreesToRadians(value) : 0; - if (!rad) { + if (typeof value !== 'number') { return; } this.signalk.api .put(this.app.skApiVersion, `${this.autopilotApiPath}/target/adjust`, { - value: rad + value: value, + units: 'deg' }) .subscribe( - // eslint-disable-next-line @typescript-eslint/no-explicit-any () => { this.app.debug(`Target adjusted: ${value} deg.`); }, diff --git a/src/app/modules/map/fb-map.component.ts b/src/app/modules/map/fb-map.component.ts index ab943354..2b087414 100644 --- a/src/app/modules/map/fb-map.component.ts +++ b/src/app/modules/map/fb-map.component.ts @@ -1755,23 +1755,18 @@ export class FBMapComponent implements OnInit, OnDestroy { // ** called by onMapMove() to render features within map extent private renderMapContents() { - if (this.fetchNotes()) { + if (this.shouldFetchNotes()) { this.skres.getNotes(); this.app.debug(`fetching Notes...`); } + if (this.shouldFetchResourceSets()) { + this.app.debug(`fetching ResourceSets...`); + this.skresOther.getItemsInBounds(); + } } - // ** returns true if skres.getNotes() should be called - private fetchNotes() { - this.display.layer.notes = - this.app.config.notes && - this.app.config.map.zoomLevel >= this.app.config.selections.notesMinZoom; - - this.app.debug(`lastGet: ${this.app.data.lastGet}`); - this.app.debug(`getRadius: ${this.app.config.resources.notes.getRadius}`); - if (this.fbMap.zoomLevel < this.app.config.selections.notesMinZoom) { - return false; - } + // returns true when map center has moved a distance > (threshold / 2) + private mapMoveThresholdExceeded(threshold: number): boolean { if (!this.app.data.lastGet) { this.app.data.lastGet = this.app.config.map.center; return true; @@ -1785,14 +1780,46 @@ export class FBMapComponent implements OnInit, OnDestroy { // ** if d is more than half the getRadius const cr = this.app.config.units.distance === 'ft' - ? Convert.nauticalMilesToKm(this.app.config.resources.notes.getRadius) * - 1000 - : this.app.config.resources.notes.getRadius * 1000; + ? Convert.nauticalMilesToKm(threshold) * 1000 + : threshold * 1000; + this.app.debug(`mapMoveThresholdExceeded: ${d >= cr / 2}`); if (d >= cr / 2) { this.app.data.lastGet = this.app.config.map.center; return true; + } else { + return false; + } + } + + // ** returns true if skresOther.getItemsInBounds() should be called + private shouldFetchResourceSets() { + if ( + this.app.config.resources.fetchRadius && + this.app.config.resources.fetchFilter + ) { + if (!this.skresOther.hasSelections()) { + return false; + } + return this.mapMoveThresholdExceeded(50); + } else { + return false; } - return false; + } + + // ** returns true if skres.getNotes() should be called + private shouldFetchNotes() { + this.display.layer.notes = + this.app.config.notes && + this.app.config.map.zoomLevel >= this.app.config.selections.notesMinZoom; + + this.app.debug(`lastGet: ${this.app.data.lastGet}`); + this.app.debug(`getRadius: ${this.app.config.resources.notes.getRadius}`); + if (this.fbMap.zoomLevel < this.app.config.selections.notesMinZoom) { + return false; + } + return this.mapMoveThresholdExceeded( + this.app.config.resources.notes.getRadius + ); } } diff --git a/src/app/modules/map/popovers/resource-popover.component.ts b/src/app/modules/map/popovers/resource-popover.component.ts index 96965a88..1a83d5f8 100644 --- a/src/app/modules/map/popovers/resource-popover.component.ts +++ b/src/app/modules/map/popovers/resource-popover.component.ts @@ -73,6 +73,7 @@ id: string - resource id (click)="emitModify()" matTooltip="Modify / Move" matTooltipPosition="after" + [disabled]="this.ctrl.isReadOnly" > touch_app MOVE @@ -97,6 +98,7 @@ id: string - resource id (click)="emitDelete()" matTooltip="Delete" matTooltipPosition="after" + [disabled]="this.ctrl.isReadOnly" > delete DELETE @@ -220,7 +222,8 @@ export class ResourcePopoverComponent { showNotesButton: false, canActivate: false, isActive: false, - activeText: 'ACTIVE' + activeText: 'ACTIVE', + isReadOnly: false }; constructor(public app: AppInfo) {} @@ -293,6 +296,7 @@ export class ResourcePopoverComponent { this.ctrl.showAddNoteButton = false; this.ctrl.showPointsButton = false; this.ctrl.showRelatedButton = false; + this.ctrl.isReadOnly = this.resource[1].feature.properties?.readOnly; this.properties = []; if (this.resource[1].name) { this.properties.push(['Name', this.resource[1].name]); @@ -334,6 +338,7 @@ export class ResourcePopoverComponent { this.ctrl.showAddNoteButton = false; this.ctrl.showRelatedButton = false; this.ctrl.showDeleteButton = this.ctrl.isActive ? false : true; + this.ctrl.isReadOnly = this.resource[1].feature.properties?.readOnly; } this.properties = []; this.properties.push(['Name', this.resource[1].name]); @@ -366,6 +371,7 @@ export class ResourcePopoverComponent { : false; this.properties = []; this.properties.push(['Name', this.resource[1].name]); + this.ctrl.isReadOnly = this.resource[1].properties?.readOnly; } parseRegion() { @@ -380,6 +386,7 @@ export class ResourcePopoverComponent { this.ctrl.showNotesButton = true; this.ctrl.showPointsButton = false; this.ctrl.showRelatedButton = false; + this.ctrl.isReadOnly = this.resource[1].feature.properties?.readOnly; this.properties = []; this.properties.push(['Name', this.resource[1].name]); this.properties.push(['Description', this.resource[1].description]); diff --git a/src/app/modules/map/popovers/vessel-popover.component.ts b/src/app/modules/map/popovers/vessel-popover.component.ts index dbd3441f..3f4058be 100644 --- a/src/app/modules/map/popovers/vessel-popover.component.ts +++ b/src/app/modules/map/popovers/vessel-popover.component.ts @@ -43,6 +43,23 @@ isSelf: boolean - true if vessel 'self' ], template: ` + @if(vessel.callsignVhf) { +
+
Callsign (VHF):
+
+
+ } @else if(vessel.callsignHf) { +
+
Callsign (HF):
+
+
+ }
Latitude:
- @for(i of facade.list.resourceRadius;track i) { + @for(i of facade.list.noteRadius;track i) { {{i}} } @@ -1044,6 +1044,52 @@
RESOURCES: LAYERS
+
+
+ + {{'Fetch radius: (' + + facade.availableUnits.distance.get(distunits.value) + + ')'}} + + @for(i of facade.list.resourceRadius;track i) { + {{i}} + } + + +
+
+
diff --git a/src/app/modules/settings/settings.facade.ts b/src/app/modules/settings/settings.facade.ts index 0bea6d8f..bf78476e 100644 --- a/src/app/modules/settings/settings.facade.ts +++ b/src/app/modules/settings/settings.facade.ts @@ -73,7 +73,8 @@ export class SettingsFacade { list = { nextPointTriggers: ['perpendicularPassed', 'arrivalCircleEntered'], minZoom: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17], - resourceRadius: [5, 10, 20, 50, 100, 150, 200, 500], + resourceRadius: [0, 5, 10, 20, 50, 100, 150, 200, 500], + noteRadius: [5, 10, 20, 50, 100, 150, 200, 500], applications: [], favourites: [], resourcePaths: [], @@ -100,7 +101,9 @@ export class SettingsFacade { ]), aisCogLine: new Map([ [0, 'Off'], - [10, 'On'] + [10, '10 min'], + [30, '30 min'], + [60, '60 min'] ]), aisProfiles: new Map([[0, 'Default']]) //,[1,'Navigation'] ]) }; diff --git a/src/app/modules/skresources/components/ais/ais-properties-modal.ts b/src/app/modules/skresources/components/ais/ais-properties-modal.ts index 26796f50..7ed2f94a 100644 --- a/src/app/modules/skresources/components/ais/ais-properties-modal.ts +++ b/src/app/modules/skresources/components/ais/ais-properties-modal.ts @@ -221,10 +221,14 @@ export class AISPropertiesModal implements OnInit { } if (typeof v['communication'] !== 'undefined') { if (typeof v['communication']['callsignVhf'] !== 'undefined') { - this.vInfo.callsignVhf = v['communication']['callsignVhf']['value']; + this.vInfo.callsignVhf = + v['communication']['callsignVhf']['value'] ?? + v['communication']['callsignVhf']; } if (typeof v['communication']['callsignHf'] !== 'undefined') { - this.vInfo.callsignHf = v['communication']['callsignHf']['value']; + this.vInfo.callsignHf = + v['communication']['callsignHf']['value'] ?? + v['communication']['callsignHf']; } } if (typeof v['navigation'] !== 'undefined') { diff --git a/src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts b/src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts index 76d05df9..df0b223b 100644 --- a/src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts +++ b/src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts @@ -48,7 +48,7 @@ import { SKResourceSet } from '../../resourceset-class'; matTooltipPosition="right" (click)="clearSelections()" > - check_box_outline_blank + clear_all