diff --git a/angular.json b/angular.json index 14947c4a..6125909d 100644 --- a/angular.json +++ b/angular.json @@ -29,10 +29,8 @@ ], "scripts": [], "allowedCommonJsDependencies": [ - "compare-versions", + "canvas-gauges", "rx-dom-html", - "chart.js", - "css-element-queries", "howler" ] }, diff --git a/package-lock.json b/package-lock.json index e204d206..0f6d2c96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@angular/platform-server": "14.3.0", "@angular/router": "14.3.0", "@types/canvas-gauges": "^2.1.2", + "@types/howler": "^2.2.10", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "^2.0.9", "@types/js-quantities": "^1.6.4", @@ -38,12 +39,12 @@ "angular-split": "^14.1.0", "canvas-gauges": "^2.1.7", "chart.js": "^3.5.1", - "chartjs-adapter-moment": "^1.0.0", + "chartjs-adapter-moment": "^1.0.1", "codelyzer": "^6.0.0", "compare-versions": "^5.0.1", "core-js": "^3.13.1", "hammerjs": "^2.0.8", - "howler": "^2.2.1", + "howler": "^2.2.4", "jasmine-core": "~4.0.1", "jasmine-spec-reporter": "~5.0.0", "js-quantities": "^1.8.0", @@ -53,12 +54,12 @@ "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.6.0", - "moment": "^2.29.1", + "moment": "^2.29.4", "protractor": "~7.0.0", "rx-dom-html": "^7.0.3", "rxjs": "^7.5.7", "sass": "^1.49.9", - "screenfull": "^5.1.0", + "screenfull": "^6.0.2", "ts-node": "^8.10.2", "tslib": "^2.2.0", "tslint": "^6.1.3", @@ -3601,6 +3602,12 @@ "@types/send": "*" } }, + "node_modules/@types/howler": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.10.tgz", + "integrity": "sha512-MnDUQZBaDhIFTQATeO4tzOCBobtPjo2MmR0co70tMyk+HNUJB0loojn+PRkky7SrKzfvU90SqufIlQlCrtjr+A==", + "dev": true + }, "node_modules/@types/http-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", @@ -4938,12 +4945,12 @@ "dev": true }, "node_modules/chartjs-adapter-moment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", - "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", "dev": true, "peerDependencies": { - "chart.js": "^3.0.0", + "chart.js": ">=3.0.0", "moment": "^2.10.2" } }, @@ -7609,9 +7616,9 @@ } }, "node_modules/howler": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", - "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==", "dev": true }, "node_modules/hpack.js": { @@ -12598,12 +12605,12 @@ "dev": true }, "node_modules/screenfull": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", - "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz", + "integrity": "sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": "^14.13.1 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -17230,6 +17237,12 @@ "@types/send": "*" } }, + "@types/howler": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@types/howler/-/howler-2.2.10.tgz", + "integrity": "sha512-MnDUQZBaDhIFTQATeO4tzOCBobtPjo2MmR0co70tMyk+HNUJB0loojn+PRkky7SrKzfvU90SqufIlQlCrtjr+A==", + "dev": true + }, "@types/http-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", @@ -18312,9 +18325,9 @@ "dev": true }, "chartjs-adapter-moment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.0.tgz", - "integrity": "sha512-PqlerEvQcc5hZLQ/NQWgBxgVQ4TRdvkW3c/t+SUEQSj78ia3hgLkf2VZ2yGJtltNbEEFyYGm+cA6XXevodYvWA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", "dev": true, "requires": {} }, @@ -20240,9 +20253,9 @@ } }, "howler": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.3.tgz", - "integrity": "sha512-QM0FFkw0LRX1PR8pNzJVAY25JhIWvbKMBFM4gqk+QdV+kPXOhleWGCB6AiAF/goGjIHK2e/nIElplvjQwhr0jg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz", + "integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w==", "dev": true }, "hpack.js": { @@ -23907,9 +23920,9 @@ } }, "screenfull": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", - "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz", + "integrity": "sha512-AQdy8s4WhNvUZ6P8F6PB21tSPIYKniic+Ogx0AacBMjKP1GUHN2E9URxQHtCusiwxudnCKkdy4GrHXPPJSkCCw==", "dev": true }, "select-hose": { diff --git a/package.json b/package.json index e79472da..24bbdbca 100644 --- a/package.json +++ b/package.json @@ -22,9 +22,9 @@ "author": "Thomas St.Pierre ", "scripts": { "ng": "ng", - "start": "ng serve --configuration=dev", - "build": "ng build --base-href=/@mxtommy/kip/", - "build-npm": "ng build --configuration=production --base-href=/@mxtommy/kip/", + "start": "ng serve --configuration=dev --serve-path=/", + "build-dev": "ng build --configuration=dev --base-href=/@mxtommy/kip/", + "build-prod": "ng build --configuration=production --base-href=/@mxtommy/kip/", "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" @@ -51,6 +51,7 @@ "@angular/platform-server": "14.3.0", "@angular/router": "14.3.0", "@types/canvas-gauges": "^2.1.2", + "@types/howler": "^2.2.10", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "^2.0.9", "@types/js-quantities": "^1.6.4", @@ -60,12 +61,12 @@ "angular-split": "^14.1.0", "canvas-gauges": "^2.1.7", "chart.js": "^3.5.1", - "chartjs-adapter-moment": "^1.0.0", + "chartjs-adapter-moment": "^1.0.1", "codelyzer": "^6.0.0", "compare-versions": "^5.0.1", "core-js": "^3.13.1", "hammerjs": "^2.0.8", - "howler": "^2.2.1", + "howler": "^2.2.4", "jasmine-core": "~4.0.1", "jasmine-spec-reporter": "~5.0.0", "js-quantities": "^1.8.0", @@ -75,12 +76,12 @@ "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.6.0", - "moment": "^2.29.1", + "moment": "^2.29.4", "protractor": "~7.0.0", "rx-dom-html": "^7.0.3", "rxjs": "^7.5.7", "sass": "^1.49.9", - "screenfull": "^5.1.0", + "screenfull": "^6.0.2", "ts-node": "^8.10.2", "tslib": "^2.2.0", "tslint": "^6.1.3", diff --git a/src/app/app-interfaces.ts b/src/app/app-interfaces.ts index 13e94949..867bacec 100644 --- a/src/app/app-interfaces.ts +++ b/src/app/app-interfaces.ts @@ -1,11 +1,8 @@ /********************************************************************************* - * This file contains App (Kip) internal data interfaces. + * This file contains the most, but not all, common KIP App internal data types and + * struture interfaces. They are used by various services, componenets and widgets. * - * Those interfaces describe most (only is reuse is needed) shared app data types - * and data structures used in the application. They are use by various services, - * componenets and widgets. - * - * For Signal K data interfaces (external data), see signalk-interfaces file. + * For external data interfaces, such as Signal K, see signalk-interfaces file. *********************************************************************************/ import { ISignalKMetadata, State, Method } from "./signalk-interfaces"; diff --git a/src/app/app-settings.interfaces.ts b/src/app/app-settings.interfaces.ts index 7f1b2b6a..496d2173 100644 --- a/src/app/app-settings.interfaces.ts +++ b/src/app/app-settings.interfaces.ts @@ -1,6 +1,6 @@ import { IDataSet } from './data-set.service'; import { ISplitSet } from './layout-splits.service'; -import { IWidget } from './widget-manager.service'; +import { IWidget } from './widgets-interface'; import { IUnitDefaults } from './units.service'; export interface IConnectionConfig { diff --git a/src/app/app-settings.service.ts b/src/app/app-settings.service.ts index d1b0bdfe..c085dc09 100644 --- a/src/app/app-settings.service.ts +++ b/src/app/app-settings.service.ts @@ -5,7 +5,7 @@ import { Router } from '@angular/router'; import { IDataSet } from './data-set.service'; import { ISplitSet } from './layout-splits.service'; -import { IWidget } from './widget-manager.service'; +import { IWidget } from './widgets-interface'; import { IUnitDefaults } from './units.service'; import { IConfig, IAppConfig, IConnectionConfig, IThemeConfig, IWidgetConfig, ILayoutConfig, IZonesConfig, INotificationConfig, IZone, ISignalKUrl } from "./app-settings.interfaces"; diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 273e4c0d..753fed28 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -20,7 +20,7 @@ background-color: mat.get-color-from-palette($foreground, divider); } - } +} .fullheight { height: 100%; diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9d8b68b4..500bc9f2 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -96,10 +96,8 @@ export class AppComponent implements OnInit, OnDestroy { }); if (!this.appSettingsService.getNotificationConfig().sound.disableSound && !appNotification.silent) { - let sound = new Howl({ + const sound = new Howl({ src: ['assets/notification.mp3'], - autoUnlock: true, - autoSuspend: false, autoplay: true, preload: true, loop: false, @@ -118,6 +116,8 @@ export class AppComponent implements OnInit, OnDestroy { } }); sound.play(); + Howler.autoUnlock = true; + Howler.autoSuspend = false; } } ); diff --git a/src/app/app.module.ts b/src/app/app.module.ts index dca4f56c..e233d9b8 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -59,51 +59,51 @@ import { AuthenticationInterceptor } from "./authentication-interceptor"; //Components import { AppComponent } from './app.component'; import { AppHelpComponent } from './app-help/app-help.component'; -import { WidgetBlankComponent } from './widget-blank/widget-blank.component'; -import { WidgetUnknownComponent } from './widget-unknown/widget-unknown.component'; -import { WidgetTextGenericComponent } from './widget-text-generic/widget-text-generic.component'; -import { WidgetDateGenericComponent } from './widget-date-generic/widget-date-generic.component'; +import { WidgetBlankComponent } from './widgets/widget-blank/widget-blank.component'; +import { WidgetUnknownComponent } from './widgets/widget-unknown/widget-unknown.component'; +import { WidgetTextGenericComponent } from './widgets/widget-text-generic/widget-text-generic.component'; +import { WidgetDateGenericComponent } from './widgets/widget-date-generic/widget-date-generic.component'; import { DynamicWidgetContainerComponent, DynamicWidgetContainerModalComponent } from './dynamic-widget-container/dynamic-widget-container.component'; -import { SettingsComponent } from './settings/settings.component'; +import { SettingsTabsComponent } from './settings/tabs/tabs.component'; import { RootDisplayComponent } from './root-display/root-display.component'; -import { WidgetNumericComponent } from './widget-numeric/widget-numeric.component'; -import { SettingsDatasetsComponent, SettingsDatasetsModalComponent } from './settings-datasets/settings-datasets.component'; -import { SettingsSignalkComponent } from './settings-signalk/settings-signalk.component'; -import { WidgetHistoricalComponent } from './widget-historical/widget-historical.component'; +import { WidgetNumericComponent } from './widgets/widget-numeric/widget-numeric.component'; +import { SettingsDatasetsComponent, SettingsDatasetsModalComponent } from './settings/datasets/datasets.component'; +import { SettingsSignalkComponent } from './settings/signalk/signalk.component'; +import { WidgetHistoricalComponent } from './widgets/widget-historical/widget-historical.component'; import { LayoutSplitComponent } from './layout-split/layout-split.component'; -import { WidgetWindComponent, } from './widget-wind/widget-wind.component'; -import { SvgWindComponent } from './svg-wind/svg-wind.component'; -import { WidgetGaugeComponent } from './widget-gauge/widget-gauge.component'; -import { GaugeSteelComponent } from './gauge-steel/gauge-steel.component'; -import { WidgetTutorialComponent } from './widget-tutorial/widget-tutorial.component'; +import { WidgetWindComponent, } from './widgets/widget-wind/widget-wind.component'; +import { SvgWindComponent } from './widgets/svg-wind/svg-wind.component'; +import { WidgetGaugeComponent } from './widgets/widget-gauge/widget-gauge.component'; +import { GaugeSteelComponent } from './widgets/gauge-steel/gauge-steel.component'; +import { WidgetTutorialComponent } from './widgets/widget-tutorial/widget-tutorial.component'; import { ResetConfigComponent } from './reset-config/reset-config.component'; -import { WidgetButtonComponent } from './widget-button/widget-button.component'; -import { ModalWidgetComponent } from './modal-widget/modal-widget.component'; -import { WidgetSwitchComponent } from './widget-switch/widget-switch.component' +import { WidgetButtonComponent } from './widgets/widget-button/widget-button.component'; +import { ModalWidgetConfigComponent } from './modal-widget-config/modal-widget-config.component'; +import { WidgetSwitchComponent } from './widgets/widget-switch/widget-switch.component' import { ModalPathSelectorComponent } from './modal-path-selector/modal-path-selector.component'; -import { SettingsUnitsComponent } from './settings-units/settings-units.component'; -import { SettingsZonesComponent, DialogNewZone, DialogEditZone } from './settings-zones/settings-zones.component'; -import { WidgetIframeComponent } from './widget-iframe/widget-iframe.component'; -import { SettingsConfigComponent } from './settings-config/settings-config.component'; -import { WidgetGaugeNgLinearComponent } from './widget-gauge-ng-linear/widget-gauge-ng-linear.component'; -import { WidgetGaugeNgRadialComponent } from './widget-gauge-ng-radial/widget-gauge-ng-radial.component'; +import { SettingsUnitsComponent } from './settings/units/units.component'; +import { SettingsZonesComponent, DialogNewZone, DialogEditZone } from './settings/zones/zones.component'; +import { WidgetIframeComponent } from './widgets/widget-iframe/widget-iframe.component'; +import { SettingsConfigComponent } from './settings/config/config.component'; +import { WidgetGaugeNgLinearComponent } from './widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component'; +import { WidgetGaugeNgRadialComponent } from './widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component'; import { AlarmMenuComponent } from './alarm-menu/alarm-menu.component'; -import { WidgetAutopilotComponent } from './widget-autopilot/widget-autopilot.component'; -import { SvgAutopilotComponent } from './svg-autopilot/svg-autopilot.component'; -import { SettingsNotificationsComponent } from './settings-notifications/settings-notifications.component'; -import { SvgSimpleLinearGaugeComponent } from './svg-simple-linear-gauge/svg-simple-linear-gauge.component'; -import { WidgetSimpleLinearComponent } from './widget-simple-linear/widget-simple-linear.component'; +import { WidgetAutopilotComponent } from './widgets/widget-autopilot/widget-autopilot.component'; +import { SvgAutopilotComponent } from './widgets/svg-autopilot/svg-autopilot.component'; +import { SettingsNotificationsComponent } from './settings/notifications/notifications.component'; +import { SvgSimpleLinearGaugeComponent } from './widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component'; +import { WidgetSimpleLinearComponent } from './widgets/widget-simple-linear/widget-simple-linear.component'; import { DataBrowserComponent } from './data-browser/data-browser.component'; import { DataBrowserRowComponent, DialogUnitSelect } from './data-browser-row/data-browser-row.component'; import { ModalUserCredentialComponent } from './modal-user-credential/modal-user-credential.component'; -import { WidgetRaceTimerComponent } from './widget-race-timer/widget-race-timer.component'; -import { WidgetLoginComponent } from './widget-login/widget-login.component'; +import { WidgetRaceTimerComponent } from './widgets/widget-race-timer/widget-race-timer.component'; +import { WidgetLoginComponent } from './widgets/widget-login/widget-login.component'; const appRoutes: Routes = [ { path: '', redirectTo: '/page/0', pathMatch: 'full' }, { path: 'page/:id', component: RootDisplayComponent }, - { path: 'settings', component: SettingsComponent }, + { path: 'settings', component: SettingsTabsComponent }, { path: 'help', component: AppHelpComponent }, { path: 'data', component: DataBrowserComponent }, { path: 'reset', component: ResetConfigComponent }, @@ -130,7 +130,7 @@ const appNetworkInitializerFn = (appNetInitSvc: AppNetworkInitService) => { RootDisplayComponent, AppComponent, AppHelpComponent, - SettingsComponent, + SettingsTabsComponent, DynamicWidgetContainerComponent, DynamicWidgetContainerModalComponent, DialogUnitSelect, @@ -171,13 +171,13 @@ const appNetworkInitializerFn = (appNetInitSvc: AppNetworkInitService) => { DialogEditZone, LayoutSplitComponent, ResetConfigComponent, - ModalWidgetComponent, + ModalWidgetConfigComponent, ModalPathSelectorComponent, ModalUserCredentialComponent, AlarmMenuComponent, SettingsNotificationsComponent, DataBrowserComponent, - DataBrowserRowComponent, + DataBrowserRowComponent ], imports: [ BrowserModule, diff --git a/src/app/base-widget/base-widget.component.spec.ts b/src/app/base-widget/base-widget.component.spec.ts new file mode 100644 index 00000000..97dcbb46 --- /dev/null +++ b/src/app/base-widget/base-widget.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BaseWidgetComponent } from './base-widget.component'; + +describe('BaseWidgetComponent', () => { + let component: BaseWidgetComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ BaseWidgetComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(BaseWidgetComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/base-widget/base-widget.component.ts b/src/app/base-widget/base-widget.component.ts new file mode 100644 index 00000000..356a32d3 --- /dev/null +++ b/src/app/base-widget/base-widget.component.ts @@ -0,0 +1,173 @@ +import { Component, Input, inject } from '@angular/core'; +import { Observable, Observer, Subscription, sampleTime } from 'rxjs'; +import { SignalKService, pathRegistrationValue } from '../signalk.service'; +import { UnitsService } from '../units.service'; +import { ITheme, IWidget, IWidgetSvcConfig } from '../widgets-interface'; + + +interface IWidgetDataStream { + pathName: string; + observable: Observable; +}; + +@Component({ + template: '' +}) +export abstract class BaseWidgetComponent { + @Input() theme!: ITheme; + @Input() widgetProperties!: IWidget; + + /** Default Widget configuration Object properties. This Object is only used as a template once when Widget is added to KIP's UI and it is automatically pushed to the AppSettings service (the configuration storage service). From then on, any configuration changes made by users using the Widget Options UI are only stored in AppSettings service. defaultConfig is never used again. */ + public defaultConfig: IWidgetSvcConfig = undefined; + /** Array of data paths use for observable automatic setup and cleanup */ + protected dataStream: Array = undefined; + /** Single Observable Subcription objec for all data paths */ + private dataSubscription: Subscription = undefined; + /** Signal K data stream service to obtain/observe server data */ + protected signalKService = inject(SignalKService); + /** Unit conversion service to convert a wide range of numerical data formats */ + protected unitsService = inject(UnitsService); + + constructor() { + } + + /** + * Will iterate and creates all Widget Observables based on the Widget's widgetProperties.config.paths + * child Objects definitions. If no widgetProperties.config.paths child Objects definitions + * exists, execution returns without further execution. + * + * This method will be automalically called by observeDataStream() if it finds that no Observable + * have been created. + * + * This methode can be called manually if you are not using observeDataStream() and you are manually + * handling Observer operations for your custom needs. + * + * @protected + * @return {*} {void} + * @memberof BaseWidgetComponent + */ + protected createDataOservable(): void { + // check if Widget has properties + if (this.widgetProperties === undefined) return; + if (Object.keys(this.widgetProperties.config?.paths).length == 0) { + this.dataStream = undefined; + return; + } else { + this.dataStream = []; + } + + Object.keys(this.widgetProperties.config.paths).forEach(pathKey => { + // check if Widget has valide path + if (typeof(this.widgetProperties.config.paths[pathKey].path) != 'string' || this.widgetProperties.config.paths[pathKey].path == '' || this.widgetProperties.config.paths[pathKey].path == null) { + return; + } else { + this.dataStream.push({ + pathName: pathKey, + observable: this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths[pathKey].path, this.widgetProperties.config.paths[pathKey].source) + }); + } + }) + } + + /** + * Use this method the subscribe to a Signal K data path Observable and receive a + * live data stream from the server. This method apply + * widgetProperties.config.paths[pathName] Object's properties: path, source, pathType, + * convertUnitTo and sampleTime, to setup the the Observer with defined behavior. + * + * @protected + * @param {string} pathName the [key: string] name of the path IWidgetPath Object ie. paths: { "numericPath"... Look at you this.defaultConfig Object to identify the string key to use. + * @param {((value) => void)} subscribeNextFunction The callback function for the Next notification delivered by the Observer. The function has the same properties as a standard subscribe callback fonction. ie. observer.subscribe( x => { console.log(x) } ). + * @return {*} + * @memberof BaseWidgetComponent + */ + protected observeDataStream(pathName: string, subscribeNextFunction: ((value) => void)) { + if (this.dataStream === undefined) { + this.createDataOservable(); + } + + const observer = this.buildObserver(pathName, subscribeNextFunction); + + let pathObs = this.dataStream.find((stream: IWidgetDataStream) => { + return stream.pathName === pathName; + }) + + // check Widget paths Observable(s) + if (pathObs === undefined) return; + + if (this.dataSubscription === undefined) { + this.dataSubscription = pathObs.observable.pipe(sampleTime(this.widgetProperties.config.paths[pathName].sampleTime)).subscribe(observer); + } else { + this.dataSubscription.add(pathObs.observable.pipe(sampleTime(this.widgetProperties.config.paths[pathName].sampleTime)).subscribe(observer)); + } + } + + private buildObserver(pathKey: string, subscribeNextFunction: ((value) => void)): Observer { + const observer: Observer = { + next: (x: pathRegistrationValue) => subscribeNextFunction(x), + error: err => console.error('Observer got an error: ' + err), + complete: () => console.log('Observer got a complete notification: ' + pathKey), + }; + + switch (this.widgetProperties.config.paths[pathKey].pathType) { + case 'number': + observer.next = + (x: pathRegistrationValue) => { + x.value = this.unitsService.convertUnit(this.widgetProperties.config.paths[pathKey].convertUnitTo, x.value); + subscribeNextFunction(x); + } + break; + + default: + break; + } + return observer; + } + + /** + * This methode will automatically ensure that Widget min/max values and decimal places + * are applied. To respect decimal places a strong must be returned, else trailing + * zeros are stripped. + * + * @protected + * @param {number} v the value to format + * @return {*} {string} the final ouput to display + * @memberof BaseWidgetComponent + */ + protected formatWidgetNumberValue(v: number): string { + if (v == null) {return} + // As per Widget config + // - Limit value to Min/Max range + if (v >= this.widgetProperties.config.maxValue) { + v = this.widgetProperties.config.maxValue; + } else if (v <= this.widgetProperties.config.minValue) { + v = this.widgetProperties.config.minValue; + } + // - Strip decimals but keep as a string type for blank trailling decimal positions + let vStr: string = v.toFixed(this.widgetProperties.config.numDecimal); + + return vStr; + } + + /** + * Call this method to automatically unsubscribe all Widget Observers, cleanup KIP's Observable + * registry and reset Widget Subscriptions to free ressources. + * + * Should be called in ngOnDestroy(). + * + * @protected + * @memberof BaseWidgetComponent + */ + protected unsubscribeDataStream(): void { + if (this.dataSubscription !== undefined) { + this.dataSubscription.unsubscribe(); + // Cleanup KIP's pathRegister + Object.keys(this.widgetProperties.config.paths).forEach(pathKey => { + this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths[pathKey].path); + } + ); + this.dataSubscription = undefined; + this.dataStream = undefined; + } + } +} diff --git a/src/app/config.demo.const.ts b/src/app/config.demo.const.ts index 4477fc00..6ae2f7ab 100644 --- a/src/app/config.demo.const.ts +++ b/src/app/config.demo.const.ts @@ -66,7 +66,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "m" + "convertUnitTo": "m", + "sampleTime": 500 } }, "displayName": "Depth", @@ -88,7 +89,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "deg" + "convertUnitTo": "deg", + "sampleTime": 500 }, "trueWindAngle": { "description": "True Wind Angle", @@ -96,7 +98,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "deg" + "convertUnitTo": "deg", + "sampleTime": 500 }, "trueWindSpeed": { "description": "True Wind Speed", @@ -104,7 +107,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "knots" + "convertUnitTo": "knots", + "sampleTime": 500 }, "appWindAngle": { "description": "Apparent Wind Angle", @@ -112,7 +116,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "deg" + "convertUnitTo": "deg", + "sampleTime": 500 }, "appWindSpeed": { "description": "Apparent Wind Speed", @@ -120,7 +125,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "knots" + "convertUnitTo": "knots", + "sampleTime": 500 } }, "filterSelfPaths": true, @@ -143,7 +149,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "knots" + "convertUnitTo": "knots", + "sampleTime": 500 } }, "gaugeType": "ngRadial", @@ -169,7 +176,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "knots" + "convertUnitTo": "knots", + "sampleTime": 500 } }, "gaugeType": "ngLinearHorizontal", @@ -192,7 +200,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "knots" + "convertUnitTo": "knots", + "sampleTime": 500 } }, "displayName": "Speed", @@ -214,7 +223,8 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "knots" + "convertUnitTo": "knots", + "sampleTime": 500 } }, "displayName": "VMG", @@ -254,12 +264,14 @@ export const DemoWidgetConfig: IWidgetConfig = { "source": "default", "pathType": "number", "isPathConfigurable": true, - "convertUnitTo": "deg" + "convertUnitTo": "deg", + "sampleTime": 500 } }, "gaugeType": "ngRadial", "gaugeTicks": false, "radialSize": "baseplateCompass", + "compassUseNumbers": false, "minValue": 0, "maxValue": 360, "numInt": 1, diff --git a/src/app/data-browser/data-browser.component.ts b/src/app/data-browser/data-browser.component.ts index a447c827..80ef8840 100644 --- a/src/app/data-browser/data-browser.component.ts +++ b/src/app/data-browser/data-browser.component.ts @@ -41,7 +41,7 @@ export class DataBrowserComponent implements OnInit, AfterViewInit { setTimeout(()=>{ this.pathsSub = this.SignalKService.getPathsObservable().subscribe(paths => { this.tableData.data = paths - })},0); // settimeout to make it async otherwise delays page load + })},0); // set timeout to make it async otherwise delays page load } ngAfterViewInit() { diff --git a/src/app/data-set.service.ts b/src/app/data-set.service.ts index c2d5c004..36d956c8 100644 --- a/src/app/data-set.service.ts +++ b/src/app/data-set.service.ts @@ -1,5 +1,5 @@ -import { Injectable } from '@angular/core'; -import { Subscription , Observable , BehaviorSubject, interval } from 'rxjs'; +import { Injectable, NgZone } from '@angular/core'; +import { Subscription, BehaviorSubject, interval } from 'rxjs'; import { AppSettingsService } from './app-settings.service'; import { SignalKService } from './signalk.service'; @@ -52,12 +52,13 @@ export class DataSetService { constructor( private AppSettingsService: AppSettingsService, private SignalKService: SignalKService, + private zones: NgZone ) { this.dataSets = AppSettingsService.getDataSets(); } public startAllDataSets() { - console.log("Starting " + this.dataSets.length.toString() + " DataSets"); + console.log("[DataSet Service] Starting " + this.dataSets.length.toString() + " DataSets"); for (let i = 0; i < this.dataSets.length; i++) { this.startDataSet(this.dataSets[i].uuid); } @@ -140,9 +141,6 @@ export class DataSetService { // initialize data this.dataSetSub[dataSubIndex].data = []; - //for (let i=0; i { - this.aggregateDataCache(uuid); + // start update timer out side of zones to remove change detection triggers. We observe the array data updates, not the data directly comming from SK + this.zones.runOutsideAngular(() => { + this.dataSetSub[dataSubIndex].updateTimerSub = interval (1000 * this.dataSets[dataIndex].updateTimer).subscribe(x => { + this.aggregateDataCache(uuid); + }); }); } diff --git a/src/app/dynamic-widget-container/dynamic-widget-container.component.css b/src/app/dynamic-widget-container/dynamic-widget-container.component.css deleted file mode 100644 index a4ce1f41..00000000 --- a/src/app/dynamic-widget-container/dynamic-widget-container.component.css +++ /dev/null @@ -1,37 +0,0 @@ -.dynamicWidgetContainer { - position: relative; - width: 100%; - height: 100%; -} - -.mat-card { - display: block; - position: absolute !important; - width: calc(100% - 4px); - height: calc(100% - 5px); - margin: 3px auto auto 2px; - padding: 0px; - transition: none; - transition-property: none; -} - -.selectWidgetMenu { - position: absolute; - right: 5%; - bottom: 5%; -} - -.settingsButton { - position: absolute; - bottom: 5%; - left: 50%; - transform: translate(-50%); -} - -.mat-select-trigger { - min-width: 30px !important; -} - -.full-width { - width: 100%; -} diff --git a/src/app/dynamic-widget-container/dynamic-widget-container.component.html b/src/app/dynamic-widget-container/dynamic-widget-container.component.html index c902b29e..0340a0a4 100644 --- a/src/app/dynamic-widget-container/dynamic-widget-container.component.html +++ b/src/app/dynamic-widget-container/dynamic-widget-container.component.html @@ -10,4 +10,12 @@ + + + + + + + + diff --git a/src/app/dynamic-widget-container/dynamic-widget-container.component.scss b/src/app/dynamic-widget-container/dynamic-widget-container.component.scss new file mode 100644 index 00000000..8aaa3a4a --- /dev/null +++ b/src/app/dynamic-widget-container/dynamic-widget-container.component.scss @@ -0,0 +1,70 @@ +@use '@angular/material' as mat; + +@mixin dynamic-widget-container-theme($theme) { + + $themeEmitter: map-get($theme, ngGauge); + $themeForeground: map-get($theme, foreground); + .primary { + color: mat.get-color-from-palette($themeEmitter, primary-gaugeFaceLight); + } + .accent { + color: mat.get-color-from-palette($themeEmitter, accent-gaugeFaceLight); + } + .warn { + color: mat.get-color-from-palette($themeEmitter, warn-gaugeFaceLight); + } + .primaryDark { + color: mat.get-color-from-palette($themeEmitter, primary-gaugeFaceDark); + } + .accentDark { + color: mat.get-color-from-palette($themeEmitter, accent-gaugeFaceDark); + } + .warnDark { + color: mat.get-color-from-palette($themeEmitter, warn-gaugeFaceDark); + } + .background { + color: mat.get-color-from-palette($themeForeground, divider); + } + .text { + color: mat.get-color-from-palette($themeForeground, text); + } + +} + +.dynamicWidgetContainer { + position: relative; + width: 100%; + height: 100%; +} + +.mat-card { + display: block; + position: absolute !important; + width: calc(100% - 4px); + height: calc(100% - 5px); + margin: 3px auto auto 2px; + padding: 0px; + transition: none; + transition-property: none; +} + +.selectWidgetMenu { + position: absolute; + right: 5%; + bottom: 5%; +} + +.settingsButton { + position: absolute; + bottom: 5%; + left: 50%; + transform: translate(-50%); +} + +.mat-select-trigger { + min-width: 30px !important; +} + +.full-width { + width: 100%; +} diff --git a/src/app/dynamic-widget-container/dynamic-widget-container.component.ts b/src/app/dynamic-widget-container/dynamic-widget-container.component.ts index 91d075a4..53534258 100644 --- a/src/app/dynamic-widget-container/dynamic-widget-container.component.ts +++ b/src/app/dynamic-widget-container/dynamic-widget-container.component.ts @@ -2,81 +2,102 @@ * This component is hosted in layout-split and handles Widget framework operations and * dynamic instanciation. */ -import { Component, OnInit, Input, Inject, ComponentRef, ViewChild, ViewContainerRef } from '@angular/core'; +import { Component, OnInit, OnDestroy, Input, Inject, ViewChild, ViewContainerRef, ElementRef, SimpleChanges } from '@angular/core'; +import { Subscription } from 'rxjs'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { UntypedFormControl } from '@angular/forms'; import { cloneDeep } from "lodash-es"; -import { ModalWidgetComponent } from '../modal-widget/modal-widget.component'; -import { WidgetManagerService, IWidget } from '../widget-manager.service'; import { DynamicWidgetDirective } from '../dynamic-widget.directive'; +import { DynamicWidget, IWidget, ITheme } from '../widgets-interface'; +import { ModalWidgetConfigComponent } from '../modal-widget-config/modal-widget-config.component'; +import { AppSettingsService } from '../app-settings.service'; +import { WidgetManagerService } from '../widget-manager.service'; import { WidgetListService, widgetList } from '../widget-list.service'; -/** - * Used to add data properties to ComponentRef/Widgets so they are exposed as - * @input() decorators in the Widget instance. - * - * @export - * @abstract - * @class DynamicComponentData - */ -export abstract class DynamicComponentData { - unlockStatus: boolean; - widgetProperties: IWidget; - widgetUUID: string; -} - @Component({ selector: 'app-dynamic-widget-container', templateUrl: './dynamic-widget-container.component.html', - styleUrls: ['./dynamic-widget-container.component.css'] + styleUrls: ['./dynamic-widget-container.component.scss'] }) -export class DynamicWidgetContainerComponent implements OnInit { +export class DynamicWidgetContainerComponent implements OnInit, OnDestroy { @Input('splitUUID') splitUUID: string; // Get UUID from layout-split. We use it as the widgetUUID later for the widget @Input('unlockStatus') unlockStatus: boolean; // From layout-split. @ViewChild(DynamicWidgetDirective, {static: true, read: ViewContainerRef}) dynamicWidgetContainerRef : ViewContainerRef; // Parent layout-split container ref - + // hack to access material-theme palette colors + @ViewChild('primary', {static: true, read: ElementRef}) private primary: ElementRef; + @ViewChild('accent', {static: true, read: ElementRef}) private accent: ElementRef; + @ViewChild('warn', {static: true, read: ElementRef}) private warn: ElementRef; + @ViewChild('primaryDark', {static: true, read: ElementRef}) private primaryDark: ElementRef; + @ViewChild('accentDark', {static: true, read: ElementRef}) private accentDark: ElementRef; + @ViewChild('warnDark', {static: true, read: ElementRef}) private warnDark: ElementRef; + @ViewChild('background', {static: true, read: ElementRef}) private background: ElementRef; + @ViewChild('text', {static: true, read: ElementRef}) private text: ElementRef; + + private themeNameSub: Subscription = null; private splitWidgetSettings: IWidget; + private themeColor: ITheme = {primary: '', accent: '', warn: '', primaryDark: '', accentDark: '', warnDark: '', background: '', text: ''}; public widgetInstance; - private newContainerRefRef: ComponentRef<{}>; constructor( public dialog: MatDialog, + private appSettingsService: AppSettingsService, // need for theme change subscription private WidgetManagerService: WidgetManagerService, private widgetListService: WidgetListService) { } ngOnInit() { - this.widgetInstance = null; + this.subscribeTheme(); + } + + private loadTheme(): void { + this.themeColor.primary = getComputedStyle(this.primary.nativeElement).color; + this.themeColor.accent = getComputedStyle(this.accent.nativeElement).color; + this.themeColor.warn = getComputedStyle(this.warn.nativeElement).color; + this.themeColor.primaryDark = getComputedStyle(this.primaryDark.nativeElement).color; + this.themeColor.accentDark = getComputedStyle(this.accentDark.nativeElement).color; + this.themeColor.warnDark = getComputedStyle(this.warnDark.nativeElement).color; + this.themeColor.background = getComputedStyle(this.background.nativeElement).color; + this.themeColor.text = getComputedStyle(this.text.nativeElement).color; + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.splitUUID && !changes.splitUUID.firstChange) { + this.instanciateWidget(); + } + + if (changes.unlockStatus && !changes.unlockStatus.firstChange) { + if(this.splitWidgetSettings.type == 'WidgetTutorial') { + this.widgetInstance.unlockStatus = this.unlockStatus; // keep for Tutorial Widget + } + + } + } + + ngOnDestroy(): void { + this.unsubscribeTheme(); + } + + private instanciateWidget(): void { this.splitWidgetSettings = null; // Use parent layout-split UUID to find configured target Widgett. Split UUID is used for Widget UUID this.splitWidgetSettings = this.WidgetManagerService.getWidget(this.splitUUID); // get from parent const widgetComponentTypeName = this.widgetListService.getComponentName(this.splitWidgetSettings.type); - // Dynamically create containerRef. - // this.dynamicWidgetContainerRef.clear(); // remove vergin container ref - this.newContainerRefRef = this.dynamicWidgetContainerRef.createComponent(widgetComponentTypeName); - - // Init and add abstract class data properties and inject properties into Widget - this.widgetInstance = this.newContainerRefRef.instance; + // Dynamically create component. + this.widgetInstance = null; + this.dynamicWidgetContainerRef.clear(); // remove vergin container ref + const dynamicWidget = this.dynamicWidgetContainerRef.createComponent(widgetComponentTypeName); + this.widgetInstance = dynamicWidget.instance; if (this.splitWidgetSettings.config == null) { this.loadWidgetDefaults(); } - this.widgetInstance.unlockStatus = this.unlockStatus; //TODO(David): Remove once all Widget are updated - this.widgetInstance.widgetProperties = this.splitWidgetSettings; - this.widgetInstance.widgetUUID = this.splitWidgetSettings.uuid; //TODO(David): Remove once all Widget are updated - } - - ngOnChanges(changes: any) { - if ( ('widgetUUID' in changes ) && (!changes.widgetUUID.firstChange)) { - this.ngOnInit(); + dynamicWidget.setInput('widgetProperties', this.splitWidgetSettings); + dynamicWidget.setInput('theme', this.themeColor); + if(this.splitWidgetSettings.type == 'WidgetTutorial') { + dynamicWidget.setInput('unlockStatus', this.unlockStatus); // keep for Tutorial Widget } - - if ( ('unlockStatus' in changes ) && (!changes.unlockStatus.firstChange)) { - this.widgetInstance.unlockStatus = this.unlockStatus; //TODO(David): Remove once all Widget are updated - } - } public selectWidget(): void { @@ -90,9 +111,9 @@ export class DynamicWidgetContainerComponent implements OnInit { for (let [group, widgetList] of Object.entries(fullWidgetList)) { if (widgetList.findIndex(w => w.name == result) >= 0 ) { if (this.splitWidgetSettings.type != result) { - this.dynamicWidgetContainerRef.clear(); // remove vergin container ref + // this.dynamicWidgetContainerRef.clear(); // remove vergin container ref this.WidgetManagerService.updateWidgetType(this.splitUUID, result); - this.ngOnInit(); + this.instanciateWidget(); } } } @@ -101,7 +122,7 @@ export class DynamicWidgetContainerComponent implements OnInit { } public openWidgetSettings(): void { - const dialogRef = this.dialog.open(ModalWidgetComponent, { + const dialogRef = this.dialog.open(ModalWidgetConfigComponent, { width: '80%', data: {...this.splitWidgetSettings.config} }); @@ -118,9 +139,9 @@ export class DynamicWidgetContainerComponent implements OnInit { this.splitWidgetSettings.config = cloneDeep(result); // copy all sub objects } - this.dynamicWidgetContainerRef.clear(); + // this.dynamicWidgetContainerRef.clear(); this.WidgetManagerService.updateWidgetConfig(this.splitWidgetSettings.uuid, this.splitWidgetSettings.config); // Push to storage - this.ngOnInit(); + this.instanciateWidget(); } }); } @@ -129,13 +150,30 @@ export class DynamicWidgetContainerComponent implements OnInit { this.WidgetManagerService.updateWidgetConfig(this.splitWidgetSettings.uuid, {...this.widgetInstance.defaultConfig}); // push default to manager service for storage this.splitWidgetSettings.config = this.widgetInstance.defaultConfig; // load default in current intance. } + + private subscribeTheme() { + this.themeNameSub = this.appSettingsService.getThemeNameAsO().subscribe( + themeChange => { + setTimeout(() => { // delay so browser getComputedStyles has time to complet Material Theme style changes. + this.loadTheme(); + this.instanciateWidget(); + }, 50); + }) + } + + private unsubscribeTheme(){ + if (this.themeNameSub !== null) { + this.themeNameSub.unsubscribe(); + this.themeNameSub = null; + } + } } @Component({ selector: 'app-dynamic-widget-container-modal', templateUrl: './dynamic-widget-container.modal.html', - styleUrls: ['./dynamic-widget-container.component.css'] + styleUrls: ['./dynamic-widget-container.component.scss'] }) export class DynamicWidgetContainerModalComponent implements OnInit { diff --git a/src/app/dynamic-widget.directive.ts b/src/app/dynamic-widget.directive.ts index 3604b86a..70f3288e 100644 --- a/src/app/dynamic-widget.directive.ts +++ b/src/app/dynamic-widget.directive.ts @@ -4,7 +4,6 @@ import { Directive, ViewContainerRef } from '@angular/core'; selector: '[dynamic-widget]' }) export class DynamicWidgetDirective { - constructor(public viewContainerRef: ViewContainerRef) { viewContainerRef.constructor.name === "ViewContainerRef"; // true } diff --git a/src/app/modal-widget/modal-widget.component.html b/src/app/modal-widget-config 2/modal-widget.component.html similarity index 100% rename from src/app/modal-widget/modal-widget.component.html rename to src/app/modal-widget-config 2/modal-widget.component.html diff --git a/src/app/modal-widget/modal-widget.component.spec.ts b/src/app/modal-widget-config 2/modal-widget.component.spec.ts similarity index 100% rename from src/app/modal-widget/modal-widget.component.spec.ts rename to src/app/modal-widget-config 2/modal-widget.component.spec.ts diff --git a/src/app/modal-widget/modal-widget.component.ts b/src/app/modal-widget-config 2/modal-widget.component.ts similarity index 78% rename from src/app/modal-widget/modal-widget.component.ts rename to src/app/modal-widget-config 2/modal-widget.component.ts index 02b46b3e..00b4a446 100644 --- a/src/app/modal-widget/modal-widget.component.ts +++ b/src/app/modal-widget-config 2/modal-widget.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, Inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; import { IUnitGroup } from '../units.service'; @@ -16,7 +16,7 @@ import { IWidgetSvcConfig } from '../widget-manager.service'; export class ModalWidgetComponent implements OnInit { titleDialog: string = "Widget Options"; - formMaster: UntypedFormGroup; + formMaster: FormGroup; availableDataSets: IDataSet[]; unitList: {default?: string, conversions?: IUnitGroup[] } = {}; @@ -35,8 +35,8 @@ export class ModalWidgetComponent implements OnInit { this.formMaster.updateValueAndValidity(); } - generateFormGroups(formData: Object, objectType?: string): UntypedFormGroup { - let groups = new UntypedFormGroup({}); + generateFormGroups(formData: Object, objectType?: string): FormGroup { + let groups = new FormGroup({}); Object.keys(formData).forEach (key => { // handle Objects if ( (typeof(formData[key]) == 'object') && (formData[key] !== null) ) { @@ -57,19 +57,19 @@ export class ModalWidgetComponent implements OnInit { // If we are building units list let unitConfig = this.widgetConfig.paths[key]; if ( (unitConfig.pathType == "number") || ('datasetUUID' in this.widgetConfig)) { - groups.addControl(key, new UntypedFormControl(formData[key])); //only add control if it's a number or historical graph. Strings and booleans don't have units and conversions yet... + groups.addControl(key, new FormControl(formData[key])); //only add control if it's a number or historical graph. Strings and booleans don't have units and conversions yet... } } else { // not building Units list // Use switch in case we will need more Required form validator at some point. switch (key) { - case "path": groups.addControl(key, new UntypedFormControl(formData[key], Validators.required)); + case "path": groups.addControl(key, new FormControl(formData[key], Validators.required)); break; - case "dataSetUUID": groups.addControl(key, new UntypedFormControl(formData[key], Validators.required)); + case "dataSetUUID": groups.addControl(key, new FormControl(formData[key], Validators.required)); break; - default: groups.addControl(key, new UntypedFormControl(formData[key])); + default: groups.addControl(key, new FormControl(formData[key])); break; } } diff --git a/src/app/modal-widget/modal-widget.component.css b/src/app/modal-widget-config/modal-widget-config.component.css similarity index 91% rename from src/app/modal-widget/modal-widget.component.css rename to src/app/modal-widget-config/modal-widget-config.component.css index 4a18007b..145a716f 100644 --- a/src/app/modal-widget/modal-widget.component.css +++ b/src/app/modal-widget-config/modal-widget-config.component.css @@ -30,4 +30,8 @@ } .radio-button { margin-left: 16px; -} \ No newline at end of file +} + +.tab-content { + margin-top: 20px; +} diff --git a/src/app/modal-widget-config/modal-widget-config.component.html b/src/app/modal-widget-config/modal-widget-config.component.html new file mode 100644 index 00000000..696daa5e --- /dev/null +++ b/src/app/modal-widget-config/modal-widget-config.component.html @@ -0,0 +1,355 @@ +
+ {{titleDialog}} + + + +
+ + URL + + + + Widget Label + + +
+ + Minimum Integer Places + + + + Minimum of Decimals + + +
+
+
+ + Show Max recorded value + +
+
+ + Show Min recorded value + +
+
+
+
+ + Date format + + +
+
+ + Timezone + + +
+
+ +
+ + Layline Angle + + + + Show Laylines + +
+
+ + Wind Sector Duration + + + + Show Wind Sectors + +
+ + +
+ + Minimum Value + + + + Maximum Value + + +
+ +
+ + Background Style + + Dark Gray + Satin Gray + Light Gray + White + Black + Beige + Brown + Red + Green + Blue + Anthracite + Mud + Punched Sheet + Carbon + Stainless + Brushed Metal + Brushed Stainless + Turned + + + + Frame Style + + Black Metal + Metal + Shiny Metal + Brass + Steel + Chrome + Gold + Anthracite + Tilted Gray + Tilted Black + Glossy Metal + + +
+
+ + Color + + Primary Color + Accent Color + Warn Color + No Progress + + +
+ +
+
+ + + Paths + + + + Restrict to own vessel + + + + +
+ + Unit Label Options + + Full Label + First Letter Only + + +
+
+ + + +
+ +

+ + + Measuring + Capacity + Marine Compass + Baseplate Compass + +

+

+ + + N/E/S/W + 0/90/180/270 + +

+
+
+ + + +
+ + Gauge Type + + Vertical layout + Horizontal layout + + +

+ + Display Gauge Ticks + +

+
+
+ + + +
+ + Gauge Type + + Linear + Radial + + +

+ + Digital display + +

+

+ + 1/4 + 1/2 + 3/4 + Full + +

+
+
+ + + + +
+

+ + Enable Put Requests + +

+

+ + Momentary mode (instead of switching between on/off) + +

+

+ + Value to send on button push (checked = on, unchecked = off) + +

+ +
+
+ + + + + + Dataset + +
+ + Configured Datasets + + + {{ds.name}} + + + + + Display Format + + + + {{unit.description}} + + + + +

+ + Show vertical graph + +

+

+ + Invert Data (multiply by -1) + +

+

+ + Display Min/Max value + +

+

+ + Y axis always start from zero + +

+
+
+ + +
+
+ + + + +
diff --git a/src/app/modal-widget-config/modal-widget-config.component.spec.ts b/src/app/modal-widget-config/modal-widget-config.component.spec.ts new file mode 100644 index 00000000..4c6bb13f --- /dev/null +++ b/src/app/modal-widget-config/modal-widget-config.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { ModalWidgetConfigComponent } from './modal-widget-config.component'; + +describe('ModalWidgetComponent', () => { + let component: ModalWidgetConfigComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ ModalWidgetConfigComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ModalWidgetConfigComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modal-widget-config/modal-widget-config.component.ts b/src/app/modal-widget-config/modal-widget-config.component.ts new file mode 100644 index 00000000..1ccfa16d --- /dev/null +++ b/src/app/modal-widget-config/modal-widget-config.component.ts @@ -0,0 +1,84 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; + +import { IUnitGroup } from '../units.service'; +import { SignalKService } from '../signalk.service'; +import { DataSetService, IDataSet } from '../data-set.service'; +import { IWidgetSvcConfig } from '../widgets-interface'; + + +@Component({ + selector: 'modal-widget-config', + templateUrl: './modal-widget-config.component.html', + styleUrls: ['./modal-widget-config.component.css'] +}) +export class ModalWidgetConfigComponent implements OnInit { + + titleDialog: string = "Widget Options"; + formMaster: UntypedFormGroup; + availableDataSets: IDataSet[]; + unitList: {default?: string, conversions?: IUnitGroup[] } = {}; + + + constructor( + public dialogRef:MatDialogRef, + private DataSetService: DataSetService, + private signalKService: SignalKService, + @Inject(MAT_DIALOG_DATA) public widgetConfig: IWidgetSvcConfig + ) { } + + ngOnInit() { + this.availableDataSets = this.DataSetService.getDataSets().sort(); + this.unitList = this.signalKService.getConversionsForPath(''); // array of Group or Groups: "angle", "speed", etc... + this.formMaster = this.generateFormGroups(this.widgetConfig); + this.formMaster.updateValueAndValidity(); + } + + generateFormGroups(formData: Object, objectType?: string): UntypedFormGroup { + let groups = new UntypedFormGroup({}); + Object.keys(formData).forEach (key => { + // handle Objects + if ( (typeof(formData[key]) == 'object') && (formData[key] !== null) ) { + switch (objectType) { + case "paths": + //if we are building Paths sub formGroups, skip none configurable + if (this.widgetConfig.paths[key].isPathConfigurable) { + groups.addControl(key, this.generateFormGroups(formData[key], key)); + } + break; + + default: groups.addControl(key, this.generateFormGroups(formData[key], key)); + break; + } + } else { + // Handle Primitives - property values + if (objectType == "convertUnitTo") { + // If we are building units list + let unitConfig = this.widgetConfig.paths[key]; + if ( (unitConfig.pathType == "number") || ('datasetUUID' in this.widgetConfig)) { + groups.addControl(key, new UntypedFormControl(formData[key])); //only add control if it's a number or historical graph. Strings and booleans don't have units and conversions yet... + } + } else { + // not building Units list + // Use switch in case we will need more Required form validator at some point. + switch (key) { + case "path": groups.addControl(key, new UntypedFormControl(formData[key], Validators.required)); + break; + + case "dataSetUUID": groups.addControl(key, new UntypedFormControl(formData[key], Validators.required)); + break; + + default: groups.addControl(key, new UntypedFormControl(formData[key])); + break; + } + } + } + }); + return groups; + } + + submitConfig() { + this.dialogRef.close(this.formMaster.value); + } +} diff --git a/src/app/notifications.service.ts b/src/app/notifications.service.ts index 9cc25923..641f6b37 100644 --- a/src/app/notifications.service.ts +++ b/src/app/notifications.service.ts @@ -351,10 +351,8 @@ export class NotificationsService { */ getPlayer(track: number): Howl { this.activeAlarmSoundtrack = track; - let player = new Howl({ + const player = new Howl({ src: ['assets/' + alarmTrack[track] + '.mp3'], - autoUnlock: true, - autoSuspend: false, autoplay: false, preload: true, loop: true, diff --git a/src/app/reset-config/reset-config.component.ts b/src/app/reset-config/reset-config.component.ts index f302a57e..17c6350e 100644 --- a/src/app/reset-config/reset-config.component.ts +++ b/src/app/reset-config/reset-config.component.ts @@ -17,7 +17,7 @@ export class ResetConfigComponent implements OnInit { private AppSettingsService: AppSettingsService, private route: ActivatedRoute) { } - + ngOnInit() { this.route.url.subscribe(url => { if (url[0].path == 'demo') { diff --git a/src/app/settings-units/settings-units.component.html b/src/app/settings-units/settings-units.component.html deleted file mode 100644 index c87803eb..00000000 --- a/src/app/settings-units/settings-units.component.html +++ /dev/null @@ -1,24 +0,0 @@ -
- - - Application Default Units - - - Set the default units preferences per types of unit groups. The values will be automatically converted for display. - - - - - {{ unit.description }} - - - - - - - - -
diff --git a/src/app/settings/config/config.component.html b/src/app/settings/config/config.component.html new file mode 100644 index 00000000..bbeae29c --- /dev/null +++ b/src/app/settings/config/config.component.html @@ -0,0 +1,396 @@ +
+

Configuration Management

+

+ Recommended Reading: To understand configuration management and storage + options, consult the + Configuration Management Help section. +

+
+
+
+

Save

+

Save current configuration to server

+
+ + Scope + + + Global + + + User + + + + + Configuration Name + + +
+ Writing to the server requires user Sign in or a Device token +
+
+ +
+

Sign in or Device Token required

+
+
+
+ + +
+
+
+
+
+

Delete

+

Delete a configuration from the server

+
+ + Configuration + + + {{ config.scope }} / {{ config.name }} + + + +
+ +
+

Sign in or Device Token required

+
+
+
+ + +
+
+
+
+
+

Copy

+

Create, duplicate and overwrite configurations.

+
+
+ + + + {{ location }} + + + + Configuration + + + {{ config.scope }} / {{ config.name }} + + + +
+
+ + + + {{ location }} + + + + Configuration + + + {{ config.scope }} / {{ config.name }} + + + +
+
+
+ + +
+
+
+
+
+
+

Operations

+

Load predefined configuration or reset configurations to defaults.

+
+
+
+ Load Demo configuration and connect to Signal K demo serve to + see Kip in action. Warning: this will reset your connection + settings and local layout configuration. + +
+
+ +
+
+ Reset the current application configuration (Layouts, Widgets, + etc.) and restores defaults. The default configuration has a + single Getting Started instruction widget to get you started. + The server connections settingd will be kept. + +
+
+ +
+
+ Reset the current connection configuration to defaults. The + default connection configuration is empty and ready for + configuration. This will not affect the app configuration + (Layouts, widgets, etc.). + +
+
+ +
+
+ Enable the configuration editor. The editor modifies the active in-memory configuration. This + configuration represent the current state of the application. You can edit + those values by configuration areas and save them to persist your changes. + +
+
+ Show Editor +
+
+
+
+
+ +

Local Configration Editor

+ Config is in raw json and no validation on save. Make sure you double + check your changes, else you lose your configuration. A good choice is + to back up first using the Save feature above first! +
+
+
+ + Connection + + + + + + + +
+
+
+ + General + + + + + + + +
+
+
+ + Widgets + + + + + + + +
+
+
+ + Layouts + + + + + + + +
+
+
+ + Theme + + + + + + + +
+
+
+ + Zones + + + + + + + +
+
+
+
diff --git a/src/app/settings/config/config.component.scss b/src/app/settings/config/config.component.scss new file mode 100644 index 00000000..4303d236 --- /dev/null +++ b/src/app/settings/config/config.component.scss @@ -0,0 +1,161 @@ +@use '@angular/material' as mat; +@mixin theme-settings-config($theme) { + + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + .descriptiveTexts { + color: mat.get-color-from-palette($foreground, text); + font-size: 14px; + } + + .flex-item { + flex: 1 1 29%; + background-color: mat.get-color-from-palette($foreground, dividers); + padding: 20px; + border-radius: 4px; + } + + .flex-item-copy { + flex: 2 1 40%; + background-color: mat.get-color-from-palette($foreground, dividers); + padding: 10px 20px 10px 20px; + border-radius: 4px; + } +} + +a { + font-size: 14px; +} +a:hover { + text-decoration: underline; + cursor: pointer; +} + +a:link, a:visited { + color: rgb(138, 180, 248); + text-decoration: none; +} + +.confirmTextarea { + resize: none; +} + +.config-size { + width: 100%; +} + +.textheight { + height: 120px; + background-color: black; +} + +.warningText { + padding-left: 15px; +} + +.no-token-notice { + height: 58px; + contain: content; + text-align: center; + font-style: italic; +} + +.mat-radio-button ~ .mat-radio-button { + margin-right: 16px; + margin-left: 16px; +} + +.config-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + width: 100%; +} + +.config-column { + display: flex; + flex-direction: column; + flex-basis: 100%; + flex: 1; + margin: 0px 10px 0px 10px; +} + +.flex-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + align-items: stretch; + gap: 10px; + min-height: 100%; + height: 100%; +} + +.sources-radio-group { + display: flex; + flex-direction: column; + margin: 15px 0; +} + +.sources-radio-button { + margin: 5px; + margin-left: 0px !important; +} + +.select-config { + margin-left: 0px; +} + +.btn-div { + align-self: center; +} + +.btn-div button { + width: -webkit-fill-available; +} + +.config-operation-container { + display: grid; + grid-template-columns: [col-start] auto [col1-end] min-content [col2-end]; + grid-template-rows: [row-start] max-content [row1-end] max-content [row2-end]; + grid-template-areas: + "demo-txt demo-btn" + "reset-txt reset-btn" + "config-txt config-btn" + "editor-txt editor-btn"; + row-gap: 20px; + column-gap: 10px; +} + +.demo-txt { + grid-area: demo-txt; +} + +.demo-btn { + grid-area: demo-btn; +} + +.reset-txt { + grid-area: reset-txt; +} + +.reset-btn { + grid-area: reset-btn; +} + +.config-txt { + grid-area: config-txt; +} + +.config-btn { + grid-area: config-btn; +} + +.editor-txt { + grid-area: editor-txt; +} + +.editor-btn { + grid-area: editor-btn; +} diff --git a/src/app/settings/config/config.component.spec.ts b/src/app/settings/config/config.component.spec.ts new file mode 100644 index 00000000..5ee8810c --- /dev/null +++ b/src/app/settings/config/config.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { SettingsConfigComponent } from './config.component'; + +describe('SettingsConfigComponent', () => { + let component: SettingsConfigComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsConfigComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsConfigComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/config/config.component.ts b/src/app/settings/config/config.component.ts new file mode 100644 index 00000000..71cb2804 --- /dev/null +++ b/src/app/settings/config/config.component.ts @@ -0,0 +1,339 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { UntypedFormBuilder, UntypedFormGroup, FormControl, Validators, NgForm } from '@angular/forms'; + +import { AuththeticationService, IAuthorizationToken } from '../../auththetication.service'; +import { AppSettingsService } from '../../app-settings.service'; +import { IConfig, IAppConfig, IConnectionConfig, IWidgetConfig, ILayoutConfig, IThemeConfig, IZonesConfig } from '../../app-settings.interfaces'; +import { NotificationsService } from '../../notifications.service'; +import { StorageService } from '../../storage.service'; + +interface IRemoteConfig { + scope: string, + name: string +} + +@Component({ + selector: 'settings-config', + templateUrl: './config.component.html', + styleUrls: ['./config.component.scss'] +}) +export class SettingsConfigComponent implements OnInit, OnDestroy{ + + public hasToken: boolean = false; + public isTokenTypeDevice: boolean = false; + private tokenSub: Subscription; + + public supportApplicationData: boolean = false; + public serverConfigList: IRemoteConfig[] = []; + + public copyConfigForm: UntypedFormGroup; + public storageLocation: string = null; + public locations: string[] = ["Local Storage", "Remote Storage"]; + + public saveConfigName: string = null; + public saveConfigScope: string = null; + public deleteConfigItem: IRemoteConfig; + + // Raw Editor + public liveAppConfig: IAppConfig; + public liveConnectionConfig: IConnectionConfig; + public liveWidgetConfig: IWidgetConfig; + public liveLayoutConfig: ILayoutConfig; + public liveThemeConfig: IThemeConfig; + public liveZonesConfig: IZonesConfig; + public showRawEditor = false; + + constructor( + private appSettingsService: AppSettingsService, + private storageSvc: StorageService, + private notificationsService: NotificationsService, + private auth: AuththeticationService, + private fb: UntypedFormBuilder, + ) { } + + ngOnInit() { + // Token observer + this.tokenSub = this.auth.authToken$.subscribe((token: IAuthorizationToken) => { + if (token && token.token) { + this.hasToken = true; + this.isTokenTypeDevice = token.isDeviceAccessToken; + if (!token.isDeviceAccessToken) { + this.saveConfigScope ='user'; + } else { + this.saveConfigScope ='glodal'; + } + } else { + this.hasToken = false; + } + }); + + this.copyConfigForm = this.fb.group({ + copySource: ['', Validators.required], + sourceTarget: [{value: '', disabled: true}, Validators.required], + copyDestination: ['', Validators.required], + destinationTarget: [{value: '', disabled: true}, Validators.required], + }); + + // set control form options + if (!this.hasToken) { + let src = this.copyConfigForm.get('copySource'); + src.setValue('Remote Storage'); + src.disable(); + this.copyConfigForm.get('sourceTarget').enable(); + + let dest = this.copyConfigForm.get('copyDestination'); + dest.setValue('Local Storage'); + dest.disable(); + } + + this.supportApplicationData = this.storageSvc.isAppDataSupported; + this.getLiveConfig(); + this.getServerConfigList(); + } + + public getServerConfigList() { + if (this.supportApplicationData) { + this.storageSvc.listConfigs() + .then((configs) => { + this.serverConfigList = configs; + }) + .catch(error => { + this.notificationsService.sendSnackbarNotification("Error listing server configurations: " + error, 3000, false); + }); + } + } + + public saveConfig(conf: IConfig, scope: string, name: string) { + if (this.supportApplicationData) { // TOD: add to form to block display + if (this.storageSvc.setConfig(scope, name, conf)) { + this.notificationsService.sendSnackbarNotification(`Configuration [${name}] saved to [${scope}] storage scope`, 5000, false); + this.getServerConfigList(); + } else { + this.notificationsService.sendSnackbarNotification("Error saving configuration to server", 0, false); + } + } + } + + public async copyConfig() { + if (this.copyConfigForm.value.copySource === 'Local Storage') { + if (this.copyConfigForm.value.copyDestination === 'Remote Storage') { + // local to remote + if (this.copyConfigForm.value.destinationTarget.scope === 'user' && this.copyConfigForm.value.destinationTarget.name === 'default' && this.hasToken && !this.isTokenTypeDevice) { + this.notificationsService.sendSnackbarNotification("Local Storage cannot be copied to [user / default] when Sign in option is enabled. Use another copy source", 0, false); + } else { + this.saveConfig(this.getLocalConfig(), this.copyConfigForm.value.destinationTarget.scope, this.copyConfigForm.value.destinationTarget.name); + } + + } else if(this.copyConfigForm.value.copyDestination === 'Local Storage') { + // local to local + this.notificationsService.sendSnackbarNotification("Local Storage cannot be copies to Local Storage ", 0, false); + } + + } else { + let conf: IConfig = null; + try { + await this.storageSvc.getConfig(this.copyConfigForm.value.sourceTarget.scope, this.copyConfigForm.value.sourceTarget.name) + .then((config: IConfig) => { + conf = config + }); + } catch (error) { + this.notificationsService.sendSnackbarNotification("Error retreiving configuration from server: " + error.statusText, 3000, false); + return; + } + + if (this.copyConfigForm.value.copyDestination === 'Remote Storage') { + //remote to remote + this.saveConfig(conf, this.copyConfigForm.value.destinationTarget.scope, this.copyConfigForm.value.destinationTarget.name); + if (this.copyConfigForm.value.destinationTarget.scope === 'user' && this.copyConfigForm.value.destinationTarget.name === 'default' && this.hasToken && !this.isTokenTypeDevice) { + this.appSettingsService.reloadApp(); + } + } else { + // remote to local + this.appSettingsService.replaceConfig("appConfig", conf.app, false); + this.appSettingsService.replaceConfig("widgetConfig", conf.widget, false); + this.appSettingsService.replaceConfig("layoutConfig", conf.layout, false); + this.appSettingsService.replaceConfig("themeConfig", conf.theme, false); + this.appSettingsService.replaceConfig("zonesConfig", conf.zones, true); + } + } + } + + public deleteConfig (scope: string, name: string) { + this.storageSvc.removeItem(scope, name); + this.getServerConfigList(); + this.notificationsService.sendSnackbarNotification(`Configuration [${name}] deleted from [${scope}] storage scope`, 5000, false); + } + + public rawConfigSave(configType: string) { + switch (configType) { + case "IConnectionConfig": + this.appSettingsService.replaceConfig('connectionConfig', this.liveConnectionConfig, true); + break; + + case "IAppConfig": + if (this.hasToken && !this.isTokenTypeDevice) { + this.storageSvc.patchConfig(configType, this.liveAppConfig); + } else { + this.appSettingsService.replaceConfig('appConfig', this.liveAppConfig, true); + } + break; + + case "IWidgetConfig": + if (this.hasToken && !this.isTokenTypeDevice) { + this.storageSvc.patchConfig(configType, this.liveWidgetConfig); + } else { + this.appSettingsService.replaceConfig('widgetConfig', this.liveWidgetConfig, true); + } + break; + + case "ILayoutConfig": + if (this.hasToken && !this.isTokenTypeDevice) { + this.storageSvc.patchConfig(configType, this.liveLayoutConfig); + } else { + this.appSettingsService.replaceConfig('layoutConfig', this.liveLayoutConfig, true); + } + break; + + case "IThemeConfig": + if (this.hasToken && !this.isTokenTypeDevice) { + this.storageSvc.patchConfig(configType, this.liveThemeConfig); + } else { + this.appSettingsService.replaceConfig('themeConfig', this.liveThemeConfig, true); + } + break; + + case "IZonesConfig": + if (this.hasToken && !this.isTokenTypeDevice) { + this.storageSvc.patchConfig(configType, this.liveZonesConfig); + } else { + this.appSettingsService.replaceConfig("zonesConfig", this.liveZonesConfig, true); + } + break; + } + } + + public resetConfigToDefault() { + this.appSettingsService.resetSettings(); + } + + public resetConnectionToDefault() { + this.appSettingsService.resetConnection(); + } + + public loadDemoConfig() { + this.appSettingsService.loadDemoConfig(); + } + + private getLiveConfig(): void { + this.liveAppConfig = this.appSettingsService.getAppConfig(); + this.liveConnectionConfig = this.appSettingsService.getConnectionConfig(); + this.liveWidgetConfig = this.appSettingsService.getWidgetConfig(); + this.liveLayoutConfig = this.appSettingsService.getLayoutConfig(); + this.liveThemeConfig = this.appSettingsService.getThemeConfig(); + this.liveZonesConfig = this.appSettingsService.getZonesConfig(); + } + + get jsonZonesConfig() { + return JSON.stringify(this.liveZonesConfig, null, 2); + } + + set jsonZonesConfig(v) { + try{ + this.liveZonesConfig = JSON.parse(v);} + catch(error) { + console.log(`JSON syntax error: ${error}`); + }; + } + + get jsonThemeConfig() { + return JSON.stringify(this.liveThemeConfig, null, 2); + } + + set jsonThemeConfig(v) { + try{ + this.liveThemeConfig = JSON.parse(v);} + catch(error) { + console.log(`JSON syntax error: ${error}`); + }; + } + + get jsonLayoutConfig() { + return JSON.stringify(this.liveLayoutConfig, null, 2); + } + + set jsonLayoutConfig(v) { + try{ + this.liveLayoutConfig = JSON.parse(v);} + catch(error) { + console.log(`JSON syntax error: ${error}`); + }; + } + + get jsonWidgetConfig() { + return JSON.stringify(this.liveWidgetConfig, null, 2); + } + + set jsonWidgetConfig(v) { + try{ + this.liveWidgetConfig = JSON.parse(v);} + catch(error) { + console.log(`JSON syntax error: ${error}`); + }; + } + + get jsonAppConfig() { + return JSON.stringify(this.liveAppConfig, null, 2); + } + + set jsonAppConfig(v) { + try{ + this.liveAppConfig = JSON.parse(v);} + catch(error) { + console.log(`JSON syntax error: ${error}`); + }; + } + + get jsonConnectionConfig() { + return JSON.stringify(this.liveConnectionConfig, null, 2); + } + + set jsonConnectionConfig(v) { + try{ + this.liveConnectionConfig = JSON.parse(v);} + catch(error) { + console.log(`JSON syntax error: ${error}`); + }; + } + + public getLocalConfig(): IConfig { + let localConfig: IConfig = { + "app": this.appSettingsService.getAppConfig(), + "widget": this.appSettingsService.getWidgetConfig(), + "layout": this.appSettingsService.getLayoutConfig(), + "theme": this.appSettingsService.getThemeConfig(), + "zones": this.appSettingsService.getZonesConfig(), + }; + return localConfig; + } + + public onSourceSelectChange(event): void { + if (event.value === 'Local Storage') { + this.copyConfigForm.get('sourceTarget').disable(); + } else { + this.copyConfigForm.get('sourceTarget').enable(); + } + } + + public onDestinationSelectChange(event): void { + if (event.value === 'Local Storage') { + this.copyConfigForm.get('destinationTarget').disable(); + } else { + this.copyConfigForm.get('destinationTarget').enable(); + } + } + + ngOnDestroy() { + this.tokenSub.unsubscribe(); + } +} diff --git a/src/app/settings-config/settings-config.component.html b/src/app/settings/config/settings-config.component.html similarity index 100% rename from src/app/settings-config/settings-config.component.html rename to src/app/settings/config/settings-config.component.html diff --git a/src/app/settings-config/settings-config.component.scss b/src/app/settings/config/settings-config.component.scss similarity index 100% rename from src/app/settings-config/settings-config.component.scss rename to src/app/settings/config/settings-config.component.scss diff --git a/src/app/settings-config/settings-config.component.spec.ts b/src/app/settings/config/settings-config.component.spec.ts similarity index 100% rename from src/app/settings-config/settings-config.component.spec.ts rename to src/app/settings/config/settings-config.component.spec.ts diff --git a/src/app/settings-config/settings-config.component.ts b/src/app/settings/config/settings-config.component.ts similarity index 98% rename from src/app/settings-config/settings-config.component.ts rename to src/app/settings/config/settings-config.component.ts index 83f5f94b..ba7d4d57 100644 --- a/src/app/settings-config/settings-config.component.ts +++ b/src/app/settings/config/settings-config.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; -import { UntypedFormBuilder, UntypedFormGroup, FormControl, Validators, NgForm } from '@angular/forms'; +import { FormBuilder, FormGroup, FormControl, Validators, NgForm } from '@angular/forms'; import { AuththeticationService, IAuthorizationToken } from './../auththetication.service'; import { AppSettingsService } from '../app-settings.service'; @@ -27,7 +27,7 @@ export class SettingsConfigComponent implements OnInit, OnDestroy{ public supportApplicationData: boolean = false; public serverConfigList: IRemoteConfig[] = []; - public copyConfigForm: UntypedFormGroup; + public copyConfigForm: FormGroup; public storageLocation: string = null; public locations: string[] = ["Local Storage", "Remote Storage"]; @@ -49,7 +49,7 @@ export class SettingsConfigComponent implements OnInit, OnDestroy{ private storageSvc: StorageService, private notificationsService: NotificationsService, private auth: AuththeticationService, - private fb: UntypedFormBuilder, + private fb: FormBuilder, ) { } ngOnInit() { diff --git a/src/app/settings/datasets/datasets.component.html b/src/app/settings/datasets/datasets.component.html new file mode 100644 index 00000000..eb4b0204 --- /dev/null +++ b/src/app/settings/datasets/datasets.component.html @@ -0,0 +1,55 @@ +
+
+

Datasets Configuration

+

Create historical datasets to record data values over time and display them on charts with the Historical DataSet widget.

+ + Filter + + +
+ + + + + Path + {{element.path}} + + + + + Interval + every {{element.updateTimer}} sec + + + + + Data Points + {{element.dataPoints}} times + + + + + + + + + + + + + + + + No data matching the filter "{{input.value}}" + + +
+
+ +
+
+ + +
+
+
diff --git a/src/app/settings-datasets/settings-datasets.component.scss b/src/app/settings/datasets/datasets.component.scss similarity index 100% rename from src/app/settings-datasets/settings-datasets.component.scss rename to src/app/settings/datasets/datasets.component.scss diff --git a/src/app/settings/datasets/datasets.component.spec.ts b/src/app/settings/datasets/datasets.component.spec.ts new file mode 100644 index 00000000..f03effc4 --- /dev/null +++ b/src/app/settings/datasets/datasets.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { SettingsDatasetsComponent } from './datasets.component'; + +describe('SettingsDatasetsComponent', () => { + let component: SettingsDatasetsComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsDatasetsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsDatasetsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/datasets/datasets.component.ts b/src/app/settings/datasets/datasets.component.ts new file mode 100644 index 00000000..6f0c6d1c --- /dev/null +++ b/src/app/settings/datasets/datasets.component.ts @@ -0,0 +1,171 @@ +import { Component, OnInit, Inject, AfterViewInit, ViewChild, ChangeDetectorRef } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatTableDataSource } from '@angular/material/table'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; + +import { SignalKService } from '../../signalk.service'; +import { DataSetService, IDataSet } from '../../data-set.service'; + +interface settingsForm { + selectedPath: string; + selectedSource: string; + interval: number; + dataPoints: number; +}; + +@Component({ + selector: 'settings-datasets', + templateUrl: './datasets.component.html', + styleUrls: ['./datasets.component.scss'] +}) +export class SettingsDatasetsComponent implements OnInit, AfterViewInit { + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + selectedDataSet: string; + dataSets: IDataSet[]; + tableData = new MatTableDataSource([]); + displayedColumns: string[] = ['path', 'updateTimer', 'dataPoints', 'actions']; + + constructor( + public dialog: MatDialog, + private cdRef: ChangeDetectorRef, + private SignalKService: SignalKService, + private DataSetService: DataSetService + ) { } + + ngOnInit() { + this.loadDataSets(); + } + + private loadDataSets() { + this.tableData.data = this.DataSetService.getDataSets(); + } + + ngAfterViewInit() { + this.tableData.paginator = this.paginator; + this.tableData.sort = this.sort; + this.tableData.filter = ""; + this.cdRef.detectChanges(); + } + + public openDatasetModal(uuid?: string) { + let dialogRef; + + if (uuid) { + const thisDataset: IDataSet = this.tableData.data.find((dataset: IDataSet) => { + return dataset.uuid === uuid; + }); + + if (thisDataset) { + dialogRef = this.dialog.open(SettingsDatasetsModalComponent, { + data: thisDataset + }); + } + } else { + dialogRef = this.dialog.open(SettingsDatasetsModalComponent, { + }); + } + + dialogRef.afterClosed().subscribe((dataset: IDataSet) => { + if (dataset === undefined || !dataset) { + return; //clicked Cancel, click outside the dialog, or navigated await from page using url bar. + } else { + if (dataset.uuid) { + this.editDataset(dataset); + } else { + this.addDataset(dataset); + } + + this.loadDataSets(); + } + }); + } + + private addDataset(dataset: IDataSet) { + this.DataSetService.addDataSet(dataset.path, dataset.signalKSource, dataset.updateTimer, dataset.dataPoints); + } + + private editDataset(dataset: IDataSet) { + this.DataSetService.updateDataset(dataset); + } + + public deleteDataset(uuid: string) { + this.DataSetService.deleteDataSet(uuid); //TODO, bit bruteforce, can cause errors cause dataset deleted before subscrioptions canceled + this.loadDataSets(); + } + + public trackByUuid(index: number, item: IDataSet): string { + return `${item.uuid}`; + } + + public applyFilter(event: Event) { + const filterValue = (event.target as HTMLInputElement).value; + this.tableData.filter = filterValue.trim().toLowerCase(); + + if (this.tableData.paginator) { + this.tableData.paginator.firstPage(); + } + } + +} + +@Component({ + selector: 'settings-datasets-modal', + templateUrl: './datasets.modal.html', + styleUrls: ['./datasets.component.scss'] +}) +export class SettingsDatasetsModalComponent implements OnInit { + public titleDialog: string = null; + public newDataset: IDataSet = { + uuid: null, + path: null, + signalKSource: null, + updateTimer: 1, + dataPoints: 30, + name: null, + } + + public formDataset: IDataSet = null; + + public availablePaths: string[] = []; + public availableSources: string[] = []; + public filterSelfPaths:boolean = true; + + constructor( + private SignalKService: SignalKService, + public dialogRef:MatDialogRef, + @Inject(MAT_DIALOG_DATA) public dataset: IDataSet + ) { } + + ngOnInit() { + if (this.dataset) { + this.titleDialog = "Edit Dataset"; + this.formDataset = this.dataset; + + let pathObject = this.SignalKService.getPathObject(this.formDataset.path); + if (pathObject !== null) { + this.availableSources = ['default'].concat(Object.keys(pathObject.sources)); + } + + } else { + this.titleDialog = "Add Dataset"; + this.formDataset = this.newDataset; + } + + this.availablePaths = this.SignalKService.getPathsByType('number').sort(); + } + + public changePath() { // called when we choose a new path. Resets the form old value with default info of this path + let pathObject = this.SignalKService.getPathObject(this.formDataset.path); + if (pathObject === null) { return; } + this.availableSources = ['default'].concat(Object.keys(pathObject.sources)); + this.formDataset.signalKSource = 'default'; + } + + public closeForm() { + this.dialogRef.close(this.formDataset); + } +} diff --git a/src/app/settings-datasets/settings-datasets.modal.html b/src/app/settings/datasets/datasets.modal.html similarity index 100% rename from src/app/settings-datasets/settings-datasets.modal.html rename to src/app/settings/datasets/datasets.modal.html diff --git a/src/app/settings-datasets/settings-datasets.component.html b/src/app/settings/datasets/settings-datasets.component.html similarity index 100% rename from src/app/settings-datasets/settings-datasets.component.html rename to src/app/settings/datasets/settings-datasets.component.html diff --git a/src/app/settings/datasets/settings-datasets.component.scss b/src/app/settings/datasets/settings-datasets.component.scss new file mode 100644 index 00000000..a0820e73 --- /dev/null +++ b/src/app/settings/datasets/settings-datasets.component.scss @@ -0,0 +1,95 @@ +@use '@angular/material' as mat; + +@mixin theme-settings-data($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + +} + +.full-display { + width: 100%; + height: 100%; + position: relative; + z-index: 500; +} + +.full-width { + width: 100%; +} + +.mat-column-actions { + text-align: end; +} + +.buttons { + margin-right: 5px; +} + +.pathCell { + flex: 1 1 50%; +} + +.pathHeader { + flex: 1 1 50%; +} + +.dataHeader { + flex: 1 1 10%; + justify-content: center; +} + +.dataCell { + flex: 1 1 10%; + justify-content: center; +} + +.actionHeader { + flex: 1 1 20%; +} + +.actionCell { + flex: 1 1 20%; + justify-content: end; +} + +@media screen and (max-width: 750px) { + .pathHeader { + flex: 1 1 30%; + } + + .dataHeader { + flex: 1 1 30%; + } + + .actionHeader { + display: none; + } + + .mat-table .mat-cell:before { + content: attr(data-label); + float: left; + padding-right: 5px; + } + + mat-row::after { + min-height: auto; + padding-bottom: 10px; + } + + .dataRow { + flex-direction: column; + align-items: flex-start; + } + + .dataCell { + margin-left: 24px; + } + + .actionCell { + margin-left: 24px; + } + +} diff --git a/src/app/settings-datasets/settings-datasets.component.spec.ts b/src/app/settings/datasets/settings-datasets.component.spec.ts similarity index 100% rename from src/app/settings-datasets/settings-datasets.component.spec.ts rename to src/app/settings/datasets/settings-datasets.component.spec.ts diff --git a/src/app/settings-datasets/settings-datasets.component.ts b/src/app/settings/datasets/settings-datasets.component.ts similarity index 96% rename from src/app/settings-datasets/settings-datasets.component.ts rename to src/app/settings/datasets/settings-datasets.component.ts index 6ff10a65..681916ba 100644 --- a/src/app/settings-datasets/settings-datasets.component.ts +++ b/src/app/settings/datasets/settings-datasets.component.ts @@ -4,8 +4,8 @@ import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort } from '@angular/material/sort'; -import { SignalKService } from '../signalk.service'; -import { DataSetService, IDataSet } from '../data-set.service'; +import { SignalKService } from '../../signalk.service'; +import { DataSetService, IDataSet } from '../../data-set.service'; interface settingsForm { selectedPath: string; @@ -32,7 +32,6 @@ export class SettingsDatasetsComponent implements OnInit, AfterViewInit { constructor( public dialog: MatDialog, private cdRef: ChangeDetectorRef, - private SignalKService: SignalKService, private DataSetService: DataSetService ) { } diff --git a/src/app/settings/datasets/settings-datasets.modal.html b/src/app/settings/datasets/settings-datasets.modal.html new file mode 100644 index 00000000..148fcd16 --- /dev/null +++ b/src/app/settings/datasets/settings-datasets.modal.html @@ -0,0 +1,101 @@ +

{{titleDialog}}

+ +
+ + +
+ + Signal K Path + + + {{path}} + + + + + Restrict to own vessel + +

+ + Source + + + {{source}} + + + +
+ +
+ +
+ + Set data capture interval (sec) + + + + Total most recent data points that makes the dataset + + + + +
+
+
+
+
diff --git a/src/app/settings-notifications/settings-notifications.component.css b/src/app/settings/notifications/notifications.component.css similarity index 100% rename from src/app/settings-notifications/settings-notifications.component.css rename to src/app/settings/notifications/notifications.component.css diff --git a/src/app/settings/notifications/notifications.component.html b/src/app/settings/notifications/notifications.component.html new file mode 100644 index 00000000..d0208599 --- /dev/null +++ b/src/app/settings/notifications/notifications.component.html @@ -0,0 +1,47 @@ +
+
+

Server Notifications

+

Notifications are a special type of data sent from Signal K and displayed in the + notification menu. They are meant to alert or inform operators. Set server + notification preferences such as types of messages to display and audio prompts.

+ Disable All Notifications + + + + + Messages + + + Control what messages the server will send + + + Show Devices Informational notifications + + + + + Audio + + + Configure sound options + + + Disable All Audio notification +
+ Disable Information notifications +
+ Disable Alert Severity notifications +
+ Disable Warning notifications +
+ Disable Alarm Severity notifications +
+ Disable Emergency Severity notifications +
+
+
+ + +
+
+
diff --git a/src/app/settings/notifications/notifications.component.spec.ts b/src/app/settings/notifications/notifications.component.spec.ts new file mode 100644 index 00000000..80259f69 --- /dev/null +++ b/src/app/settings/notifications/notifications.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { SettingsNotificationsComponent } from './notifications.component'; + +describe('SettingsNotificationsComponent', () => { + let component: SettingsNotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsNotificationsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsNotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/notifications/notifications.component.ts b/src/app/settings/notifications/notifications.component.ts new file mode 100644 index 00000000..aec208e8 --- /dev/null +++ b/src/app/settings/notifications/notifications.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit } from '@angular/core'; +import { INotificationConfig } from '../../app-settings.interfaces'; +import { AppSettingsService } from '../../app-settings.service'; +import { NotificationsService } from '../../notifications.service'; + + +@Component({ + selector: 'settings-notifications', + templateUrl: './notifications.component.html', + styleUrls: ['./notifications.component.css'] +}) +export class SettingsNotificationsComponent implements OnInit { + + notificationConfig: INotificationConfig; + + constructor( + private notificationsService: NotificationsService, + private appSettingsService: AppSettingsService, + ) { } + + ngOnInit() { + this.notificationConfig = this.appSettingsService.getNotificationConfig(); + } + + saveNotificationsSettings() { + this.appSettingsService.setNotificationConfig(this.notificationConfig); + this.notificationsService.sendSnackbarNotification("Notification configuration saved", 5000, false); + } + +} diff --git a/src/app/settings-notifications/settings-notifications.component.html b/src/app/settings/notifications/settings-notifications.component.html similarity index 100% rename from src/app/settings-notifications/settings-notifications.component.html rename to src/app/settings/notifications/settings-notifications.component.html diff --git a/src/app/settings-notifications/settings-notifications.component.spec.ts b/src/app/settings/notifications/settings-notifications.component.spec.ts similarity index 100% rename from src/app/settings-notifications/settings-notifications.component.spec.ts rename to src/app/settings/notifications/settings-notifications.component.spec.ts diff --git a/src/app/settings-notifications/settings-notifications.component.ts b/src/app/settings/notifications/settings-notifications.component.ts similarity index 100% rename from src/app/settings-notifications/settings-notifications.component.ts rename to src/app/settings/notifications/settings-notifications.component.ts diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts deleted file mode 100644 index 36298744..00000000 --- a/src/app/settings/settings.component.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - - -@Component({ - selector: 'app-settings', - templateUrl: './settings.component.html', - styleUrls: ['./settings.component.css'] -}) -export class SettingsComponent implements OnInit { - - constructor() { } - - - ngOnInit() { - } - - - - - - -} diff --git a/src/app/settings-signalk/settings-signalk.component.html b/src/app/settings/signalk/settings-signalk.component.html similarity index 100% rename from src/app/settings-signalk/settings-signalk.component.html rename to src/app/settings/signalk/settings-signalk.component.html diff --git a/src/app/settings-signalk/settings-signalk.component.spec.ts b/src/app/settings/signalk/settings-signalk.component.spec.ts similarity index 100% rename from src/app/settings-signalk/settings-signalk.component.spec.ts rename to src/app/settings/signalk/settings-signalk.component.spec.ts diff --git a/src/app/settings-signalk/settings-signalk.component.ts b/src/app/settings/signalk/settings-signalk.component.ts similarity index 100% rename from src/app/settings-signalk/settings-signalk.component.ts rename to src/app/settings/signalk/settings-signalk.component.ts diff --git a/src/app/settings/signalk/signalk.component.html b/src/app/settings/signalk/signalk.component.html new file mode 100644 index 00000000..3acde416 --- /dev/null +++ b/src/app/settings/signalk/signalk.component.html @@ -0,0 +1,101 @@ +
+
+

Settings

+

Set server connection properties, Sign in credential and manage Device Authorisation token.

+ + Signal K URL + + + Valid URL is required. Ex. "https://demo.signalK.com" or "http://my.server.com:3000" + + +
+ + Enable user Sign in and configuration sharing + +
+
+ +
+ + + + + + + + +
+
+ +
+

+ Connection Status +

+
+
+ + + + + + + + + + + + + + + + + +
+ Version: + + {{ endpointServiceStatus.serverDescrption }} +
+ Authorization: + +
+ +
+ Type: Session +
+
+ Type: Device Access +
+ Token: {{authToken.token| slice : 0 : 20}}... +
+
+ No Authorization Token +
+
+ API Endpoint: + + + + {{ endpointServiceStatus.message }} +
+ Data Stream: + + + + {{ streamStatus.message }} - Token: {{ streamStatus.hasToken }} +
+
+
+ +
+
+
+
diff --git a/src/app/settings-signalk/settings-signalk.component.css b/src/app/settings/signalk/signalk.component.scss similarity index 51% rename from src/app/settings-signalk/settings-signalk.component.css rename to src/app/settings/signalk/signalk.component.scss index 952af660..8fa67caa 100644 --- a/src/app/settings-signalk/settings-signalk.component.css +++ b/src/app/settings/signalk/signalk.component.scss @@ -1,3 +1,21 @@ +@use '@angular/material' as mat; + +@mixin theme-settings-sk($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $background: map-get($theme, background); + $foreground: map-get($theme, foreground); + + // mat.get-color-from-palette($foreground, dividers); + .serverStatus { + background-color: mat.get-color-from-palette($foreground, dividers); + margin-top: 20px; + margin: 7% 5% 0% 5%; + padding: 10px 20px 20px 20px; + } +} + .connectUrlInput { width: 100%; } @@ -6,6 +24,7 @@ } .flex-container { + width: 100%; display: flex; flex-direction: row; flex-wrap: wrap; @@ -15,14 +34,14 @@ } .flex-item-detail { - flex: 0 1 350px; + flex: 0 0 290px; } .flex-item-chart { flex: 1 1 350px; position: relative; margin: 0px; - height: 100%; + height: 130px; width: 100%; border: 2px inset; } diff --git a/src/app/settings/signalk/signalk.component.spec.ts b/src/app/settings/signalk/signalk.component.spec.ts new file mode 100644 index 00000000..9a8c07e9 --- /dev/null +++ b/src/app/settings/signalk/signalk.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { SettingsSignalkComponent } from './signalk.component'; + +describe('SettingsSignalkComponent', () => { + let component: SettingsSignalkComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsSignalkComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsSignalkComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/signalk/signalk.component.ts b/src/app/settings/signalk/signalk.component.ts new file mode 100644 index 00000000..39f3a19a --- /dev/null +++ b/src/app/settings/signalk/signalk.component.ts @@ -0,0 +1,276 @@ +import { ViewChild, ElementRef, Component, OnInit } from '@angular/core'; +import { Subscription } from 'rxjs'; +import Chart from 'chart.js/auto'; +import { MatDialog } from '@angular/material/dialog'; + +import { AppSettingsService } from '../../app-settings.service'; +import { IConnectionConfig } from "../../app-settings.interfaces"; +import { SignalKConnectionService, IEndpointStatus } from '../../signalk-connection.service'; +import { SignalKService } from '../../signalk.service'; +import { SignalKDeltaService, IStreamStatus } from '../../signalk-delta.service'; +import { AuththeticationService, IAuthorizationToken } from '../../auththetication.service'; +import { SignalkRequestsService } from '../../signalk-requests.service'; +import { NotificationsService } from '../../notifications.service'; +import { ModalUserCredentialComponent } from '../../modal-user-credential/modal-user-credential.component'; +import { HttpErrorResponse } from '@angular/common/http'; +import { compare } from 'compare-versions'; + + +@Component({ + selector: 'settings-signalk', + templateUrl: './signalk.component.html', + styleUrls: ['./signalk.component.scss'], +}) + +export class SettingsSignalkComponent implements OnInit { + + @ViewChild('lineGraph', {static: true, read: ElementRef}) lineGraph: ElementRef; + + connectionConfig: IConnectionConfig; + + authTokenSub: Subscription; + authToken: IAuthorizationToken; + isLoggedInSub: Subscription; + isLoggedIn: boolean; + + endpointServiceStatus: IEndpointStatus; + skEndpointServiceStatusSub: Subscription; + streamStatus: IStreamStatus; + skStreamStatusSub: Subscription; + + + updatesSecondSub: Subscription; + + lastSecondsUpdate: number; //number of updates from server in last second + updatesSeconds: number[] = []; + + chartCtx; + chart = null; + textColor; // store the color of text for the graph... + + // dynamics theme support + themeNameSub: Subscription = null; + + constructor( + public dialog: MatDialog, + private appSettingsService: AppSettingsService, + private notificationsService: NotificationsService, + private signalKService: SignalKService, + private signalKConnectionService: SignalKConnectionService, + private signalkRequestsService: SignalkRequestsService, + private deltaService: SignalKDeltaService, + public auth: AuththeticationService) + { } + + ngOnInit() { + // init current value. IsLoggedInSub BehaviorSubject will send last value and component will triggger last notifications even if old + if (this.auth.isLoggedIn$) { + this.isLoggedIn = true; + } else { + this.isLoggedIn = false; + } + + // get Signal K connection configuration + this.connectionConfig = this.appSettingsService.getConnectionConfig(); + + // get token status + this.authTokenSub = this.auth.authToken$.subscribe((token: IAuthorizationToken) => { + if (token) { + this.authToken = token; + } else { + this.authToken = null; + } + }); + + // get logged in status + this.isLoggedInSub = this.auth.isLoggedIn$.subscribe(isLoggedIn => { + this.isLoggedIn = isLoggedIn; + }); + + // get for Signal K connection status + this.skEndpointServiceStatusSub = this.signalKConnectionService.getServiceEndpointStatusAsO().subscribe((status: IEndpointStatus) => { + this.endpointServiceStatus = status; + }); + + // get Delta Service status + this.skStreamStatusSub = this.deltaService.getDataStreamStatusAsO().subscribe((status: IStreamStatus): void => { + this.streamStatus = status; + }); + + //get WebSocket Stream performance update + this.updatesSecondSub = this.signalKService.getupdateStatsSecond().subscribe(newSecondsData => { + this.lastSecondsUpdate = newSecondsData[newSecondsData.length-1]; + this.updatesSeconds = newSecondsData; + if (this.chart !== null) { + this.chart.config.data.datasets[0].data = newSecondsData; + this.chart.update('none'); + } + }); + + this.textColor = window.getComputedStyle(this.lineGraph.nativeElement).color; + this.chartCtx = this.lineGraph.nativeElement.getContext('2d'); + this.startChart(); + this.subscribeTheme(); + } + + public openUserCredentialModal(errorMsg: string) { + let dialogRef = this.dialog.open(ModalUserCredentialComponent, { + data: { + user: this.connectionConfig.loginName, + password: this.connectionConfig.loginPassword, + error: errorMsg + } + }); + + dialogRef.afterClosed().subscribe(data => { + if (!data) {return} //clicked cancel + this.connectionConfig.loginName = data.user; + this.connectionConfig.loginPassword = data.password; + this.connectToServer(); + }); + } + + public connectToServer() { + if (this.connectionConfig.useSharedConfig && (!this.connectionConfig.loginName || !this.connectionConfig.loginPassword)) { + this.openUserCredentialModal("Credentials required"); + return; + } + + if (this.connectionConfig.signalKUrl != this.appSettingsService.signalkUrl.url) { + this.appSettingsService.setConnectionConfig(this.connectionConfig); + + if (this.connectionConfig.useSharedConfig) { + this.serverLogin(this.connectionConfig.signalKUrl); + } else if ( this.authToken) { + this.auth.deleteToken(); + location.reload(); + } else { + location.reload(); + } + + } else { + this.appSettingsService.setConnectionConfig(this.connectionConfig); + // Same URL - no need to resetSignalK(). Just login, new token reset will reload WebSockets + // and HTTP_INTERCEPTOR will incert the new token automatically on all HTTP calls (exdcept for WebSocket). + if ((this.authToken && this.authToken.isDeviceAccessToken) && this.connectionConfig.useSharedConfig) { + this.serverLogin(this.connectionConfig.signalKUrl); + } else if ((this.authToken && !this.authToken.isDeviceAccessToken) && !this.connectionConfig.useSharedConfig) { + this.deleteToken(); + location.reload(); + } else if (this.connectionConfig.useSharedConfig) { + this.serverLogin(this.connectionConfig.signalKUrl); + } else { + location.reload(); + } + } + } + + private serverLogin(newUrl?: string) { + this.auth.login({ usr: this.connectionConfig.loginName, pwd: this.connectionConfig.loginPassword, newUrl }) + .then( _ => { + location.reload(); + }) + .catch((error: HttpErrorResponse) => { + if (error.status == 401) { + this.openUserCredentialModal("Sign in failed: Incorrect user/password. Enter valide credentials"); + console.log("[Setting-SignalK Component] Sign in failed: " + error.error.message); + } else if (error.status == 404) { + this.notificationsService.sendSnackbarNotification("Sign in failed: Login API not found", 5000, false); + console.log("[Setting-SignalK Component] Sign in failed: " + error.error.message); + } else if (error.status == 0) { + this.notificationsService.sendSnackbarNotification("Sign in failed: Cannot reach server at Signal K URL", 5000, false); + console.log("[Setting-SignalK Component] Sign in failed: Cannot reach server at Signal K URL:" + error.message); + } else { + this.notificationsService.sendSnackbarNotification("Unknown authentication failure: " + JSON.stringify(error), 5000, false); + console.log("[Setting-SignalK Component] Unknown login error response: " + JSON.stringify(error)); + } + }); + } + + public requestDeviceAccessToken() { + this.signalkRequestsService.requestDeviceAccessToken(); + } + + public deleteToken() { + this.auth.deleteToken(); + } + + private startChart() { + if (this.chart !== null) { + this.chart.destroy(); + } + + this.chart = new Chart(this.chartCtx,{ + type: 'line', + data: { + labels: Array.from(Array(60).keys()).reverse(), + datasets: [ + { + label: "Updates Per Second", + data: this.updatesSeconds, + //fill: 'false', + borderColor: this.textColor + }, + ] + }, + options: { + maintainAspectRatio: false, + scales: { + x: { + beginAtZero: true, + position: 'bottom', + ticks: { + autoSkip: true, + autoSkipPadding: 30 + } + }, + y: { + beginAtZero: true, + type: 'linear', + position: 'left', + }, + }, + plugins:{ + legend: { + labels: { + color: this.textColor, + } + } + } + } + }); + } + + // Subscribe to theme event + private subscribeTheme() { + this.themeNameSub = this.appSettingsService.getThemeNameAsO().subscribe( + themeChange => { + setTimeout(() => { // need a delay so browser getComputedStyles has time to complete theme application. + this.textColor = window.getComputedStyle(this.lineGraph.nativeElement).color; + this.startChart() + }, 100); + }) + } + + public useSharedConfigToggleClick(e) { + if(e.checked) { + let version = this.signalKConnectionService.serverVersion$.getValue(); + if (!compare(version, '1.46.2', ">=")) { + this.notificationsService.sendSnackbarNotification("Configuration sharing requires Signal K version 1.46.2 or better",0); + this.connectionConfig.useSharedConfig = false; + return; + } + this.openUserCredentialModal(null); + } + }; + + ngOnDestroy() { + this.skEndpointServiceStatusSub.unsubscribe(); + this.skStreamStatusSub.unsubscribe(); + this.authTokenSub.unsubscribe(); + this.isLoggedInSub.unsubscribe(); + // this.updatesMinutesSub.unsubscribe(); + this.updatesSecondSub.unsubscribe(); + this.themeNameSub.unsubscribe(); + } +} diff --git a/src/app/settings/settings.component.html b/src/app/settings/tabs/settings.component.html similarity index 100% rename from src/app/settings/settings.component.html rename to src/app/settings/tabs/settings.component.html diff --git a/src/app/settings/settings.component.spec.ts b/src/app/settings/tabs/settings.component.spec.ts similarity index 100% rename from src/app/settings/settings.component.spec.ts rename to src/app/settings/tabs/settings.component.spec.ts diff --git a/src/app/settings/settings.component.css b/src/app/settings/tabs/tabs.component.css similarity index 52% rename from src/app/settings/settings.component.css rename to src/app/settings/tabs/tabs.component.css index 6b81a96a..ed5292bf 100644 --- a/src/app/settings/settings.component.css +++ b/src/app/settings/tabs/tabs.component.css @@ -1,9 +1,11 @@ .settingsWindow { padding-left: 0px; + min-height: 100%; + height: 100%; } .settingsPanels { display: block; - padding: 3px; + margin: 20px; + margin-top: 10px; } - diff --git a/src/app/settings/tabs/tabs.component.html b/src/app/settings/tabs/tabs.component.html new file mode 100644 index 00000000..b4e7f724 --- /dev/null +++ b/src/app/settings/tabs/tabs.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/app/settings/tabs/tabs.component.spec.ts b/src/app/settings/tabs/tabs.component.spec.ts new file mode 100644 index 00000000..cbc8d55e --- /dev/null +++ b/src/app/settings/tabs/tabs.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { SettingsTabsComponent } from './tabs.component'; + +describe('SettingsTabsComponent', () => { + let component: SettingsTabsComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsTabsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsTabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/tabs/tabs.component.ts b/src/app/settings/tabs/tabs.component.ts new file mode 100644 index 00000000..cf6b6b8e --- /dev/null +++ b/src/app/settings/tabs/tabs.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + + +@Component({ + selector: 'settings-tabs', + templateUrl: './tabs.component.html', + styleUrls: ['./tabs.component.css'] +}) +export class SettingsTabsComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } +} diff --git a/src/app/settings/units/.settings-units.component.html.icloud b/src/app/settings/units/.settings-units.component.html.icloud new file mode 100644 index 00000000..3e97b0a1 Binary files /dev/null and b/src/app/settings/units/.settings-units.component.html.icloud differ diff --git a/src/app/settings-units/settings-units.component.spec.ts b/src/app/settings/units/settings-units.component.spec.ts similarity index 100% rename from src/app/settings-units/settings-units.component.spec.ts rename to src/app/settings/units/settings-units.component.spec.ts diff --git a/src/app/settings-units/settings-units.component.ts b/src/app/settings/units/settings-units.component.ts similarity index 87% rename from src/app/settings-units/settings-units.component.ts rename to src/app/settings/units/settings-units.component.ts index 5a1f2f6e..da204d90 100644 --- a/src/app/settings-units/settings-units.component.ts +++ b/src/app/settings/units/settings-units.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'; +import { FormGroup, FormControl } from '@angular/forms'; import { AppSettingsService } from '../app-settings.service'; import { NotificationsService } from '../notifications.service'; @@ -12,7 +12,7 @@ import { IUnitDefaults, UnitsService, IUnit } from '../units.service'; }) export class SettingsUnitsComponent implements OnInit { - formUnitMaster: UntypedFormGroup; + formUnitMaster: FormGroup; groupUnits: {[key: string]: IUnit}[] = []; defaultUnits: IUnitDefaults; @@ -44,9 +44,9 @@ export class SettingsUnitsComponent implements OnInit { } //generate formGroup - let groups = new UntypedFormGroup({}); + let groups = new FormGroup({}); Object.keys(this.defaultUnits).forEach(key => { - groups.addControl(key, new UntypedFormControl(this.defaultUnits[key])); + groups.addControl(key, new FormControl(this.defaultUnits[key])); }); this.formUnitMaster = groups; diff --git a/src/app/settings-units/settings-units.component.css b/src/app/settings/units/units.component.css similarity index 100% rename from src/app/settings-units/settings-units.component.css rename to src/app/settings/units/units.component.css diff --git a/src/app/settings/units/units.component.html b/src/app/settings/units/units.component.html new file mode 100644 index 00000000..12800a39 --- /dev/null +++ b/src/app/settings/units/units.component.html @@ -0,0 +1,18 @@ +
+
+

Application Default Units

+

Set the default units preferences per types of unit groups. The values will be automatically converted for display.

+ + + {{ unit.description }} + + +
+ + +
+
+
diff --git a/src/app/settings/units/units.component.spec.ts b/src/app/settings/units/units.component.spec.ts new file mode 100644 index 00000000..2d524f1c --- /dev/null +++ b/src/app/settings/units/units.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; + +import { SettingsUnitsComponent } from './units.component'; + +describe('SettingsUnitsComponent', () => { + let component: SettingsUnitsComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ SettingsUnitsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsUnitsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/settings/units/units.component.ts b/src/app/settings/units/units.component.ts new file mode 100644 index 00000000..598ee041 --- /dev/null +++ b/src/app/settings/units/units.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormGroup, UntypedFormControl } from '@angular/forms'; +import { AppSettingsService } from '../../app-settings.service'; +import { NotificationsService } from '../../notifications.service'; + +import { IUnitDefaults, UnitsService, IUnit } from '../../units.service'; + +@Component({ + selector: 'settings-units', + templateUrl: './units.component.html', + styleUrls: ['./units.component.css'] +}) +export class SettingsUnitsComponent implements OnInit { + + formUnitMaster: UntypedFormGroup; + + groupUnits: {[key: string]: IUnit}[] = []; + defaultUnits: IUnitDefaults; + + + + constructor( + private UnitsService: UnitsService, + private appSettingsService: AppSettingsService, + private notificationsService: NotificationsService, + ) { } + + ngOnInit() { + + this.defaultUnits = this.appSettingsService.getDefaultUnits(); + + //format unit group data a bit better for consumption in template + let unitGroupsRaw = this.UnitsService.getConversions(); + + for (let gindex = 0; gindex < unitGroupsRaw.length; gindex++) { + const unitGroup = unitGroupsRaw[gindex]; + let units = []; + + for (let index = 0; index < unitGroup.units.length; index++) { + const unit = unitGroup.units[index]; + units.push(unit); + } + this.groupUnits[unitGroup.group] = units; + } + + //generate formGroup + let groups = new UntypedFormGroup({}); + Object.keys(this.defaultUnits).forEach(key => { + groups.addControl(key, new UntypedFormControl(this.defaultUnits[key])); + }); + + this.formUnitMaster = groups; + this.formUnitMaster.updateValueAndValidity(); + //console.log(this.formUnitMaster); + } + + submitConfig() { + this.appSettingsService.setDefaultUnits(this.formUnitMaster.value); + this.notificationsService.sendSnackbarNotification("Default units configuration saved", 5000, false); + } + +} diff --git a/src/app/settings-zones/settings-edit-zone.modal.css b/src/app/settings/zones/edit-zone.modal.css similarity index 100% rename from src/app/settings-zones/settings-edit-zone.modal.css rename to src/app/settings/zones/edit-zone.modal.css diff --git a/src/app/settings-zones/settings-edit-zone.modal.html b/src/app/settings/zones/edit-zone.modal.html similarity index 100% rename from src/app/settings-zones/settings-edit-zone.modal.html rename to src/app/settings/zones/edit-zone.modal.html diff --git a/src/app/settings-zones/settings-new-zone.modal.css b/src/app/settings/zones/new-zone.modal.css similarity index 100% rename from src/app/settings-zones/settings-new-zone.modal.css rename to src/app/settings/zones/new-zone.modal.css diff --git a/src/app/settings-zones/settings-new-zone.modal.html b/src/app/settings/zones/new-zone.modal.html similarity index 100% rename from src/app/settings-zones/settings-new-zone.modal.html rename to src/app/settings/zones/new-zone.modal.html diff --git a/src/app/settings/zones/settings-edit-zone.modal.html b/src/app/settings/zones/settings-edit-zone.modal.html new file mode 100644 index 00000000..b2e0ae65 --- /dev/null +++ b/src/app/settings/zones/settings-edit-zone.modal.html @@ -0,0 +1,81 @@ +
+

Edit Zone

+ + + + Signal K Path + + + +
+ + Lower value + + + + + Upper value + + + + + + State + + Normal + Warning + Alarm + + + +
+
+ + + + + At least one value is required (lower / upper) + + +
diff --git a/src/app/settings/zones/settings-new-zone.modal.html b/src/app/settings/zones/settings-new-zone.modal.html new file mode 100644 index 00000000..404b087a --- /dev/null +++ b/src/app/settings/zones/settings-new-zone.modal.html @@ -0,0 +1,75 @@ +

Add Zone

+
+ +
+ Restrict to own vessel + + + + + Lower value + + + + + Upper value + + + + + State + + Normal + Warning + Alarm + + +
+
+ + + + + At least one value is required (lower / upper) + + +
diff --git a/src/app/settings-zones/settings-zones.component.css b/src/app/settings/zones/settings-zones.component.css similarity index 100% rename from src/app/settings-zones/settings-zones.component.css rename to src/app/settings/zones/settings-zones.component.css diff --git a/src/app/settings-zones/settings-zones.component.html b/src/app/settings/zones/settings-zones.component.html similarity index 100% rename from src/app/settings-zones/settings-zones.component.html rename to src/app/settings/zones/settings-zones.component.html diff --git a/src/app/settings-zones/settings-zones.component.ts b/src/app/settings/zones/settings-zones.component.ts similarity index 88% rename from src/app/settings-zones/settings-zones.component.ts rename to src/app/settings/zones/settings-zones.component.ts index 30ebdd80..52f0fa31 100644 --- a/src/app/settings-zones/settings-zones.component.ts +++ b/src/app/settings/zones/settings-zones.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit, Inject, Input, ViewChild, ChangeDetectorRef, AfterViewInit } from '@angular/core'; -import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; +import { FormGroup, FormControl, Validators } from '@angular/forms'; import { MatTableDataSource } from '@angular/material/table'; import { Subscription, Observable } from 'rxjs'; import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; @@ -137,17 +137,17 @@ export class SettingsZonesComponent implements OnInit, AfterViewInit { }) export class DialogNewZone { - zoneForm: UntypedFormGroup = new UntypedFormGroup({ - upper: new UntypedFormControl(null), - lower: new UntypedFormControl(null), - state: new UntypedFormControl('0', Validators.required), - filterSelfPaths: new UntypedFormControl(true), - path: new UntypedFormGroup({ - path: new UntypedFormControl(null), - isPathConfigurable: new UntypedFormControl(true), - convertUnitTo: new UntypedFormControl("unitless"), - pathType: new UntypedFormControl("number"), - source: new UntypedFormControl(null) + zoneForm: FormGroup = new FormGroup({ + upper: new FormControl(null), + lower: new FormControl(null), + state: new FormControl('0', Validators.required), + filterSelfPaths: new FormControl(true), + path: new FormGroup({ + path: new FormControl(null), + isPathConfigurable: new FormControl(true), + convertUnitTo: new FormControl("unitless"), + pathType: new FormControl("number"), + source: new FormControl(null) }) }, this.rangeValidationFunction); @@ -161,7 +161,7 @@ export class DialogNewZone { public dialogRef: MatDialogRef) { } - rangeValidationFunction(formGroup: UntypedFormGroup): any { + rangeValidationFunction(formGroup: FormGroup): any { let upper = formGroup.get('upper').value; let lower = formGroup.get('lower').value; return ((upper === null) && (lower === null)) ? { needUpperLower: true } : null; diff --git a/src/app/settings/zones/zones.component.css b/src/app/settings/zones/zones.component.css new file mode 100644 index 00000000..4b9c6c90 --- /dev/null +++ b/src/app/settings/zones/zones.component.css @@ -0,0 +1,80 @@ +.full-display { + width: 100%; + height: 100%; + position: relative; + z-index: 500; +} + +.full-width { + width: 100%; +} + +.buttons { + margin-right: 5px; +} + +.pathCell { + flex: 1 1 40%; +} + +.pathHeader { + flex: 1 1 40%; +} + +.dataHeader { + flex: 1 1 8%; + justify-content: center; +} + +.dataCell { + flex: 1 1 8%; + justify-content: center; +} + +.actionHeader { + flex: 1 1 20%; +} + +.actionCell { + flex: 1 1 20%; + justify-content: end; +} + +@media screen and (max-width: 750px) { + .pathHeader { + flex: 1 1 30%; + } + + .dataHeader { + flex: 1 1 30%; + } + + .actionHeader { + display: none; + } + + .mat-table .mat-cell:before { + content: attr(data-label); + float: left; + padding-right: 5px; + } + + mat-row::after { + min-height: auto; + padding-bottom: 10px; + } + + .dataRow { + flex-direction: column; + align-items: flex-start; + } + + .dataCell { + margin-left: 24px; + } + + .actionCell { + margin-left: 24px; + } + +} diff --git a/src/app/settings/zones/zones.component.html b/src/app/settings/zones/zones.component.html new file mode 100644 index 00000000..0c2ce647 --- /dev/null +++ b/src/app/settings/zones/zones.component.html @@ -0,0 +1,74 @@ +
+

Zones Configuration

+

Zones can be used to inform Kip about the state the data it receives. For example + is a battery voltage of 12.1V normal, requires attention or is in critical state.

+ + Filter + + +
+ + + + + Path + {{element.path}} + + + + + Unit + {{element.unit}} + + + + + Lower + {{element.lower}} + + + + + Upper + {{element.upper}} + + + + + State + +
+
Normal
+
Warning
+
Alarm
+
+
+
+ + + + + + + + + + + + + + + + + No data matching the filter "{{input.value}}" + +
+
+
+ +
+
+ + +
+
diff --git a/src/app/settings/zones/zones.component.ts b/src/app/settings/zones/zones.component.ts new file mode 100644 index 00000000..223bcbdc --- /dev/null +++ b/src/app/settings/zones/zones.component.ts @@ -0,0 +1,198 @@ +import { Component, OnInit, Inject, Input, ViewChild, ChangeDetectorRef, AfterViewInit } from '@angular/core'; +import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; +import { MatTableDataSource } from '@angular/material/table'; +import { Subscription, Observable } from 'rxjs'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; + +import { AppSettingsService } from '../../app-settings.service'; +import { IPathMetaData } from "../../app-interfaces"; +import { IZone } from "../../app-settings.interfaces"; + +@Component({ + selector: 'settings-zones', + templateUrl: './zones.component.html', + styleUrls: ['./zones.component.css'] +}) +export class SettingsZonesComponent implements OnInit, AfterViewInit { + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + tableData = new MatTableDataSource([]); + + displayedColumns: string[] = ['path', 'unit', 'lower', 'upper', 'state', "actions"]; + + zonesSub: Subscription; + + constructor( + private appSettingsService: AppSettingsService, + public dialog: MatDialog, + private cdRef: ChangeDetectorRef, + ) { } + + ngOnInit() { + this.zonesSub = this.appSettingsService.getZonesAsO().subscribe(zones => { + this.tableData.data = zones; + }); + } + + ngAfterViewInit() { + this.tableData.paginator = this.paginator; + this.tableData.sort = this.sort; + this.tableData.filter = ""; + this.cdRef.detectChanges(); + } + + public trackByUuid(index: number, item: IZone): string { + return `${item.uuid}`; + } + + public applyFilter(event: Event) { + const filterValue = (event.target as HTMLInputElement).value; + this.tableData.filter = filterValue.trim().toLowerCase(); + + if (this.tableData.paginator) { + this.tableData.paginator.firstPage(); + } + } + + public openZoneDialog(uuid?: string): void { + let dialogRef; + + if (uuid) { + const thisZone: IZone = this.tableData.data.find((zone: IZone) => { + return zone.uuid === uuid; + }); + + if (thisZone) { + dialogRef = this.dialog.open(DialogEditZone, { + data: thisZone + }); + } + } else { + dialogRef = this.dialog.open(DialogNewZone, { + }); + } + + dialogRef.afterClosed().subscribe((zone: IZone) => { + if (zone === undefined || !zone) { + return; //clicked Cancel, click outside the dialog, or navigated await from page using url bar. + } else { + if (zone.uuid) { + this.editZone(zone); + } else { + zone.uuid = this.newUuid(); + this.addZone(zone); + } + } + }); + } + + public addZone(zone: IZone) { + let zones: IZone[] = this.appSettingsService.getZones(); + zones.push(zone); + this.appSettingsService.saveZones(zones); + } + + public editZone(zone: IZone) { + if (zone.uuid) { // is existing zone + const zones: IZone[] = this.appSettingsService.getZones(); + const index = zones.findIndex(zones => zones.uuid === zone.uuid ); + + if(index >= 0) { + zones.splice(index, 1, zone); + this.appSettingsService.saveZones(zones); + } + } + } + + public deleteZone(uuid: string) { + let zones = this.appSettingsService.getZones(); + //find index + let index = zones.findIndex(zone => zone.uuid === uuid); + if (index >= 0) { + zones.splice(index, 1); + this.appSettingsService.saveZones(zones); + } + } + + private newUuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + } +} + + +// Add zone compoment +@Component({ + selector: 'dialog-new-zone', + templateUrl: 'new-zone.modal.html', + styleUrls: ['./new-zone.modal.css'] +}) +export class DialogNewZone { + + zoneForm: UntypedFormGroup = new UntypedFormGroup({ + upper: new UntypedFormControl(null), + lower: new UntypedFormControl(null), + state: new UntypedFormControl('0', Validators.required), + filterSelfPaths: new UntypedFormControl(true), + path: new UntypedFormGroup({ + path: new UntypedFormControl(null), + isPathConfigurable: new UntypedFormControl(true), + convertUnitTo: new UntypedFormControl("unitless"), + pathType: new UntypedFormControl("number"), + source: new UntypedFormControl(null) + }) + }, this.rangeValidationFunction); + + @Input() filterSelfPaths: boolean; + availablePaths: IPathMetaData[]; + filteredPaths: Observable = new Observable; + + selectedUnit = null; + + constructor( + public dialogRef: MatDialogRef) { + } + + rangeValidationFunction(formGroup: UntypedFormGroup): any { + let upper = formGroup.get('upper').value; + let lower = formGroup.get('lower').value; + return ((upper === null) && (lower === null)) ? { needUpperLower: true } : null; + } + + closeForm() { + let zone: IZone = { + uuid: null, + upper: this.zoneForm.get('upper').value, + lower: this.zoneForm.get('lower').value, + path: this.zoneForm.get('path.path').value, + unit: this.zoneForm.get('path.convertUnitTo').value, + state: parseInt(this.zoneForm.get('state').value) + }; + this.dialogRef.close(zone); + } +} + + +// Edit zone compoment +@Component({ + selector: 'dialog-edit-zone', + templateUrl: 'edit-zone.modal.html', + styleUrls: ['./edit-zone.modal.css'] +}) +export class DialogEditZone { + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public zone: IZone, + ) { } + + closeForm() { + this.dialogRef.close(this.zone); + } +} diff --git a/src/app/signalk-delta.service.ts b/src/app/signalk-delta.service.ts index ddd1d04b..70a3fc1f 100644 --- a/src/app/signalk-delta.service.ts +++ b/src/app/signalk-delta.service.ts @@ -1,8 +1,8 @@ -import { Injectable } from '@angular/core'; +import { Injectable, NgZone } from '@angular/core'; import { BehaviorSubject, delay, Observable , retryWhen, Subject, tap } from 'rxjs'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; -import { ISignalKDeltaMessage, ISignalKMeta, ISignalKMetadata, ISignalKUpdateMessage } from './signalk-interfaces'; +import { ISignalKDeltaMessage, ISignalKMeta, ISignalKUpdateMessage } from './signalk-interfaces'; import { IMeta, INotification, IPathValueData } from "./app-interfaces"; import { SignalKConnectionService, IEndpointStatus } from './signalk-connection.service' import { AuththeticationService, IAuthorizationToken } from './auththetication.service'; @@ -67,6 +67,7 @@ export class SignalKDeltaService { constructor( private server: SignalKConnectionService, private auth: AuththeticationService, + private zones: NgZone ) { // Monitor Connection Service Endpoint Status @@ -159,17 +160,20 @@ export class SignalKDeltaService { this.streamEndpoint$.next(this.streamEndpoint); this.socketWS$ = this.getNewWebSocket(); - this.socketWS$.pipe( - retryWhen(errors => - errors.pipe( - tap(err => { - console.error("[Delta Service] WebSocket error: " + JSON.stringify(err, ["code", "message", "type"])) - }), - delay(this.WS_RECONNECT_INTERVAL) + // Every WebSocket onmessage listener event (data cmming in) generates fires a ChangeDetection cycles that is not relevent in KIP. KIP sends socket messages to internal service data array only, so no UI updates (change detection) are necessary. UI Updates observing the internal data array updates. Running outside zones.js to eliminate unnessesary changedetection cycle. + this.zones.runOutsideAngular(() => { + this.socketWS$.pipe( + retryWhen(errors => + errors.pipe( + tap(err => { + console.error("[Delta Service] WebSocket error: " + JSON.stringify(err, ["code", "message", "type"])) + }), + delay(this.WS_RECONNECT_INTERVAL) + ) ) - ) - ).subscribe(msgWS => { - this.processWebsocketMessage(msgWS); + ).subscribe(msgWS => { + this.processWebsocketMessage(msgWS); + }); }); } diff --git a/src/app/signalk.service.ts b/src/app/signalk.service.ts index 43546a21..5558358e 100644 --- a/src/app/signalk.service.ts +++ b/src/app/signalk.service.ts @@ -8,16 +8,24 @@ import { UnitsService, IUnitDefaults, IUnitGroup } from './units.service'; import { NotificationsService } from './notifications.service'; import Qty from 'js-quantities'; -interface pathRegistrationValue { +export interface pathRegistrationValue { value: any; state: IZoneState; }; +/** + * + * @param {string} uuid The UUID for the widget registering the path + * @param {string} path A Signal K path + * @param {string} source Set Signal K data path source when multiple sources exists for the same path. If set, the Signal K default source will be ignored. + * @param {string} subject A rxjs BehaviorSubject of Type pathRegistrationValue used to return Observable + * @interface pathRegistration + */ interface pathRegistration { uuid: string; path: string; - source: string; // if this is set, updates to observable are the direct value of this source... - observable: BehaviorSubject; + source: string; + subject: BehaviorSubject; } export interface updateStatistics { @@ -138,13 +146,13 @@ export class SignalKService { } subscribePath(uuid: string, path: string, source: string) { - //see if already subscribed, if yes return that... + // see if already subscribed, if yes return that... let registerIndex = this.pathRegister.findIndex(registration => (registration.path == path) && (registration.uuid == uuid)); if (registerIndex >= 0) { // exists - return this.pathRegister[registerIndex].observable.asObservable(); + return this.pathRegister[registerIndex].subject.asObservable(); } - //find if we already have a value for this path to return. + // find if we already have a value for this path to return. let currentValue = null; let state = IZoneState.normal; let pathIndex = this.paths.findIndex(pathObject => pathObject.path == path); @@ -159,18 +167,19 @@ export class SignalKService { state = this.paths[pathIndex].state; } - let newRegister = { + let newRegister: pathRegistration = { uuid: uuid, path: path, source: source, - observable: new BehaviorSubject({ value: currentValue, state: state }) + subject: new BehaviorSubject({ value: currentValue, state: state }) }; - //register + // Add to register array this.pathRegister.push(newRegister); - // should be subscribed now, use search now as maybe someone else adds something and it's no longer last in array :P + // rxjs Subject should be created now. We use search now as maybe someone else adds something and it's no longer last in array :P pathIndex = this.pathRegister.findIndex(registration => (registration.path == path) && (registration.uuid == uuid)); - return this.pathRegister[pathIndex].observable.asObservable(); + // Create Subject observable and and return + return this.pathRegister[pathIndex].subject.asObservable(); } private setSelfUrn(value: string) { @@ -289,7 +298,7 @@ export class SignalKService { console.warn(`Failed updating zone state. Source unknown or not defined for path: ${pathRegister.source}`); } if (source !== null) { - pathRegister.observable.next({ + pathRegister.subject.next({ value: this.paths[pathIndex].sources[source].value, state: this.paths[pathIndex].state }); diff --git a/src/app/widget-base.service.spec.ts b/src/app/widget-base.service.spec.ts new file mode 100644 index 00000000..dfd7c68c --- /dev/null +++ b/src/app/widget-base.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { WidgetBaseService } from './widget-base.service'; + +describe('WidgetBaseService', () => { + let service: WidgetBaseService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(WidgetBaseService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/widget-base.service.ts b/src/app/widget-base.service.ts new file mode 100644 index 00000000..555e50d1 --- /dev/null +++ b/src/app/widget-base.service.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@angular/core'; +import { AppSettingsService } from './app-settings.service'; +import { SignalKService } from './signalk.service'; +import { UnitsService } from './units.service'; + +@Injectable({ + providedIn: 'root' +}) +export class WidgetBaseService { + constructor( + public signalKService: SignalKService, + public unitsService: UnitsService, + public appSettingsService: AppSettingsService + ) { + } +} diff --git a/src/app/widget-blank/widget-blank.component.ts b/src/app/widget-blank/widget-blank.component.ts deleted file mode 100644 index 44fa3ad7..00000000 --- a/src/app/widget-blank/widget-blank.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; - -@Component({ - selector: 'app-widget-blank', - templateUrl: './widget-blank.component.html', - styleUrls: ['./widget-blank.component.scss'] -}) -export class WidgetBlankComponent implements OnInit { - @Input('widgetProperties') widgetProperties!: IWidget; - - defaultConfig: IWidgetSvcConfig = { - displayName: '' - }; - - constructor() { - - } - - ngOnInit() { - } - -} diff --git a/src/app/widget-button/widget-button.component.scss b/src/app/widget-button/widget-button.component.scss deleted file mode 100644 index f7c2c3bc..00000000 --- a/src/app/widget-button/widget-button.component.scss +++ /dev/null @@ -1,93 +0,0 @@ -@use '@angular/material' as mat; - -@mixin widget-button-theme($theme) { - - $ngGauge: map-get($theme, ngGauge); - $themeForeground: map-get($theme, foreground); - $themeBackground: map-get($theme, background); - - app-widget-button { - .primary { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceLight); - } - .accent { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceLight); - } - .warn { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceLight); - } - .primaryDark { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceDark); - } - .accentDark { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceDark); - } - .warnDark { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceDark); - } - .background { - color: mat.get-color-from-palette($themeForeground, divider); - } - .text { - color: mat.get-color-from-palette($themeForeground, text); - } - } -} - -.switchWrapper { - position: relative; - margin: 0px; - top: 47%; - -ms-transform: translateY(-47%); - transform: translateY(-47%); - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; -} - -.light { - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-radius: 50%; - border-color: #262626; - border-style: solid; - border-width: medium; -} - -.square { - position: relative; - width: 10%; /* desired width */ -} - -.square:before{ - content: ""; - display: block; - padding-top: 100%; /* ratio of 1:1*/ -} - -.label { - position: relative; - width: 60%; -} - -.button { - position: relative; - width: 25%; - box-sizing: border-box; - border: inset 4px; - border-radius: 20px; -} - -.button:before{ - content: ""; - display: block; - padding-top: 70%; /* ratio of 1:1*/ -} - -.button:active { - border-style: outset; -} diff --git a/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.scss b/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.scss deleted file mode 100644 index 38c02d09..00000000 --- a/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.scss +++ /dev/null @@ -1,74 +0,0 @@ -@use '@angular/material' as mat; -@use "sass:math"; - -@mixin widget-ngGauge-linear-theme($theme) { - - $ngGauge: map-get($theme, ngGauge); - $themeForeground: map-get($theme, foreground); - - app-widget-gauge-ng-linear { - .primary { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceLight); - } - .accent { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceLight); - } - .warn { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceLight); - } - .primaryDark { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceDark); - } - .accentDark { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceDark); - } - .warnDark { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceDark); - } - .background { - color: mat.get-color-from-palette($themeForeground, divider); - } - .text { - color: mat.get-color-from-palette($themeForeground, text); - } - } -} - -.verticalLinearWrapper { - position: relative; - top: 3%; - height: 97%; - width: 100%; - - > .linearGauge { - position: absolute; - top:0; - right: 0; - bottom: 0; - left: 0; - text-align: center; - } - -} - -.horizontalLinearWrapper { - position: relative; - margin: 0px; - top: 47%; - -ms-transform: translateY(-47%); - transform: translateY(-47%); - &:before { - display:block; - content: ""; - width: 100%; - padding-top: math.div(1, 4) * 97%; - margin-top: 3%; - } - > .linearGauge { - position: absolute; - top:0; - right: 0; - bottom: 0; - left: 0; - } -} diff --git a/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss b/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss deleted file mode 100644 index ea37d897..00000000 --- a/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss +++ /dev/null @@ -1,48 +0,0 @@ -@use '@angular/material' as mat; - -@mixin widget-ngGauge-radial-theme($theme) { - - $ngGauge: map-get($theme, ngGauge); - $themeForeground: map-get($theme, foreground); - - app-widget-gauge-ng-radial { - .primary { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceLight); - } - .accent { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceLight); - } - .warn { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceLight); - } - .primaryDark { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceDark); - } - .accentDark { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceDark); - } - .warnDark { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceDark); - } - .background { - color: mat.get-color-from-palette($themeForeground, divider); - } - .text { - color: mat.get-color-from-palette($themeForeground, text); - } - } -} - -radial-gauge.radialGauge { - position: relative; - width: 94% !important; - height: auto !important; - top: 6%; -} - -.ngRadialWrapper { - position: relative; - width: 100%; - height: 100%; - text-align: center; - } diff --git a/src/app/widget-gauge/widget-gauge.component.ts b/src/app/widget-gauge/widget-gauge.component.ts deleted file mode 100644 index 9de190b4..00000000 --- a/src/app/widget-gauge/widget-gauge.component.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; -import { Subscription } from 'rxjs'; - -import { SignalKService } from '../signalk.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { UnitsService } from '../units.service'; - - -@Component({ - selector: 'app-widget-gauge', - templateUrl: './widget-gauge.component.html', - styleUrls: ['./widget-gauge.component.css'] -}) -export class WidgetGaugeComponent implements OnInit, OnDestroy { - - @Input('widgetProperties') widgetProperties!: IWidget; - - defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - paths: { - "gaugePath": { - description: "Numeric Data", - path: null, - source: null, - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "unitless" - } - }, - gaugeType: 'linear', - barGraph: false, // if linear/radial, is it digital? - radialSize: 'full', - minValue: 0, - maxValue: 100, - rotateFace: false, - backgroundColor: 'carbon', - frameColor: 'anthracite' - }; - - dataValue: any = null; - valueSub: Subscription = null; - - constructor( - private SignalKService: SignalKService, - private UnitsService: UnitsService) { - } - - ngOnInit() { - this.subscribePath(); - } - - ngOnDestroy() { - this.unsubscribePath(); - } - - - subscribePath() { - this.unsubscribePath(); - if (typeof(this.widgetProperties.config.paths['gaugePath'].path) != 'string') { return } // nothing to sub to... - - this.valueSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path, this.widgetProperties.config.paths['gaugePath'].source).subscribe( - newValue => { - this.dataValue = this.UnitsService.convertUnit(this.widgetProperties.config.paths['gaugePath'].convertUnitTo, newValue.value); - } - ); - } - - unsubscribePath() { - if (this.valueSub !== null) { - this.valueSub.unsubscribe(); - this.valueSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path) - } - } -} diff --git a/src/app/widget-historical/widget-historical.component.scss b/src/app/widget-historical/widget-historical.component.scss deleted file mode 100644 index e69de29b..00000000 diff --git a/src/app/widget-iframe/widget-iframe.component.ts b/src/app/widget-iframe/widget-iframe.component.ts deleted file mode 100644 index 6e3c3c32..00000000 --- a/src/app/widget-iframe/widget-iframe.component.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; - -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; - -@Component({ - selector: 'app-widget-iframe', - templateUrl: './widget-iframe.component.html', - styleUrls: ['./widget-iframe.component.css'] -}) -export class WidgetIframeComponent implements OnInit { - @Input('widgetProperties') widgetProperties!: IWidget; - - defaultConfig: IWidgetSvcConfig = { - widgetUrl: null - }; - - widgetUrl: string = null; - - constructor() { - } - - ngOnInit() { - this.widgetUrl = this.widgetProperties.config.widgetUrl; - } - -} diff --git a/src/app/widget-list.service.ts b/src/app/widget-list.service.ts index 64847db2..cdf09816 100644 --- a/src/app/widget-list.service.ts +++ b/src/app/widget-list.service.ts @@ -1,22 +1,22 @@ import { Injectable } from '@angular/core'; -import { WidgetBlankComponent } from './widget-blank/widget-blank.component'; -import { WidgetUnknownComponent } from './widget-unknown/widget-unknown.component'; -import { WidgetNumericComponent } from './widget-numeric/widget-numeric.component'; -import { WidgetTextGenericComponent } from './widget-text-generic/widget-text-generic.component'; -import { WidgetDateGenericComponent } from './widget-date-generic/widget-date-generic.component'; -import { WidgetHistoricalComponent } from './widget-historical/widget-historical.component'; -import { WidgetWindComponent } from './widget-wind/widget-wind.component'; -import { WidgetGaugeComponent } from './widget-gauge/widget-gauge.component'; -import { WidgetButtonComponent } from './widget-button/widget-button.component'; -import { WidgetSwitchComponent } from './widget-switch/widget-switch.component'; -import { WidgetIframeComponent } from './widget-iframe/widget-iframe.component'; -import { WidgetTutorialComponent } from './widget-tutorial/widget-tutorial.component'; -import { WidgetGaugeNgLinearComponent} from './widget-gauge-ng-linear/widget-gauge-ng-linear.component'; -import { WidgetGaugeNgRadialComponent} from './widget-gauge-ng-radial/widget-gauge-ng-radial.component'; -import { WidgetAutopilotComponent } from "./widget-autopilot/widget-autopilot.component"; -import { WidgetSimpleLinearComponent } from "./widget-simple-linear/widget-simple-linear.component"; -import { WidgetRaceTimerComponent } from './widget-race-timer/widget-race-timer.component'; +import { WidgetBlankComponent } from './widgets/widget-blank/widget-blank.component'; +import { WidgetUnknownComponent } from './widgets/widget-unknown/widget-unknown.component'; +import { WidgetNumericComponent } from './widgets/widget-numeric/widget-numeric.component'; +import { WidgetTextGenericComponent } from './widgets/widget-text-generic/widget-text-generic.component'; +import { WidgetDateGenericComponent } from './widgets/widget-date-generic/widget-date-generic.component'; +import { WidgetHistoricalComponent } from './widgets/widget-historical/widget-historical.component'; +import { WidgetWindComponent } from './widgets/widget-wind/widget-wind.component'; +import { WidgetGaugeComponent } from './widgets/widget-gauge/widget-gauge.component'; +import { WidgetButtonComponent } from './widgets/widget-button/widget-button.component'; +import { WidgetSwitchComponent } from './widgets/widget-switch/widget-switch.component'; +import { WidgetIframeComponent } from './widgets/widget-iframe/widget-iframe.component'; +import { WidgetTutorialComponent } from './widgets/widget-tutorial/widget-tutorial.component'; +import { WidgetGaugeNgLinearComponent} from './widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component'; +import { WidgetGaugeNgRadialComponent} from './widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component'; +import { WidgetAutopilotComponent } from "./widgets/widget-autopilot/widget-autopilot.component"; +import { WidgetSimpleLinearComponent } from "./widgets/widget-simple-linear/widget-simple-linear.component"; +import { WidgetRaceTimerComponent } from './widgets/widget-race-timer/widget-race-timer.component'; class widgetInfo { name: string; diff --git a/src/app/widget-manager.service.ts b/src/app/widget-manager.service.ts index 12ca62ea..60f43646 100644 --- a/src/app/widget-manager.service.ts +++ b/src/app/widget-manager.service.ts @@ -4,114 +4,7 @@ */ import { Injectable } from '@angular/core'; import { AppSettingsService } from './app-settings.service'; -import { Format, Policy } from './signalk-interfaces'; -/** - * This interface defines possible Widget properties. - * - * @export - * @interface IWidget - */ -export interface IWidget { - uuid: string; - type: string; - config: IWidgetSvcConfig; -} - -/** - * This interface defines all possible Widget configuration settings. - * Usage: Widgets for configuration storage and Widget Manager service. - * - * Note: Used by IWidget interface. - * - * @export - * @interface IWidgetSvcConfig - */ -export interface IWidgetSvcConfig { - displayName?: string; - filterSelfPaths?: boolean; // widget filter self paths only? - paths?: { - [key: string]: IWidgetPaths; - }; - convertUnitTo?: string; - usage?: { - [key: string]: string[]; // Autopilot: key should match key in paths, specifies autopilot widget possible paths for AP mode - }; - typeVal?: { - [key: string]: string; // Autopilot: key should match key in paths, specifies autopilot widget paths value type for AP mode - }; - - // numeric data - numDecimal?: number; // number of decimal places if a number - numInt?: number; - showMin?: boolean; - showMax?: boolean; - - // date data - dateFormat?: string; - dateTimezone?: string; - - // Wind Gauge data - windSectorEnable?: boolean; - windSectorWindowSeconds?: number; - laylineEnable?: boolean; - laylineAngle?: number; - - // gauge Data - gaugeType?: string; - gaugeUnitLabelFormat?: string; - gaugeTicks?: boolean; - barGraph?: boolean; - backgroundColor?: string; - frameColor?: string; - barColor?: string; - radialSize?: string; - minValue?: number; - maxValue?: number; - rotateFace?: boolean; - autoStart?: boolean; - compassUseNumbers?: boolean; - - // Historical - dataSetUUID?: string; - invertData?: boolean; - displayMinMax?: boolean; - animateGraph?: boolean; - includeZero?: boolean; - verticalGraph?: boolean; - - // Puts - putEnable?: boolean; - putMomentary?: boolean; - putMomentaryValue?: boolean; - - // iFrame - widgetUrl?: string; - - - // Race Timer - timerLength?: number; -} -/** - * Defines all possible properties for data paths. Combines both - * both KIP and Signal K path features. - * - * @interface IWidgetPaths - */ -interface IWidgetPaths { - description: string; - path: string | null; - source: string | null; - pathType: string | null; - pathFilter?: string; // Future - use to filter path list ie. self.navigation.* or *.navigation.*.blabla.* - convertUnitTo?: string; // Convert SignalK value to specific format for display. Also used as a source to identify conversion group - isPathConfigurable: boolean; // should we show this path in Widget Path config or is it static and hidden - period?: number; // Signal K - period=[millisecs] becomes the transmission rate, e.g. every period/1000 seconds. Default: 1000 - format?: Format; // Signal K - format=[delta|full] specifies delta or full format. Default: delta - policy?: Policy; // Signal K - policy=[instant|ideal|fixed]. Default: ideal - minPeriod?: number; // Signal K - minPeriod=[millisecs] becomes the fastest message transmission rate allowed, e.g. every minPeriod/1000 seconds. This is only relevant for policy='instant' to avoid swamping the client or network. -} - - +import { IWidget } from './widgets-interface'; @Injectable() export class WidgetManagerService { diff --git a/src/app/widget-simple-linear/widget-simple-linear.component.scss b/src/app/widget-simple-linear/widget-simple-linear.component.scss deleted file mode 100644 index 0c497b01..00000000 --- a/src/app/widget-simple-linear/widget-simple-linear.component.scss +++ /dev/null @@ -1,44 +0,0 @@ -@use '@angular/material' as mat; - -@mixin widget-simple-linear-theme($theme) { - - $ngGauge: map-get($theme, ngGauge); - $themeForeground: map-get($theme, foreground); - - app-widget-simple-linear { - .primary { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceLight); - } - .accent { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceLight); - } - .warn { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceLight); - } - .primaryDark { - color: mat.get-color-from-palette($ngGauge, primary-gaugeFaceDark); - } - .accentDark { - color: mat.get-color-from-palette($ngGauge, accent-gaugeFaceDark); - } - .warnDark { - color: mat.get-color-from-palette($ngGauge, warn-gaugeFaceDark); - } - .background { - color: mat.get-color-from-palette($themeForeground, divider); - } - .text { - color: mat.get-color-from-palette($themeForeground, text); - } - } -} - -.simpleLinearGauge { - display:block; - position: relative; - border:none; - margin: 0px; - padding: 5px 0px; - width: 100%; - height: 100%; -} diff --git a/src/app/widget-simple-linear/widget-simple-linear.component.ts b/src/app/widget-simple-linear/widget-simple-linear.component.ts deleted file mode 100644 index f2c63c85..00000000 --- a/src/app/widget-simple-linear/widget-simple-linear.component.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { ViewChild, Input, ElementRef, Component, OnInit, OnDestroy } from '@angular/core'; -import { Subscription, sampleTime } from 'rxjs'; - -import { SignalKService } from '../signalk.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { UnitsService } from '../units.service'; -import { AppSettingsService } from '../app-settings.service'; - -@Component({ - selector: 'app-widget-simple-linear', - templateUrl: './widget-simple-linear.component.html', - styleUrls: ['./widget-simple-linear.component.scss'] -}) -export class WidgetSimpleLinearComponent implements OnInit, OnDestroy { - @Input('widgetProperties') widgetProperties!: IWidget; - @ViewChild('primary', {static: true, read: ElementRef}) private primaryElement: ElementRef; - @ViewChild('accent', {static: true, read: ElementRef}) private accentElement: ElementRef; - @ViewChild('warn', {static: true, read: ElementRef}) private warnElement: ElementRef; - @ViewChild('primaryDark', {static: true, read: ElementRef}) private primaryDarkElement: ElementRef; - @ViewChild('accentDark', {static: true, read: ElementRef}) private accentDarkElement: ElementRef; - @ViewChild('warnDark', {static: true, read: ElementRef}) private warnDarkElement: ElementRef; - @ViewChild('background', {static: true, read: ElementRef}) private backgroundElement: ElementRef; - @ViewChild('text', {static: true, read: ElementRef}) private textElement: ElementRef; - - defaultConfig: IWidgetSvcConfig = { - displayName: "Display Name", - filterSelfPaths: true, - paths: { - "gaugePath": { - description: "Numeric Data", - path: null, - source: null, - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "v" - } - }, - minValue: 0, - maxValue: 15, - numInt: 1, - numDecimal: 2, - gaugeType: "simpleLinear", // Applied to Units label. abr = first letter only. full = full string - gaugeUnitLabelFormat: "full", // Applied to Units label. abr = first letter only. full = full string - barColor: 'accent', - }; - - // main gauge value variable - public unitsLabel:string = ""; - public dataValue: string = "0"; - public gaugeValue: Number = 0; - public barColor: string = ""; - public barColorGradient: string = ""; - public barColorBackground: string = ""; - - - private valueSub$: Subscription = null; - private sample: number = 500; - - // dynamics theme support - themeNameSub: Subscription = null; - - constructor( - private signalKService: SignalKService, - private unitsService: UnitsService, - private appSettingsService: AppSettingsService, // need for theme change subscription - ) { } - - ngOnInit(): void { - this.updateGaugeSettings(); - this.subscribePath(); - this.subscribeTheme(); - } - - updateGaugeSettings() { - this.barColorBackground = window.getComputedStyle(this.backgroundElement.nativeElement).color; - - switch (this.widgetProperties.config.barColor) { - case "primary": - this.barColor = getComputedStyle(this.primaryElement.nativeElement).color; - this.barColorGradient = getComputedStyle(this.primaryDarkElement.nativeElement).color; - break; - - case "accent": - this.barColor = getComputedStyle(this.accentElement.nativeElement).color; - this.barColorGradient = getComputedStyle(this.accentDarkElement.nativeElement).color; - break; - - case "warn": - this.barColor = getComputedStyle(this.warnElement.nativeElement).color; - this.barColorGradient = getComputedStyle(this.warnDarkElement.nativeElement).color; - break; - } - } - - subscribePath() { - this.unsubscribePath(); - - // set Units label sting based on gauge config - if (this.widgetProperties.config.gaugeUnitLabelFormat == "abr") { - this.unitsLabel = this.widgetProperties.config.paths['gaugePath'].convertUnitTo.substr(0,1); - } else { - this.unitsLabel = this.widgetProperties.config.paths['gaugePath'].convertUnitTo; - } - - if (typeof(this.widgetProperties.config.paths['gaugePath'].path) != 'string') { return } // nothing to sub to... - - this.valueSub$ = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path, this.widgetProperties.config.paths['gaugePath'].source).pipe(sampleTime(this.sample)).subscribe( - newValue => { - if (newValue.value == null) {return} - - // convert to unit and format value using widget settings - let value = this.unitsService.convertUnit(this.widgetProperties.config.paths['gaugePath'].convertUnitTo, newValue.value).toFixed(this.widgetProperties.config.numDecimal); - - // Format display value using widget settings - let displayValue = value; - if (this.widgetProperties.config.numDecimal != 0){ - this.dataValue = displayValue.padStart((this.widgetProperties.config.numInt + this.widgetProperties.config.numDecimal + 1), "0"); - } else { - this.dataValue = displayValue.padStart(this.widgetProperties.config.numInt, "0"); - } - - // Format value for gauge bar - let gaugeValue = Number(value); - - // Limit gauge bar animation overflow to gauge settings - if (gaugeValue >= this.widgetProperties.config.maxValue) { - this.gaugeValue = this.widgetProperties.config.maxValue; - } else if (gaugeValue <= this.widgetProperties.config.minValue) { - this.gaugeValue = this.widgetProperties.config.minValue; - } else { - this.gaugeValue = gaugeValue; - } - } - ); - } - - unsubscribePath() { - if (this.valueSub$ !== null) { - this.valueSub$.unsubscribe(); - this.valueSub$ = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path) - } - } - - // Subscribe to theme event - subscribeTheme() { - this.themeNameSub = this.appSettingsService.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // delay so browser getComputedStyles has time to complete theme style change. - this.updateGaugeSettings(); - }, 50); - }) - } - - unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; - } - } - - ngOnDestroy() { - this.unsubscribePath(); - this.unsubscribeTheme(); - } - -} diff --git a/src/app/widget-switch/widget-switch.component.ts b/src/app/widget-switch/widget-switch.component.ts deleted file mode 100644 index 6a9541be..00000000 --- a/src/app/widget-switch/widget-switch.component.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Component, Input, OnInit, OnDestroy, Inject } from '@angular/core'; -import { Subscription } from 'rxjs'; - -import { MatDialog } from '@angular/material/dialog'; - -import { ModalWidgetComponent } from '../modal-widget/modal-widget.component'; -import { SignalKService } from '../signalk.service'; -import { SignalkRequestsService, skRequest } from '../signalk-requests.service'; -import { WidgetManagerService, IWidget, IWidgetSvcConfig } from '../widget-manager.service'; - - -const defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - paths: { - "statePath": { - description: "State Data", - path: null, - source: null, - pathType: "boolean", - isPathConfigurable: true, - convertUnitTo: "unitless" - } - }, -}; - -@Component({ - selector: 'app-widget-switch', - templateUrl: './widget-switch.component.html', - styleUrls: ['./widget-switch.component.css'] -}) -export class WidgetSwitchComponent implements OnInit, OnDestroy { - - @Input('widgetUUID') widgetUUID: string; - @Input('unlockStatus') unlockStatus: boolean; - - activeWidget: IWidget; - config: IWidgetSvcConfig; - - dataValue: number = null; - dataTimestamp: number = Date.now(); - valueSub: Subscription = null; - - skRequestSub: Subscription = null; - - state: boolean = null; - - constructor( - public dialog:MatDialog, - private SignalKService: SignalKService, - private SignalkRequestsService: SignalkRequestsService, - private WidgetManagerService: WidgetManagerService) { - } - - ngOnInit() { - this.activeWidget = this.WidgetManagerService.getWidget(this.widgetUUID); - if (this.activeWidget.config === null) { - // no data, let's set some! - this.WidgetManagerService.updateWidgetConfig(this.widgetUUID, defaultConfig); - this.config = defaultConfig; // load default config. - } else { - this.config = this.activeWidget.config; - } - this.subscribePath(); - this.subscribeSKRequest(); - } - - ngOnDestroy() { - this.unsubscribePath(); - this.unsubscribeSKRequest(); - } - - subscribePath() { - this.unsubscribePath(); - if (typeof(this.config.paths['statePath'].path) != 'string') { return } // nothing to sub to... - - this.valueSub = this.SignalKService.subscribePath(this.widgetUUID, this.config.paths['statePath'].path, this.config.paths['statePath'].source).subscribe( - newValue => { - this.state = newValue.value; - } - ); - } - - unsubscribePath() { - if (this.valueSub !== null) { - this.valueSub.unsubscribe(); - this.valueSub = null; - this.SignalKService.unsubscribePath(this.widgetUUID, this.config.paths['statePath'].path); - } - } - - subscribeSKRequest() { - this.skRequestSub = this.SignalkRequestsService.subscribeRequest().subscribe(requestResult => { - if (requestResult.widgetUUID == this.widgetUUID) { - if (requestResult.statusCode != 200){ - let errMsg = requestResult.statusCode + " - " +requestResult.statusCodeDescription; - if (requestResult.message){ - errMsg = errMsg + " Server Message: " + requestResult.message; - } - alert('[Widget Name: ' + errMsg); - } else { - console.log("AP Received: \n" + JSON.stringify(requestResult)); - } - } - }); - } - - unsubscribeSKRequest() { - this.skRequestSub.unsubscribe(); - } - - sendDelta(value: boolean) { - this.SignalkRequestsService.putRequest(this.config.paths['statePath'].path, value, this.widgetUUID); - } - - openWidgetSettings() { - let dialogRef = this.dialog.open(ModalWidgetComponent, { - width: '80%', - data: this.config - }); - - dialogRef.afterClosed().subscribe(result => { - // save new settings - if (result) { - console.log(result); - this.unsubscribePath();//unsub now as we will change variables so wont know what was subbed before... - this.config = result; - this.WidgetManagerService.updateWidgetConfig(this.widgetUUID, this.config); - this.subscribePath(); - } - }); - } - -} diff --git a/src/app/widget-tutorial/widget-tutorial.component.ts b/src/app/widget-tutorial/widget-tutorial.component.ts deleted file mode 100644 index f62d12b9..00000000 --- a/src/app/widget-tutorial/widget-tutorial.component.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; - -@Component({ - selector: 'app-widget-tutorial', - templateUrl: './widget-tutorial.component.html', - styleUrls: ['./widget-tutorial.component.css'] -}) -export class WidgetTutorialComponent implements OnInit { - @Input('widgetUUID') widgetUUID: string; - @Input('unlockStatus') unlockStatus: boolean; - - constructor() { } - - ngOnInit() { - } - -} diff --git a/src/app/widget-unknown/widget-unknown.component.ts b/src/app/widget-unknown/widget-unknown.component.ts deleted file mode 100644 index 090c990f..00000000 --- a/src/app/widget-unknown/widget-unknown.component.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Component, OnInit } from '@angular/core'; - -@Component({ - selector: 'app-widget-unknown', - templateUrl: './widget-unknown.component.html', - styleUrls: ['./widget-unknown.component.css'] -}) -export class WidgetUnknownComponent implements OnInit { - - constructor() { } - - ngOnInit() { - } - -} diff --git a/src/app/widget-wind/widget-wind.component.ts b/src/app/widget-wind/widget-wind.component.ts deleted file mode 100644 index 187c5230..00000000 --- a/src/app/widget-wind/widget-wind.component.ts +++ /dev/null @@ -1,316 +0,0 @@ -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; -import { Subscription, interval } from 'rxjs'; - -import { SignalKService } from '../signalk.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { UnitsService } from '../units.service'; - -@Component({ - selector: 'app-widget-wind', - templateUrl: './widget-wind.component.html', - styleUrls: ['./widget-wind.component.css'] -}) -export class WidgetWindComponent implements OnInit, OnDestroy { - @Input('widgetProperties') widgetProperties!: IWidget; - - defaultConfig: IWidgetSvcConfig = { - filterSelfPaths: true, - paths: { - "headingPath": { - description: "Heading", - path: 'self.navigation.headingTrue', - source: 'default', - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "deg" - }, - "trueWindAngle": { - description: "True Wind Angle", - path: 'self.environment.wind.angleTrueWater', - source: 'default', - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "deg" - }, - "trueWindSpeed": { - description: "True Wind Speed", - path: 'self.environment.wind.speedTrue', - source: 'default', - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "knots" - }, - "appWindAngle": { - description: "Apparent Wind Angle", - path: 'self.environment.wind.angleApparent', - source: 'default', - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "deg" - }, - "appWindSpeed": { - description: "Apparent Wind Speed", - path: 'self.environment.wind.speedApparent', - source: 'default', - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "knots" - }, - }, - windSectorEnable: true, - windSectorWindowSeconds: 10, - laylineEnable: true, - laylineAngle: 35, - }; - - currentHeading: number = 0; - headingSub: Subscription = null; - - appWindAngle: number = null; - appWindAngleSub: Subscription = null; - - appWindSpeed: number = null; - appWindSpeedSub: Subscription = null; - - trueWindAngle: number = null; - trueWindAngleSub: Subscription = null; - - trueWindSpeed: number = null; - trueWindSpeedSub: Subscription = null; - - trueWindHistoric: { - timestamp: number; - heading: number; - }[] = []; - trueWindMinHistoric: number; - trueWindMidHistoric: number; - trueWindMaxHistoric: number; - - windSectorObservableSub: Subscription; - - - constructor( - private signalKService: SignalKService, - private unitsService: UnitsService) { - } - - - ngOnInit() { - this.startAll(); - } - - ngOnDestroy() { - this.stopAll(); - } - - startAll() { - this.subscribeHeading(); - this.subscribeAppWindAngle(); - this.subscribeAppWindSpeed(); - this.subscribeTrueWindAngle(); - this.subscribeTrueWindSpeed(); - this.startWindSectors(); - } - - stopAll() { - this.unsubscribeHeading(); - this.unsubscribeAppWindAngle(); - this.unsubscribeAppWindSpeed(); - this.unsubscribeTrueWindAngle(); - this.unsubscribeTrueWindSpeed(); - this.stopWindSectors(); - } - - subscribeHeading() { - this.unsubscribeHeading(); - if (typeof(this.widgetProperties.config.paths['headingPath'].path) != 'string') { return } // nothing to sub to... - this.headingSub = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['headingPath'].path, this.widgetProperties.config.paths['headingPath'].source).subscribe( - newValue => { - if (newValue.value === null) { - this.currentHeading = 0; - } else { - this.currentHeading = this.unitsService.convertUnit('deg', newValue.value); - } - - } - ); - } - - subscribeAppWindAngle() { - this.unsubscribeAppWindAngle(); - if (typeof(this.widgetProperties.config.paths['appWindAngle'].path) != 'string') { return } // nothing to sub to... - - this.appWindAngleSub = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['appWindAngle'].path, this.widgetProperties.config.paths['appWindAngle'].source).subscribe( - newValue => { - if (newValue.value === null) { - this.appWindAngle = null; - return; - } - - let converted = this.unitsService.convertUnit('deg', newValue.value); - // 0-180+ for stb - // -0 to -180 for port - // need in 0-360 - if (converted < 0) {// stb - this.appWindAngle= 360 + converted; // adding a negative number subtracts it... - } else { - this.appWindAngle = converted; - } - - } - ); - } - - subscribeAppWindSpeed() { - this.unsubscribeAppWindSpeed(); - if (typeof(this.widgetProperties.config.paths['appWindSpeed'].path) != 'string') { return } // nothing to sub to... - - this.appWindSpeedSub = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['appWindSpeed'].path, this.widgetProperties.config.paths['appWindSpeed'].source).subscribe( - newValue => { - this.appWindSpeed = this.unitsService.convertUnit(this.widgetProperties.config.paths['appWindSpeed'].convertUnitTo, newValue.value); - } - ); - } - - subscribeTrueWindAngle() { - this.unsubscribeTrueWindAngle(); - if (typeof(this.widgetProperties.config.paths['trueWindAngle'].path) != 'string') { return } // nothing to sub to... - - this.trueWindAngleSub = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['trueWindAngle'].path, this.widgetProperties.config.paths['trueWindAngle'].source).subscribe( - newValue => { - if (newValue.value === null) { - this.trueWindAngle = null; - return; - } - - let converted = this.unitsService.convertUnit('deg', newValue.value); - - // Depending on path, this number can either be the magnetic compass heading, true compass heading, or heading relative to boat heading (-180 to 180deg)... Ugh... - // 0-180+ for stb - // -0 to -180 for port - // need in 0-360 - - if (this.widgetProperties.config.paths['trueWindAngle'].path.match('angleTrueWater')|| - this.widgetProperties.config.paths['trueWindAngle'].path.match('angleTrueGround')) { - //-180 to 180 - this.trueWindAngle = this.addHeading(this.currentHeading, converted); - } else if (this.widgetProperties.config.paths['trueWindAngle'].path.match('direction')) { - //0-360 - this.trueWindAngle = converted; - } else { - // some other path... assume it's the angle - this.trueWindAngle = converted; - } - - //add to historical for wind sectors - if (this.widgetProperties.config.windSectorEnable) { - this.addHistoricalTrue(this.trueWindAngle); - } - } - ); - } - - subscribeTrueWindSpeed() { - this.unsubscribeTrueWindSpeed(); - if (typeof(this.widgetProperties.config.paths['trueWindSpeed'].path) != 'string') { return } // nothing to sub to... - - this.trueWindSpeedSub = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['trueWindSpeed'].path, this.widgetProperties.config.paths['trueWindSpeed'].source).subscribe( - newValue => { - this.trueWindSpeed = this.unitsService.convertUnit(this.widgetProperties.config.paths['trueWindSpeed'].convertUnitTo, newValue.value); - } - ); - } - - startWindSectors() { - this.windSectorObservableSub = interval (500).subscribe(x => { - this.historicalCleanup(); - }); - } - - addHistoricalTrue (windHeading) { - this.trueWindHistoric.push({ - timestamp: Date.now(), - heading: windHeading - }); - let arr = this.arcForAngles(this.trueWindHistoric.map(d => d.heading)); - this.trueWindMinHistoric = arr[0]; - this.trueWindMaxHistoric = arr[1]; - this.trueWindMidHistoric = arr[2]; - } - - arcForAngles (data) { - return data.slice(1).reduce((acc, theValue) => { - let value = theValue - while (value < acc[0] - 180) { - value += 360 - } - while (value > acc[1] + 180) { - value -= 360 - } - acc[0] = Math.min(acc[0], value) - acc[1] = Math.max(acc[1], value) - acc[2] = ((acc[1]-acc[0])/2)+acc[0]; - return acc - }, [data[0], data[0]]) - } - - historicalCleanup() { - let n = Date.now()-(this.widgetProperties.config.windSectorWindowSeconds*1000); - for (var i = this.trueWindHistoric.length - 1; i >= 0; --i) { - if (this.trueWindHistoric[i].timestamp < n) { - this.trueWindHistoric.splice(i,1); - } - } - } - - stopWindSectors() { - this.windSectorObservableSub.unsubscribe(); - } - - unsubscribeHeading() { - if (this.headingSub !== null) { - this.headingSub.unsubscribe(); - this.headingSub = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['headingPath'].path); - } - } - - unsubscribeAppWindAngle() { - if (this.appWindAngleSub !== null) { - this.appWindAngleSub.unsubscribe(); - this.appWindAngleSub = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['appWindAngle'].path); - } - } - - unsubscribeAppWindSpeed() { - if (this.appWindSpeedSub !== null) { - this.appWindSpeedSub.unsubscribe(); - this.appWindSpeedSub = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['appWindSpeed'].path); - } - } - - unsubscribeTrueWindAngle() { - if (this.trueWindAngleSub !== null) { - this.trueWindAngleSub.unsubscribe(); - this.trueWindAngleSub = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['trueWindAngle'].path); - } - } - - unsubscribeTrueWindSpeed() { - if (this.trueWindSpeedSub !== null) { - this.trueWindSpeedSub.unsubscribe(); - this.trueWindSpeedSub = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['trueWindSpeed'].path); - } - } - - addHeading(h1: number, h2: number) { - let h3 = h1 + h2; - while (h3 > 359) { h3 = h3 - 359; } - while (h3 < 0) { h3 = h3 + 359; } - return h3; - } -} diff --git a/src/app/widgets-interface.ts b/src/app/widgets-interface.ts new file mode 100644 index 00000000..ad21f86b --- /dev/null +++ b/src/app/widgets-interface.ts @@ -0,0 +1,220 @@ +import { Format, Policy } from './signalk-interfaces'; + +/** + * KIP Dynamic Widgets interface. + * + * @export + * @interface DynamicWidget + */ +export interface DynamicWidget { + widgetProperties: IWidget; + theme: ITheme; + defaultConfig: IWidgetSvcConfig; + unlockStatus?: boolean; // only used by Tutorial +} + +/** + * Description of standard Angualr Material Theme colors. + * + * @export + * @interface ITheme + */ +export interface ITheme { + primary: string; + accent: string; + warn: string; + primaryDark: string; + accentDark: string; + warnDark: string; + background: string; + text: string; +} + +/** + * This interface defines possible Widget properties. + * + * @export + * @interface IWidget + */ +export interface IWidget { + /** The Widget's unique identifyer */ + uuid: string; + /** The Widget's type. Value are defined in widget-list.service */ + type: string; + /** The Widget's configuration Object */ + config: IWidgetSvcConfig; +} + +/** + * Array of Signal K data path configuration. + * + * @export + * @interface IPathArray + */ +export interface IPathArray { + /** Key string use to name/identifie the path IWidgetPath object. Used for Observable setup */ + [key: string]: IWidgetPath; +} + +/** + * This interface defines all possible Widget configuration settings. + * Usage: Widgets for configuration storage and Widget Manager service. + * + * Note: Used by IWidget interface. + * + * @export + * @interface IWidgetSvcConfig + * + */ +export interface IWidgetSvcConfig { + /** The Widget's display label */ + displayName?: string; + /** Set to True to limit all Widget's data paths selection of the configuration UI Paths panel to Self. ie. the user's vessel. Value of True will prevent listing of all Signal K known paths that come from buoy, towers, other vessels, etc. Else all Signal K know data will be listed for selection. Should be set to True unless you need non-self data paths such as monitoring remote vessels, etc. */ + filterSelfPaths?: boolean; + /** An [key:string] Array of Signal K paths configuration. Used to name/identifie the path IWidgetPath object. Used for Observable setup*/ + paths?:IPathArray; + /** Use by Autopilot Widget: key should match key in paths, specifies autopilot widget possible paths for AP mode */ + usage?: { + [key: string]: string[]; + }; + /** Use by Autopilot Widget: key should match key in paths, specifies autopilot widget paths value type for AP mode */ + typeVal?: { + [key: string]: string; + }; + + /** Used by multiple Widget: number of fixed decimal places to display */ + numDecimal?: number; + /** Used by multiple Widget: number of fixed Integer places to display */ + numInt?: number; + /** Used by numeric data Widget: Show minimum registered value since started */ + showMin?: boolean; + /** Used by numeric data Widget: Show maximum registered value since started */ + showMax?: boolean; + + /** Used by date Widget: configurable display format of the date/time value */ + dateFormat?: string; + /** Used by date Widget: Time zone value to apply to the data/time value */ + dateTimezone?: string; + + /** Used by wind Widget: enable/disable wind sector UI feature */ + windSectorEnable?: boolean; + /** Used by wind Widget: duration to track wind shift in the sector UI feature */ + windSectorWindowSeconds?: number; + /** Used by wind Widget: enable/disable layline UI feature */ + laylineEnable?: boolean; + /** Used by wind Widget: upwind layline angle of the vessel applied to the UI feature */ + laylineAngle?: number; + + /** Used by multiple gauge Widget: defines the UI layout */ + gaugeType?: string; + /** Used by multiple gauge Widget */ + gaugeUnitLabelFormat?: string; + /** Used by multiple gauge Widget */ + gaugeTicks?: boolean; + /** Used by multiple gauge Widget */ + barGraph?: boolean; + /** Used by multiple gauge Widget */ + backgroundColor?: string; + /** Used by multiple gauge Widget */ + frameColor?: string; + /** Used by multiple gauge Widget */ + barColor?: string; + /** Used by multiple gauge Widget */ + radialSize?: string; + /** Used by multiple Widget to set minimum data range to display. */ + minValue?: number; + /** Used by multiple Widget to set maximum data range to display. */ + maxValue?: number; + /** Used by multiple gauge Widget: Should the needle or the faceplate rotate */ + rotateFace?: boolean; + /** Used by Autopilot Widget: Should the Widget start automatically on load or should the user press Power/On. */ + autoStart?: boolean; + /** Used by multiple gauge Widget: Use cardinal points or angle numbers as direction labels */ + compassUseNumbers?: boolean; + + /** Used by historicaldata Widget: Set the data conversion format. !!! Do not use for other Widget !!! */ + convertUnitTo?: string; + /** Used by historicaldata Widget */ + dataSetUUID?: string; + /** Used by historicaldata Widget */ + invertData?: boolean; + /** Used by historicaldata Widget */ + displayMinMax?: boolean; + /** Used by historicaldata Widget */ + animateGraph?: boolean; + /** Used by historicaldata Widget */ + includeZero?: boolean; + /** Used by historicaldata Widget */ + verticalGraph?: boolean; + + /** Option for widget that supports Signal K PUT command */ + putEnable?: boolean; + /** Option for widget that supports Signal K PUT command */ + putMomentary?: boolean; + /** Option for widget that supports Signal K PUT command */ + putMomentaryValue?: boolean; + + /** Used by IFrame widget: URL lo load in the iframe */ + widgetUrl?: string; + + + /** Use by racetimer widget */ + timerLength?: number; +} +/** + * Widget Zones data highlights interface. Used to defined how current path data + * value should be displayed/highlighted with respect to the zones configuration. + * + * @exports + * @interface IDataHighlight + * @extends {Array<{ + * from : number; + * to : number; + * color: string; + * }>} + */ +export interface IDataHighlight extends Array<{ + from: number; + to: number; + color: string; +}> {}; + +/** + * Defines all possible properties for data paths. Combines both + * both KIP and Signal K path features. + * + * @interface IWidgetPaths + */ + +/** + * + * + * @export + * @interface IWidgetPath + */ +export interface IWidgetPath { + /** Required: Path description label used in the Widget settings UI */ + description: string | null | ''; + /** Required: Signal K path (ie. self.environment.wind.angleTrueWater) of the data to be received or null value. See KIP's Data Browser or Signal K's Data Browser UI to identified possible available paths. NOTE: Not all setup will have the same paths. Path availabiltiy depends on network components and Signal K configuration that exists on each vessel. */ + path: string | null; + /** Required: Enforce a prefered Signal K "data" Source for the path when/if multiple Sources are available (ie. the vessel has multiple depth thruhulls, wind vanes, engines, fuel tanks, ect.). Use null value to use Signal K's default Source configuration. Source defaults and priorities are configured in Signal K. */ + source: string | null; + /** Required: Used by the Widget Options UI to filter the list of Signal K path the user can select from. Format can be: number, string, boolean or null to list all types */ + pathType: string | null; + /** NOT IMPLEMENTED - Used by the Widget Options UI to filter path list ie. self.navigation.* or *.navigation.* */ + pathFilter?: string; //TODO: to implment in the future to facilitate sub path selection + /** Used in Widget Options UI and by observeDataStream() method to convert Signal K transmitted values to a specified format. Also used as a source to identify conversion group. */ + convertUnitTo?: string; + /** Used by the Widget Options UI to hide the path in the POaths configuration panel went it should not be modified */ + isPathConfigurable: boolean; + /** Required: Used to throttle/limit the path's Observer emited values frequency and reduce Angular change detection cycles. Configure according to data type and human perception. Value in milliseconds */ + sampleTime: number; + /** NOT IMPLEMENTED -Signal K - period=[millisecs] becomes the transmission rate, e.g. every period/1000 seconds. Default: 1000 */ + period?: number; + /** NOT IMPLEMENTED -Signal K - format=[delta|full] specifies delta or full format. Default: delta */ + format?: Format; + /** NOT IMPLEMENTED -Signal K - policy=[instant|ideal|fixed]. Default: ideal */ + policy?: Policy; + /** NOT IMPLEMENTED -Signal K - minPeriod=[millisecs] becomes the fastest message transmission rate allowed, e.g. every minPeriod/1000 seconds. This is only relevant for policy='instant' to avoid swamping the client or network. */ + minPeriod?: number; +} diff --git a/src/app/gauge-steel/gauge-steel.component.css b/src/app/widgets/gauge-steel/gauge-steel.component.css similarity index 100% rename from src/app/gauge-steel/gauge-steel.component.css rename to src/app/widgets/gauge-steel/gauge-steel.component.css diff --git a/src/app/gauge-steel/gauge-steel.component.html b/src/app/widgets/gauge-steel/gauge-steel.component.html similarity index 100% rename from src/app/gauge-steel/gauge-steel.component.html rename to src/app/widgets/gauge-steel/gauge-steel.component.html diff --git a/src/app/gauge-steel/gauge-steel.component.spec.ts b/src/app/widgets/gauge-steel/gauge-steel.component.spec.ts similarity index 100% rename from src/app/gauge-steel/gauge-steel.component.spec.ts rename to src/app/widgets/gauge-steel/gauge-steel.component.spec.ts diff --git a/src/app/gauge-steel/gauge-steel.component.ts b/src/app/widgets/gauge-steel/gauge-steel.component.ts similarity index 97% rename from src/app/gauge-steel/gauge-steel.component.ts rename to src/app/widgets/gauge-steel/gauge-steel.component.ts index 249f2e4b..e8f4860e 100644 --- a/src/app/gauge-steel/gauge-steel.component.ts +++ b/src/app/widgets/gauge-steel/gauge-steel.component.ts @@ -1,4 +1,4 @@ -import { Component, Input, AfterViewInit, OnInit, OnChanges, SimpleChanges, ViewChild, ElementRef } from '@angular/core'; +import { Component, Input, AfterViewInit, OnChanges, SimpleChanges, ViewChild, ElementRef } from '@angular/core'; import { ResizedEvent } from 'angular-resize-event'; declare var steelseries: any; // 3rd party @@ -44,7 +44,7 @@ export const SteelFrameColors = { templateUrl: './gauge-steel.component.html', styleUrls: ['./gauge-steel.component.css'] }) -export class GaugeSteelComponent implements OnInit, AfterViewInit, OnChanges { +export class GaugeSteelComponent implements AfterViewInit, OnChanges { @ViewChild('sgWrapperDiv', {static: true, read: ElementRef}) sgWrapperDiv: ElementRef; @@ -81,10 +81,6 @@ export class GaugeSteelComponent implements OnInit, AfterViewInit, OnChanges { sections; - - ngOnInit() { - } - ngAfterViewInit() { if (!this.gaugeType) { this.gaugeType = 'radial'; } } diff --git a/src/app/svg-autopilot/svg-autopilot.component.html b/src/app/widgets/svg-autopilot/svg-autopilot.component.html similarity index 100% rename from src/app/svg-autopilot/svg-autopilot.component.html rename to src/app/widgets/svg-autopilot/svg-autopilot.component.html diff --git a/src/app/svg-autopilot/svg-autopilot.component.spec.ts b/src/app/widgets/svg-autopilot/svg-autopilot.component.spec.ts similarity index 100% rename from src/app/svg-autopilot/svg-autopilot.component.spec.ts rename to src/app/widgets/svg-autopilot/svg-autopilot.component.spec.ts diff --git a/src/app/svg-autopilot/svg-autopilot.component.ts b/src/app/widgets/svg-autopilot/svg-autopilot.component.ts similarity index 100% rename from src/app/svg-autopilot/svg-autopilot.component.ts rename to src/app/widgets/svg-autopilot/svg-autopilot.component.ts diff --git a/src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.html b/src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.html similarity index 100% rename from src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.html rename to src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.html diff --git a/src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.scss b/src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.scss similarity index 100% rename from src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.scss rename to src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.scss diff --git a/src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.spec.ts b/src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.spec.ts similarity index 100% rename from src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.spec.ts rename to src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.spec.ts diff --git a/src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.ts b/src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.ts similarity index 100% rename from src/app/svg-simple-linear-gauge/svg-simple-linear-gauge.component.ts rename to src/app/widgets/svg-simple-linear-gauge/svg-simple-linear-gauge.component.ts diff --git a/src/app/svg-wind/svg-wind.component.css b/src/app/widgets/svg-wind/svg-wind.component.css similarity index 100% rename from src/app/svg-wind/svg-wind.component.css rename to src/app/widgets/svg-wind/svg-wind.component.css diff --git a/src/app/svg-wind/svg-wind.component.html b/src/app/widgets/svg-wind/svg-wind.component.html similarity index 100% rename from src/app/svg-wind/svg-wind.component.html rename to src/app/widgets/svg-wind/svg-wind.component.html diff --git a/src/app/svg-wind/svg-wind.component.scss b/src/app/widgets/svg-wind/svg-wind.component.scss similarity index 100% rename from src/app/svg-wind/svg-wind.component.scss rename to src/app/widgets/svg-wind/svg-wind.component.scss diff --git a/src/app/svg-wind/svg-wind.component.spec.ts b/src/app/widgets/svg-wind/svg-wind.component.spec.ts similarity index 100% rename from src/app/svg-wind/svg-wind.component.spec.ts rename to src/app/widgets/svg-wind/svg-wind.component.spec.ts diff --git a/src/app/svg-wind/svg-wind.component.ts b/src/app/widgets/svg-wind/svg-wind.component.ts similarity index 97% rename from src/app/svg-wind/svg-wind.component.ts rename to src/app/widgets/svg-wind/svg-wind.component.ts index de9f3193..cb7dd1ab 100644 --- a/src/app/svg-wind/svg-wind.component.ts +++ b/src/app/widgets/svg-wind/svg-wind.component.ts @@ -247,33 +247,10 @@ export class SvgWindComponent { } - - - addHeading(h1: number = 0, h2: number = 0) { let h3 = h1 + h2; while (h3 > 359) { h3 = h3 - 359; } while (h3 < 0) { h3 = h3 + 359; } return h3; } - - } - - - - - - -/* - - -*/ diff --git a/src/app/widget-autopilot/widget-autopilot.component.html b/src/app/widgets/widget-autopilot/widget-autopilot.component.html similarity index 88% rename from src/app/widget-autopilot/widget-autopilot.component.html rename to src/app/widgets/widget-autopilot/widget-autopilot.component.html index 52355c1d..706ef22f 100644 --- a/src/app/widget-autopilot/widget-autopilot.component.html +++ b/src/app/widgets/widget-autopilot/widget-autopilot.component.html @@ -61,13 +61,5 @@ - - - - - - - - diff --git a/src/app/widget-autopilot/widget-autopilot.component.scss b/src/app/widgets/widget-autopilot/widget-autopilot.component.scss similarity index 100% rename from src/app/widget-autopilot/widget-autopilot.component.scss rename to src/app/widgets/widget-autopilot/widget-autopilot.component.scss diff --git a/src/app/widget-autopilot/widget-autopilot.component.spec.ts b/src/app/widgets/widget-autopilot/widget-autopilot.component.spec.ts similarity index 100% rename from src/app/widget-autopilot/widget-autopilot.component.spec.ts rename to src/app/widgets/widget-autopilot/widget-autopilot.component.spec.ts diff --git a/src/app/widget-autopilot/widget-autopilot.component.ts b/src/app/widgets/widget-autopilot/widget-autopilot.component.ts similarity index 64% rename from src/app/widget-autopilot/widget-autopilot.component.ts rename to src/app/widgets/widget-autopilot/widget-autopilot.component.ts index 9b9166a4..015a089d 100644 --- a/src/app/widget-autopilot/widget-autopilot.component.ts +++ b/src/app/widgets/widget-autopilot/widget-autopilot.component.ts @@ -1,11 +1,10 @@ -import { ViewChild, Input, ElementRef, Component, OnInit, OnDestroy } from '@angular/core'; +import { ViewChild, Component, OnInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import { MatButton } from '@angular/material/button'; -import { SignalKService } from '../signalk.service'; -import { SignalkRequestsService, skRequest } from '../signalk-requests.service'; -import { WidgetManagerService, IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { UnitsService } from '../units.service'; +import { SignalkRequestsService, skRequest } from '../../signalk-requests.service'; +import { WidgetManagerService} from '../../widget-manager.service'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; const defaultPpreferedDisplayMode = { wind: 'windAngleApparent', @@ -39,9 +38,7 @@ const timeoutBlink = 250; templateUrl: './widget-autopilot.component.html', styleUrls: ['./widget-autopilot.component.scss'], }) -export class WidgetAutopilotComponent implements OnInit, OnDestroy { - @Input('widgetProperties') widgetProperties!: IWidget; - +export class WidgetAutopilotComponent extends BaseWidgetComponent implements OnInit, OnDestroy { // AP keypad @ViewChild('powerBtn') powerBtn: MatButton; @ViewChild('stbTackBtn') stbTackBtn: MatButton; @@ -60,128 +57,13 @@ export class WidgetAutopilotComponent implements OnInit, OnDestroy { // AP Screen @ViewChild('appSvgAutopilot') apScreen : any; - // hack to access material-theme palette colors - @ViewChild('primary') private primaryElement: ElementRef; - @ViewChild('accent') private accentElement: ElementRef; - @ViewChild('warn') private warnElement: ElementRef; - @ViewChild('primaryDark') private primaryDarkElement: ElementRef; - @ViewChild('accentDark') private accentDarkElement: ElementRef; - @ViewChild('warnDark') private warnDarkElement: ElementRef; - @ViewChild('background') private backgroundElement: ElementRef; - @ViewChild('text') private textElement: ElementRef; - - defaultConfig: IWidgetSvcConfig = { - displayName: 'N2k Autopilot', - filterSelfPaths: true, - paths: { - "apState": { - description: "Autopilot State", - path: 'self.steering.autopilot.state', - source: 'default', - pathType: "string", - isPathConfigurable: false, - convertUnitTo: "", - }, - "apTargetHeadingMag": { - description: "Autopilot Target Heading Mag", - path: 'self.steering.autopilot.target.headingMagnetic', - source: 'default', - pathType: "number", - convertUnitTo: "deg", - isPathConfigurable: true, - }, - "apTargetWindAngleApp": { - description: "Autopilot Target Wind Angle Apparent", - path: 'self.steering.autopilot.target.windAngleApparent', - source: 'default', - pathType: "number", - convertUnitTo: "deg", - isPathConfigurable: true, - }, - "apNotifications": { - description: "Autopilot Notifications", - path: 'self.notifications.autopilot.*', //TODO(David): need to add support for .* type subscription paths in sk service and widget config modal - source: 'default', - pathType: "string", - convertUnitTo: "", - isPathConfigurable: false, - }, - "headingMag": { - description: "Heading Magnetic", - path: 'self.navigation.headingMagnetic', - source: 'default', - pathType: "number", - convertUnitTo: "deg", - isPathConfigurable: true, - }, - "headingTrue": { - description: "Heading True", - path: 'self.navigation.headingTrue', - source: 'default', - pathType: "number", - convertUnitTo: "deg", - isPathConfigurable: true, - }, - "windAngleApparent": { - description: "Wind Angle Apparent", - path: 'self.environment.wind.angleApparent', - source: 'default', - pathType: "number", - convertUnitTo: "deg", - isPathConfigurable: true, - }, - "windAngleTrueWater": { - description: "Wind Angle True Water", - path: 'self.environment.wind.angleTrueWater', - source: 'default', - pathType: "number", - convertUnitTo: "deg", - isPathConfigurable: true, - }, - "rudderAngle": { - description: "Rudder Angle", - path: 'self.steering.rudderAngle', - source: 'default', - pathType: "number", - convertUnitTo: "deg", - isPathConfigurable: true, - }, - }, - usage: { - "headingMag": ['wind', 'route', 'auto', 'standby'], - "headingTrue": ['wind', 'route', 'auto', 'standby'], - "windAngleApparent": ['wind'], - "windAngleTrueWater": ['wind'], - }, - typeVal: { - "headingMag": 'Mag', - "headingTrue": 'True', - "windAngleApparent": 'AWA', - "windAngleTrueWater": 'TWA', - }, - barColor: 'accent', // theme palette to select - autoStart: false, - }; - - activeWidget: IWidget; - config: IWidgetSvcConfig; displayName: string; - // Subscription stuff currentAPState: any = null; // Current Pilot Mode - used for display, keyboard state and buildCommand function - apStateSub: Subscription = null; - currentAPTargetAppWind: number = 0; - apTargetAppWindSub: Subscription = null; - currentHeading: number = 0; - headingSub: Subscription = null; - currentAppWindAngle: number = null; - appWindAngleSub: Subscription = null; - currentRudder: number = null; - rudderSub: Subscription = null; skApNotificationSub = new Subscription; skRequestSub = new Subscription; // signalk-Request result observer @@ -204,13 +86,113 @@ export class WidgetAutopilotComponent implements OnInit, OnDestroy { notificationTest = {}; - constructor( - private SignalKService: SignalKService, - private SignalkRequestsService: SignalkRequestsService, - private UnitsService: UnitsService, - private widgetManagerService: WidgetManagerService - ) { } + public signalkRequestsService: SignalkRequestsService, + public widgetManagerService: WidgetManagerService) { + super(); + + this.defaultConfig = { + displayName: 'N2k Autopilot', + filterSelfPaths: true, + paths: { + "apState": { + description: "Autopilot State", + path: 'self.steering.autopilot.state', + source: 'default', + pathType: "string", + isPathConfigurable: false, + convertUnitTo: "", + sampleTime: 500 + }, + "apTargetHeadingMag": { + description: "Autopilot Target Heading Mag", + path: 'self.steering.autopilot.target.headingMagnetic', + source: 'default', + pathType: "number", + convertUnitTo: "deg", + isPathConfigurable: true, + sampleTime: 500 + }, + "apTargetWindAngleApp": { + description: "Autopilot Target Wind Angle Apparent", + path: 'self.steering.autopilot.target.windAngleApparent', + source: 'default', + pathType: "number", + convertUnitTo: "deg", + isPathConfigurable: true, + sampleTime: 500 + }, + "apNotifications": { + description: "Autopilot Notifications", + path: 'self.notifications.autopilot.*', //TODO(David): need to add support for .* path subscription paths in sk service and widget config modal + source: 'default', + pathType: "string", + convertUnitTo: "", + isPathConfigurable: false, + sampleTime: 500 + }, + "headingMag": { + description: "Heading Magnetic", + path: 'self.navigation.headingMagnetic', + source: 'default', + pathType: "number", + convertUnitTo: "deg", + isPathConfigurable: true, + sampleTime: 500 + }, + "headingTrue": { + description: "Heading True", + path: 'self.navigation.headingTrue', + source: 'default', + pathType: "number", + convertUnitTo: "deg", + isPathConfigurable: true, + sampleTime: 500 + }, + "windAngleApparent": { + description: "Wind Angle Apparent", + path: 'self.environment.wind.angleApparent', + source: 'default', + pathType: "number", + convertUnitTo: "deg", + isPathConfigurable: true, + sampleTime: 500 + }, + "windAngleTrueWater": { + description: "Wind Angle True Water", + path: 'self.environment.wind.angleTrueWater', + source: 'default', + pathType: "number", + convertUnitTo: "deg", + isPathConfigurable: true, + sampleTime: 500 + }, + "rudderAngle": { + description: "Rudder Angle", + path: 'self.steering.rudderAngle', + source: 'default', + pathType: "number", + convertUnitTo: "deg", + isPathConfigurable: true, + sampleTime: 500 + }, + }, + usage: { + "headingMag": ['wind', 'route', 'auto', 'standby'], + "headingTrue": ['wind', 'route', 'auto', 'standby'], + "windAngleApparent": ['wind'], + "windAngleTrueWater": ['wind'], + }, + typeVal: { + "headingMag": 'Mag', + "headingTrue": 'True', + "windAngleApparent": 'AWA', + "windAngleTrueWater": 'TWA', + }, + barColor: 'accent', // theme palette to select + autoStart: false, + }; + } ngOnInit() { if (this.widgetProperties.config.autoStart) { @@ -220,31 +202,74 @@ export class WidgetAutopilotComponent implements OnInit, OnDestroy { } demoMode() { - // this.setNotificationMessage('{"path":"notifications.autopilot.PilotWarningWindShift","value":{"state":"alarm","message":"Pilot Warning Wind Shift"}}'); } ngOnDestroy() { - this.stopAllSubscriptions(); + this.unsubscribeDataStream(); + this.unsubscribeSKRequest(); + this.unsubscribeAPNotification(); + console.log("Autopilot Subs Stopped"); } startAllSubscriptions() { - this.subscribeHeading(); - this.subscribeAppWindAngle(); - this.subscribeRudder(); - this.subscribeAPState(); - this.subscribeAPTargetAppWind(); + this.observeDataStream('apState', newValue => { + this.currentAPState = newValue.value; + this.SetKeyboardMode(this.currentAPState); + } + ); + + this.observeDataStream('headingMag', newValue => { + if (newValue.value === null) { + this.currentHeading = 0; + } else { + this.currentHeading = newValue.value; + } + } + ); + + this.observeDataStream('windAngleApparent', newValue => { + if (newValue.value === null) { + this.currentAppWindAngle = null; + return; + } + // 0-180+ for stb + // -0 to -180 for port + // need in 0-360 + + if (newValue.value < 0) {// stb + this.currentAppWindAngle = 360 + newValue.value; // adding a negative number subtracts it... + } else { + this.currentAppWindAngle = newValue.value; + } + } + ); + + this.observeDataStream('rudderAngle', newValue => { + if (newValue.value === null) { + this.currentRudder = 0; + } else { + this.currentRudder = newValue.value; + } + } + ); + + this.observeDataStream('apTargetWindAngleApp', newValue => { + if (newValue.value === null) { + this.currentAPTargetAppWind = 0; + } else { + this.currentAPTargetAppWind = newValue.value; + } + } + ); + this.subscribeSKRequest(); this.subscribeAPNotification(); - console.log("Autopilot Sub Started"); + console.log("Autopilot Subs Started"); } stopAllSubscriptions() { - this.unsubscribeHeading(); - this.unsubscribeAppWindAngle(); - this.unsubscribeRudder(); - this.unsubscribeAPState(); - this.unsubscribeAPTargetAppWind(); + this.unsubscribeDataStream(); this.unsubscribeSKRequest(); this.unsubscribeAPNotification(); console.log("Autopilot Subs Stopped"); @@ -252,9 +277,8 @@ export class WidgetAutopilotComponent implements OnInit, OnDestroy { subscribeAPNotification() { if (typeof(this.widgetProperties.config.paths['apNotifications'].path) != 'string') { return } // nothing to sub to... - this.skApNotificationSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apNotifications'].path, this.widgetProperties.config.paths['apNotifications'].source).subscribe( + this.skApNotificationSub = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apNotifications'].path, this.widgetProperties.config.paths['apNotifications'].source).subscribe( newValue => { - if (!newValue.value == null) { this.setNotificationMessage(newValue.value); console.log(newValue.value); @@ -267,12 +291,12 @@ export class WidgetAutopilotComponent implements OnInit, OnDestroy { if (this.skApNotificationSub !== null) { this.skApNotificationSub.unsubscribe(); this.skApNotificationSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apNotifications'].path); + this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apNotifications'].path); } } subscribeSKRequest() { - this.skRequestSub = this.SignalkRequestsService.subscribeRequest().subscribe(requestResult => { + this.skRequestSub = this.signalkRequestsService.subscribeRequest().subscribe(requestResult => { if (requestResult.widgetUUID == this.widgetProperties.uuid) { this.commandReceived(requestResult); } @@ -286,127 +310,6 @@ export class WidgetAutopilotComponent implements OnInit, OnDestroy { } } - subscribeAPTargetAppWind() { - this.unsubscribeAPTargetAppWind(); - if (typeof(this.widgetProperties.config.paths['apTargetWindAngleApp'].path) != 'string') { return } // nothing to sub to... - this.apTargetAppWindSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apTargetWindAngleApp'].path, this.widgetProperties.config.paths['apTargetWindAngleApp'].source).subscribe( - newValue => { - if (newValue.value === null) { - this.currentAPTargetAppWind = 0; - } else { - this.currentAPTargetAppWind = this.UnitsService.convertUnit('deg', newValue.value); - } - } - ); - } - - unsubscribeAPTargetAppWind() { - if (this.apTargetAppWindSub !== null) { - this.apTargetAppWindSub.unsubscribe(); - this.apTargetAppWindSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apTargetWindAngleApp'].path); - } - } - - subscribeAPState() { - if (typeof(this.widgetProperties.config.paths['apState'].path) != 'string') { return } // nothing to sub to... - this.apStateSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apState'].path, this.widgetProperties.config.paths['apState'].source).subscribe( - newValue => { - this.currentAPState = newValue.value; - this.SetKeyboardMode(this.currentAPState); - } - ); - } - - unsubscribeAPState() { - if (this.apStateSub !== null) { - this.apStateSub.unsubscribe(); - this.apStateSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['apState'].path); - } - } - - subscribeHeading() { - this.unsubscribeHeading(); - if (typeof(this.widgetProperties.config.paths['headingMag'].path) != 'string') { return } // nothing to sub to... - this.headingSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['headingMag'].path, this.widgetProperties.config.paths['headingMag'].source).subscribe( - newValue => { - if (newValue.value === null) { - this.currentHeading = 0; - } else { - - this.currentHeading = this.UnitsService.convertUnit('deg', newValue.value); - } - - } - ); - } - - unsubscribeHeading() { - if (this.headingSub !== null) { - this.headingSub.unsubscribe(); - this.headingSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['headingMag'].path); - } - } - - subscribeAppWindAngle() { - this.unsubscribeAppWindAngle(); - if (typeof(this.widgetProperties.config.paths['windAngleApparent'].path) != 'string') { return } // nothing to sub to... - - this.appWindAngleSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['windAngleApparent'].path, this.widgetProperties.config.paths['windAngleApparent'].source).subscribe( - newValue => { - if (newValue.value === null) { - this.currentAppWindAngle = null; - return; - } - - let converted = this.UnitsService.convertUnit('deg', newValue.value); - // 0-180+ for stb - // -0 to -180 for port - // need in 0-360 - - if (converted < 0) {// stb - this.currentAppWindAngle = 360 + converted; // adding a negative number subtracts it... - } else { - this.currentAppWindAngle = converted; - } - } - ); - } - - unsubscribeAppWindAngle() { - if (this.appWindAngleSub !== null) { - this.appWindAngleSub.unsubscribe(); - this.appWindAngleSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['windAngleApparent'].path); - } - } - - subscribeRudder() { - this.unsubscribeRudder(); - if (typeof(this.widgetProperties.config.paths['rudderAngle'].path) != 'string') { return } // nothing to sub to... - this.rudderSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['rudderAngle'].path, this.widgetProperties.config.paths['rudderAngle'].source).subscribe( - newValue => { - if (newValue.value === null) { - this.currentRudder = 0; - } else { - - this.currentRudder = this.UnitsService.convertUnit('deg', newValue.value); - } - - } - ); - } - - unsubscribeRudder() { - if (this.rudderSub !== null) { - this.rudderSub.unsubscribe(); - this.rudderSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['rudderAngle'].path); - } - } - addHeading(h1: number, h2: number) { let h3 = h1 + h2; while (h3 > 359) { h3 = h3 - 359; } @@ -568,7 +471,7 @@ export class WidgetAutopilotComponent implements OnInit, OnDestroy { } sendCommand(cmdAction) { - let requestId = this.SignalkRequestsService.putRequest(cmdAction["path"], cmdAction["value"], this.widgetProperties.uuid); + let requestId = this.signalkRequestsService.putRequest(cmdAction["path"], cmdAction["value"], this.widgetProperties.uuid); this.apScreen.activityIconVisibility = "visible"; setTimeout(() => {this.apScreen.activityIconVisibility = 'hidden';}, timeoutBlink); diff --git a/src/app/widget-blank/widget-blank.component.html b/src/app/widgets/widget-blank/widget-blank.component.html similarity index 100% rename from src/app/widget-blank/widget-blank.component.html rename to src/app/widgets/widget-blank/widget-blank.component.html diff --git a/src/app/widget-blank/widget-blank.component.scss b/src/app/widgets/widget-blank/widget-blank.component.scss similarity index 100% rename from src/app/widget-blank/widget-blank.component.scss rename to src/app/widgets/widget-blank/widget-blank.component.scss diff --git a/src/app/widget-blank/widget-blank.component.spec.ts b/src/app/widgets/widget-blank/widget-blank.component.spec.ts similarity index 100% rename from src/app/widget-blank/widget-blank.component.spec.ts rename to src/app/widgets/widget-blank/widget-blank.component.spec.ts diff --git a/src/app/widgets/widget-blank/widget-blank.component.ts b/src/app/widgets/widget-blank/widget-blank.component.ts new file mode 100644 index 00000000..6ea13bd2 --- /dev/null +++ b/src/app/widgets/widget-blank/widget-blank.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; + +@Component({ + selector: 'app-widget-blank', + templateUrl: './widget-blank.component.html', + styleUrls: ['./widget-blank.component.scss'] +}) +export class WidgetBlankComponent extends BaseWidgetComponent { + + constructor() { + super(); + + this.defaultConfig = { + displayName: '' + }; + } +} diff --git a/src/app/widget-button/widget-button.component.html b/src/app/widgets/widget-button/widget-button.component.html similarity index 54% rename from src/app/widget-button/widget-button.component.html rename to src/app/widgets/widget-button/widget-button.component.html index b67450b9..1cf9bf6e 100644 --- a/src/app/widget-button/widget-button.component.html +++ b/src/app/widgets/widget-button/widget-button.component.html @@ -17,14 +17,5 @@ (mouseout)="handleClickUp()" > - - - - - - - - - diff --git a/src/app/widgets/widget-button/widget-button.component.scss b/src/app/widgets/widget-button/widget-button.component.scss new file mode 100644 index 00000000..e71eb047 --- /dev/null +++ b/src/app/widgets/widget-button/widget-button.component.scss @@ -0,0 +1,57 @@ +.switchWrapper { + position: relative; + margin: 0px; + top: 47%; + -ms-transform: translateY(-47%); + transform: translateY(-47%); + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +} + +.light { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 50%; + border-color: #262626; + border-style: solid; + border-width: medium; +} + +.square { + position: relative; + width: 10%; /* desired width */ +} + +.square:before{ + content: ""; + display: block; + padding-top: 100%; /* ratio of 1:1*/ +} + +.label { + position: relative; + width: 60%; +} + +.button { + position: relative; + width: 25%; + box-sizing: border-box; + border: inset 4px; + border-radius: 20px; +} + +.button:before{ + content: ""; + display: block; + padding-top: 70%; /* ratio of 1:1*/ +} + +.button:active { + border-style: outset; +} diff --git a/src/app/widget-button/widget-button.component.spec.ts b/src/app/widgets/widget-button/widget-button.component.spec.ts similarity index 100% rename from src/app/widget-button/widget-button.component.spec.ts rename to src/app/widgets/widget-button/widget-button.component.spec.ts diff --git a/src/app/widget-button/widget-button.component.ts b/src/app/widgets/widget-button/widget-button.component.ts similarity index 55% rename from src/app/widget-button/widget-button.component.ts rename to src/app/widgets/widget-button/widget-button.component.ts index 05415414..e2dd8c87 100644 --- a/src/app/widget-button/widget-button.component.ts +++ b/src/app/widgets/widget-button/widget-button.component.ts @@ -1,11 +1,9 @@ -import { Component, Input, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; +import { Component, OnInit, OnChanges, OnDestroy, AfterViewChecked, ViewChild, ElementRef, SimpleChanges } from '@angular/core'; import { Subscription } from 'rxjs'; -import { SignalKService } from '../signalk.service'; -import { SignalkRequestsService } from '../signalk-requests.service'; -import { AppSettingsService } from '../app-settings.service'; -import { NotificationsService } from '../notifications.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; +import { SignalkRequestsService } from '../../signalk-requests.service'; +import { NotificationsService } from '../../notifications.service'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; @Component({ @@ -13,45 +11,11 @@ import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; templateUrl: './widget-button.component.html', styleUrls: ['./widget-button.component.scss'] }) -export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestroy { - - @Input('widgetProperties') widgetProperties!: IWidget; - @ViewChild('primary', {static: true, read: ElementRef}) private primaryElement: ElementRef; - @ViewChild('accent', {static: true, read: ElementRef}) private accentElement: ElementRef; - @ViewChild('warn', {static: true, read: ElementRef}) private warnElement: ElementRef; - @ViewChild('primaryDark', {static: true, read: ElementRef}) private primaryDarkElement: ElementRef; - @ViewChild('accentDark', {static: true, read: ElementRef}) private accentDarkElement: ElementRef; - @ViewChild('warnDark', {static: true, read: ElementRef}) private warnDarkElement: ElementRef; - @ViewChild('background', {static: true, read: ElementRef}) private backgroundElement: ElementRef; - @ViewChild('text', {static: true, read: ElementRef}) private textElement: ElementRef; +export class WidgetButtonComponent extends BaseWidgetComponent implements OnInit, OnChanges, OnDestroy, AfterViewChecked { @ViewChild('btnDiv', {static: true, read: ElementRef}) divBtnElement: ElementRef; @ViewChild('lightDiv', {static: true, read: ElementRef}) divLightElement: ElementRef; @ViewChild('btnLabelCanvas', {static: true, read: ElementRef}) canvasBtnTxtElement: ElementRef; - defaultConfig: IWidgetSvcConfig = { - displayName: 'switch label', - filterSelfPaths: true, - paths: { - "boolPath": { - description: "Boolean Data", - path: null, - source: null, - pathType: "boolean", - isPathConfigurable: true, - convertUnitTo: "unitless" - } - }, - putEnable: false, - putMomentary: false, - putMomentaryValue: true, - barColor: 'accent', - }; - - valueSub: Subscription = null; - - // dynamics theme support - private themeNameSub: Subscription = null; - public buttonBorberColorOn: string = ""; public buttonColorOn: string = ""; public buttonLabelColorOn: string = ""; @@ -72,51 +36,72 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro skRequestSub = new Subscription; // Request result observer constructor( - private SignalKService: SignalKService, - private SignalkRequestsService: SignalkRequestsService, - private notification: NotificationsService, - private appSettings: AppSettingsService, + private signalkRequestsService: SignalkRequestsService, + private notification: NotificationsService ) { + super(); + + this.defaultConfig = { + displayName: 'Switch Label', + filterSelfPaths: true, + paths: { + "boolPath": { + description: "Boolean Data", + path: null, + source: null, + pathType: "boolean", + isPathConfigurable: true, + convertUnitTo: "unitless", + sampleTime: 500 + } + }, + putEnable: false, + putMomentary: false, + putMomentaryValue: true, + barColor: 'accent', + }; } ngOnInit() { - this.updateGaugeSettings(); this.canvasButtonTxt = this.canvasBtnTxtElement.nativeElement.getContext('2d'); - this.subscribePath(); + this.observeDataStream('boolPath', newValue => { + this.state = newValue.value; + this.updateBtnCanvas(); + } + ); this.subscribeSKRequest(); - this.subscribeTheme(); } private updateGaugeSettings() { - this.buttonColorOff = ''; //window.getComputedStyle(this.backgroundElement.nativeElement).color; - this.buttonColorOn = window.getComputedStyle(this.backgroundElement.nativeElement).color; + this.buttonColorOff = ''; //this.theme.background; + this.buttonColorOn = this.theme.background; switch (this.widgetProperties.config.barColor) { case "primary": - this.buttonLabelColorOff = window.getComputedStyle(this.backgroundElement.nativeElement).color; - this.buttonLabelColorOn = window.getComputedStyle(this.primaryElement.nativeElement).color; - this.buttonBorberColorOff = window.getComputedStyle(this.primaryElement.nativeElement).color; - this.buttonBorberColorOn = window.getComputedStyle(this.primaryDarkElement.nativeElement).color; - this.lightColorOff = window.getComputedStyle(this.backgroundElement.nativeElement).color; - this.lightColorOn = window.getComputedStyle(this.primaryDarkElement.nativeElement).color; + this.buttonLabelColorOff = this.theme.background; + this.buttonLabelColorOn = this.theme.primary; + this.buttonBorberColorOff = this.theme.primary; + this.buttonBorberColorOn = this.theme.primaryDark; + this.lightColorOff = this.theme.background; + this.lightColorOn = this.theme.primaryDark; break; case "accent": - this.buttonLabelColorOff = window.getComputedStyle(this.backgroundElement.nativeElement).color; - this.buttonLabelColorOn = window.getComputedStyle(this.accentElement.nativeElement).color; - this.buttonBorberColorOff = window.getComputedStyle(this.accentElement.nativeElement).color; - this.buttonBorberColorOn = window.getComputedStyle(this.accentDarkElement.nativeElement).color; - this.lightColorOff = window.getComputedStyle(this.backgroundElement.nativeElement).color; - this.lightColorOn = window.getComputedStyle(this.accentDarkElement.nativeElement).color; + this.buttonLabelColorOff = this.theme.background; + this.buttonLabelColorOn = this.theme.accent; + this.buttonBorberColorOff = this.theme.accent; + this.buttonBorberColorOn = this.theme.accentDark; + this.lightColorOff = this.theme.background; + this.lightColorOn = this.theme.accentDark; break; case "warn": - this.buttonLabelColorOff = window.getComputedStyle(this.backgroundElement.nativeElement).color; - this.buttonLabelColorOn = window.getComputedStyle(this.warnElement.nativeElement).color; - this.buttonBorberColorOff = window.getComputedStyle(this.warnElement.nativeElement).color; - this.buttonBorberColorOn = window.getComputedStyle(this.warnDarkElement.nativeElement).color; - this.lightColorOff = window.getComputedStyle(this.backgroundElement.nativeElement).color; - this.lightColorOn = window.getComputedStyle(this.warnDarkElement.nativeElement).color; + this.buttonLabelColorOff = this.theme.background; + this.buttonLabelColorOn = this.theme.warn; + this.buttonBorberColorOff = this.theme.warn;; + this.buttonBorberColorOn = this.theme.warnDark; + this.lightColorOff = this.theme.background; + this.lightColorOn = this.theme.warnDark; break; } } @@ -125,6 +110,13 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro this.resizeWidget(); } + ngOnChanges(changes: SimpleChanges): void { + if (changes.theme) { + this.updateGaugeSettings(); + this.updateBtnCanvas(); + } + } + private resizeWidget() { let rect = this.divBtnElement.nativeElement.getBoundingClientRect(); @@ -139,28 +131,8 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro } - private subscribePath() { - this.unsubscribePath(); - if (typeof(this.widgetProperties.config.paths['boolPath'].path) != 'string') { return } // nothing to sub to... - - this.valueSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source).subscribe( - newValue => { - this.state = newValue.value; - this.updateBtnCanvas(); - } - ); - } - - private unsubscribePath() { - if (this.valueSub !== null) { - this.valueSub.unsubscribe(); - this.valueSub = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['boolPath'].path) - } - } - private subscribeSKRequest() { - this.skRequestSub = this.SignalkRequestsService.subscribeRequest().subscribe(requestResult => { + this.skRequestSub = this.signalkRequestsService.subscribeRequest().subscribe(requestResult => { if (requestResult.widgetUUID == this.widgetProperties.uuid) { let errMsg = `Button ${this.widgetProperties.config.displayName}: `; if (requestResult.statusCode != 200){ @@ -179,30 +151,12 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro this.skRequestSub.unsubscribe(); } - // Subscribe to theme event - private subscribeTheme() { - this.themeNameSub = this.appSettings.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // delay so browser getComputedStyles has time to complete theme style change. - this.updateGaugeSettings(); - this.updateBtnCanvas(); - },50); - }) - } - - private unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; - } - } - public handleClickDown() { if (!this.widgetProperties.config.putEnable) { return; } if (!this.widgetProperties.config.putMomentary) { //on/off mode. Send whatever we're not :) - this.SignalkRequestsService.putRequest( + this.signalkRequestsService.putRequest( this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source, this.widgetProperties.uuid @@ -216,11 +170,11 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro this.pressed = true; // send it once to start - this.SignalkRequestsService.putRequest(this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source, this.widgetProperties.uuid); + this.signalkRequestsService.putRequest(this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source, this.widgetProperties.uuid); //send it again every 20ms this.timeoutHandler = setInterval(() => { - this.SignalkRequestsService.putRequest(this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source, this.widgetProperties.uuid); + this.signalkRequestsService.putRequest(this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source, this.widgetProperties.uuid); this.widgetProperties.config.putMomentaryValue; }, 100); @@ -235,7 +189,7 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro this.pressed = false; clearInterval(this.timeoutHandler); // momentary mode - this.SignalkRequestsService.putRequest(this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source, this.widgetProperties.uuid); + this.signalkRequestsService.putRequest(this.widgetProperties.config.paths['boolPath'].path, this.widgetProperties.config.paths['boolPath'].source, this.widgetProperties.uuid); if (!this.widgetProperties.config.putMomentaryValue) { return; } @@ -243,9 +197,8 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro } ngOnDestroy() { - this.unsubscribePath(); + this.unsubscribeDataStream(); this.unsubscribeSKRequest(); - this.unsubscribeTheme(); } /* ******************************************************************************************* */ @@ -299,7 +252,7 @@ export class WidgetButtonComponent implements OnInit, AfterViewChecked, OnDestro this.canvasButtonTxt.font = this.valueFontSize.toString() + "px Arial"; this.canvasButtonTxt.textAlign = "center"; this.canvasButtonTxt.textBaseline="middle"; - this.canvasButtonTxt.fillStyle = window.getComputedStyle(this.textElement.nativeElement).color; + this.canvasButtonTxt.fillStyle = this.theme.text; this.canvasButtonTxt.fillText(valueText,this.canvasBtnTxtElement.nativeElement.width/2,(this.canvasBtnTxtElement.nativeElement.height/2)+(this.valueFontSize/15), maxTextWidth); } diff --git a/src/app/widget-date-generic/widget-date-generic.component.css b/src/app/widgets/widget-date-generic/widget-date-generic.component.css similarity index 99% rename from src/app/widget-date-generic/widget-date-generic.component.css rename to src/app/widgets/widget-date-generic/widget-date-generic.component.css index 63fa6de0..3ec2fd47 100644 --- a/src/app/widget-date-generic/widget-date-generic.component.css +++ b/src/app/widgets/widget-date-generic/widget-date-generic.component.css @@ -4,7 +4,6 @@ canvas { left: 0; } - .dateGenericWrapper { position: relative; width: 100%; @@ -14,7 +13,6 @@ canvas { position: absolute; top: 5%; left: 5%; - } .dateGenericValue { diff --git a/src/app/widget-date-generic/widget-date-generic.component.html b/src/app/widgets/widget-date-generic/widget-date-generic.component.html similarity index 100% rename from src/app/widget-date-generic/widget-date-generic.component.html rename to src/app/widgets/widget-date-generic/widget-date-generic.component.html diff --git a/src/app/widget-date-generic/widget-date-generic.component.spec.ts b/src/app/widgets/widget-date-generic/widget-date-generic.component.spec.ts similarity index 100% rename from src/app/widget-date-generic/widget-date-generic.component.spec.ts rename to src/app/widgets/widget-date-generic/widget-date-generic.component.spec.ts diff --git a/src/app/widget-date-generic/widget-date-generic.component.ts b/src/app/widgets/widget-date-generic/widget-date-generic.component.ts similarity index 67% rename from src/app/widget-date-generic/widget-date-generic.component.ts rename to src/app/widgets/widget-date-generic/widget-date-generic.component.ts index 7c1ebfcd..086c355b 100644 --- a/src/app/widget-date-generic/widget-date-generic.component.ts +++ b/src/app/widgets/widget-date-generic/widget-date-generic.component.ts @@ -1,45 +1,20 @@ -import { Component, Input, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; import { formatDate } from '@angular/common'; -import { Subscription } from 'rxjs'; -import { SignalKService } from '../signalk.service'; -import { AppSettingsService } from '../app-settings.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; @Component({ selector: 'app-widget-date-generic', templateUrl: './widget-date-generic.component.html', styleUrls: ['./widget-date-generic.component.css'] }) -export class WidgetDateGenericComponent implements OnInit, AfterViewChecked, OnDestroy { - - @Input('widgetProperties') widgetProperties!: IWidget; +export class WidgetDateGenericComponent extends BaseWidgetComponent implements OnInit, AfterViewChecked, OnDestroy { @ViewChild('canvasEl', {static: true, read: ElementRef}) canvasEl: ElementRef; @ViewChild('canvasBG', {static: true, read: ElementRef}) canvasBG: ElementRef; @ViewChild('wrapperDiv', {static: true, read: ElementRef}) wrapperDiv: ElementRef; - defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - paths: { - 'stringPath': { - description: 'String Data', - path: null, - source: null, - pathType: 'string', - isPathConfigurable: true, - } - }, - dateFormat: 'dd/MM/yyyy HH:mm:ss', - dateTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone - }; - - activeWidget: IWidget; - dataValue: any = null; - dataTimestamp: number = Date.now(); - valueFontSize = 1; // length (in charaters) of value text to be displayed. if changed from last time, need to recalculate font size... @@ -47,31 +22,40 @@ export class WidgetDateGenericComponent implements OnInit, AfterViewChecked, OnD canvasCtx; canvasBGCtx; - // subs - valueSub: Subscription = null; - - // dynamics theme support - themeNameSub: Subscription = null; - - - constructor( - private signalKService: SignalKService, - private appSettingsService: AppSettingsService, // need for theme change subscription - ) { + constructor() { + super(); + + this.defaultConfig = { + displayName: 'Time Label', + filterSelfPaths: true, + paths: { + 'gaugePath': { + description: 'String Data', + path: null, + source: null, + pathType: 'string', + isPathConfigurable: true, + sampleTime: 500 + } + }, + dateFormat: 'dd/MM/yyyy HH:mm:ss', + dateTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone + }; } ngOnInit() { + this.observeDataStream('gaugePath', newValue => { + this.dataValue = newValue.value; + this.updateCanvas(); + }); + this.canvasCtx = this.canvasEl.nativeElement.getContext('2d'); this.canvasBGCtx = this.canvasBG.nativeElement.getContext('2d'); - - this.subscribePath(); - this.subscribeTheme(); this.resizeWidget(); } ngOnDestroy() { - this.unsubscribePath(); - this.unsubscribeTheme(); + this.unsubscribeDataStream(); } ngAfterViewChecked() { @@ -96,56 +80,9 @@ export class WidgetDateGenericComponent implements OnInit, AfterViewChecked, OnD } } - - - subscribePath() { - - this.unsubscribePath(); - if (typeof(this.widgetProperties.config.paths['stringPath'].path) != 'string') { return; } // nothing to sub to... - - this.valueSub = this.signalKService - .subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['stringPath'].path, this.widgetProperties.config.paths['stringPath'].source).subscribe( - newValue => { - this.dataValue = newValue.value; - this.updateCanvas(); - } - ); - } - - unsubscribePath() { - if (this.valueSub !== null) { - this.valueSub.unsubscribe(); - this.valueSub = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['stringPath'].path); - } - } - // Subscribe to theme event - subscribeTheme() { - this.themeNameSub = this.appSettingsService.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // need a delay so browser getComputedStyles has time to complete theme application. - this.drawTitle(); - this.drawValue(); - }, 100); - }); - } - - unsubscribeTheme() { - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; - } - } - - -/* ******************************************************************************************* */ -/* ******************************************************************************************* */ /* ******************************************************************************************* */ /* Canvas */ /* ******************************************************************************************* */ -/* ******************************************************************************************* */ -/* ******************************************************************************************* */ - updateCanvas() { if (this.canvasCtx) { this.canvasCtx.clearRect(0, 0, this.canvasEl.nativeElement.width, this.canvasEl.nativeElement.height); diff --git a/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.html b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.html similarity index 89% rename from src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.html rename to src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.html index ca1ed5f1..641e186d 100644 --- a/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.html +++ b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.html @@ -108,13 +108,4 @@ [attr.color-needle-shadow-down]="gaugeOptions.colorNeedleShadowDown" > - - - - - - - - - diff --git a/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.scss b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.scss new file mode 100644 index 00000000..69cf563c --- /dev/null +++ b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.scss @@ -0,0 +1,39 @@ +@use "sass:math"; +.verticalLinearWrapper { + position: relative; + top: 3%; + height: 97%; + width: 100%; + + > .linearGauge { + position: absolute; + top:0; + right: 0; + bottom: 0; + left: 0; + text-align: center; + } + +} + +.horizontalLinearWrapper { + position: relative; + margin: 0px; + top: 47%; + -ms-transform: translateY(-47%); + transform: translateY(-47%); + &:before { + display:block; + content: ""; + width: 100%; + padding-top: math.div(1, 4) * 97%; + margin-top: 3%; + } + > .linearGauge { + position: absolute; + top:0; + right: 0; + bottom: 0; + left: 0; + } +} diff --git a/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.spec.ts b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.spec.ts similarity index 100% rename from src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.spec.ts rename to src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.spec.ts diff --git a/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts similarity index 58% rename from src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts rename to src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts index a0d6dad0..33cbc30a 100644 --- a/src/app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts +++ b/src/app/widgets/widget-gauge-ng-linear/widget-gauge-ng-linear.component.ts @@ -1,20 +1,12 @@ -import { ViewChild, ElementRef, Component, OnInit, AfterContentInit, Input, OnDestroy } from '@angular/core'; -import { Subscription, sampleTime} from 'rxjs'; +import { ViewChild, ElementRef, Component, OnInit, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; +import { Subscription } from 'rxjs'; import { ResizedEvent } from 'angular-resize-event'; -import { IZone, IZoneState } from '../app-settings.interfaces'; -import { AppSettingsService } from '../app-settings.service'; -import { SignalKService } from '../signalk.service'; -import { UnitsService } from '../units.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { LinearGauge, LinearGaugeOptions } from '../gauges-module/linear-gauge'; - - -interface IDataHighlight extends Array<{ - from : number; - to : number; - color: string; -}> {}; +import { IZone, IZoneState } from '../../app-settings.interfaces'; +import { IDataHighlight } from '../../widgets-interface'; +import { LinearGauge, LinearGaugeOptions } from '../../gauges-module/linear-gauge'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; +import { AppSettingsService } from './../../app-settings.service'; @Component({ selector: 'app-widget-gauge-ng-linear', @@ -22,51 +14,15 @@ interface IDataHighlight extends Array<{ styleUrls: ['./widget-gauge-ng-linear.component.scss'] }) -export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterContentInit { +export class WidgetGaugeNgLinearComponent extends BaseWidgetComponent implements OnInit, OnDestroy, OnChanges { @ViewChild('linearWrapperDiv', {static: true, read: ElementRef}) private wrapper: ElementRef; @ViewChild('linearGauge', {static: true, read: ElementRef}) protected linearGauge: ElementRef; - @Input('widgetProperties') widgetProperties!: IWidget; - - @ViewChild('primary', {static: true, read: ElementRef}) private primaryElement: ElementRef; - @ViewChild('accent', {static: true, read: ElementRef}) private accentElement: ElementRef; - @ViewChild('warn', {static: true, read: ElementRef}) private warnElement: ElementRef; - @ViewChild('primaryDark', {static: true, read: ElementRef}) private primaryDarkElement: ElementRef; - @ViewChild('accentDark', {static: true, read: ElementRef}) private accentDarkElement: ElementRef; - @ViewChild('warnDark', {static: true, read: ElementRef}) private warnDarkElement: ElementRef; - @ViewChild('background', {static: true, read: ElementRef}) private backgroundElement: ElementRef; - - defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - paths: { - "gaugePath": { - description: "Numeric Data", - path: null, - source: null, - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "unitless" - } - }, - gaugeType: 'ngLinearVertical', //ngLinearVertical or ngLinearHorizontal - gaugeTicks: false, - minValue: 0, - maxValue: 100, - numInt: 1, - numDecimal: 0, - barColor: 'accent', - }; - // main gauge value variable public dataValue = 0; public dataValueTrimmed = 0; - private valueSub$: Subscription = null; private sample: number = 500; - // dynamics theme support - themeNameSub: Subscription = null; - // Gauge options public gaugeOptions = {} as LinearGaugeOptions; public isGaugeVertical: Boolean = true; @@ -75,86 +31,74 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon zones: Array = []; zonesSub: Subscription; - constructor( - private SignalKService: SignalKService, - private UnitsService: UnitsService, - private AppSettingsService: AppSettingsService, // need for theme change subscription - ) {} - - ngOnInit() { - this.subscribePath(); - this.subscribeTheme(); - this.subscribeZones(); - } - - ngOnDestroy() { - this.unsubscribePath(); - this.unsubscribeTheme(); - this.unsubscribeZones(); - } + constructor(private appSettingsService: AppSettingsService) { + super(); + + this.defaultConfig = { + displayName: "Gauge Label", + filterSelfPaths: true, + paths: { + "gaugePath": { + description: "Numeric Data", + path: null, + source: null, + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "unitless", + sampleTime: 500 + } + }, + gaugeType: 'ngLinearVertical', //ngLinearVertical or ngLinearHorizontal + gaugeTicks: false, + minValue: 0, + maxValue: 100, + numInt: 1, + numDecimal: 0, + barColor: 'accent', + }; - ngAfterContentInit(){ - this.updateGaugeConfig(); } - subscribePath() { - this.unsubscribePath(); - if (typeof(this.widgetProperties.config.paths['gaugePath'].path) != 'string') { return } // nothing to sub to... - - this.valueSub$ = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path, this.widgetProperties.config.paths['gaugePath'].source).pipe(sampleTime(this.sample)).subscribe( - newValue => { + ngOnInit() { + this.observeDataStream('gaugePath', newValue => { // Only push new values formated to gauge settings to reduce gauge paint requests let oldValue = this.dataValue; - let temp = this.formatDataValue(this.UnitsService.convertUnit(this.widgetProperties.config.paths['gaugePath'].convertUnitTo, newValue.value)); - if (oldValue != temp) { + let temp: any = this.formatWidgetNumberValue(newValue.value); + if (oldValue != (temp as number)) { this.dataValue = temp; } // set colors for zone state switch (newValue.state) { case IZoneState.warning: - this.gaugeOptions.colorValueText = getComputedStyle(this.warnDarkElement.nativeElement).color; + this.gaugeOptions.colorValueText = this.theme.warnDark; break; case IZoneState.alarm: - this.gaugeOptions.colorValueText = getComputedStyle(this.warnDarkElement.nativeElement).color; + this.gaugeOptions.colorValueText = this.theme.warnDark; break; default: this.gaugeOptions.colorValueText = getComputedStyle(this.wrapper.nativeElement).color; - } - } ); - } - unsubscribePath() { - if (this.valueSub$ !== null) { - this.valueSub$.unsubscribe(); - this.valueSub$ = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path) - } + this.subscribeZones(); } - // Subscribe to theme event - subscribeTheme() { - this.themeNameSub = this.AppSettingsService.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // delay so browser getComputedStyles has time to complet theme style change. - this.updateGaugeConfig(); - }, 50); - }) + ngOnDestroy() { + this.unsubscribeDataStream(); + this.unsubscribeZones(); } - unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; + ngOnChanges(changes: SimpleChanges): void { + if (changes.theme) { + this.updateGaugeConfig(); } } // Subscribe to Zones subscribeZones() { - this.zonesSub = this.AppSettingsService.getZonesAsO().subscribe( + this.zonesSub = this.appSettingsService.getZonesAsO().subscribe( zones => { this.zones = zones; this.updateGaugeConfig(); @@ -168,26 +112,14 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon } } - private formatDataValue(value:number): number { - // make sure we are within range of the gauge settings, else the needle can go outside the range - if (value < this.widgetProperties.config.minValue || value == null) { - value = this.widgetProperties.config.minValue; - } - if (value > this.widgetProperties.config.maxValue) { - value = this.widgetProperties.config.maxValue; - } - return value; - } - updateGaugeConfig(){ - //// Hack to get Theme colors using hidden minxin, DIV and @ViewChild let themePaletteColor = ""; let themePaletteDarkColor = ""; this.gaugeOptions.colorTitle = this.gaugeOptions.colorUnits = this.gaugeOptions.colorValueText = window.getComputedStyle(this.wrapper.nativeElement).color; this.gaugeOptions.colorPlate = window.getComputedStyle(this.wrapper.nativeElement).backgroundColor; - this.gaugeOptions.colorBar = getComputedStyle(this.backgroundElement.nativeElement).color; + this.gaugeOptions.colorBar = this.theme.background; this.gaugeOptions.colorMajorTicks = this.gaugeOptions.colorTitle; this.gaugeOptions.colorMinorTicks = this.gaugeOptions.colorTitle; @@ -197,8 +129,8 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon switch (this.widgetProperties.config.barColor) { case "primary": - themePaletteColor = getComputedStyle(this.primaryElement.nativeElement).color; - themePaletteDarkColor = getComputedStyle(this.primaryDarkElement.nativeElement).color; + themePaletteColor = this.theme.primary; + themePaletteDarkColor = this.theme.primaryDark; this.gaugeOptions.colorBarProgress = themePaletteColor; this.gaugeOptions.colorBarProgressEnd = themePaletteDarkColor; this.gaugeOptions.colorNeedle = themePaletteDarkColor; @@ -206,8 +138,8 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon break; case "accent": - themePaletteColor = getComputedStyle(this.accentElement.nativeElement).color; - themePaletteDarkColor = getComputedStyle(this.accentDarkElement.nativeElement).color; + themePaletteColor = this.theme.accent; + themePaletteDarkColor = this.theme.accentDark; this.gaugeOptions.colorBarProgress = themePaletteColor; this.gaugeOptions.colorBarProgressEnd = themePaletteDarkColor; this.gaugeOptions.colorNeedle = themePaletteDarkColor; @@ -215,8 +147,8 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon break; case "warn": - themePaletteColor = getComputedStyle(this.warnElement.nativeElement).color; - themePaletteDarkColor = getComputedStyle(this.warnDarkElement.nativeElement).color; + themePaletteColor = this.theme.warn; + themePaletteDarkColor = this.theme.warnDark; this.gaugeOptions.colorBarProgress = themePaletteColor; this.gaugeOptions.colorBarProgressEnd = themePaletteDarkColor; this.gaugeOptions.colorNeedle = themePaletteDarkColor; @@ -224,8 +156,8 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon break; case "nobar": - themePaletteColor = getComputedStyle(this.backgroundElement.nativeElement).color; - themePaletteDarkColor = getComputedStyle(this.warnDarkElement.nativeElement).color; + themePaletteColor = this.theme.background; + themePaletteDarkColor = this.theme.warnDark; this.gaugeOptions.colorBar = themePaletteColor; this.gaugeOptions.colorBarProgress = themePaletteColor; this.gaugeOptions.colorBarProgressEnd = themePaletteColor; @@ -240,25 +172,25 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon // highlights let myZones: IDataHighlight = []; this.zones.forEach(zone => { - // get zones for our path - if (zone.path == this.widgetProperties.config.paths['gaugePath'].path) { - let lower = zone.lower || this.widgetProperties.config.minValue; - let upper = zone.upper || this.widgetProperties.config.maxValue; - let color: string; - switch (zone.state) { - case 1: - color = getComputedStyle(this.warnElement.nativeElement).color; - break; - case IZoneState.alarm: - color = getComputedStyle(this.warnDarkElement.nativeElement).color; - break; - default: - color = getComputedStyle(this.primaryElement.nativeElement).color; + // get zones for our path + if (zone.path == this.widgetProperties.config.paths['gaugePath'].path) { + let lower = zone.lower || this.widgetProperties.config.minValue; + let upper = zone.upper || this.widgetProperties.config.maxValue; + let color: string; + switch (zone.state) { + case 1: + color = this.theme.warn; + break; + case IZoneState.alarm: + color = this.theme.warnDark; + break; + default: + color = this.theme.primary; + } + myZones.push({from: lower, to: upper, color: color}); } - - myZones.push({from: lower, to: upper, color: color}); } - }); + ); this.gaugeOptions.highlights = myZones; @@ -405,5 +337,4 @@ export class WidgetGaugeNgLinearComponent implements OnInit, OnDestroy, AfterCon this.gaugeOptions.width = event.newRect.width; } } - } diff --git a/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.html b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.html similarity index 90% rename from src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.html rename to src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.html index e95d5820..41f1f312 100644 --- a/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.html +++ b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.html @@ -104,12 +104,4 @@ [attr.color-needle-circle-outer]="gaugeOptions.colorNeedleCircleOuter" [attr.color-needle-circle-outer-end]="gaugeOptions.colorNeedleCircleOuterEnd" > - - - - - - - - diff --git a/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss new file mode 100644 index 00000000..9e0fb23e --- /dev/null +++ b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss @@ -0,0 +1,13 @@ +radial-gauge.radialGauge { + position: relative; + width: 94% !important; + height: auto !important; + top: 6%; +} + +.ngRadialWrapper { + position: relative; + width: 100%; + height: 100%; + text-align: center; + } diff --git a/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.spec.ts b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.spec.ts similarity index 100% rename from src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.spec.ts rename to src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.spec.ts diff --git a/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts similarity index 70% rename from src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts rename to src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts index 0cc7307b..4c6b7633 100644 --- a/src/app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts +++ b/src/app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.ts @@ -1,74 +1,28 @@ -import { ViewChild, ElementRef, Component, Input, OnInit, OnDestroy, AfterContentInit, AfterContentChecked, Pipe } from '@angular/core'; -import { Subscription, sampleTime } from 'rxjs'; +import { ViewChild, ElementRef, Component, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; +import { Subscription } from 'rxjs'; import { ResizedEvent } from 'angular-resize-event'; -import { IZone, IZoneState } from '../app-settings.interfaces'; -import { AppSettingsService } from '../app-settings.service'; -import { SignalKService } from '../signalk.service'; -import { UnitsService } from '../units.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { RadialGauge, RadialGaugeOptions } from '../gauges-module/radial-gauge'; +import { IZone, IZoneState } from '../../app-settings.interfaces'; +import { IDataHighlight } from '../../widgets-interface'; -interface IDataHighlight extends Array<{ - from : number; - to : number; - color: string; - }> {}; +import { RadialGauge, RadialGaugeOptions } from '../../gauges-module/radial-gauge'; +import { AppSettingsService } from './../../app-settings.service'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; @Component({ selector: 'app-widget-gauge-ng-radial', templateUrl: './widget-gauge-ng-radial.component.html', styleUrls: ['./widget-gauge-ng-radial.component.scss'] }) -export class WidgetGaugeNgRadialComponent implements OnInit, OnDestroy, AfterContentInit, AfterContentChecked { - +export class WidgetGaugeNgRadialComponent extends BaseWidgetComponent implements OnInit, OnChanges, OnDestroy { @ViewChild('ngRadialWrapperDiv', {static: true, read: ElementRef}) private wrapper: ElementRef; @ViewChild('radialGauge', {static: true, read: RadialGauge}) public radialGauge: RadialGauge; - @Input('widgetProperties') widgetProperties!: IWidget; - - // hack to access material-theme palette colors - @ViewChild('primary', {static: true, read: ElementRef}) private primaryElement: ElementRef; - @ViewChild('accent', {static: true, read: ElementRef}) private accentElement: ElementRef; - @ViewChild('warn', {static: true, read: ElementRef}) private warnElement: ElementRef; - @ViewChild('primaryDark', {static: true, read: ElementRef}) private primaryDarkElement: ElementRef; - @ViewChild('accentDark', {static: true, read: ElementRef}) private accentDarkElement: ElementRef; - @ViewChild('warnDark', {static: true, read: ElementRef}) private warnDarkElement: ElementRef; - @ViewChild('background', {static: true, read: ElementRef}) private backgroundElement: ElementRef; - @ViewChild('text', {static: true, read: ElementRef}) private textElement: ElementRef; - - defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - paths: { - "gaugePath": { - description: "Numeric Data", - path: null, - source: null, - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "unitless" - } - }, - gaugeType: 'ngRadial', //ngLinearVertical or ngLinearHorizontal - gaugeTicks: false, - radialSize: 'measuring', - compassUseNumbers: false, - minValue: 0, - maxValue: 100, - numInt: 1, - numDecimal: 0, - barColor: 'accent', // theme palette to select - }; - // main gauge value variable public dataValue = 0; private valueSub$: Subscription = null; private sample: number = 500; - // dynamics theme support - themeNameSub: Subscription = null; - // Gauge options public gaugeOptions = {} as RadialGaugeOptions; // fix for RadialGauge GaugeOptions object ** missing color-stroke-ticks property @@ -79,90 +33,74 @@ export class WidgetGaugeNgRadialComponent implements OnInit, OnDestroy, AfterCon zones: Array = []; zonesSub: Subscription; - constructor( - private SignalKService: SignalKService, - private UnitsService: UnitsService, - private AppSettingsService: AppSettingsService, // need for theme change subscription - ) { } - - ngOnInit() { - this.subscribePath(); - this.subscribeTheme(); - this.subscribeZones(); - } - - ngOnDestroy() { - this.unsubscribePath(); - this.unsubscribeTheme(); - this.unsubscribeZones(); - } - - ngAfterContentInit() { - this.updateGaugeConfig(); - } - - ngAfterContentChecked() { - // this.resizeWidget(); - } - - subscribePath() { - this.unsubscribePath(); - if (typeof(this.widgetProperties.config.paths['gaugePath'].path) != 'string') { return } // nothing to sub to... - - this.valueSub$ = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path, this.widgetProperties.config.paths['gaugePath'].source).pipe(sampleTime(this.sample)).subscribe( - newValue => { - // Only push new values formated to gauge settings to reduce gauge paint requests - let oldValue = this.dataValue; - let temp = this.formatDataValue(this.UnitsService.convertUnit(this.widgetProperties.config.paths['gaugePath'].convertUnitTo, newValue.value)); - if (oldValue != temp) { - this.dataValue = temp; + constructor(private appSettingsService: AppSettingsService) { + super(); + + this.defaultConfig = { + displayName: null, + filterSelfPaths: true, + paths: { + "gaugePath": { + description: "Numeric Data", + path: null, + source: null, + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "unitless", + sampleTime: 500 } + }, + gaugeType: 'ngRadial', //ngLinearVertical or ngLinearHorizontal + gaugeTicks: false, + radialSize: 'measuring', + compassUseNumbers: false, + minValue: 0, + maxValue: 100, + numInt: 1, + numDecimal: 0, + barColor: 'accent', // theme palette to select + }; + } - // set zone state colors - switch (newValue.state) { - case IZoneState.warning: - this.gaugeOptions.colorValueText = getComputedStyle(this.warnDarkElement.nativeElement).color; - break; - case IZoneState.alarm: - this.gaugeOptions.colorValueText = getComputedStyle(this.warnDarkElement.nativeElement).color; - break; - default: - this.gaugeOptions.colorValueText = getComputedStyle(this.textElement.nativeElement).color; + ngOnInit() { + this.observeDataStream('gaugePath', newValue => { + let oldValue = this.dataValue; + let temp: any = this.formatWidgetNumberValue(newValue.value); - } + if (oldValue != (temp as number)) { + this.dataValue = temp; + } + // set zone state colors + switch (newValue.state) { + case IZoneState.warning: + this.gaugeOptions.colorValueText = this.theme.warnDark; + break; + case IZoneState.alarm: + this.gaugeOptions.colorValueText = this.theme.warnDark; + break; + default: + this.gaugeOptions.colorValueText = this.theme.text; } - ); - } + }); - unsubscribePath() { - if (this.valueSub$ !== null) { - this.valueSub$.unsubscribe(); - this.valueSub$ = null; - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['gaugePath'].path) - } - } + this.subscribeZones(); + } - // Subscribe to theme event - subscribeTheme() { - this.themeNameSub = this.AppSettingsService.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // need a delay so browser getComputedStyles has time to complete theme application. - this.updateGaugeConfig(); - }, 50); - }) + ngOnDestroy() { + this.unsubscribeDataStream(); + this.unsubscribeZones(); } - unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; + ngOnChanges(changes: SimpleChanges): void { + if (changes.theme) { + this.updateGaugeConfig(); } } // Subscribe to Zones subscribeZones() { - this.zonesSub = this.AppSettingsService.getZonesAsO().subscribe( + this.zonesSub = this.appSettingsService.getZonesAsO().subscribe( zones => { this.zones = zones; this.updateGaugeConfig(); @@ -176,26 +114,15 @@ export class WidgetGaugeNgRadialComponent implements OnInit, OnDestroy, AfterCon } } - private formatDataValue(value:number): number { - // make sure we are within range of the gauge settings, else the needle can go outside the range - if (value < this.widgetProperties.config.minValue || value == null) { - value = this.widgetProperties.config.minValue; - } - if (value > this.widgetProperties.config.maxValue) { - value = this.widgetProperties.config.maxValue; - } - return value; - } - updateGaugeConfig(){ //// Hack to get Theme colors using hidden mixin, DIV and @ViewChild let themePaletteColor = ""; let themePaletteDarkColor = ""; - this.gaugeOptions.colorTitle = this.gaugeOptions.colorUnits = getComputedStyle(this.textElement.nativeElement).color; + this.gaugeOptions.colorTitle = this.gaugeOptions.colorUnits =this.theme.text; this.gaugeOptions.colorPlate = getComputedStyle(this.wrapper.nativeElement).backgroundColor; - this.gaugeOptions.colorBar = getComputedStyle(this.backgroundElement.nativeElement).color; + this.gaugeOptions.colorBar = this.theme.background; this.gaugeOptions.colorNeedleShadowUp = ""; this.gaugeOptions.colorNeedleShadowDown = "black"; this.gaugeOptions.colorNeedleCircleInner = this.gaugeOptions.colorPlate; @@ -206,24 +133,24 @@ export class WidgetGaugeNgRadialComponent implements OnInit, OnDestroy, AfterCon // Theme colors switch (this.widgetProperties.config.barColor) { case "primary": - themePaletteColor = getComputedStyle(this.primaryElement.nativeElement).color; - themePaletteDarkColor = getComputedStyle(this.primaryDarkElement.nativeElement).color; + themePaletteColor = this.theme.primary; + themePaletteDarkColor = this.theme.primaryDark; this.gaugeOptions.colorBarProgress = themePaletteColor; this.gaugeOptions.colorNeedle = themePaletteDarkColor; this.gaugeOptions.colorNeedleEnd = themePaletteDarkColor; break; case "accent": - themePaletteColor = getComputedStyle(this.accentElement.nativeElement).color; - themePaletteDarkColor = getComputedStyle(this.accentDarkElement.nativeElement).color; + themePaletteColor = this.theme.accent; + themePaletteDarkColor = this.theme.accentDark; this.gaugeOptions.colorBarProgress = themePaletteColor; this.gaugeOptions.colorNeedle = themePaletteDarkColor; this.gaugeOptions.colorNeedleEnd = themePaletteDarkColor; break; case "warn": - themePaletteColor = getComputedStyle(this.warnElement.nativeElement).color; - themePaletteDarkColor = getComputedStyle(this.warnDarkElement.nativeElement).color; + themePaletteColor = this.theme.warn; + themePaletteDarkColor = this.theme.warnDark; this.gaugeOptions.colorBarProgress = themePaletteColor; this.gaugeOptions.colorNeedle = themePaletteDarkColor; this.gaugeOptions.colorNeedleEnd = themePaletteDarkColor; @@ -243,13 +170,13 @@ export class WidgetGaugeNgRadialComponent implements OnInit, OnDestroy, AfterCon let color: string; switch (zone.state) { case 1: - color = getComputedStyle(this.warnElement.nativeElement).color; + color = this.theme.warn; break; case IZoneState.alarm: - color = getComputedStyle(this.warnDarkElement.nativeElement).color; + color = this.theme.warnDark; break; default: - color = getComputedStyle(this.primaryElement.nativeElement).color; + color = this.theme.primary; } myZones.push({from: lower, to: upper, color: color}); @@ -264,7 +191,7 @@ export class WidgetGaugeNgRadialComponent implements OnInit, OnDestroy, AfterCon this.gaugeOptions.majorTicksInt = this.widgetProperties.config.numInt; this.gaugeOptions.majorTicksDec = this.widgetProperties.config.numDecimal; - this.gaugeOptions.animationDuration = this.sample - 25; // prevent data/amnimation collisions + this.gaugeOptions.animationDuration = this.sample - 25; // prevent data and amnimation delay collisions // Radial gauge type switch(this.widgetProperties.config.radialSize) { diff --git a/src/app/widget-gauge/widget-gauge.component.css b/src/app/widgets/widget-gauge/widget-gauge.component.css similarity index 100% rename from src/app/widget-gauge/widget-gauge.component.css rename to src/app/widgets/widget-gauge/widget-gauge.component.css diff --git a/src/app/widget-gauge/widget-gauge.component.html b/src/app/widgets/widget-gauge/widget-gauge.component.html similarity index 100% rename from src/app/widget-gauge/widget-gauge.component.html rename to src/app/widgets/widget-gauge/widget-gauge.component.html diff --git a/src/app/widget-gauge/widget-gauge.component.spec.ts b/src/app/widgets/widget-gauge/widget-gauge.component.spec.ts similarity index 100% rename from src/app/widget-gauge/widget-gauge.component.spec.ts rename to src/app/widgets/widget-gauge/widget-gauge.component.spec.ts diff --git a/src/app/widgets/widget-gauge/widget-gauge.component.ts b/src/app/widgets/widget-gauge/widget-gauge.component.ts new file mode 100644 index 00000000..97b71150 --- /dev/null +++ b/src/app/widgets/widget-gauge/widget-gauge.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; + +@Component({ + selector: 'app-widget-gauge', + templateUrl: './widget-gauge.component.html', + styleUrls: ['./widget-gauge.component.css'] +}) +export class WidgetGaugeComponent extends BaseWidgetComponent implements OnInit, OnDestroy { + dataValue: any = 0; + + constructor() { + super(); + + this.defaultConfig = { + displayName: "Gauge Label", + filterSelfPaths: true, + paths: { + "gaugePath": { + description: "Numeric Data", + path: null, + source: null, + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "unitless", + sampleTime: 500 + } + }, + gaugeType: 'linear', + barGraph: false, // if linear/radial, is it digital? + radialSize: 'full', + minValue: 0, + maxValue: 100, + rotateFace: false, + backgroundColor: 'carbon', + frameColor: 'anthracite' + }; + } + + ngOnInit() { + this.observeDataStream('gaugePath', newValue => { + this.dataValue = newValue.value + } + ); + } + + ngOnDestroy() { + this.unsubscribeDataStream(); + } +} diff --git a/src/app/widget-historical/widget-historical.component.html b/src/app/widgets/widget-historical/widget-historical.component.html similarity index 100% rename from src/app/widget-historical/widget-historical.component.html rename to src/app/widgets/widget-historical/widget-historical.component.html diff --git a/src/app/widget-historical/widget-historical.component.css b/src/app/widgets/widget-historical/widget-historical.component.scss similarity index 100% rename from src/app/widget-historical/widget-historical.component.css rename to src/app/widgets/widget-historical/widget-historical.component.scss diff --git a/src/app/widget-historical/widget-historical.component.spec.ts b/src/app/widgets/widget-historical/widget-historical.component.spec.ts similarity index 100% rename from src/app/widget-historical/widget-historical.component.spec.ts rename to src/app/widgets/widget-historical/widget-historical.component.spec.ts diff --git a/src/app/widget-historical/widget-historical.component.ts b/src/app/widgets/widget-historical/widget-historical.component.ts similarity index 79% rename from src/app/widget-historical/widget-historical.component.ts rename to src/app/widgets/widget-historical/widget-historical.component.ts index b427070c..096e4155 100644 --- a/src/app/widget-historical/widget-historical.component.ts +++ b/src/app/widgets/widget-historical/widget-historical.component.ts @@ -1,13 +1,11 @@ -import { Component, OnInit, ViewChild, ElementRef, OnDestroy, Input } from '@angular/core'; +import { Component, OnInit, ViewChild, ElementRef, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs'; import Chart from 'chart.js/auto'; import 'chartjs-adapter-moment'; -import { DataSetService } from '../data-set.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { UnitsService } from '../units.service'; -import { AppSettingsService } from '../app-settings.service'; +import { DataSetService } from '../../data-set.service'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; interface IDataSetOptions { label: string; @@ -20,25 +18,11 @@ interface IDataSetOptions { @Component({ selector: 'app-widget-historical', templateUrl: './widget-historical.component.html', - styleUrls: ['./widget-historical.component.css'] + styleUrls: ['./widget-historical.component.scss'] }) -export class WidgetHistoricalComponent implements OnInit, OnDestroy { - @Input('widgetProperties') widgetProperties!: IWidget; +export class WidgetHistoricalComponent extends BaseWidgetComponent implements OnInit, OnDestroy { @ViewChild('lineGraph', {static: true, read: ElementRef}) lineGraph: ElementRef; - defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - convertUnitTo: "unitless", - dataSetUUID: null, - invertData: false, - displayMinMax: false, - includeZero: true, - minValue: null, - maxValue: null, - verticalGraph: false, - }; - chartCtx; chart = null; @@ -50,14 +34,22 @@ export class WidgetHistoricalComponent implements OnInit, OnDestroy { dataSetSub: Subscription = null; - // dynamics theme support - themeNameSub: Subscription = null; - - constructor( - private DataSetService: DataSetService, - private UnitsService: UnitsService, - private AppSettingsService: AppSettingsService, // need for theme change subscription - ) { } + constructor(private dataSetService: DataSetService) { + super(); + + this.defaultConfig = { + displayName: 'Display Label', + filterSelfPaths: true, + convertUnitTo: "unitless", + dataSetUUID: null, + invertData: false, + displayMinMax: false, + includeZero: true, + minValue: null, + maxValue: null, + verticalGraph: false, + }; + } ngOnInit() { this.textColor = window.getComputedStyle(this.lineGraph.nativeElement).color; @@ -65,7 +57,6 @@ export class WidgetHistoricalComponent implements OnInit, OnDestroy { this.startChart(); this.subscribeDataSet(); - this.subscribeTheme(); } private startChart() { @@ -180,7 +171,7 @@ export class WidgetHistoricalComponent implements OnInit, OnDestroy { this.unsubscribeDataSet(); if (this.widgetProperties.config.dataSetUUID === null) { return } // nothing to sub to... - this.dataSetSub = this.DataSetService.subscribeDataSet(this.widgetProperties.uuid, this.widgetProperties.config.dataSetUUID).subscribe( + this.dataSetSub = this.dataSetService.subscribeDataSet(this.widgetProperties.uuid, this.widgetProperties.config.dataSetUUID).subscribe( dataSet => { if (dataSet === null) { return; // we will get null back if we subscribe to a dataSet before the app has started it. When it learns about it we will get first value @@ -196,7 +187,7 @@ export class WidgetHistoricalComponent implements OnInit, OnDestroy { } this.chartDataAvg.push({ x: dataSet[i].timestamp, - y: (this.UnitsService.convertUnit(this.widgetProperties.config.convertUnitTo, dataSet[i].average) * invert) + y: (this.unitsService.convertUnit(this.widgetProperties.config.convertUnitTo, dataSet[i].average) * invert) }); } this.chart.config.data.datasets[0].data = this.chartDataAvg; @@ -212,11 +203,11 @@ export class WidgetHistoricalComponent implements OnInit, OnDestroy { } else { this.chartDataMin.push({ x: dataSet[i].timestamp, - y: (this.UnitsService.convertUnit(this.widgetProperties.config.convertUnitTo, dataSet[i].minValue) * invert) + y: (this.unitsService.convertUnit(this.widgetProperties.config.convertUnitTo, dataSet[i].minValue) * invert) }); this.chartDataMax.push({ x: dataSet[i].timestamp, - y: (this.UnitsService.convertUnit(this.widgetProperties.config.convertUnitTo, dataSet[i].maxValue) * invert) + y: (this.unitsService.convertUnit(this.widgetProperties.config.convertUnitTo, dataSet[i].maxValue) * invert) }); } } @@ -247,27 +238,7 @@ export class WidgetHistoricalComponent implements OnInit, OnDestroy { } } - // Subscribe to theme event - private subscribeTheme() { - this.themeNameSub = this.AppSettingsService.getThemeNameAsO().subscribe( themeChange => { - setTimeout(() => { // need a delay so browser getComputedStyles has time to complete theme application. - this.textColor = window.getComputedStyle(this.lineGraph.nativeElement).color; - this.startChart() - }, 100); - }) - } - - private unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; - } - } - ngOnDestroy() { this.unsubscribeDataSet(); - this.unsubscribeTheme(); - console.log("stopped Sub"); } - } diff --git a/src/app/widget-iframe/widget-iframe.component.css b/src/app/widgets/widget-iframe/widget-iframe.component.css similarity index 100% rename from src/app/widget-iframe/widget-iframe.component.css rename to src/app/widgets/widget-iframe/widget-iframe.component.css diff --git a/src/app/widget-iframe/widget-iframe.component.html b/src/app/widgets/widget-iframe/widget-iframe.component.html similarity index 100% rename from src/app/widget-iframe/widget-iframe.component.html rename to src/app/widgets/widget-iframe/widget-iframe.component.html diff --git a/src/app/widget-iframe/widget-iframe.component.spec.ts b/src/app/widgets/widget-iframe/widget-iframe.component.spec.ts similarity index 100% rename from src/app/widget-iframe/widget-iframe.component.spec.ts rename to src/app/widgets/widget-iframe/widget-iframe.component.spec.ts diff --git a/src/app/widgets/widget-iframe/widget-iframe.component.ts b/src/app/widgets/widget-iframe/widget-iframe.component.ts new file mode 100644 index 00000000..6fecf266 --- /dev/null +++ b/src/app/widgets/widget-iframe/widget-iframe.component.ts @@ -0,0 +1,24 @@ +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-widget-iframe', + templateUrl: './widget-iframe.component.html', + styleUrls: ['./widget-iframe.component.css'] +}) +export class WidgetIframeComponent extends BaseWidgetComponent implements OnInit { + widgetUrl: string = null; + + constructor() { + super(); + + this.defaultConfig = { + widgetUrl: null + }; + } + + ngOnInit() { + this.widgetUrl = this.widgetProperties.config.widgetUrl; + } + +} diff --git a/src/app/widget-login/login.component.spec.ts b/src/app/widgets/widget-login/login.component.spec.ts similarity index 100% rename from src/app/widget-login/login.component.spec.ts rename to src/app/widgets/widget-login/login.component.spec.ts diff --git a/src/app/widget-login/widget-login.component.css b/src/app/widgets/widget-login/widget-login.component.css similarity index 100% rename from src/app/widget-login/widget-login.component.css rename to src/app/widgets/widget-login/widget-login.component.css diff --git a/src/app/widget-login/widget-login.component.html b/src/app/widgets/widget-login/widget-login.component.html similarity index 100% rename from src/app/widget-login/widget-login.component.html rename to src/app/widgets/widget-login/widget-login.component.html diff --git a/src/app/widget-login/widget-login.component.ts b/src/app/widgets/widget-login/widget-login.component.ts similarity index 89% rename from src/app/widget-login/widget-login.component.ts rename to src/app/widgets/widget-login/widget-login.component.ts index 625fe626..0b92ac8b 100644 --- a/src/app/widget-login/widget-login.component.ts +++ b/src/app/widgets/widget-login/widget-login.component.ts @@ -1,10 +1,10 @@ import { Component, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { AuththeticationService } from "../auththetication.service"; -import { AppSettingsService } from '../app-settings.service'; -import { NotificationsService } from '../notifications.service'; -import { ModalUserCredentialComponent } from '../modal-user-credential/modal-user-credential.component'; -import { IConnectionConfig } from "../app-settings.interfaces"; +import { AuththeticationService } from "../../auththetication.service"; +import { AppSettingsService } from '../../app-settings.service'; +import { NotificationsService } from '../../notifications.service'; +import { ModalUserCredentialComponent } from '../../modal-user-credential/modal-user-credential.component'; +import { IConnectionConfig } from "../../app-settings.interfaces"; import { HttpErrorResponse } from '@angular/common/http'; diff --git a/src/app/widget-numeric/widget-numeric.component.html b/src/app/widgets/widget-numeric/widget-numeric.component.html similarity index 100% rename from src/app/widget-numeric/widget-numeric.component.html rename to src/app/widgets/widget-numeric/widget-numeric.component.html diff --git a/src/app/widget-numeric/widget-numeric.component.scss b/src/app/widgets/widget-numeric/widget-numeric.component.scss similarity index 100% rename from src/app/widget-numeric/widget-numeric.component.scss rename to src/app/widgets/widget-numeric/widget-numeric.component.scss diff --git a/src/app/widget-numeric/widget-numeric.component.spec.ts b/src/app/widgets/widget-numeric/widget-numeric.component.spec.ts similarity index 100% rename from src/app/widget-numeric/widget-numeric.component.spec.ts rename to src/app/widgets/widget-numeric/widget-numeric.component.spec.ts diff --git a/src/app/widget-numeric/widget-numeric.component.ts b/src/app/widgets/widget-numeric/widget-numeric.component.ts similarity index 71% rename from src/app/widget-numeric/widget-numeric.component.ts rename to src/app/widgets/widget-numeric/widget-numeric.component.ts index 1f9abec6..f5aaccda 100644 --- a/src/app/widget-numeric/widget-numeric.component.ts +++ b/src/app/widgets/widget-numeric/widget-numeric.component.ts @@ -1,44 +1,20 @@ -import { Component, Input, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; -import { IZoneState } from "../app-settings.interfaces"; -import { AppSettingsService } from '../app-settings.service'; -import { SignalKService } from '../signalk.service'; -import { UnitsService } from '../units.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; +import { IZoneState } from "../../app-settings.interfaces"; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; @Component({ selector: 'app-widget-numeric', templateUrl: './widget-numeric.component.html', styleUrls: ['./widget-numeric.component.scss'] }) -export class WidgetNumericComponent implements OnInit, OnDestroy, AfterViewChecked { - @Input('widgetProperties') widgetProperties!: IWidget; +export class WidgetNumericComponent extends BaseWidgetComponent implements OnInit, OnDestroy, AfterViewChecked { @ViewChild('canvasEl', {static: true, read: ElementRef}) canvasEl: ElementRef; @ViewChild('canvasBG', {static: true, read: ElementRef}) canvasBG: ElementRef; @ViewChild('NumWrapperDiv', {static: true, read: ElementRef}) wrapperDiv: ElementRef; @ViewChild('warn', {static: true, read: ElementRef}) private warnElement: ElementRef; @ViewChild('warncontrast', {static: true, read: ElementRef}) private warnContrastElement: ElementRef; - defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - paths: { - "numericPath": { - description: "Numeric Data", - path: null, - source: null, - pathType: "number", - isPathConfigurable: true, - convertUnitTo: "unitless" - } - }, - showMax: false, - showMin: false, - numDecimal: 1, - numInt: 1 - }; - dataValue: number = null; IZoneState: IZoneState = null; maxValue: number = null; @@ -51,25 +27,63 @@ export class WidgetNumericComponent implements OnInit, OnDestroy, AfterViewCheck flashOn: boolean = false; flashInterval; - //subs - valueSub: Subscription = null; - - // dynamics theme support - themeNameSub: Subscription = null; - canvasCtx; canvasBGCtx; - constructor( - private SignalKService: SignalKService, - private UnitsService: UnitsService, - private AppSettingsService: AppSettingsService, // need for theme change subscription - ) { + constructor() { + super(); + + this.defaultConfig = { + displayName: 'Gauge Label', + filterSelfPaths: true, + paths: { + "numericPath": { + description: "Numeric Data", + path: null, + source: null, + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "unitless", + sampleTime: 500 + } + }, + showMax: false, + showMin: false, + numDecimal: 1, + numInt: 1 + }; } ngOnInit() { - this.subscribePath(); - this.subscribeTheme(); + this.observeDataStream('numericPath', newValue => { + this.dataValue = newValue.value; + + // init min/max + if (this.minValue === null) { this.minValue = this.dataValue; } + if (this.maxValue === null) { this.maxValue = this.dataValue; } + if (this.dataValue > this.maxValue) { this.maxValue = this.dataValue; } + if (this.dataValue < this.minValue) { this.minValue = this.dataValue; } + + + + this.IZoneState = newValue.state; + //start flashing if alarm + if (this.IZoneState == IZoneState.alarm && !this.flashInterval) { + this.flashInterval = setInterval(() => { + this.flashOn = !this.flashOn; + this.updateCanvas(); + }, 350); // used to flash stuff in alarm + } else if (this.IZoneState != IZoneState.alarm) { + // stop alarming if not in alarm state + if (this.flashInterval) { + clearInterval(this.flashInterval); + this.flashInterval = null; + } + } + + this.updateCanvas(); + } + ); this.canvasCtx = this.canvasEl.nativeElement.getContext('2d'); this.canvasBGCtx = this.canvasBG.nativeElement.getContext('2d'); @@ -77,8 +91,8 @@ export class WidgetNumericComponent implements OnInit, OnDestroy, AfterViewCheck } ngOnDestroy() { - this.unsubscribePath(); - this.unsubscribeTheme(); + this.unsubscribeDataStream(); + if (this.flashInterval) { clearInterval(this.flashInterval); this.flashInterval = null; @@ -107,73 +121,10 @@ export class WidgetNumericComponent implements OnInit, OnDestroy, AfterViewCheck } - private subscribePath() { - this.unsubscribePath(); - if (typeof(this.widgetProperties.config.paths['numericPath'].path) != 'string') { return } // nothing to sub to... - this.valueSub = this.SignalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['numericPath'].path, this.widgetProperties.config.paths['numericPath'].source).subscribe( - newValue => { - this.dataValue = newValue.value; - this.IZoneState = newValue.state; - //start flashing if alarm - if (this.IZoneState == IZoneState.alarm && !this.flashInterval) { - this.flashInterval = setInterval(() => { - this.flashOn = !this.flashOn; - this.updateCanvas(); - }, 350); // used to flash stuff in alarm - } else if (this.IZoneState != IZoneState.alarm) { - // stop alarming if not in alarm state - if (this.flashInterval) { - clearInterval(this.flashInterval); - this.flashInterval = null; - } - } - // init min/max - if (this.minValue === null) { this.minValue = this.dataValue; } - if (this.maxValue === null) { this.maxValue = this.dataValue; } - if (this.dataValue > this.maxValue) { this.maxValue = this.dataValue; } - if (this.dataValue < this.minValue) { this.minValue = this.dataValue; } - this.updateCanvas(); - } - ); - } - - private unsubscribePath() { - if (this.valueSub !== null) { - this.valueSub.unsubscribe(); - this.valueSub = null; - - this.SignalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['numericPath'].path); - } - } - - // Subscribe to theme event - private subscribeTheme() { - this.themeNameSub = this.AppSettingsService.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // need a delay so browser getComputedStyles has time to complete theme application. - this.updateCanvas(); - this.updateCanvasBG(); - }, 100); - }) - } - - private unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; - } - } - - -/* ******************************************************************************************* */ -/* ******************************************************************************************* */ /* ******************************************************************************************* */ /* Canvas */ /* ******************************************************************************************* */ -/* ******************************************************************************************* */ -/* ******************************************************************************************* */ - private updateCanvas() { if (this.canvasCtx) { this.canvasCtx.clearRect(0,0,this.canvasEl.nativeElement.width, this.canvasEl.nativeElement.height); @@ -198,7 +149,7 @@ export class WidgetNumericComponent implements OnInit, OnDestroy, AfterViewCheck let valueText: any; if (this.dataValue !== null) { - valueText = this.formatValue(Number(this.dataValue)); + valueText = this.applyDecorations(this.formatWidgetNumberValue(this.dataValue)); } else { valueText = "--"; } @@ -322,14 +273,14 @@ export class WidgetNumericComponent implements OnInit, OnDestroy, AfterViewCheck if (this.widgetProperties.config.showMin) { if (this.minValue != null) { - valueText = " Min: " + this.formatValue(Number(this.minValue)); + valueText = " Min: " + this.applyDecorations(this.formatWidgetNumberValue(this.minValue)); } else { valueText = " Min: --"; } } if (this.widgetProperties.config.showMax) { if (this.maxValue != null) { - valueText += " Max: " + this.formatValue(Number(this.maxValue)); + valueText += " Max: " + this.applyDecorations(this.formatWidgetNumberValue(this.maxValue)); } else { valueText += valueText + " Max: --"; } @@ -367,73 +318,47 @@ export class WidgetNumericComponent implements OnInit, OnDestroy, AfterViewCheck this.canvasCtx.fillText(valueText,this.canvasEl.nativeElement.width*0.03,this.canvasEl.nativeElement.height*0.97, maxTextWidth); } - /** - * Transforms a value to be ready for presentation based on Widget configuration settings. Handles - * data conversions, format masking and decorations. - * - * Ex: for Ratio value set to display as percentage with 2 integers and 1 decimals. - * The conversion will multiply ratio by 100, apply format masking 99.9 and append a % sign. Returned - * value for a ration of 0.095345 would be: 09.5% - * - * @private - * @param {number} val the numeric value to the formated - * @return {*} {string} formated value ready for display - * @memberof WidgetNumericComponent - */ - private formatValue(val: number): string { - let valConverted: any = null; - let valText: string = null; - - // apply converstion - valConverted = this.UnitsService.convertUnit(this.widgetProperties.config.paths['numericPath'].convertUnitTo, val); - - if (!isNaN(valConverted)) { // retest as convert stuff might have returned a text string - // apply mask - valText = this.padValue(valConverted.toFixed(this.widgetProperties.config.numDecimal), this.widgetProperties.config.numInt, this.widgetProperties.config.numDecimal); - } else { - valText = valConverted; - } - // apply decoration + private applyDecorations(txtValue: string): string { + // apply decoration when requied switch (this.widgetProperties.config.paths['numericPath'].convertUnitTo) { case 'percent': - valText += '%'; + txtValue += '%'; break; case 'percentraw': - valText += '%'; + txtValue += '%'; break; default: break; } - - return valText; + return txtValue; } - private padValue(val, int, dec): string { - let i = 0; - let s, n, foo; - let strVal: string - val = parseFloat(val); - n = (val < 0); - val = Math.abs(val); - if (dec > 0) { - foo = val.toFixed(dec).toString().split('.'); - s = int - foo[0].length; - for (; i < s; ++i) { - foo[0] = '0' + foo[0]; - } - strVal = (n ? '-' : '') + foo[0] + '.' + foo[1]; - } - else { - strVal = Math.round(val).toString(); - s = int - strVal.length; - for (; i < s; ++i) { - strVal = '0' + strVal; - } - strVal = (n ? '-' : '') + strVal; - } - return strVal; - } + // private padValue(val, int, dec): string { + // let i = 0; + // let s, n, foo; + // let strVal: string + // val = parseFloat(val); + // n = (val < 0); + // val = Math.abs(val); + // if (dec > 0) { + // foo = val.toFixed(dec).toString().split('.'); + // s = int - foo[0].length; + // for (; i < s; ++i) { + // foo[0] = '0' + foo[0]; + // } + // strVal = (n ? '-' : '') + foo[0] + '.' + foo[1]; + // } + // else { + // strVal = Math.round(val).toString(); + // s = int - strVal.length; + // for (; i < s; ++i) { + // strVal = '0' + strVal; + // } + // strVal = (n ? '-' : '') + strVal; + // } + // return strVal; + // } } diff --git a/src/app/widget-race-timer/widget-race-timer.component.html b/src/app/widgets/widget-race-timer/widget-race-timer.component.html similarity index 99% rename from src/app/widget-race-timer/widget-race-timer.component.html rename to src/app/widgets/widget-race-timer/widget-race-timer.component.html index 3a012005..545d5cb8 100644 --- a/src/app/widget-race-timer/widget-race-timer.component.html +++ b/src/app/widgets/widget-race-timer/widget-race-timer.component.html @@ -1,4 +1,3 @@ -
@@ -34,7 +33,6 @@ Reset
- diff --git a/src/app/widget-race-timer/widget-race-timer.component.scss b/src/app/widgets/widget-race-timer/widget-race-timer.component.scss similarity index 100% rename from src/app/widget-race-timer/widget-race-timer.component.scss rename to src/app/widgets/widget-race-timer/widget-race-timer.component.scss diff --git a/src/app/widget-race-timer/widget-race-timer.component.spec.ts b/src/app/widgets/widget-race-timer/widget-race-timer.component.spec.ts similarity index 100% rename from src/app/widget-race-timer/widget-race-timer.component.spec.ts rename to src/app/widgets/widget-race-timer/widget-race-timer.component.spec.ts diff --git a/src/app/widget-race-timer/widget-race-timer.component.ts b/src/app/widgets/widget-race-timer/widget-race-timer.component.ts similarity index 86% rename from src/app/widget-race-timer/widget-race-timer.component.ts rename to src/app/widgets/widget-race-timer/widget-race-timer.component.ts index 1b987721..b37aa385 100644 --- a/src/app/widget-race-timer/widget-race-timer.component.ts +++ b/src/app/widgets/widget-race-timer/widget-race-timer.component.ts @@ -1,28 +1,22 @@ -import { Component, OnInit, OnDestroy, Input, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; +import { Component, OnInit, OnDestroy,ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; import { Subscription } from 'rxjs'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; -import { TimersService } from '../timers.service'; -import { IZoneState } from "../app-settings.interfaces"; -import { AppSettingsService } from '../app-settings.service'; +import { TimersService } from '../../timers.service'; +import { IZoneState } from "../../app-settings.interfaces"; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; @Component({ selector: 'app-widget-race-timer', templateUrl: './widget-race-timer.component.html', styleUrls: ['./widget-race-timer.component.scss'] }) -export class WidgetRaceTimerComponent implements OnInit, OnDestroy, AfterViewChecked { - @Input('widgetProperties') widgetProperties!: IWidget; +export class WidgetRaceTimerComponent extends BaseWidgetComponent implements OnInit, OnDestroy, AfterViewChecked { @ViewChild('canvasEl', {static: true, read: ElementRef}) canvasEl: ElementRef; @ViewChild('canvasBG', {static: true, read: ElementRef}) canvasBG: ElementRef; @ViewChild('raceTimerWrapperDiv', {static: true, read: ElementRef}) wrapperDiv: ElementRef; @ViewChild('warn', {static: true, read: ElementRef}) private warnElement: ElementRef; @ViewChild('warncontrast', {static: true, read: ElementRef}) private warnContrastElement: ElementRef; - defaultConfig: IWidgetSvcConfig = { - timerLength: 300 - }; - dataValue: number = null; IZoneState: IZoneState = null; currentValueLength: number = 0; // length (in charaters) of value text to be displayed. if changed from last time, need to recalculate font size... @@ -33,27 +27,25 @@ export class WidgetRaceTimerComponent implements OnInit, OnDestroy, AfterViewChe timerSub: Subscription = null; - // dynamics theme support - themeNameSub: Subscription = null; canvasCtx; canvasBGCtx; + constructor(private TimersService: TimersService) { + super(); - constructor( - private AppSettingsService: AppSettingsService, // need for theme change - private TimersService: TimersService - ) { } + this.defaultConfig = { + timerLength: 300 + }; + } ngOnInit(): void { this.subscribeTimer(); - this.subscribeTheme(); this.canvasCtx = this.canvasEl.nativeElement.getContext('2d'); this.canvasBGCtx = this.canvasBG.nativeElement.getContext('2d'); } ngOnDestroy() { this.unsubscribeTimer(); - this.unsubscribeTheme(); if (this.flashInterval) { clearInterval(this.flashInterval); this.flashInterval = null; @@ -118,25 +110,6 @@ export class WidgetRaceTimerComponent implements OnInit, OnDestroy, AfterViewChe ); } - - // Subscribe to theme event - subscribeTheme() { - this.themeNameSub = this.AppSettingsService.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // need a delay so browser getComputedStyles has time to complete theme application. - this.updateCanvas(); - this.updateCanvasBG(); - }, 100); - }) - } - - unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; - } - } - unsubscribeTimer() { if (this.timerSub !== null) { this.timerSub.unsubscribe(); diff --git a/src/app/widget-simple-linear/widget-simple-linear.component.html b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.html similarity index 94% rename from src/app/widget-simple-linear/widget-simple-linear.component.html rename to src/app/widgets/widget-simple-linear/widget-simple-linear.component.html index 895658ab..75a5e03b 100644 --- a/src/app/widget-simple-linear/widget-simple-linear.component.html +++ b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.html @@ -3,11 +3,11 @@ diff --git a/src/app/widgets/widget-simple-linear/widget-simple-linear.component.scss b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.scss new file mode 100644 index 00000000..9580f708 --- /dev/null +++ b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.scss @@ -0,0 +1,9 @@ +.simpleLinearGauge { + display:block; + position: relative; + border:none; + margin: 0px; + padding: 5px 0px; + width: 100%; + height: 100%; +} diff --git a/src/app/widget-simple-linear/widget-simple-linear.component.spec.ts b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.spec.ts similarity index 100% rename from src/app/widget-simple-linear/widget-simple-linear.component.spec.ts rename to src/app/widgets/widget-simple-linear/widget-simple-linear.component.spec.ts diff --git a/src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts new file mode 100644 index 00000000..00409eb4 --- /dev/null +++ b/src/app/widgets/widget-simple-linear/widget-simple-linear.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; + +@Component({ + selector: 'app-widget-simple-linear', + templateUrl: './widget-simple-linear.component.html', + styleUrls: ['./widget-simple-linear.component.scss'] +}) +export class WidgetSimpleLinearComponent extends BaseWidgetComponent implements OnInit, OnDestroy, OnChanges { + public unitsLabel:string = ""; + public dataLabelValue: string = "0"; + public dataValue: Number = 0; + public barColor: string = ""; + public barColorGradient: string = ""; + public barColorBackground: string = ""; + + constructor() { + super(); + + this.defaultConfig = { + displayName: "Gauge Label", + filterSelfPaths: true, + paths: { + "gaugePath": { + description: "Numeric Data", + path: null, + source: null, + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "V", + sampleTime: 500 + } + }, + minValue: 0, + maxValue: 15, + numInt: 1, + numDecimal: 2, + gaugeType: "simpleLinear", // Applied to Units label. abr = first letter only. full = full string + gaugeUnitLabelFormat: "full", // Applied to Units label. abr = first letter only. full = full string + barColor: 'accent', + }; + } + + ngOnInit(): void { + // set Units label sting based on gauge config + if (this.widgetProperties.config.gaugeUnitLabelFormat == "abr") { + // TODO: Improve Units service to have Full Measure label, abbriviation and descriptions so that we can use Full or abr display labels...! + this.unitsLabel = this.widgetProperties.config.paths['gaugePath'].convertUnitTo.substr(0,1); + } else { + this.unitsLabel = this.widgetProperties.config.paths['gaugePath'].convertUnitTo; + } + + this.observeDataStream('gaugePath', newValue => { + if (newValue.value == null) {return} + + newValue.value = this.formatWidgetNumberValue(newValue.value); + this.dataValue = (newValue.value as number); + // Format Widget display label value using settings + if (this.widgetProperties.config.numDecimal != 0){ + this.dataLabelValue = newValue.value.padStart((this.widgetProperties.config.numInt + 1 + this.widgetProperties.config.numDecimal), "0"); + } else { + this.dataLabelValue = newValue.value.padStart(this.widgetProperties.config.numInt, "0"); + } + } + ); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.theme) { + this.updateGaugeSettings(); + } + } + + updateGaugeSettings() { + this.barColorBackground = this.theme.background; + + switch (this.widgetProperties.config.barColor) { + case "primary": + this.barColor = this.theme.primary; + this.barColorGradient = this.theme.primaryDark; + break; + + case "accent": + this.barColor = this.theme.accent; + this.barColorGradient = this.theme.accentDark; + break; + + case "warn": + this.barColor = this.theme.warn; + this.barColorGradient = this.theme.warnDark; + break; + } + } + + ngOnDestroy() { + this.unsubscribeDataStream(); + } +} diff --git a/src/app/widget-switch/widget-switch.component.css b/src/app/widgets/widget-switch/widget-switch.component.css similarity index 100% rename from src/app/widget-switch/widget-switch.component.css rename to src/app/widgets/widget-switch/widget-switch.component.css diff --git a/src/app/widget-switch/widget-switch.component.html b/src/app/widgets/widget-switch/widget-switch.component.html similarity index 52% rename from src/app/widget-switch/widget-switch.component.html rename to src/app/widgets/widget-switch/widget-switch.component.html index 7b8eecca..907cdeb6 100644 --- a/src/app/widget-switch/widget-switch.component.html +++ b/src/app/widgets/widget-switch/widget-switch.component.html @@ -1,17 +1,8 @@
-
- - -
- -
-
diff --git a/src/app/widget-switch/widget-switch.component.spec.ts b/src/app/widgets/widget-switch/widget-switch.component.spec.ts similarity index 100% rename from src/app/widget-switch/widget-switch.component.spec.ts rename to src/app/widgets/widget-switch/widget-switch.component.spec.ts diff --git a/src/app/widgets/widget-switch/widget-switch.component.ts b/src/app/widgets/widget-switch/widget-switch.component.ts new file mode 100644 index 00000000..a973ccea --- /dev/null +++ b/src/app/widgets/widget-switch/widget-switch.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; + +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; +import { SignalkRequestsService } from './../../signalk-requests.service'; + +@Component({ + selector: 'app-widget-switch', + templateUrl: './widget-switch.component.html', + styleUrls: ['./widget-switch.component.css'] +}) +export class WidgetSwitchComponent extends BaseWidgetComponent implements OnInit, OnDestroy { + dataValue: number = null; + dataTimestamp: number = Date.now(); + skRequestSub: Subscription = null; + state: boolean = null; + + constructor(private signalkRequestsService: SignalkRequestsService) { + super(); + + this.defaultConfig = { + displayName: 'Gauge Label', + filterSelfPaths: true, + paths: { + "statePath": { + description: "State Data", + path: null, + source: null, + pathType: "boolean", + isPathConfigurable: true, + convertUnitTo: "unitless", + sampleTime: 500 + } + }, + }; + } + + ngOnInit() { + this.observeDataStream('statePath', newValue => { + this.state = newValue.value; + }); + + this.subscribeSKRequest(); + } + + ngOnDestroy() { + this.unsubscribeDataStream(); + this.skRequestSub.unsubscribe(); + } + + subscribeSKRequest() { + this.skRequestSub = this.signalkRequestsService.subscribeRequest().subscribe(requestResult => { + if (requestResult.widgetUUID == this.widgetProperties.uuid) { + if (requestResult.statusCode != 200){ + let errMsg = requestResult.statusCode + " - " +requestResult.statusCodeDescription; + if (requestResult.message){ + errMsg = errMsg + " Server Message: " + requestResult.message; + } + alert('[Widget Name: ' + errMsg); + } else { + console.log("AP Received: \n" + JSON.stringify(requestResult)); + } + } + }); + } + + sendDelta(value: boolean) { + this.signalkRequestsService.putRequest(this.widgetProperties.config.paths['statePath'].path, value, this.widgetProperties.uuid); + } + +} diff --git a/src/app/widget-text-generic/widget-text-generic.component.css b/src/app/widgets/widget-text-generic/widget-text-generic.component.css similarity index 99% rename from src/app/widget-text-generic/widget-text-generic.component.css rename to src/app/widgets/widget-text-generic/widget-text-generic.component.css index 37c768de..b2403386 100644 --- a/src/app/widget-text-generic/widget-text-generic.component.css +++ b/src/app/widgets/widget-text-generic/widget-text-generic.component.css @@ -4,7 +4,6 @@ canvas { left: 0; } - .textGenericWrapper { position: relative; width: 100%; diff --git a/src/app/widget-text-generic/widget-text-generic.component.html b/src/app/widgets/widget-text-generic/widget-text-generic.component.html similarity index 100% rename from src/app/widget-text-generic/widget-text-generic.component.html rename to src/app/widgets/widget-text-generic/widget-text-generic.component.html diff --git a/src/app/widget-text-generic/widget-text-generic.component.spec.ts b/src/app/widgets/widget-text-generic/widget-text-generic.component.spec.ts similarity index 100% rename from src/app/widget-text-generic/widget-text-generic.component.spec.ts rename to src/app/widgets/widget-text-generic/widget-text-generic.component.spec.ts diff --git a/src/app/widget-text-generic/widget-text-generic.component.ts b/src/app/widgets/widget-text-generic/widget-text-generic.component.ts similarity index 65% rename from src/app/widget-text-generic/widget-text-generic.component.ts rename to src/app/widgets/widget-text-generic/widget-text-generic.component.ts index 5adf3c5a..f7de5861 100644 --- a/src/app/widget-text-generic/widget-text-generic.component.ts +++ b/src/app/widgets/widget-text-generic/widget-text-generic.component.ts @@ -1,9 +1,7 @@ -import { Component, Input, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; -import { Subscription } from 'rxjs'; +import { Component, OnInit, OnDestroy, ViewChild, ElementRef, AfterViewChecked } from '@angular/core'; -import { SignalKService } from '../signalk.service'; -import { AppSettingsService } from '../app-settings.service'; -import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; +import { IWidgetSvcConfig } from '../../widgets-interface'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; @Component({ @@ -11,27 +9,11 @@ import { IWidget, IWidgetSvcConfig } from '../widget-manager.service'; templateUrl: './widget-text-generic.component.html', styleUrls: ['./widget-text-generic.component.css'] }) -export class WidgetTextGenericComponent implements OnInit, AfterViewChecked, OnDestroy { - - @Input('widgetProperties') widgetProperties!: IWidget; +export class WidgetTextGenericComponent extends BaseWidgetComponent implements OnInit, AfterViewChecked, OnDestroy { @ViewChild('canvasEl', {static: true, read: ElementRef}) canvasEl: ElementRef; @ViewChild('canvasBG', {static: true, read: ElementRef}) canvasBG: ElementRef; @ViewChild('textGenericWrapperDiv', {static: true, read: ElementRef}) wrapperDiv: ElementRef; - defaultConfig: IWidgetSvcConfig = { - displayName: null, - filterSelfPaths: true, - paths: { - "stringPath": { - description: "String Data", - path: null, - source: null, - pathType: "string", - isPathConfigurable: true, - } - }, - }; - dataValue: any = null; dataTimestamp: number = Date.now(); valueFontSize: number = 1; @@ -39,31 +21,38 @@ export class WidgetTextGenericComponent implements OnInit, AfterViewChecked, OnD canvasCtx; canvasBGCtx; - //subs - valueSub: Subscription = null; - - // dynamics theme support - themeNameSub: Subscription = null; - - - constructor( - private signalKService: SignalKService, - private appSettingsService: AppSettingsService, // need for theme change subscription - ) { + constructor() { + super(); + + this.defaultConfig = { + displayName: 'Gauge Label', + filterSelfPaths: true, + paths: { + "stringPath": { + description: "String Data", + path: null, + source: null, + pathType: "string", + isPathConfigurable: true, + sampleTime: 500 + } + } + }; } ngOnInit() { this.canvasCtx = this.canvasEl.nativeElement.getContext('2d'); this.canvasBGCtx = this.canvasBG.nativeElement.getContext('2d'); - - this.subscribePath(); - this.subscribeTheme(); this.resizeWidget(); + + this.observeDataStream('stringPath', newValue => { + this.dataValue = newValue.value; + this.updateCanvas(); + }); } ngOnDestroy() { - this.unsubscribePath(); - this.unsubscribeTheme(); + this.unsubscribeDataStream(); } ngAfterViewChecked() { @@ -90,49 +79,9 @@ export class WidgetTextGenericComponent implements OnInit, AfterViewChecked, OnD } - subscribePath() { - this.unsubscribePath(); - if (typeof(this.widgetProperties.config.paths['stringPath'].path) != 'string') { return } // nothing to sub to... - this.valueSub = this.signalKService.subscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['stringPath'].path, this.widgetProperties.config.paths['stringPath'].source).subscribe( - newValue => { - this.dataValue = newValue.value; - this.updateCanvas(); - } - ); - } - - unsubscribePath() { - if (this.valueSub !== null) { - this.valueSub.unsubscribe(); - this.valueSub = null; - this.signalKService.unsubscribePath(this.widgetProperties.uuid, this.widgetProperties.config.paths['stringPath'].path) - } - } - // Subscribe to theme event - subscribeTheme() { - this.themeNameSub = this.appSettingsService.getThemeNameAsO().subscribe( - themeChange => { - setTimeout(() => { // need a delay so browser getComputedStyles has time to complete theme application. - this.drawTitle(); - this.drawValue(); - }, 100); - }) - } - - unsubscribeTheme(){ - if (this.themeNameSub !== null) { - this.themeNameSub.unsubscribe(); - this.themeNameSub = null; - } - } - /* ******************************************************************************************* */ -/* ******************************************************************************************* */ -/* ******************************************************************************************* */ -/* Canvas */ -/* ******************************************************************************************* */ -/* ******************************************************************************************* */ +/* Canvas drawing */ /* ******************************************************************************************* */ updateCanvas() { @@ -207,8 +156,4 @@ export class WidgetTextGenericComponent implements OnInit, AfterViewChecked, OnD this.canvasBGCtx.fillStyle = window.getComputedStyle(this.wrapperDiv.nativeElement).color; this.canvasBGCtx.fillText(this.widgetProperties.config.displayName,this.canvasEl.nativeElement.width*0.03,this.canvasEl.nativeElement.height*0.03, maxTextWidth); } - - - - } diff --git a/src/app/widget-tutorial/widget-tutorial.component.css b/src/app/widgets/widget-tutorial/widget-tutorial.component.css similarity index 100% rename from src/app/widget-tutorial/widget-tutorial.component.css rename to src/app/widgets/widget-tutorial/widget-tutorial.component.css diff --git a/src/app/widget-tutorial/widget-tutorial.component.html b/src/app/widgets/widget-tutorial/widget-tutorial.component.html similarity index 100% rename from src/app/widget-tutorial/widget-tutorial.component.html rename to src/app/widgets/widget-tutorial/widget-tutorial.component.html diff --git a/src/app/widget-tutorial/widget-tutorial.component.spec.ts b/src/app/widgets/widget-tutorial/widget-tutorial.component.spec.ts similarity index 100% rename from src/app/widget-tutorial/widget-tutorial.component.spec.ts rename to src/app/widgets/widget-tutorial/widget-tutorial.component.spec.ts diff --git a/src/app/widgets/widget-tutorial/widget-tutorial.component.ts b/src/app/widgets/widget-tutorial/widget-tutorial.component.ts new file mode 100644 index 00000000..4b52f150 --- /dev/null +++ b/src/app/widgets/widget-tutorial/widget-tutorial.component.ts @@ -0,0 +1,18 @@ +import { Component, Input } from '@angular/core'; +import { IWidgetSvcConfig } from '../../widgets-interface'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; + +@Component({ + selector: 'app-widget-tutorial', + templateUrl: './widget-tutorial.component.html', + styleUrls: ['./widget-tutorial.component.css'] +}) +export class WidgetTutorialComponent extends BaseWidgetComponent { + @Input() unlockStatus: boolean; + + defaultConfig: IWidgetSvcConfig = {}; + constructor() { + super(); + } + +} diff --git a/src/app/widget-unknown/widget-unknown.component.css b/src/app/widgets/widget-unknown/widget-unknown.component.css similarity index 100% rename from src/app/widget-unknown/widget-unknown.component.css rename to src/app/widgets/widget-unknown/widget-unknown.component.css diff --git a/src/app/widget-unknown/widget-unknown.component.html b/src/app/widgets/widget-unknown/widget-unknown.component.html similarity index 100% rename from src/app/widget-unknown/widget-unknown.component.html rename to src/app/widgets/widget-unknown/widget-unknown.component.html diff --git a/src/app/widget-unknown/widget-unknown.component.spec.ts b/src/app/widgets/widget-unknown/widget-unknown.component.spec.ts similarity index 100% rename from src/app/widget-unknown/widget-unknown.component.spec.ts rename to src/app/widgets/widget-unknown/widget-unknown.component.spec.ts diff --git a/src/app/widgets/widget-unknown/widget-unknown.component.ts b/src/app/widgets/widget-unknown/widget-unknown.component.ts new file mode 100644 index 00000000..a7b6d21f --- /dev/null +++ b/src/app/widgets/widget-unknown/widget-unknown.component.ts @@ -0,0 +1,15 @@ +import { Component } from '@angular/core'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; + +@Component({ + selector: 'app-widget-unknown', + templateUrl: './widget-unknown.component.html', + styleUrls: ['./widget-unknown.component.css'] +}) +export class WidgetUnknownComponent extends BaseWidgetComponent { + + constructor() { + super(); + } + +} diff --git a/src/app/widget-wind/widget-wind.component.css b/src/app/widgets/widget-wind/widget-wind.component.css similarity index 100% rename from src/app/widget-wind/widget-wind.component.css rename to src/app/widgets/widget-wind/widget-wind.component.css diff --git a/src/app/widget-wind/widget-wind.component.html b/src/app/widgets/widget-wind/widget-wind.component.html similarity index 100% rename from src/app/widget-wind/widget-wind.component.html rename to src/app/widgets/widget-wind/widget-wind.component.html diff --git a/src/app/widget-wind/widget-wind.component.spec.ts b/src/app/widgets/widget-wind/widget-wind.component.spec.ts similarity index 100% rename from src/app/widget-wind/widget-wind.component.spec.ts rename to src/app/widgets/widget-wind/widget-wind.component.spec.ts diff --git a/src/app/widgets/widget-wind/widget-wind.component.ts b/src/app/widgets/widget-wind/widget-wind.component.ts new file mode 100644 index 00000000..7ea0261b --- /dev/null +++ b/src/app/widgets/widget-wind/widget-wind.component.ts @@ -0,0 +1,219 @@ +import { Component, OnInit, OnDestroy, NgZone } from '@angular/core'; +import { Subscription, interval } from 'rxjs'; +import { BaseWidgetComponent } from '../../base-widget/base-widget.component'; + + +@Component({ + selector: 'app-widget-wind', + templateUrl: './widget-wind.component.html', + styleUrls: ['./widget-wind.component.css'] +}) +export class WidgetWindComponent extends BaseWidgetComponent implements OnInit, OnDestroy { + currentHeading: number = 0; + + appWindAngle: number = null; + + appWindSpeed: number = null; + + trueWindAngle: number = null; + + trueWindSpeed: number = null; + + trueWindHistoric: { + timestamp: number; + heading: number; + }[] = []; + trueWindMinHistoric: number; + trueWindMidHistoric: number; + trueWindMaxHistoric: number; + + windSectorObservableSub: Subscription = null; + + constructor(private zones: NgZone) { + super(); + + this.defaultConfig = { + filterSelfPaths: true, + paths: { + "headingPath": { + description: "Heading", + path: 'self.navigation.headingTrue', + source: 'default', + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "deg", + sampleTime: 500 + }, + "trueWindAngle": { + description: "True Wind Angle", + path: 'self.environment.wind.angleTrueWater', + source: 'default', + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "deg", + sampleTime: 300 + }, + "trueWindSpeed": { + description: "True Wind Speed", + path: 'self.environment.wind.speedTrue', + source: 'default', + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "knots", + sampleTime: 300 + }, + "appWindAngle": { + description: "Apparent Wind Angle", + path: 'self.environment.wind.angleApparent', + source: 'default', + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "deg", + sampleTime: 300 + }, + "appWindSpeed": { + description: "Apparent Wind Speed", + path: 'self.environment.wind.speedApparent', + source: 'default', + pathType: "number", + isPathConfigurable: true, + convertUnitTo: "knots", + sampleTime: 300 + }, + }, + windSectorEnable: true, + windSectorWindowSeconds: 10, + laylineEnable: true, + laylineAngle: 35, + }; + } + + ngOnInit(): void { + this.observeDataStream('headingPath', newValue => { + if (newValue.value === null) { + this.currentHeading = 0; + } else { + this.currentHeading = newValue.value; + } + }); + + this.observeDataStream('appWindAngle', newValue => { + if (newValue.value === null) { + this.appWindAngle = null; + return; + } + + if (newValue.value < 0) {// stb + this.appWindAngle = 360 + newValue.value; // adding a negative number subtracts it... + } else { + this.appWindAngle = newValue.value; + } + } + ); + + this.observeDataStream('appWindSpeed', newValue => { + this.appWindSpeed = newValue.value; + } + ); + + this.observeDataStream('trueWindSpeed', newValue => { + this.trueWindSpeed = newValue.value; + } + ); + + this.observeDataStream('trueWindAngle', newValue => { + if (newValue.value === null) { + this.trueWindAngle = null; + return; + } + + // Depending on path, this number can either be the magnetic compass heading, true compass heading, or heading relative to boat heading (-180 to 180deg)... Ugh... + // 0-180+ for stb + // -0 to -180 for port + // need in 0-360 + if (this.widgetProperties.config.paths['trueWindAngle'].path.match('angleTrueWater')|| + this.widgetProperties.config.paths['trueWindAngle'].path.match('angleTrueGround')) { + //-180 to 180 + this.trueWindAngle = this.addHeading(this.currentHeading, newValue.value); + } else if (this.widgetProperties.config.paths['trueWindAngle'].path.match('direction')) { + //0-360 + this.trueWindAngle = newValue.value; + } else { + // some other path... assume it's the angle + this.trueWindAngle = newValue.value; + } + + //add to historical for wind sectors + if (this.widgetProperties.config.windSectorEnable) { + this.addHistoricalTrue(this.trueWindAngle); + } + } + ); + + this.startWindSectors(); + } + + ngOnDestroy() { + this.unsubscribeDataStream(); + this.stopWindSectors(); + } + + startWindSectors() { + this.zones.runOutsideAngular(() => { + this.windSectorObservableSub = interval(500).subscribe(x => { + this.historicalCleanup(); + }); + }); + } + + addHistoricalTrue (windHeading: number) { + this.trueWindHistoric.push({ + timestamp: Date.now(), + heading: windHeading + }); + let arr = this.arcForAngles(this.trueWindHistoric.map(d => d.heading)); + this.trueWindMinHistoric = arr[0]; + this.trueWindMaxHistoric = arr[1]; + this.trueWindMidHistoric = arr[2]; + } + + arcForAngles (data) { + return data.slice(1).reduce((acc, theValue) => { + let value = theValue + while (value < acc[0] - 180) { + value += 360 + } + while (value > acc[1] + 180) { + value -= 360 + } + acc[0] = Math.min(acc[0], value) + acc[1] = Math.max(acc[1], value) + acc[2] = ((acc[1]-acc[0])/2)+acc[0]; + return acc + }, [data[0], data[0]]) + } + + historicalCleanup() { + let n = Date.now()-(this.widgetProperties.config.windSectorWindowSeconds*1000); + for (var i = this.trueWindHistoric.length - 1; i >= 0; --i) { + if (this.trueWindHistoric[i].timestamp < n) { + this.trueWindHistoric.splice(i,1); + } + } + } + + stopWindSectors() { + if (this.windSectorObservableSub !== null) { + this.windSectorObservableSub.unsubscribe(); + this.windSectorObservableSub = null; + } + + } + + addHeading(h1: number, h2: number) { + let h3 = h1 + h2; + while (h3 > 359) { h3 = h3 - 359; } + while (h3 < 0) { h3 = h3 + 359; } + return h3; + } +} diff --git a/src/polyfills.ts b/src/polyfills.ts index c3f14653..43975d7c 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -57,7 +57,7 @@ import 'core-js/es/reflect'; /*************************************************************************************************** * Zone JS is required by Angular itself. */ -import 'zone.js/dist/zone'; // Included with Angular CLI. +import 'zone.js'; // Included with Angular CLI. @@ -76,5 +76,5 @@ import 'zone.js/dist/zone'; // Included with Angular CLI. // import 'intl/locale-data/jsonp/en'; // Process sharing fix for util module - Angular v12 upgratde fix. This might go away as it probably -// comes from dependencies that fails to handle proper process sharing rules -(window as any).process = { env: { DEBUG: undefined }, }; \ No newline at end of file +// comes from dependencies that fails to handle proper process sharing rules +(window as any).process = { env: { DEBUG: undefined }, }; diff --git a/src/styles-commun/settingsTabContent.scss b/src/styles-commun/settingsTabContent.scss new file mode 100644 index 00000000..4eb94969 --- /dev/null +++ b/src/styles-commun/settingsTabContent.scss @@ -0,0 +1,15 @@ +@mixin tab-content-styles { + .formActionFooter { + width: 100%; + text-align: end; + } + + .formActionButton { + margin-left: 10px; + } + + .formActionDivider{ + margin-top: 10px; + margin-bottom: 10px; + } +} diff --git a/src/styles.scss b/src/styles.scss index 68718d0a..789c9c2b 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -84,6 +84,9 @@ ); }; +//Import commun/shared styles +@import 'styles-commun/settingsTabContent.scss'; + //Import themes @import 'themes/defaultTheme.scss'; @import 'themes/signalkTheme.scss'; @@ -95,36 +98,34 @@ // import Components themes @import 'app/app.component.scss'; +@import 'app/dynamic-widget-container/dynamic-widget-container.component.scss'; @import 'app/layout-split/layout-split.component.scss'; @import 'app/modal-path-selector/modal-path-selector.component.scss'; -@import 'app/widget-blank/widget-blank.component.scss'; -@import 'app/svg-wind/svg-wind.component.scss'; // for most svg Widget components -@import 'app/widget-button/widget-button.component.scss'; -@import 'app/widget-gauge-ng-linear/widget-gauge-ng-linear.component.scss'; -@import 'app/widget-numeric/widget-numeric.component.scss'; -@import 'app/widget-race-timer/widget-race-timer.component.scss'; -@import 'app/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss'; @import 'app/alarm-menu/alarm-menu.component.scss'; -@import 'app/widget-autopilot/widget-autopilot.component.scss'; -@import 'app/widget-simple-linear/widget-simple-linear.component.scss'; -@import 'app/settings-datasets/settings-datasets.component.scss'; -@import 'app/settings-config/settings-config.component.scss'; +@import 'app/settings/signalk/signalk.component.scss'; +@import 'app/settings/datasets/datasets.component.scss'; +@import 'app/settings/config/config.component.scss'; +@import 'app/widgets/widget-blank/widget-blank.component.scss'; +@import 'app/widgets/svg-wind/svg-wind.component.scss'; // for most svg Widget components +@import 'app/widgets/widget-numeric/widget-numeric.component.scss'; +@import 'app/widgets/widget-race-timer/widget-race-timer.component.scss'; +@import 'app/widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component.scss'; +@import 'app/widgets/widget-autopilot/widget-autopilot.component.scss'; @mixin theme-components($theme) { @include mat.all-component-themes($theme); @include app-theme($theme); + @include tab-content-styles(); @include layout-split-theme($theme); + @include dynamic-widget-container-theme($theme); @include modal-path-selector-theme($theme); @include widget-blank-theme($theme); - @include svg-wind-theme($theme); // for both svg-wind and svg-autopilot - @include widget-button-theme($theme); @include widget-numeric-theme($theme); @include widget-racetimer-theme($theme); - @include widget-ngGauge-linear-theme($theme); - @include widget-ngGauge-radial-theme($theme); - @include alarm-menu-theme($theme); @include widget-autopilot-theme($theme); - @include widget-simple-linear-theme($theme); + @include svg-wind-theme($theme); // for both svg-wind and svg-autopilot + @include alarm-menu-theme($theme); + @include theme-settings-sk($theme); @include theme-settings-data($theme); @include theme-settings-config($theme); } diff --git a/src/themes/defaultTheme.scss b/src/themes/defaultTheme.scss index ed9812d9..48c2f9c3 100644 --- a/src/themes/defaultTheme.scss +++ b/src/themes/defaultTheme.scss @@ -7,7 +7,7 @@ // Generate mat-material default theme palettes. Generate now so we call pull if necessary // palette color later in this scss file. // * The warn palette is optional (defaults to $mat-red). This is the palette that should -// used for error and warning. +// be used for error and warning. $defaultTheme-primary: mat.define-palette(mat.$cyan-palette); $defaultTheme-accent: mat.define-palette(mat.$blue-palette, A200, A100, A400); $defaultTheme-warn: mat.define-palette(mat.$red-palette); diff --git a/tsconfig.json b/tsconfig.json index d7ee5937..fa3fc68f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "compileOnSave": false, + "compileOnSave": true, "compilerOptions": { "outDir": "./dist/out-tsc", "sourceMap": true, @@ -14,7 +14,7 @@ "node_modules/@types" ], "lib": [ - "es2017", + "es2020", "dom" ], "module": "es2020",