From 0ca0ce7105739f1e07aec70658dcdbabe9a8dd56 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:15:34 +0930 Subject: [PATCH] ng 18 --- CHANGELOG.md | 10 + angular.json | 19 +- package.json | 32 +- src/app-theme.scss | 39 +- src/app/app.component.css | 1 + src/app/app.component.html | 51 +- src/app/app.component.ts | 25 +- src/app/app.info.ts | 39 +- src/app/app.module.ts | 80 +- .../dialogs/common/dialogs.component.ts | 4 +- .../dialogs/{ => geojson}/geojson-dialog.css | 0 .../{ => geojson}/geojson-dialog.facade.ts | 0 .../dialogs/{ => geojson}/geojson-dialog.html | 0 .../dialogs/{ => geojson}/geojson-dialog.ts | 0 src/app/lib/components/dialogs/index.ts | 2 +- src/app/lib/components/index.ts | 6 +- src/app/lib/components/wakelock.component.ts | 1 + src/app/modules/alarms/alarms.module.ts | 48 - .../alarms/components/alarm.component.ts | 15 + .../components/alarms-dialog.component.ts | 21 +- .../components/anchor-watch.component.ts | 21 + src/app/modules/alarms/index.ts | 5 + .../autopilot}/autopilot.component.css | 0 .../autopilot}/autopilot.component.ts | 0 src/app/modules/autopilot/index.ts | 1 + .../course}/course-settings.ts | 0 src/app/modules/course/index.ts | 1 + .../experiments/experiments.component.ts | 15 + .../modules/experiments/experiments.module.ts | 32 - src/app/modules/experiments/index.ts | 1 + .../experiments/weather/weather.module.ts | 46 - src/app/modules/index.ts | 17 +- .../map/components/navigation/index.ts | 1 - .../modules/map/components/popover/index.ts | 3 - .../components/popover/popover.component.ts | 427 ----- .../map/components/profiles/default/index.ts | 1 - src/app/modules/map/fb-map.component.html | 18 +- src/app/modules/map/fb-map.component.ts | 238 +-- src/app/modules/map/index.ts | 2 + src/app/modules/map/map.module.ts | 75 - src/app/modules/map/mapconfig.ts | 177 +- src/app/modules/map/ol/index.ts | 3 + .../lib/navigation/layer-layline.component.ts | 33 +- .../layer-target-angle.component.ts | 140 ++ .../lib/resources/layer-charts.component.ts | 7 +- .../resources/layer-skvessels.component.ts | 14 +- .../map/ol/lib/vectorLayerStyleFactory.ts | 25 +- .../popovers/aircraft-popover.component.ts | 132 ++ .../map/popovers/alarm-popover.component.ts | 117 ++ .../map/popovers/aton-popover.component.ts | 150 ++ .../compass.component.svg | 0 .../popover => popovers}/compass.component.ts | 3 + .../popovers/featurelist-popover.component.ts | 68 + src/app/modules/map/popovers/index.ts | 8 + .../popover.component.scss | 0 .../modules/map/popovers/popover.component.ts | 68 + .../resource-popover.component.ts | 24 + .../vessel-popover.component.ts | 18 + src/app/modules/map/vessel-calcs.types.ts | 23 + src/app/modules/map/vessel-calcs.worker.ts | 165 ++ .../{ => components}/settings-dialog.css | 0 .../{ => components}/settings-dialog.html | 0 .../{ => components}/settings-dialog.ts | 48 +- .../signalk-preferredpaths.component.ts | 5 + .../settings/{settings.module.ts => index.ts} | 18 +- .../components/active-resource-dialog.ts | 342 ++++ .../ais/aircraft-properties-modal.ts | 143 ++ .../components/ais/ais-properties-modal.ts | 261 +++ .../{lists => components/ais}/aislist.html | 47 +- .../{lists => components/ais}/aislist.ts | 111 +- .../components/ais/aton-properties-modal.ts | 195 ++ .../charts/chart-properties-dialog.ts | 169 ++ .../charts}/chartlist.html | 62 +- .../{lists => components/charts}/chartlist.ts | 280 +-- .../{ => components}/notes/note-dialog.html | 9 +- .../{ => components}/notes/note-dialog.ts | 0 .../{lists => components/notes}/notelist.html | 0 .../{lists => components/notes}/notelist.ts | 23 +- .../{ => components}/notes/notes.css | 0 .../notes/relatednotes-dialog.html | 16 +- .../notes/relatednotes-dialog.ts | 0 .../{ => components}/notes/safe.pipe.ts | 0 .../skresources/components/resource-dialog.ts | 217 +++ .../{lists => components}/resourcelist.css | 10 +- .../resourcesets}/resource-upload-dialog.css | 0 .../resourcesets}/resource-upload-dialog.html | 0 .../resourcesets}/resource-upload-dialog.ts | 40 +- .../resourceset-feature-properties-modal.ts | 119 ++ .../resourcesets/resourceset-list-modal.ts | 223 +++ .../routes}/build-route.component.css | 3 - .../routes}/build-route.component.ts | 0 .../components/routes}/nextpoint.component.ts | 5 + .../routes}/routelist.html | 0 .../{lists => components/routes}/routelist.ts | 23 +- .../components/signalk-details.component.css | 0 .../components/signalk-details.component.ts | 2 +- .../components/tracks/track-list-modal.ts | 239 +++ .../waypoints}/waypointlist.html | 0 .../waypoints}/waypointlist.ts | 23 +- src/app/modules/skresources/index.ts | 32 + src/app/modules/skresources/notes/index.ts | 2 - .../modules/skresources/resource-dialogs.ts | 1689 ----------------- .../modules/skresources/resources.module.ts | 121 -- .../modules/skresources/resources.service.ts | 7 +- .../resource-set.ts => resourceset-class.ts} | 0 ...sets.service.ts => resourceset-service.ts} | 2 +- src/app/modules/skstream/skstream.module.ts | 15 - src/app/modules/weather/index.ts | 1 + .../weather-data.component.ts | 26 + .../weather-forecast-modal.ts} | 28 +- src/app/types/index.d.ts | 2 + src/assets/help/img/ais_shiptypes.png | Bin 0 -> 31928 bytes src/assets/help/index.html | 51 +- src/assets/img/ais_buddy.png | Bin 207 -> 286 bytes src/assets/img/ais_cargo.png | Bin 286 -> 214 bytes src/assets/img/ais_highspeed.png | Bin 0 -> 249 bytes src/assets/img/ais_passenger.png | Bin 249 -> 269 bytes src/assets/img/ais_tanker.png | Bin 269 -> 242 bytes src/index.html | 6 - tsconfig.json | 2 +- 120 files changed, 4032 insertions(+), 3057 deletions(-) rename src/app/lib/components/dialogs/{ => geojson}/geojson-dialog.css (100%) rename src/app/lib/components/dialogs/{ => geojson}/geojson-dialog.facade.ts (100%) rename src/app/lib/components/dialogs/{ => geojson}/geojson-dialog.html (100%) rename src/app/lib/components/dialogs/{ => geojson}/geojson-dialog.ts (100%) delete mode 100644 src/app/modules/alarms/alarms.module.ts create mode 100644 src/app/modules/alarms/index.ts rename src/app/{lib/components => modules/autopilot}/autopilot.component.css (100%) rename src/app/{lib/components => modules/autopilot}/autopilot.component.ts (100%) create mode 100644 src/app/modules/autopilot/index.ts rename src/app/{lib/components => modules/course}/course-settings.ts (100%) create mode 100644 src/app/modules/course/index.ts delete mode 100644 src/app/modules/experiments/experiments.module.ts create mode 100644 src/app/modules/experiments/index.ts delete mode 100644 src/app/modules/experiments/weather/weather.module.ts delete mode 100644 src/app/modules/map/components/navigation/index.ts delete mode 100644 src/app/modules/map/components/popover/index.ts delete mode 100644 src/app/modules/map/components/popover/popover.component.ts delete mode 100644 src/app/modules/map/components/profiles/default/index.ts create mode 100644 src/app/modules/map/index.ts delete mode 100644 src/app/modules/map/map.module.ts create mode 100644 src/app/modules/map/ol/lib/navigation/layer-target-angle.component.ts create mode 100644 src/app/modules/map/popovers/aircraft-popover.component.ts create mode 100644 src/app/modules/map/popovers/alarm-popover.component.ts create mode 100644 src/app/modules/map/popovers/aton-popover.component.ts rename src/app/modules/map/{components/popover => popovers}/compass.component.svg (100%) rename src/app/modules/map/{components/popover => popovers}/compass.component.ts (98%) create mode 100644 src/app/modules/map/popovers/featurelist-popover.component.ts create mode 100644 src/app/modules/map/popovers/index.ts rename src/app/modules/map/{components/popover => popovers}/popover.component.scss (100%) create mode 100644 src/app/modules/map/popovers/popover.component.ts rename src/app/modules/map/{components/popover => popovers}/resource-popover.component.ts (96%) rename src/app/modules/map/{components/profiles/default => popovers}/vessel-popover.component.ts (92%) create mode 100644 src/app/modules/map/vessel-calcs.types.ts create mode 100644 src/app/modules/map/vessel-calcs.worker.ts rename src/app/modules/settings/{ => components}/settings-dialog.css (100%) rename src/app/modules/settings/{ => components}/settings-dialog.html (100%) rename src/app/modules/settings/{ => components}/settings-dialog.ts (67%) rename src/app/modules/settings/{settings.module.ts => index.ts} (80%) create mode 100644 src/app/modules/skresources/components/active-resource-dialog.ts create mode 100644 src/app/modules/skresources/components/ais/aircraft-properties-modal.ts create mode 100644 src/app/modules/skresources/components/ais/ais-properties-modal.ts rename src/app/modules/skresources/{lists => components/ais}/aislist.html (74%) rename src/app/modules/skresources/{lists => components/ais}/aislist.ts (56%) create mode 100644 src/app/modules/skresources/components/ais/aton-properties-modal.ts create mode 100644 src/app/modules/skresources/components/charts/chart-properties-dialog.ts rename src/app/modules/skresources/{lists => components/charts}/chartlist.html (82%) rename src/app/modules/skresources/{lists => components/charts}/chartlist.ts (86%) rename src/app/modules/skresources/{ => components}/notes/note-dialog.html (98%) rename src/app/modules/skresources/{ => components}/notes/note-dialog.ts (100%) rename src/app/modules/skresources/{lists => components/notes}/notelist.html (100%) rename src/app/modules/skresources/{lists => components/notes}/notelist.ts (79%) rename src/app/modules/skresources/{ => components}/notes/notes.css (100%) rename src/app/modules/skresources/{ => components}/notes/relatednotes-dialog.html (92%) rename src/app/modules/skresources/{ => components}/notes/relatednotes-dialog.ts (100%) rename src/app/modules/skresources/{ => components}/notes/safe.pipe.ts (100%) create mode 100644 src/app/modules/skresources/components/resource-dialog.ts rename src/app/modules/skresources/{lists => components}/resourcelist.css (76%) rename src/app/modules/skresources/{sets => components/resourcesets}/resource-upload-dialog.css (100%) rename src/app/modules/skresources/{sets => components/resourcesets}/resource-upload-dialog.html (100%) rename src/app/modules/skresources/{sets => components/resourcesets}/resource-upload-dialog.ts (61%) create mode 100644 src/app/modules/skresources/components/resourcesets/resourceset-feature-properties-modal.ts create mode 100644 src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts rename src/app/{lib/components => modules/skresources/components/routes}/build-route.component.css (98%) rename src/app/{lib/components => modules/skresources/components/routes}/build-route.component.ts (100%) rename src/app/modules/{map/components/navigation => skresources/components/routes}/nextpoint.component.ts (89%) rename src/app/modules/skresources/{lists => components/routes}/routelist.html (100%) rename src/app/modules/skresources/{lists => components/routes}/routelist.ts (83%) rename src/app/{lib => modules/skresources}/components/signalk-details.component.css (100%) rename src/app/{lib => modules/skresources}/components/signalk-details.component.ts (98%) create mode 100644 src/app/modules/skresources/components/tracks/track-list-modal.ts rename src/app/modules/skresources/{lists => components/waypoints}/waypointlist.html (100%) rename src/app/modules/skresources/{lists => components/waypoints}/waypointlist.ts (84%) create mode 100644 src/app/modules/skresources/index.ts delete mode 100644 src/app/modules/skresources/notes/index.ts delete mode 100644 src/app/modules/skresources/resource-dialogs.ts delete mode 100644 src/app/modules/skresources/resources.module.ts rename src/app/modules/skresources/{sets/resource-set.ts => resourceset-class.ts} (100%) rename src/app/modules/skresources/{sets/resource-sets.service.ts => resourceset-service.ts} (98%) delete mode 100644 src/app/modules/skstream/skstream.module.ts create mode 100644 src/app/modules/weather/index.ts rename src/app/modules/{experiments/weather/components => weather}/weather-data.component.ts (89%) rename src/app/modules/{experiments/weather/weather-forecast.ts => weather/weather-forecast-modal.ts} (88%) create mode 100644 src/assets/help/img/ais_shiptypes.png create mode 100644 src/assets/img/ais_highspeed.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 542761f60..ae3d617d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # CHANGELOG: Freeboard +### v2.9.0 + +- **Added**: Ability to filter vessels by `AIS Ship Type` (#163). +- **Added**: Display `performance.beatAngle` vectors on the map. +- **Fixed**: Meteo properties `environment.water.waves` display formatting. +- **Fixed**: Anchor watch not available when plugin is installed and enabled. +- **Fixed**: Laylines not displayed if performance paths do not contain values. +- **Updated**: Don't show internet map service dialog in kiosk mode. (#166) +- **Updated**: Angular framework to v18 + ### v2.8.4 - **Fixed**: shore.basestation popover & properties not displayed. diff --git a/angular.json b/angular.json index f02497b08..234f6ae87 100644 --- a/angular.json +++ b/angular.json @@ -15,12 +15,16 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "public", + "outputPath": { + "base": "public", + "browser": "" + }, "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", + "polyfills": [ + "src/polyfills.ts" + ], "tsConfig": "tsconfig.app.json", "assets": [ "src/favicon.ico", @@ -33,9 +37,7 @@ "src/styles.scss" ], "scripts": [], - "vendorChunk": true, "extractLicenses": false, - "buildOptimizer": false, "sourceMap": true, "optimization": false, "namedChunks": true, @@ -43,7 +45,8 @@ "allowedCommonJsDependencies": [ "geolib", "simplify-ts" - ] + ], + "browser": "src/main.ts" }, "configurations": { "production": { @@ -58,8 +61,6 @@ "sourceMap": false, "namedChunks": false, "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, "budgets": [ { "type": "initial", diff --git a/package.json b/package.json index ac31c5f32..e87a848fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@signalk/freeboard-sk", - "version": "2.8.4", + "version": "2.9.0", "description": "Openlayers chart plotter implementation for Signal K", "keywords": [ "signalk-webapp", @@ -43,19 +43,19 @@ "tslib": "^2.0.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^17.3.0", - "@angular/animations": "^17.3.0", - "@angular/cdk": "^17.3.0", - "@angular/cli": "^17.3.0", - "@angular/common": "^17.3.0", - "@angular/compiler": "^17.3.0", - "@angular/compiler-cli": "^17.3.0", - "@angular/core": "^17.3.0", - "@angular/forms": "^17.3.0", - "@angular/language-service": "^17.3.0", - "@angular/material": "^17.3.0", - "@angular/platform-browser": "^17.3.0", - "@angular/platform-browser-dynamic": "^17.3.0", + "@angular-devkit/build-angular": "^18.0.5", + "@angular/animations": "^18.0.4", + "@angular/cdk": "^18.0.4", + "@angular/cli": "^18.0.5", + "@angular/common": "^18.0.4", + "@angular/compiler": "^18.0.4", + "@angular/compiler-cli": "^18.0.4", + "@angular/core": "^18.0.4", + "@angular/forms": "^18.0.4", + "@angular/language-service": "^18.0.4", + "@angular/material": "^18.0.4", + "@angular/platform-browser": "^18.0.4", + "@angular/platform-browser-dynamic": "^18.0.4", "@kolkov/angular-editor": "^2.1.0", "@types/arcgis-rest-api": "^10.4.5", "@types/express": "^4.17.17", @@ -77,7 +77,7 @@ "karma-coverage-istanbul-reporter": "~3.0.2", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", - "ng-packagr": "^17.3.0", + "ng-packagr": "^18.0.0", "ngeohash": "^0.6.3", "ol": "^9.0.0", "ol-mapbox-style": "^12.2.1", @@ -98,4 +98,4 @@ "xml2js": "^0.6.2", "zone.js": "~0.14.4" } -} +} \ No newline at end of file diff --git a/src/app-theme.scss b/src/app-theme.scss index bcffae9fa..146f9c463 100644 --- a/src/app-theme.scss +++ b/src/app-theme.scss @@ -4,33 +4,33 @@ @include mat.core(); // Define a theme. -$my-primary: mat.define-palette(mat.$indigo-palette, 500,700,200); -$my-accent: mat.define-palette(mat.$amber-palette, 500,700,200); +$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette, 500,700,200); +$my-accent: mat.m2-define-palette(mat.$m2-amber-palette, 500,700,200); // The "warn" palette is optional and defaults to red if not specified. -$my-warn: mat.define-palette(mat.$red-palette, 500,700,200); +$my-warn: mat.m2-define-palette(mat.$m2-red-palette, 500,700,200); -$my-theme: mat.define-light-theme(( +$my-theme: mat.m2-define-light-theme(( color: ( primary: $my-primary, accent: $my-accent, warn: $my-warn, ), - typography: mat.define-typography-config(), + typography: mat.m2-define-typography-config(), density: 0, )); // Define a dark theme. -$my-dark-primary: mat.define-palette(mat.$light-blue-palette, 200,300,400); -$my-dark-accent: mat.define-palette(mat.$amber-palette, A400, A100, A700); -$my-dark-warn: mat.define-palette(mat.$deep-orange-palette, A400); +$my-dark-primary: mat.m2-define-palette(mat.$m2-light-blue-palette, 200,300,400); +$my-dark-accent: mat.m2-define-palette(mat.$m2-amber-palette, A400, A100, A700); +$my-dark-warn: mat.m2-define-palette(mat.$m2-deep-orange-palette, A400); -$my-dark-theme: mat.define-dark-theme(( +$my-dark-theme: mat.m2-define-dark-theme(( color: ( primary: $my-dark-primary, accent: $my-dark-accent, warn: $my-dark-warn, ), - typography: mat.define-typography-config(), + typography: mat.m2-define-typography-config(), density: 0, )); @@ -38,11 +38,11 @@ $my-dark-theme: mat.define-dark-theme(( @include mat.all-component-colors($my-dark-theme); .about-row .item a { - color: mat.get-color-from-palette($my-dark-accent); + color: mat.m2-get-color-from-palette($my-dark-accent); } .welcome a { - color: mat.get-color-from-palette($my-dark-accent); + color: mat.m2-get-color-from-palette($my-dark-accent); } .popover > .arrow:after { @@ -55,12 +55,12 @@ $my-dark-theme: mat.define-dark-theme(( border-top-color: mat.get-theme-color($my-dark-theme, background, background); } - /* Track */ + ::-webkit-scrollbar-track { background: rgba(0,0,0,.5); } - /* Handle on hover */ + ::-webkit-scrollbar-thumb:hover { background: #999; } @@ -82,26 +82,26 @@ $my-dark-theme: mat.define-dark-theme(( // std elements .about-row .item a { - color: mat.get-color-from-palette($my-primary); + color: mat.m2-get-color-from-palette($my-primary); } -/* width */ + ::-webkit-scrollbar { width: 6px; height: 6px; } -/* Track */ + ::-webkit-scrollbar-track { background: #f1f1f1; } -/* Handle */ + ::-webkit-scrollbar-thumb { background: #888; } -/* Handle on hover */ + ::-webkit-scrollbar-thumb:hover { background: #555; } @@ -110,4 +110,3 @@ $my-dark-theme: mat.define-dark-theme(( // Include all theme styles for the components. @include mat.all-component-themes($my-theme); - diff --git a/src/app/app.component.css b/src/app/app.component.css index 4cbaee309..4e00c52d7 100644 --- a/src/app/app.component.css +++ b/src/app/app.component.css @@ -39,6 +39,7 @@ mat-nav-list { padding-left: 0px; z-index: 5000; background-color: rgba(200,200,200,.4); + width: 315px; } .buttonPanel { diff --git a/src/app/app.component.html b/src/app/app.component.html index c8e7c0fa9..e0c9bf779 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -6,14 +6,14 @@ mat-menu-item (click)="sidemenu.close(); displayLeftMenu('routeList', true)" > - directions + directions  Routes } @default { @@ -128,11 +128,11 @@ - directions + directions Draw Route - directions + directions Build Route @@ -143,24 +143,24 @@ skres.showWaypointEditor(null, app.data.vessels.active.position) " > - add_location + add_location Add Waypoint at Vessel } - edit_location + edit_location Drop Waypoint @if(this.app.config.map.zoomLevel >= this.app.config.selections.notesMinZoom) { - local_offer + local_offer Add Note } - tab_unselected + tab_unselected Draw Region @@ -375,6 +375,7 @@

 
@@ -617,8 +620,13 @@
@@ -843,13 +855,14 @@
- close + close {{ draw.modify ? 'FINISH' : 'CANCEL' }}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts index d790bf36b..71d3b2586 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -17,8 +17,16 @@ import { GPXImportDialog, GPXExportDialog } from 'src/app/lib/components/dialogs'; -import { CourseSettingsModal } from 'src/app/lib/components'; + import { + AISPropertiesModal, + AtoNPropertiesModal, + AircraftPropertiesModal, + ActiveResourcePropertiesModal, + TracksModal, + ResourceImportDialog, + ResourceSetModal, + ResourceSetFeatureModal, SettingsDialog, SKStreamFacade, SKSTREAM_MODE, @@ -32,15 +40,8 @@ import { SKAtoN, SKOtherResources, SKRegion, - AISPropertiesModal, - AtoNPropertiesModal, - AircraftPropertiesModal, - ActiveResourcePropertiesModal, - TracksModal, - ResourceSetModal, - ResourceSetFeatureModal, - ResourceImportDialog, - WeatherForecastModal + WeatherForecastModal, + CourseSettingsModal } from 'src/app/modules'; import { SignalKClient } from 'signalk-client-angular'; @@ -562,14 +563,12 @@ export class AppComponent { charts: false, pmTiles: false }; + this.app.data.anchor.hasApi = false; res.plugins.forEach((p: { id: string; version: string }) => { // anchor alarm if (p.id === 'anchoralarm') { this.app.debug('*** found anchoralarm plugin'); this.app.data.anchor.hasApi = true; - } else { - this.app.debug('*** anchoralarm plugin not found!'); - this.app.data.anchor.hasApi = false; } // charts v2 api support if (p.id === 'charts') { diff --git a/src/app/app.info.ts b/src/app/app.info.ts index faf95a62b..9b0deeafb 100644 --- a/src/app/app.info.ts +++ b/src/app/app.info.ts @@ -55,7 +55,7 @@ const FreeboardConfig: FBAppConfig = { anchorRadius: 40, // most recent anchor radius setting plugins: { instruments: '/@signalk/instrumentpanel', - startOnOpen: false, + startOnOpen: true, parameters: null }, units: { @@ -89,6 +89,8 @@ const FreeboardConfig: FBAppConfig = { }, positionFormat: 'XY', aisTargets: null, + aisTargetTypes: [], + aisFilterByShipType: false, aisWindApparent: false, aisWindMinZoom: 15, aisShowTrack: false, @@ -274,7 +276,7 @@ export class AppInfo extends Info { this.name = 'Freeboard-SK'; this.shortName = 'Freeboard'; this.description = `Signal K Chart Plotter.`; - this.version = '2.8.4'; + this.version = '2.9.0'; this.url = 'https://github.com/signalk/freeboard-sk'; this.logo = './assets/img/app_logo.png'; @@ -451,12 +453,14 @@ export class AppInfo extends Info { console.warn('No Internet connection detected!'); const mapsel = this.config.selections.charts; if (mapsel.includes('openstreetmap') || mapsel.includes('openseamap')) { - this.showAlert( - 'Internet Map Service Unavailable: ', - `Unable to display Open Street / Sea Maps!\n - Please check your Internet connection or select maps from the local network.\n - ` - ); + if (!this.data.kioskMode) { + this.showAlert( + 'Internet Map Service Unavailable: ', + `Unable to display Open Street / Sea Maps!\n + Please check your Internet connection or select maps from the local network.\n + ` + ); + } } }); } @@ -683,6 +687,14 @@ export class AppInfo extends Info { settings.selections.aisShowTrack = false; } + if (typeof settings.selections.aisTargetTypes === 'undefined') { + settings.selections.aisTargetTypes = []; + } + + if (typeof settings.selections.aisFilterByShipType === 'undefined') { + settings.selections.aisFilterByShipType = false; + } + if (typeof settings.selections.labelsMinZoom === 'undefined') { settings.selections.labelsMinZoom = 8; } @@ -918,16 +930,15 @@ export class AppInfo extends Info { for more details.` }, 'whats-new': [ - /*{ + { type: 'signalk-server-node', - title: 'OpenWeather 3.0 Support', + title: 'AIS Vessels', message: ` - OpenWeather is deprecating support for v2.5 of their API in April 2024! + Freeboard-SK now supports filtering the disply of vessels by AIS ship type.
 
- Freeboard-SK now supports the v3.0 API which will require you to supply - a new API Key in the configuration. + Select Vessels from the menu and turn on View by Vessel type. ` - }*/ + } ] }; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index dccac6b38..24c08428b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,89 +1,79 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { HttpClientModule } from '@angular/common/http'; +import { + provideHttpClient, + withInterceptorsFromDi +} from '@angular/common/http'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { DragDropModule } from '@angular/cdk/drag-drop'; import { MatBadgeModule } from '@angular/material/badge'; import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialogModule } from '@angular/material/dialog'; import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; import { MatListModule } from '@angular/material/list'; import { MatMenuModule } from '@angular/material/menu'; -import { MatSelectModule } from '@angular/material/select'; import { MatSidenavModule } from '@angular/material/sidenav'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatSnackBarModule } from '@angular/material/snack-bar'; -// *** import { AppComponent } from './app.component'; + import { - AlarmsModule, - SettingsModule, - ExperimentsModule, - SKStreamModule, - SignalKResourcesModule, - FBMapModule + FBMapComponent, + ExperimentsComponent, + AnchorWatchComponent, + AlarmComponent, + AlarmsDialog, + AutopilotComponent, + RouteNextPointComponent, + RouteListComponent, + WaypointListComponent, + ChartListComponent, + NoteListComponent, + AISListComponent, + BuildRouteComponent } from './modules'; -import { CommonDialogs, GPXModule } from './lib/components/dialogs'; + import { TextDialComponent, FileInputComponent, PiPVideoComponent, WakeLockComponent, - AutopilotComponent, - BuildRouteComponent, Measurements } from './lib/components'; @NgModule({ declarations: [AppComponent], + exports: [], + bootstrap: [AppComponent], imports: [ MatMenuModule, - MatToolbarModule, MatSidenavModule, - MatCardModule, - MatSlideToggleModule, MatBadgeModule, - MatSelectModule, - MatInputModule, - MatCheckboxModule, MatButtonModule, MatListModule, MatIconModule, MatTooltipModule, - MatDialogModule, - DragDropModule, - MatSnackBarModule, - GPXModule, BrowserModule, BrowserAnimationsModule, - FormsModule, - HttpClientModule, - CommonDialogs, - ExperimentsModule, - SignalKResourcesModule, - FBMapModule, - GPXModule, - SettingsModule, - AlarmsModule, - SKStreamModule, + FBMapComponent, TextDialComponent, FileInputComponent, PiPVideoComponent, WakeLockComponent, AutopilotComponent, BuildRouteComponent, - Measurements + Measurements, + RouteListComponent, + WaypointListComponent, + ChartListComponent, + NoteListComponent, + AISListComponent, + BuildRouteComponent, + ExperimentsComponent, + AnchorWatchComponent, + AlarmComponent, + AlarmsDialog, + RouteNextPointComponent ], - exports: [], - providers: [], - bootstrap: [AppComponent] + providers: [provideHttpClient(withInterceptorsFromDi())] }) export class AppModule {} diff --git a/src/app/lib/components/dialogs/common/dialogs.component.ts b/src/app/lib/components/dialogs/common/dialogs.component.ts index 48fedd40e..3f7267bfc 100644 --- a/src/app/lib/components/dialogs/common/dialogs.component.ts +++ b/src/app/lib/components/dialogs/common/dialogs.component.ts @@ -275,7 +275,9 @@ export class ConfirmDialog implements OnInit { @if(data.url) {
} diff --git a/src/app/lib/components/dialogs/geojson-dialog.css b/src/app/lib/components/dialogs/geojson/geojson-dialog.css similarity index 100% rename from src/app/lib/components/dialogs/geojson-dialog.css rename to src/app/lib/components/dialogs/geojson/geojson-dialog.css diff --git a/src/app/lib/components/dialogs/geojson-dialog.facade.ts b/src/app/lib/components/dialogs/geojson/geojson-dialog.facade.ts similarity index 100% rename from src/app/lib/components/dialogs/geojson-dialog.facade.ts rename to src/app/lib/components/dialogs/geojson/geojson-dialog.facade.ts diff --git a/src/app/lib/components/dialogs/geojson-dialog.html b/src/app/lib/components/dialogs/geojson/geojson-dialog.html similarity index 100% rename from src/app/lib/components/dialogs/geojson-dialog.html rename to src/app/lib/components/dialogs/geojson/geojson-dialog.html diff --git a/src/app/lib/components/dialogs/geojson-dialog.ts b/src/app/lib/components/dialogs/geojson/geojson-dialog.ts similarity index 100% rename from src/app/lib/components/dialogs/geojson-dialog.ts rename to src/app/lib/components/dialogs/geojson/geojson-dialog.ts diff --git a/src/app/lib/components/dialogs/index.ts b/src/app/lib/components/dialogs/index.ts index 7f102b0db..a111871d6 100644 --- a/src/app/lib/components/dialogs/index.ts +++ b/src/app/lib/components/dialogs/index.ts @@ -1,5 +1,5 @@ export * from './common/dialogs.module'; export * from './playback-dialog'; -export * from './geojson-dialog'; +export * from './geojson/geojson-dialog'; export * from './trail2route-dialog'; export * from './gpx/gpx.module'; diff --git a/src/app/lib/components/index.ts b/src/app/lib/components/index.ts index 87943a79d..7d6b6bc56 100644 --- a/src/app/lib/components/index.ts +++ b/src/app/lib/components/index.ts @@ -1,9 +1,7 @@ -export * from './autopilot.component'; -export * from './course-settings'; export * from './dial-text'; export * from './file-input.component'; export * from './pip.component'; -export * from './signalk-details.component'; +export * from '../../modules/skresources/components/signalk-details.component'; export * from './wakelock.component'; -export * from './build-route.component'; export * from './measurements.component'; +export * from './dialogs'; diff --git a/src/app/lib/components/wakelock.component.ts b/src/app/lib/components/wakelock.component.ts index 09fdcd429..f89aa3224 100644 --- a/src/app/lib/components/wakelock.component.ts +++ b/src/app/lib/components/wakelock.component.ts @@ -27,6 +27,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
+ + + } + + + + `, + styles: [ + ` + ._ap-dest { + font-family: arial; + min-width: 300px; + } + ._ap-dest .key-label { + width: 50px; + font-weight: bold; + } + ._ap-dest .selected { + background-color: silver; + } + .point-drop-placeholder { + background: #ccc; + border: dotted 3px #999; + min-height: 60px; + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); + } + .cdk-drag-preview { + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), + 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); + } + ` + ] +}) +export class ActiveResourcePropertiesModal implements OnInit { + protected points: Array = []; + protected pointMeta: Array<{ name: string; description: string }> = []; + protected legs: { bearing: string; distance: string }[] = []; + protected selIndex = -1; + protected clearButtonText = 'Clear'; + protected showClearButton = false; + + constructor( + public app: AppInfo, + public modalRef: MatBottomSheetRef, + @Inject(MAT_BOTTOM_SHEET_DATA) + public data: { + title: string; + type: string; + resource: SKWaypoint | SKRoute; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + skres: any; + } + ) {} + + ngOnInit() { + if ( + this.data.resource[1].feature && + this.data.resource[1].feature.geometry.coordinates + ) { + if (this.data.type === 'route') { + this.points = [].concat( + this.data.resource[1].feature.geometry.coordinates + ); + this.legs = this.getLegs(); + + this.data.title = this.data.resource[1].name + ? `${this.data.resource[1].name} Points` + : 'Route Points'; + + if (this.data.resource[0] === this.app.data.activeRoute) { + this.selIndex = this.app.data.navData.pointIndex; + this.showClearButton = true; + } + this.pointMeta = this.getPointMeta(); + } + } + } + + getLegs() { + const pos = this.app.data.vessels.self.position; + return GeoUtils.routeLegs(this.points, pos).map((l) => { + return { + bearing: this.app.formatValueForDisplay(l.bearing, 'deg'), + distance: this.app.formatValueForDisplay(l.distance, 'm') + }; + }); + } + + getPointMeta() { + if ( + this.data.resource[1].feature.properties.coordinatesMeta && + Array.isArray(this.data.resource[1].feature.properties.coordinatesMeta) + ) { + const pointsMeta = + this.data.resource[1].feature.properties.coordinatesMeta; + let idx = 0; + return pointsMeta.map((pt) => { + idx++; + if (pt.href) { + const id = pt.href.split('/').slice(-1); + const wpt = this.data.skres.fromCache('waypoints', id[0]); + return wpt + ? { + name: `* ${wpt[1].name}`, + description: `* ${wpt[1].description}` + } + : { + name: '!wpt reference!', + description: '' + }; + } else { + return { + name: pt.name ?? `RtePt-${('000' + String(idx)).slice(-3)}`, + description: pt.description ?? `` + }; + } + }); + } else { + let idx = 0; + return this.points.map(() => { + return { + name: `RtePt-${('000' + String(++idx)).slice(-3)}`, + description: '' + }; + }); + } + } + + selectPoint(idx: number) { + if (this.points.length < 2 || this.selIndex < 0) { + return; + } + this.selIndex = idx; + if (this.data.skres) { + this.data.skres.coursePointIndex(this.selIndex); + } + } + + drop(e: CdkDragDrop<{ previousIndex: number; currentIndex: number }>) { + if (this.data.type === 'route') { + const selPosition = this.points[this.selIndex]; + moveItemInArray(this.points, e.previousIndex, e.currentIndex); + this.legs = this.getLegs(); + if (this.data.resource[1].feature.properties.coordinatesMeta) { + moveItemInArray( + this.data.resource[1].feature.properties.coordinatesMeta, + e.previousIndex, + e.currentIndex + ); + } + this.pointMeta = this.getPointMeta(); + + this.updateFlag(selPosition); + this.data.skres.updateRouteCoords( + this.data.resource[0], + this.points, + this.data.resource[1].feature.properties.coordinatesMeta + ); + } + } + + updateFlag(selPosition: Position) { + if (!selPosition) { + return; + } + let idx = 0; + this.points.forEach((p: Position) => { + if (p[0] === selPosition[0] && p[1] === selPosition[1]) { + this.selIndex = idx; + } + idx++; + }); + } + + close() { + this.modalRef.dismiss(); + } + + // ** deactivate route / clear destination + deactivate() { + this.modalRef.dismiss(true); + } +} diff --git a/src/app/modules/skresources/components/ais/aircraft-properties-modal.ts b/src/app/modules/skresources/components/ais/aircraft-properties-modal.ts new file mode 100644 index 000000000..a28e40a03 --- /dev/null +++ b/src/app/modules/skresources/components/ais/aircraft-properties-modal.ts @@ -0,0 +1,143 @@ +import { Component, Inject } from '@angular/core'; +import { + MatBottomSheetRef, + MAT_BOTTOM_SHEET_DATA +} from '@angular/material/bottom-sheet'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +import { AppInfo } from 'src/app/app.info'; +import { SignalKClient } from 'signalk-client-angular'; +import { SKAircraft } from 'src/app/modules/skresources/resource-classes'; +import { SignalKDetailsComponent } from '../../components/signalk-details.component'; + +/********* AircraftPropertiesModal ********** + data: { + title: "" title text, + target: "" aid to navigation + } +***********************************/ +@Component({ + selector: 'ap-aircraft-modal', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + SignalKDetailsComponent + ], + template: ` +
+ + + airplanemode_active + + + {{ data.title }} + + + + + + + + +
+
+
Name:
+
{{ data.target.name }}
+
+
+
MMSI:
+
{{ data.target.mmsi }}
+
+
+
Call sign:
+
{{ data.target.callsign }}
+
+ + @if(showProperties) { + + } +
+
+
+
+ `, + styles: [ + ` + ._ap-aircraft { + font-family: arial; + min-width: 300px; + } + ._ap-aircraft .key-label { + width: 150px; + font-weight: bold; + } + ` + ] +}) +export class AircraftPropertiesModal { + protected showProperties = true; + protected properties: { [key: string]: string | number | null }; + + constructor( + private sk: SignalKClient, + private app: AppInfo, + public modalRef: MatBottomSheetRef, + @Inject(MAT_BOTTOM_SHEET_DATA) + public data: { + title: string; + target: SKAircraft; + id: string; + icon: string; + } + ) {} + + ngOnInit() { + this.getAircraftInfo(); + } + + // fetch object information + private getAircraftInfo() { + if (!this.data.id) { + return; + } + const path = this.data.id.split('.').join('/'); + + this.sk.api.get(path).subscribe((v) => { + this.properties = this.parseAircraft(v); + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private parseAircraft(data: any) { + const res = {}; + + if (data.navigation && data.navigation.position) { + res['navigation.position'] = data.navigation.position.value; + } + return res; + } + + toggleProperties() { + this.showProperties = !this.showProperties; + } +} diff --git a/src/app/modules/skresources/components/ais/ais-properties-modal.ts b/src/app/modules/skresources/components/ais/ais-properties-modal.ts new file mode 100644 index 000000000..1c44877bc --- /dev/null +++ b/src/app/modules/skresources/components/ais/ais-properties-modal.ts @@ -0,0 +1,261 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { + MatBottomSheetRef, + MAT_BOTTOM_SHEET_DATA +} from '@angular/material/bottom-sheet'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +import { AppInfo } from 'src/app/app.info'; +import { SignalKClient } from 'signalk-client-angular'; +import { SKVessel } from 'src/app/modules/skresources/resource-classes'; +import { Convert } from 'src/app/lib/convert'; + +/********* AISPropertiesModal ********** + data: { + title: "" title text, + target: "" vessel, + id: vessel id + } +***********************************/ +@Component({ + selector: 'ap-ais-modal', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule + ], + template: ` +
+ + + directions_boat + + + {{ data.title }} + + + + + + + + +
+
+
Name:
+
{{ vInfo.name }}
+
+
+
MMSI:
+
{{ vInfo.mmsi }}
+
+ @if(vInfo.shipType) { +
+
Type:
+
{{ vInfo.shipType }}
+
+ } @if(vInfo.flag) { +
+
Flag:
+
{{ vInfo.flag }}
+
+ } @if(vInfo.port) { +
+
Port:
+
{{ vInfo.port }}
+
+ } @if(vInfo.callsign) { +
+
Call sign:
+
{{ vInfo.callsign }}
+
+ } @if(vInfo.length) { +
+
Dimensions:
+
+ {{ vInfo.length }} x {{ vInfo.beam }} +
+
+ } @if(vInfo.draft) { +
+
Draft:
+
{{ vInfo.draft }}
+
+ } @if(vInfo.height) { +
+
Height:
+
{{ vInfo.height }}
+
+ } @if(vInfo.state) { +
+
State:
+
{{ vInfo.state }}
+
+ } @if(vInfo.destination) { +
+
Destination:
+
{{ vInfo.destination }}
+
+ } @if(vInfo.eta) { +
+
ETA:
+
+ {{ vInfo.eta.toLocaleString() }} +
+
+ } +
+
+
+
+ `, + styles: [ + ` + ._ap-ais { + font-family: arial; + } + ._ap-ais .key-label { + width: 150px; + font-weight: bold; + } + ` + ] +}) +export class AISPropertiesModal implements OnInit { + public vInfo = { + name: null, + mmsi: null, + callsign: null, + length: null, + beam: null, + draft: null, + height: null, + shipType: null, + destination: null, + eta: null, + state: null, + flag: null, + port: null + }; + + constructor( + public app: AppInfo, + private sk: SignalKClient, + public modalRef: MatBottomSheetRef, + @Inject(MAT_BOTTOM_SHEET_DATA) + public data: { + title: string; + target: SKVessel; + id: string; + } + ) {} + + ngOnInit() { + this.getVesselInfo(); + } + + formatDegrees(val: number) { + return val + ? `${Convert.radiansToDegrees(val).toFixed(1)} ${String.fromCharCode( + 186 + )}` + : '0.0'; + } + + formatKnots(val: number) { + return val ? `${Convert.msecToKnots(val).toFixed(1)} kn` : '0.0'; + } + + private getVesselInfo() { + let path: string; + if (!this.data.id) { + path = 'vessels/self'; + } else { + path = this.data.id.split('.').join('/'); + } + + this.sk.api.get(path).subscribe((v) => { + if (typeof v['name'] !== 'undefined') { + this.vInfo.name = v['name']; + } + if (typeof v['mmsi'] !== 'undefined') { + this.vInfo.mmsi = v['mmsi']; + } + if (typeof v['flag'] !== 'undefined') { + this.vInfo.flag = v['flag']; + } + if (typeof v['port'] !== 'undefined') { + this.vInfo.port = v['port']; + } + if (typeof v['communication'] !== 'undefined') { + if (typeof v['communication']['callsignVhf'] !== 'undefined') { + this.vInfo.callsign = v['communication']['callsignVhf']; + } + } + if (typeof v['navigation'] !== 'undefined') { + if (typeof v['navigation']['destination'] !== 'undefined') { + if ( + typeof v['navigation']['destination']['commonName'] !== 'undefined' + ) { + this.vInfo.destination = + v['navigation']['destination']['commonName']['value']; + } + if (typeof v['navigation']['destination']['eta'] !== 'undefined') { + this.vInfo.eta = new Date( + v['navigation']['destination']['eta']['value'] + ).toUTCString(); + } + } + if ( + typeof v['navigation']['state'] !== 'undefined' && + typeof v['navigation']['state']['value'] !== 'undefined' + ) { + this.vInfo.state = v['navigation']['state']['value']; + } + } + if (typeof v['design'] !== 'undefined') { + if ( + typeof v['design']['length'] !== 'undefined' && + v['design']['length']['value']['overall'] + ) { + this.vInfo.length = v['design']['length']['value']['overall']; + } + if (typeof v['design']['beam'] !== 'undefined') { + this.vInfo.beam = v['design']['beam']['value']; + } + if ( + typeof v['design']['draft'] !== 'undefined' && + v['design']['draft']['value'] + ) { + if (typeof v['design']['draft']['value']['maximum'] !== 'undefined') { + this.vInfo.draft = `${v['design']['draft']['value']['maximum']} (max)`; + } else if ( + typeof v['design']['draft']['value']['current'] !== 'undefined' + ) { + this.vInfo.draft = `${v['design']['draft']['value']['current']} (current)`; + } + } + if (typeof v['design']['airHeight'] !== 'undefined') { + this.vInfo.height = v['design']['airHeight']['value']; + } + if (typeof v['design']['aisShipType'] !== 'undefined') { + this.vInfo.shipType = v['design']['aisShipType']['value']['name']; + } + } + }); + } +} diff --git a/src/app/modules/skresources/lists/aislist.html b/src/app/modules/skresources/components/ais/aislist.html similarity index 74% rename from src/app/modules/skresources/lists/aislist.html rename to src/app/modules/skresources/components/ais/aislist.html index b7aea59ac..7bacd2632 100644 --- a/src/app/modules/skresources/lists/aislist.html +++ b/src/app/modules/skresources/components/ais/aislist.html @@ -24,6 +24,7 @@ #ftext type="text" matInput + [disabled]="app.config.selections.aisFilterByShipType" [value]="filterText" (keyup)="filterKeyUp(ftext.value)" /> @@ -40,6 +41,7 @@ (click)="initItems()" matTooltip="Reload Vessels" matTooltipPosition="left" + [disabled]="app.config.selections.aisFilterByShipType" > refresh @@ -49,6 +51,7 @@ #selall color="primary" [checked]="allSel" + [disabled]="app.config.selections.aisFilterByShipType" [indeterminate]="someSel" (change)="selectAll($event.checked)" [matTooltip]="(!selall.checked || someSel) ? 'Select All' : 'Deselect All'" @@ -58,9 +61,50 @@ + + + Filter by Vessel type + + -
+
+ @if (app.config.selections.aisFilterByShipType) { + + + @for (i of shipTypes; track i.id) { +
+
+ +
+
+ + {{i.description}} + + @if(i.id !==0){ ({{i.id}}-{{i.id + 9}}) } + + +
+
+ + +
+
+ } +
+
+ + } @else { + }
diff --git a/src/app/modules/skresources/lists/aislist.ts b/src/app/modules/skresources/components/ais/aislist.ts similarity index 56% rename from src/app/modules/skresources/lists/aislist.ts rename to src/app/modules/skresources/components/ais/aislist.ts index 3bbe2e4b3..bf998a796 100644 --- a/src/app/modules/skresources/lists/aislist.ts +++ b/src/app/modules/skresources/components/ais/aislist.ts @@ -5,6 +5,17 @@ import { EventEmitter, ChangeDetectionStrategy } from '@angular/core'; + +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { FormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { ScrollingModule } from '@angular/cdk/scrolling'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; + import { AppInfo } from 'src/app/app.info'; import { SKVessel } from 'src/app/modules'; import { Position } from 'src/app/types'; @@ -12,9 +23,21 @@ import { Position } from 'src/app/types'; //** AIS Dialog ** @Component({ selector: 'ais-list', + standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './aislist.html', - styleUrls: ['./resourcelist.css'] + styleUrls: ['../resourcelist.css'], + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatCheckboxModule, + MatButtonModule, + FormsModule, + MatInputModule, + ScrollingModule, + MatSlideToggleModule + ] }) export class AISListComponent { @Input() aisTargets: Map; @@ -31,11 +54,69 @@ export class AISListComponent { filterText = ''; someSel = false; allSel = false; + shipTypes = [ + { + id: 10, + description: 'Unspecified', + selected: false, + icon: './assets/img/ais_active.png' + }, + { + id: 20, + description: 'Wing in Ground', + selected: false, + icon: './assets/img/ais_active.png' + }, + { + id: 30, + description: 'Pleasure', + selected: false, + icon: './assets/img/ais_active.png' + }, + { + id: 40, + description: 'High Speed', + selected: false, + icon: './assets/img/ais_highspeed.png' + }, + { + id: 50, + description: 'Special', + selected: false, + icon: './assets/img/ais_special.png' + }, + { + id: 60, + description: 'Passenger', + selected: false, + icon: './assets/img/ais_passenger.png' + }, + { + id: 70, + description: 'Cargo', + selected: false, + icon: './assets/img/ais_cargo.png' + }, + { + id: 80, + description: 'Tanker', + selected: false, + icon: './assets/img/ais_tanker.png' + }, + { + id: 90, + description: 'Other', + selected: false, + icon: './assets/img/ais_special.png' + } + ]; + otherShiptypes = [10, 20, 30, 90]; constructor(public app: AppInfo) {} ngOnInit() { this.initItems(); + this.alignSelections(); } close() { @@ -72,6 +153,34 @@ export class AISListComponent { this.filterList = this.aisAvailable.slice(0); } + alignSelections() { + this.shipTypes.forEach((i) => { + i.selected = this.app.config.selections.aisTargetTypes.includes(i.id); + }); + } + + toggleFilterType(e: boolean) { + this.app.config.selections.aisFilterByShipType = e; + if (e) { + this.alignSelections(); + } + this.app.saveConfig(); + } + + shipTypeSelect(e: boolean, id: number) { + const t = [].concat(this.app.config.selections.aisTargetTypes); + if (e) { + t.push(id); + } else { + if (t.includes(id)) { + t.splice(t.indexOf(id), 1); + } + } + this.app.config.selections.aisTargetTypes = [].concat(t); + this.alignSelections(); + this.app.saveConfig(); + } + checkSelections() { let c = false; let u = false; diff --git a/src/app/modules/skresources/components/ais/aton-properties-modal.ts b/src/app/modules/skresources/components/ais/aton-properties-modal.ts new file mode 100644 index 000000000..b87792982 --- /dev/null +++ b/src/app/modules/skresources/components/ais/aton-properties-modal.ts @@ -0,0 +1,195 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { + MatBottomSheetRef, + MAT_BOTTOM_SHEET_DATA +} from '@angular/material/bottom-sheet'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; + +import { AppInfo } from 'src/app/app.info'; +import { SignalKClient } from 'signalk-client-angular'; +import { SKAtoN } from 'src/app/modules/skresources/resource-classes'; +import { SignalKDetailsComponent } from '../../components/signalk-details.component'; + +/********* AtoNPropertiesModal ********** + data: { + title: "" title text, + target: "" aid to navigation + icon: + } +***********************************/ +@Component({ + selector: 'ap-aton-modal', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + SignalKDetailsComponent + ], + template: ` +
+ + + {{ data.icon }} + + + {{ data.title }} + + + + + + + + +
+
+
Name:
+
{{ data.target.name }}
+
+
+
MMSI:
+
{{ data.target.mmsi }}
+
+
+
Type:
+
{{ data.target.type.name }}
+
+ + @if(showProperties) { + + } +
+
+
+
+ `, + styles: [ + ` + ._ap-aton { + font-family: arial; + min-width: 300px; + } + ._ap-aton .key-label { + width: 150px; + font-weight: bold; + } + ` + ] +}) +export class AtoNPropertiesModal implements OnInit { + protected showProperties = true; + protected properties: { [key: string]: string | number | null }; + + constructor( + private sk: SignalKClient, + private app: AppInfo, + public modalRef: MatBottomSheetRef, + @Inject(MAT_BOTTOM_SHEET_DATA) + public data: { + title: string; + target: SKAtoN; + id: string; + icon: string; + type: 'aton' | 'sar' | 'meteo'; + } + ) {} + + ngOnInit() { + this.getAtoNInfo(); + } + + toggleProperties() { + this.showProperties = !this.showProperties; + } + + // fetch object information + private getAtoNInfo() { + if (!this.data.id) { + return; + } + const path = this.data.id.split('.').join('/'); + + this.sk.api.get(path).subscribe((v) => { + if (this.data.type === 'meteo') { + this.properties = this.parseMeteo(v); + } else { + this.properties = this.parseAtoN(v); + } + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private parseMeteo(data: any) { + const res = {}; + + if (data.navigation && data.navigation.position) { + res['navigation.position'] = data.navigation.position.value; + } + const bk = data.environment.observation ?? data.environment; + const pk = data.environment.observation + ? 'environment.observation' + : 'environment'; + if (bk) { + this.processPathObject(res, bk, pk); + } + return res; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private processPathObject(res: any, bk: any, pk: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Object.keys(bk).forEach((k: any) => { + const pathRoot = `${pk}.${k}`; + const g = bk[k]; + if (k === 'water') { + this.processPathObject(res, g, pathRoot); + } else if (g.meta) { + res[pathRoot] = this.app.formatValueForDisplay( + g.value, + g.meta.units ? g.meta.units : '', + k.toLowerCase().includes('level') || + k.toLowerCase().includes('height') // depth values + ); + } else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Object.entries(g).forEach((i: any) => { + const key = `${pathRoot}.${i[0]}`; + res[key] = this.app.formatValueForDisplay( + i[1].value, + i[1].meta && i[1].meta.units ? i[1].meta.units : '', + i[0].toLowerCase().includes('level') || + i[0].toLowerCase().includes('height') // depth values + ); + }); + } + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private parseAtoN(data: any) { + const res = {}; + if (data.navigation && data.navigation.position) { + res['navigation.position'] = data.navigation.position.value; + } + return Object.assign(res, this.data.target.properties); + } +} diff --git a/src/app/modules/skresources/components/charts/chart-properties-dialog.ts b/src/app/modules/skresources/components/charts/chart-properties-dialog.ts new file mode 100644 index 000000000..85b7980a0 --- /dev/null +++ b/src/app/modules/skresources/components/charts/chart-properties-dialog.ts @@ -0,0 +1,169 @@ +import { Component, Inject } from '@angular/core'; +import { + MatDialogModule, + MatDialogRef, + MAT_DIALOG_DATA +} from '@angular/material/dialog'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { AppInfo } from 'src/app/app.info'; +import { SKChart } from 'src/app/modules/skresources/resource-classes'; +import { PipesModule } from 'src/app/lib/pipes'; + +/********* ChartPropertiesDialog ********** + data: +***********************************/ +@Component({ + selector: 'ap-chartproperties', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + MatDialogModule, + PipesModule + ], + template: ` +
+ + {{ isLocal(data['url']) }} + Chart Properties + + + + + +
+
+
Name:
+
{{ data['name'] }}
+
+
+
Description:
+
{{ data['description'] }}
+
+
+
Scale:
+
{{ data['scale'] }}
+
+
+
Zoom:
+
+
+ Min: + {{ data['minZoom'] }}, + Max: + {{ data['maxZoom'] }} +
+
+
+ @if(data['bounds']) { +
+
Bounds:
+
+
+ +
+ + +
+
+ +
+ + +
+
+
+ } +
+
Format:
+
{{ data['format'] }}
+
+
+
Type:
+
+ {{ data['type'] }} +
+
+
+
Layers:
+
{{ data['layers'] }}
+
+
+
URL:
+
+ {{ data['url'] }} +
+
+
+
+
+ `, + styles: [ + ` + ._ap-chartinfo { + font-family: arial; + min-width: 300px; + } + .ap-confirm-icon { + min-width: 35px; + max-width: 35px; + color: darkorange; + text-align: left; + } + + ._ap-chartinfo .key-label { + width: 150px; + font-weight: 500; + } + + @media only screen and (min-device-width: 768px) and (max-device-width: 1024px), + only screen and (min-width: 800px) { + .ap-confirm-icon { + min-width: 25%; + max-width: 25%; + } + } + ` + ] +}) +export class ChartPropertiesDialog { + public icon: string; + + constructor( + public app: AppInfo, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: SKChart + ) {} + + isLocal(url: string) { + return url && url.indexOf('signalk') !== -1 ? 'map' : 'language'; + } +} diff --git a/src/app/modules/skresources/lists/chartlist.html b/src/app/modules/skresources/components/charts/chartlist.html similarity index 82% rename from src/app/modules/skresources/lists/chartlist.html rename to src/app/modules/skresources/components/charts/chartlist.html index 819664352..ec6ce8794 100644 --- a/src/app/modules/skresources/lists/chartlist.html +++ b/src/app/modules/skresources/components/charts/chartlist.html @@ -3,18 +3,6 @@
Charts:
-
- -
-
- -
} + + @if(!displayChartLayers) { + + + } + -
- @if(!displayChartLayers) { + @if(!displayChartLayers) { +
- } @else { +
+ } @else { +
- }
+ }
diff --git a/src/app/modules/skresources/lists/chartlist.ts b/src/app/modules/skresources/components/charts/chartlist.ts similarity index 86% rename from src/app/modules/skresources/lists/chartlist.ts rename to src/app/modules/skresources/components/charts/chartlist.ts index d164ca56f..c174277a5 100644 --- a/src/app/modules/skresources/lists/chartlist.ts +++ b/src/app/modules/skresources/components/charts/chartlist.ts @@ -6,138 +6,32 @@ import { EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { FormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { ScrollingModule } from '@angular/cdk/scrolling'; + import { MatDialog } from '@angular/material/dialog'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; +import { + DragDropModule, + CdkDragDrop, + moveItemInArray +} from '@angular/cdk/drag-drop'; + import { AppInfo } from 'src/app/app.info'; -import { ChartInfoDialog } from '../resource-dialogs'; +import { ChartPropertiesDialog } from './chart-properties-dialog'; import { FBCharts, FBChart, FBResourceSelect } from 'src/app/types'; -@Component({ - selector: 'chart-list', - changeDetection: ChangeDetectionStrategy.OnPush, - templateUrl: './chartlist.html', - styleUrls: ['./resourcelist.css'] -}) -export class ChartListComponent { - @Input() charts: FBCharts; - @Output() select: EventEmitter = new EventEmitter(); - @Output() refresh: EventEmitter = new EventEmitter(); - @Output() closed: EventEmitter = new EventEmitter(); - @Output() orderChange: EventEmitter = new EventEmitter(); - - filterList = []; - filterText = ''; - someSel = false; - allSel = false; - - displayChartLayers = false; - - constructor(protected app: AppInfo, private dialog: MatDialog) {} - - ngOnInit() { - this.initItems(); - } - - ngOnChanges() { - this.initItems(); - } - - close() { - this.app.data.chartBounds = false; - this.closed.emit(); - } - - initItems() { - this.checkSelections(); - this.buildFilterList(); - } - - buildFilterList(e?: string) { - if (typeof e !== 'undefined' || this.filterText) { - if (typeof e !== 'undefined') { - this.filterText = e; - } - this.filterList = this.charts.filter((i: FBChart) => { - if ( - i[1].name.toLowerCase().indexOf(this.filterText.toLowerCase()) !== -1 - ) { - return i; - } - }); - } else { - this.filterList = this.charts.slice(0); - } - - this.checkSelections(); - - this.filterList.sort((a, b) => { - const x = a[1].name.toUpperCase(); - const y = b[1].name.toUpperCase(); - return x <= y ? -1 : 1; - }); - } - - isLocal(url: string) { - return url && url.indexOf('signalk') !== -1 ? 'map' : 'language'; - } - - toggleChartBoundaries() { - this.app.data.chartBounds = !this.app.data.chartBounds; - } - - checkSelections() { - let c = false; - let u = false; - this.filterList.forEach((i: FBChart) => { - c = i[2] ? true : c; - u = !i[2] ? true : u; - }); - this.allSel = c && !u ? true : false; - this.someSel = c && u ? true : false; - } - - selectAll(value: boolean) { - this.filterList.forEach((i: FBChart) => { - i[2] = value; - }); - this.buildFilterList(); - this.someSel = false; - this.allSel = value ? true : false; - this.select.emit({ id: 'all', value: value }); - } - - itemSelect(e: boolean, id: string) { - this.filterList.forEach((i: FBChart) => { - if (i[0] === id) { - i[2] = e; - } - }); - this.checkSelections(); - this.buildFilterList(); - this.select.emit({ id: id, value: e }); - } - - itemRefresh() { - this.refresh.emit(); - } - - itemProperties(id: string) { - const ch = this.charts.filter((c: FBChart) => c[0] === id)[0][1]; - this.dialog.open(ChartInfoDialog, { data: ch }); - } - - showChartLayers(show = false) { - this.displayChartLayers = show; - } - - handleOrderChange(e: string[]) { - this.orderChange.emit(e); - } -} - /********* ChartLayersList***********/ @Component({ selector: 'ap-chartlayers', + standalone: true, + imports: [MatTooltipModule, MatIconModule, MatCardModule, DragDropModule], template: `
@@ -270,3 +164,139 @@ export class ChartLayers implements OnInit { return url && url.indexOf('signalk') !== -1 ? 'map' : 'language'; } } + +@Component({ + selector: 'chart-list', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './chartlist.html', + styleUrls: ['../resourcelist.css'], + imports: [ + CommonModule, + MatTooltipModule, + MatIconModule, + MatCardModule, + MatCheckboxModule, + MatButtonModule, + FormsModule, + MatInputModule, + ScrollingModule, + ChartLayers + ] +}) +export class ChartListComponent { + @Input() charts: FBCharts; + @Output() select: EventEmitter = new EventEmitter(); + @Output() refresh: EventEmitter = new EventEmitter(); + @Output() closed: EventEmitter = new EventEmitter(); + @Output() orderChange: EventEmitter = new EventEmitter(); + + filterList = []; + filterText = ''; + someSel = false; + allSel = false; + + displayChartLayers = false; + + constructor(protected app: AppInfo, private dialog: MatDialog) {} + + ngOnInit() { + this.initItems(); + } + + ngOnChanges() { + this.initItems(); + } + + close() { + this.app.data.chartBounds = false; + this.closed.emit(); + } + + initItems() { + this.checkSelections(); + this.buildFilterList(); + } + + buildFilterList(e?: string) { + if (typeof e !== 'undefined' || this.filterText) { + if (typeof e !== 'undefined') { + this.filterText = e; + } + this.filterList = this.charts.filter((i: FBChart) => { + if ( + i[1].name.toLowerCase().indexOf(this.filterText.toLowerCase()) !== -1 + ) { + return i; + } + }); + } else { + this.filterList = this.charts.slice(0); + } + + this.checkSelections(); + + this.filterList.sort((a, b) => { + const x = a[1].name.toUpperCase(); + const y = b[1].name.toUpperCase(); + return x <= y ? -1 : 1; + }); + } + + isLocal(url: string) { + return url && url.indexOf('signalk') !== -1 ? 'map' : 'language'; + } + + toggleChartBoundaries() { + this.app.data.chartBounds = !this.app.data.chartBounds; + } + + checkSelections() { + let c = false; + let u = false; + this.filterList.forEach((i: FBChart) => { + c = i[2] ? true : c; + u = !i[2] ? true : u; + }); + this.allSel = c && !u ? true : false; + this.someSel = c && u ? true : false; + } + + selectAll(value: boolean) { + this.filterList.forEach((i: FBChart) => { + i[2] = value; + }); + this.buildFilterList(); + this.someSel = false; + this.allSel = value ? true : false; + this.select.emit({ id: 'all', value: value }); + } + + itemSelect(e: boolean, id: string) { + this.filterList.forEach((i: FBChart) => { + if (i[0] === id) { + i[2] = e; + } + }); + this.checkSelections(); + this.buildFilterList(); + this.select.emit({ id: id, value: e }); + } + + itemRefresh() { + this.refresh.emit(); + } + + itemProperties(id: string) { + const ch = this.charts.filter((c: FBChart) => c[0] === id)[0][1]; + this.dialog.open(ChartPropertiesDialog, { data: ch }); + } + + showChartLayers(show = false) { + this.displayChartLayers = show; + } + + handleOrderChange(e: string[]) { + this.orderChange.emit(e); + } +} diff --git a/src/app/modules/skresources/notes/note-dialog.html b/src/app/modules/skresources/components/notes/note-dialog.html similarity index 98% rename from src/app/modules/skresources/notes/note-dialog.html rename to src/app/modules/skresources/components/notes/note-dialog.html index 0d2762cd2..bdbcb873f 100644 --- a/src/app/modules/skresources/notes/note-dialog.html +++ b/src/app/modules/skresources/components/notes/note-dialog.html @@ -1,9 +1,4 @@ @if(!data.editable) { -
+
local_offer Note Details @@ -141,7 +136,7 @@
} @else { -
+
{{(!data.title) ? (data.addMode) ? 'New Note' : 'Edit Note' : diff --git a/src/app/modules/skresources/notes/note-dialog.ts b/src/app/modules/skresources/components/notes/note-dialog.ts similarity index 100% rename from src/app/modules/skresources/notes/note-dialog.ts rename to src/app/modules/skresources/components/notes/note-dialog.ts diff --git a/src/app/modules/skresources/lists/notelist.html b/src/app/modules/skresources/components/notes/notelist.html similarity index 100% rename from src/app/modules/skresources/lists/notelist.html rename to src/app/modules/skresources/components/notes/notelist.html diff --git a/src/app/modules/skresources/lists/notelist.ts b/src/app/modules/skresources/components/notes/notelist.ts similarity index 79% rename from src/app/modules/skresources/lists/notelist.ts rename to src/app/modules/skresources/components/notes/notelist.ts index 6ea5665f5..406ef5610 100644 --- a/src/app/modules/skresources/lists/notelist.ts +++ b/src/app/modules/skresources/components/notes/notelist.ts @@ -5,15 +5,36 @@ import { EventEmitter, ChangeDetectionStrategy } from '@angular/core'; + +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { FormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { ScrollingModule } from '@angular/cdk/scrolling'; + import { AppInfo } from 'src/app/app.info'; import { Position } from 'src/app/types'; import { FBNotes, FBNote, FBResourceSelect, SKPosition } from 'src/app/types'; @Component({ selector: 'note-list', + standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './notelist.html', - styleUrls: ['./resourcelist.css'] + styleUrls: ['../resourcelist.css'], + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatCheckboxModule, + MatButtonModule, + FormsModule, + MatInputModule, + ScrollingModule + ] }) export class NoteListComponent { @Input() notes: FBNotes; diff --git a/src/app/modules/skresources/notes/notes.css b/src/app/modules/skresources/components/notes/notes.css similarity index 100% rename from src/app/modules/skresources/notes/notes.css rename to src/app/modules/skresources/components/notes/notes.css diff --git a/src/app/modules/skresources/notes/relatednotes-dialog.html b/src/app/modules/skresources/components/notes/relatednotes-dialog.html similarity index 92% rename from src/app/modules/skresources/notes/relatednotes-dialog.html rename to src/app/modules/skresources/components/notes/relatednotes-dialog.html index d97ca9423..6c2803a8a 100644 --- a/src/app/modules/skresources/notes/relatednotes-dialog.html +++ b/src/app/modules/skresources/components/notes/relatednotes-dialog.html @@ -1,9 +1,4 @@ -
+
- 360 + local_offer {{relatedBy}} Notes + +
+ +
+ `, + styles: [ + ` + ._ap-resource { + font-family: arial; + min-width: 300px; + } + .ap-confirm-icon { + min-width: 35px; + max-width: 35px; + color: darkorange; + text-align: left; + } + + @media only screen and (min-device-width: 768px) and (max-device-width: 1024px), + only screen and (min-width: 800px) { + .ap-confirm-icon { + min-width: 25%; + max-width: 25%; + } + } + ` + ] +}) +export class ResourceDialog implements OnInit { + public icon: string; + + public resourceTypeList = [ + { type: '', name: 'Waypoint', icon: './assets/img/marker-yellow.png' }, + { + type: 'pseudoAtoN', + name: 'Pseudo AtoN', + icon: './assets/img/marker-red.png' + } + ]; + + constructor( + public app: AppInfo, + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) + public data: { + title: string; + name: string; + comment: string; + type?: string; + } + ) {} + + //** lifecycle: events ** + ngOnInit() { + this.data['name'] = this.data['name'] || ''; + this.data['comment'] = this.data['comment'] || ''; + this.data['title'] = this.data['title'] || ''; + this.data['position'] = this.data['position'] || [null, null]; + this.data['addMode'] = this.data['addMode'] || false; + this.data['type'] = this.data['type'] || 'waypoint'; + this.data['skType'] = this.data['skType'] || ''; + + this.icon = + this.data['type'] === 'route' + ? 'directions' + : this.data['type'] === 'region' + ? '360' + : this.data['type'] === 'note' + ? 'local_offer' + : this.data['addMode'] + ? 'add_location' + : 'edit_location'; + } +} diff --git a/src/app/modules/skresources/lists/resourcelist.css b/src/app/modules/skresources/components/resourcelist.css similarity index 76% rename from src/app/modules/skresources/lists/resourcelist.css rename to src/app/modules/skresources/components/resourcelist.css index ffdf33064..33db68145 100644 --- a/src/app/modules/skresources/lists/resourcelist.css +++ b/src/app/modules/skresources/components/resourcelist.css @@ -21,12 +21,18 @@ left: 0; right: 0; bottom: 0; - top: 108px; + top: 130px; +} + +.resourcelist .resources.vessels, +.resourcelist .resources.charts + { + top: 175px; } .resourcelist .vscroller { position: absolute; - top: 10px; + top: 0px; bottom: 0; left: 0; right: 0; diff --git a/src/app/modules/skresources/sets/resource-upload-dialog.css b/src/app/modules/skresources/components/resourcesets/resource-upload-dialog.css similarity index 100% rename from src/app/modules/skresources/sets/resource-upload-dialog.css rename to src/app/modules/skresources/components/resourcesets/resource-upload-dialog.css diff --git a/src/app/modules/skresources/sets/resource-upload-dialog.html b/src/app/modules/skresources/components/resourcesets/resource-upload-dialog.html similarity index 100% rename from src/app/modules/skresources/sets/resource-upload-dialog.html rename to src/app/modules/skresources/components/resourcesets/resource-upload-dialog.html diff --git a/src/app/modules/skresources/sets/resource-upload-dialog.ts b/src/app/modules/skresources/components/resourcesets/resource-upload-dialog.ts similarity index 61% rename from src/app/modules/skresources/sets/resource-upload-dialog.ts rename to src/app/modules/skresources/components/resourcesets/resource-upload-dialog.ts index 842a631a8..fda1c886a 100644 --- a/src/app/modules/skresources/sets/resource-upload-dialog.ts +++ b/src/app/modules/skresources/components/resourcesets/resource-upload-dialog.ts @@ -1,14 +1,45 @@ import { Component, OnInit, Inject } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { SignalKClient } from 'signalk-client-angular'; +import { FormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { + MatDialogModule, + MatDialogRef, + MAT_DIALOG_DATA +} from '@angular/material/dialog'; + +import { PipesModule } from 'src/app/lib/pipes'; +import { SignalKClient } from 'signalk-client-angular'; +import { FileInputComponent } from 'src/app/lib/components'; import { AppInfo } from 'src/app/app.info'; //** Resources upload dialog ** @Component({ selector: 'resource-upload-dialog', templateUrl: './resource-upload-dialog.html', - styleUrls: ['./resource-upload-dialog.css'] + styleUrls: ['./resource-upload-dialog.css'], + standalone: true, + imports: [ + FormsModule, + MatInputModule, + MatSelectModule, + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + MatCheckboxModule, + MatDialogModule, + PipesModule, + FileInputComponent + ] }) export class ResourceImportDialog implements OnInit { public resPaths: Array = []; @@ -61,7 +92,8 @@ export class ResourceImportDialog implements OnInit { this.dialogRef.close({ path: this.targetPath, data: this.source.data }); } - parseFile(e) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parseFile(e: any) { this.source = e; this.source.type = 'Unknown'; try { diff --git a/src/app/modules/skresources/components/resourcesets/resourceset-feature-properties-modal.ts b/src/app/modules/skresources/components/resourcesets/resourceset-feature-properties-modal.ts new file mode 100644 index 000000000..05ac1c36a --- /dev/null +++ b/src/app/modules/skresources/components/resourcesets/resourceset-feature-properties-modal.ts @@ -0,0 +1,119 @@ +/** Resource Dialog Components ** + ********************************/ + +import { Component, OnInit, Inject, ChangeDetectorRef } from '@angular/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { + MatBottomSheetRef, + MAT_BOTTOM_SHEET_DATA +} from '@angular/material/bottom-sheet'; +import { AppInfo } from 'src/app/app.info'; +import { SKResourceSet } from '../../resourceset-class'; +import { SignalKDetailsComponent } from '../../components/signalk-details.component'; + +/********* ResourceSetFeatureModal ********** + * Displays information about a ResourceSet feature + data: { + id: string + skres: SKResourceSet; + } +***********************************/ +@Component({ + selector: 'ap-resourceset-feature-modal', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + MatCheckboxModule, + SignalKDetailsComponent + ], + template: ` +
+ + + + {{ title }} + + + + + + + + + + + +
+ `, + styles: [ + ` + ._ap-resource-set-feature { + font-family: arial; + min-width: 300px; + } + ._ap-resource-set-feature .key-label { + font-weight: 500; + } + ._ap-resource-set-feature .key-desc { + font-style: italic; + } + ` + ] +}) +export class ResourceSetFeatureModal implements OnInit { + protected properties = {}; + protected title = 'ResourceSet Feature: '; + + constructor( + public app: AppInfo, + private cdr: ChangeDetectorRef, + public modalRef: MatBottomSheetRef, + @Inject(MAT_BOTTOM_SHEET_DATA) + public data: { + id: string; + skres: SKResourceSet; + } + ) {} + + ngOnInit() { + this.parse(); + } + + closeModal() { + this.modalRef.dismiss(); + } + + parse() { + const t = this.data.id.split('.'); + const fIndex = Number(t[t.length - 1]); + const features = this.data.skres.values.features; + const feature = fIndex < features.length ? features[fIndex] : features[0]; + + this.title = feature.properties.name ?? 'Feature'; + this.properties = { + name: feature.properties.name ?? '', + description: feature.properties.description ?? '', + 'position.latitude': feature.geometry.coordinates[1], + 'position.longitude': feature.geometry.coordinates[0], + 'resourceset.name': this.data.skres.name, + 'resourceset.description': this.data.skres.description, + 'resourceset.collection': t[1] + }; + } +} diff --git a/src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts b/src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts new file mode 100644 index 000000000..697e67e33 --- /dev/null +++ b/src/app/modules/skresources/components/resourcesets/resourceset-list-modal.ts @@ -0,0 +1,223 @@ +/** Resource Dialog Components ** + ********************************/ + +import { Component, OnInit, Inject, ChangeDetectorRef } from '@angular/core'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatButtonModule } from '@angular/material/button'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { + MatBottomSheetRef, + MAT_BOTTOM_SHEET_DATA +} from '@angular/material/bottom-sheet'; +import { AppInfo } from 'src/app/app.info'; +import { SignalKClient } from 'signalk-client-angular'; +import { SKResourceSet } from '../../resourceset-class'; + +/********* ResourceSetModal ********** + * Fetches ResouorceSets from server for selection + data: { + path: "" resource path + skres: SKResource + } +***********************************/ +@Component({ + selector: 'ap-resourceset-modal', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + MatCheckboxModule + ], + template: ` +
+ + + + + + + + {{ title }} + + + + + + @for(res of resList; track res; let idx = $index) { + + +
+
+ +
+
+
+ {{ res.name }} +
+
+ {{ res.description }} +
+
+
+
+
+
+ } +
+ `, + styles: [ + ` + ._ap-resource-set { + font-family: arial; + min-width: 300px; + } + ._ap-resource-set .key-label { + font-weight: 500; + } + ._ap-resource-set .key-desc { + font-style: italic; + } + ` + ] +}) +export class ResourceSetModal implements OnInit { + public resList: Array; + public selRes = []; + public title = 'Resources: '; + public isResourceSet = false; + + constructor( + public app: AppInfo, + private cdr: ChangeDetectorRef, + private sk: SignalKClient, + public modalRef: MatBottomSheetRef, + @Inject(MAT_BOTTOM_SHEET_DATA) + public data: { + path: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + skres: any; + } + ) {} + + ngOnInit() { + if (this.data.path !== 'undefined') { + this.title += this.data.path; + } + this.getItems(); + } + + closeModal() { + this.modalRef.dismiss(); + } + + getItems() { + this.sk.api + .get(this.app.skApiVersion, `resources/${this.data.path}`) + .subscribe( + (resSet) => { + this.resList = this.data.skres.processItems(resSet); + this.selRes = []; + if ( + this.resList.length !== 0 && + this.resList[0].type === 'ResourceSet' + ) { + this.isResourceSet = true; + for (let i = 0; i < this.resList.length; i++) { + this.selRes.push( + this.app.config.selections.resourceSets[ + this.data.path + ].includes(this.resList[i].id) + ? true + : false + ); + } + } + this.cdr.detectChanges(); + }, + () => { + this.resList = []; + this.selRes = []; + this.cdr.detectChanges(); + } + ); + } + + handleCheck(checked: boolean, id: string, idx: number) { + if (!this.isResourceSet) { + return; + } + this.selRes[idx] = checked; + if (checked) { + this.app.config.selections.resourceSets[this.data.path].push(id); + } else { + const i = + this.app.config.selections.resourceSets[this.data.path].indexOf(id); + if (i !== -1) { + this.app.config.selections.resourceSets[this.data.path].splice(i, 1); + } + } + this.app.saveConfig(); + this.updateItems(); + this.data.skres.resourceSelected(); + } + + clearSelections() { + if (!this.isResourceSet) { + return; + } + this.selRes = []; + for (let i = 0; i < this.resList.length; i++) { + this.selRes[i] = false; + } + this.app.config.selections.resourceSets[this.data.path] = []; + this.app.saveConfig(); + this.updateItems(); + this.data.skres.resourceSelected(); + } + + updateItems() { + this.app.data.resourceSets[this.data.path] = this.resList.filter((t) => { + return this.app.config.selections.resourceSets[this.data.path].includes( + t.id + ) + ? true + : false; + }); + } +} diff --git a/src/app/lib/components/build-route.component.css b/src/app/modules/skresources/components/routes/build-route.component.css similarity index 98% rename from src/app/lib/components/build-route.component.css rename to src/app/modules/skresources/components/routes/build-route.component.css index d4874652d..056194fdb 100644 --- a/src/app/lib/components/build-route.component.css +++ b/src/app/modules/skresources/components/routes/build-route.component.css @@ -89,9 +89,6 @@ transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } - .wpt-box:last-child { - } - .wpt-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) { transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); opacity: .5; diff --git a/src/app/lib/components/build-route.component.ts b/src/app/modules/skresources/components/routes/build-route.component.ts similarity index 100% rename from src/app/lib/components/build-route.component.ts rename to src/app/modules/skresources/components/routes/build-route.component.ts diff --git a/src/app/modules/map/components/navigation/nextpoint.component.ts b/src/app/modules/skresources/components/routes/nextpoint.component.ts similarity index 89% rename from src/app/modules/map/components/navigation/nextpoint.component.ts rename to src/app/modules/skresources/components/routes/nextpoint.component.ts index 5eebf99ee..3c74f9b7b 100644 --- a/src/app/modules/map/components/navigation/nextpoint.component.ts +++ b/src/app/modules/skresources/components/routes/nextpoint.component.ts @@ -8,6 +8,9 @@ import { EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; /*********** NextPoint *************** index: number - index of current point, @@ -15,6 +18,8 @@ total: number - total number of points ***********************************/ @Component({ selector: 'route-nextpoint', + standalone: true, + imports: [MatButtonModule, MatTooltipModule, MatIconModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
" title text + skres: SKTrack + } +***********************************/ +@Component({ + selector: 'ap-tracks-modal', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + MatCheckboxModule + ], + template: ` +
+ + + + + + + + {{ data.title }} + + + + + + @for(trk of trackList; track trk; let idx = $index) { + + +
+
+ +
+
+
+ {{ trk[1].feature?.properties?.name }} +
+
+ {{ trk[1].feature?.properties?.description }} +
+
+
+ +
+
+
+
+ } +
+ `, + styles: [ + ` + ._ap-tracks { + font-family: arial; + min-width: 300px; + } + ._ap-tracks .key-label { + font-weight: 500; + } + ._ap-tracks .key-desc { + font-style: italic; + } + ` + ] +}) +export class TracksModal implements OnInit { + public trackList: Array<[string, SKTrack]>; + public selTrk = []; + + constructor( + public app: AppInfo, + private cdr: ChangeDetectorRef, + private sk: SignalKClient, + public modalRef: MatBottomSheetRef, + @Inject(MAT_BOTTOM_SHEET_DATA) + public data: { + title: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + skres: any; + } + ) {} + + ngOnInit() { + if (this.data.title === 'undefined') { + this.data['title'] = 'Tracks'; + } + this.getTracks(); + } + + closeModal() { + this.modalRef.dismiss(); + } + + getTracks() { + this.sk.api.get(this.app.skApiVersion, '/resources/tracks').subscribe( + (trks) => { + this.trackList = Object.entries(trks).map((trk: [string, SKTrack]) => { + trk[1]['feature']['id'] = trk[0].toString(); + delete trk[1]['$source']; + delete trk[1]['timestamp']; + return trk; + }); + this.selTrk = []; + for (let i = 0; i < this.trackList.length; i++) { + this.selTrk.push( + this.app.config.selections.tracks.includes(this.trackList[i][0]) + ? true + : false + ); + } + this.cdr.detectChanges(); + }, + () => { + this.trackList = []; + this.selTrk = []; + this.cdr.detectChanges(); + } + ); + } + + handleDelete(id: string) { + if (!this.data.skres) { + return; + } + this.trackList = this.trackList.filter((t) => { + return t[0] === id ? false : true; + }); + this.data.skres.showTrackDelete(id).subscribe((ok) => { + if (ok) { + const i = this.app.config.selections.tracks.indexOf(id); + if (i !== -1) { + this.app.config.selections.tracks.splice(i, 1); + } + this.data.skres.deleteResource('tracks', id); + setTimeout(this.getTracks.bind(this), 2000); + this.app.saveConfig(); + } else { + this.getTracks(); + } + }); + } + + handleCheck(checked: boolean, id: string, idx: number) { + this.selTrk[idx] = checked; + if (checked) { + this.app.config.selections.tracks.push(id); + } else { + const i = this.app.config.selections.tracks.indexOf(id); + if (i !== -1) { + this.app.config.selections.tracks.splice(i, 1); + } + } + this.app.saveConfig(); + this.updateTracks(); + } + + clearSelections() { + this.selTrk = []; + for (let i = 0; i < this.trackList.length; i++) { + this.selTrk[i] = false; + } + this.app.config.selections.tracks = []; + this.app.saveConfig(); + this.updateTracks(); + } + + updateTracks() { + this.app.data.tracks = this.trackList + .map((trk) => { + return trk[1]; + }) + .filter((t) => { + return this.app.config.selections.tracks.includes(t.feature.id) + ? true + : false; + }); + if (this.data.skres) { + this.data.skres.trackSelected(); + } + } +} diff --git a/src/app/modules/skresources/lists/waypointlist.html b/src/app/modules/skresources/components/waypoints/waypointlist.html similarity index 100% rename from src/app/modules/skresources/lists/waypointlist.html rename to src/app/modules/skresources/components/waypoints/waypointlist.html diff --git a/src/app/modules/skresources/lists/waypointlist.ts b/src/app/modules/skresources/components/waypoints/waypointlist.ts similarity index 84% rename from src/app/modules/skresources/lists/waypointlist.ts rename to src/app/modules/skresources/components/waypoints/waypointlist.ts index 04966b8bf..91ba0b4bb 100644 --- a/src/app/modules/skresources/lists/waypointlist.ts +++ b/src/app/modules/skresources/components/waypoints/waypointlist.ts @@ -5,15 +5,36 @@ import { EventEmitter, ChangeDetectionStrategy } from '@angular/core'; + +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatIconModule } from '@angular/material/icon'; +import { MatCardModule } from '@angular/material/card'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatButtonModule } from '@angular/material/button'; +import { FormsModule } from '@angular/forms'; +import { MatInputModule } from '@angular/material/input'; +import { ScrollingModule } from '@angular/cdk/scrolling'; + import { AppInfo } from 'src/app/app.info'; import { Position } from 'src/app/types'; import { FBWaypoints, FBWaypoint, FBResourceSelect } from 'src/app/types'; @Component({ selector: 'waypoint-list', + standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './waypointlist.html', - styleUrls: ['./resourcelist.css'] + styleUrls: ['../resourcelist.css'], + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatCheckboxModule, + MatButtonModule, + FormsModule, + MatInputModule, + ScrollingModule + ] }) export class WaypointListComponent { @Input() waypoints: FBWaypoints; diff --git a/src/app/modules/skresources/index.ts b/src/app/modules/skresources/index.ts new file mode 100644 index 000000000..283c971a5 --- /dev/null +++ b/src/app/modules/skresources/index.ts @@ -0,0 +1,32 @@ +export * from './resources.service'; +export * from './resource-classes'; +export * from './resourceset-class'; +export * from './resourceset-service'; + +export * from './components/resource-dialog'; +export * from './components/active-resource-dialog'; +export * from './components/signalk-details.component'; + +export * from './components/notes/notelist'; +export * from './components/notes/note-dialog'; +export * from './components/notes/relatednotes-dialog'; + +export * from './components/ais/aislist'; +export * from './components/ais/ais-properties-modal'; +export * from './components/ais/aton-properties-modal'; +export * from './components/ais/aircraft-properties-modal'; + +export * from './components/charts/chartlist'; +export * from './components/charts/chart-properties-dialog'; + +export * from './components/tracks/track-list-modal'; + +export * from './components/routes/routelist'; +export * from './components/routes/build-route.component'; +export { RouteNextPointComponent } from './components/routes/nextpoint.component'; + +export * from './components/waypoints/waypointlist'; + +export * from './components/resourcesets/resourceset-list-modal'; +export * from './components/resourcesets/resourceset-feature-properties-modal'; +export * from './components/resourcesets/resource-upload-dialog'; diff --git a/src/app/modules/skresources/notes/index.ts b/src/app/modules/skresources/notes/index.ts deleted file mode 100644 index 74ff93ec0..000000000 --- a/src/app/modules/skresources/notes/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './note-dialog'; -export * from './relatednotes-dialog'; diff --git a/src/app/modules/skresources/resource-dialogs.ts b/src/app/modules/skresources/resource-dialogs.ts deleted file mode 100644 index f79d5ac19..000000000 --- a/src/app/modules/skresources/resource-dialogs.ts +++ /dev/null @@ -1,1689 +0,0 @@ -/** Resource Dialog Components ** - ********************************/ - -import { Component, OnInit, Inject, ChangeDetectorRef } from '@angular/core'; -import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; -import { - MatBottomSheetRef, - MAT_BOTTOM_SHEET_DATA -} from '@angular/material/bottom-sheet'; -import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop'; -import { Convert } from 'src/app/lib/convert'; -import { Position } from 'src/app/types'; -import { AppInfo } from 'src/app/app.info'; -import { SignalKClient } from 'signalk-client-angular'; -import { - SKVessel, - SKAtoN, - SKAircraft, - SKChart, - SKTrack, - SKWaypoint, - SKRoute -} from './resource-classes'; -import { SKResourceSet } from './sets/resource-set'; -import { GeoUtils } from 'src/app/lib/geoutils'; - -/********* ResourceDialog ********** - data: { - title: "" title text, - name: ""resource name, - comment: ""resource comment, - } -***********************************/ -@Component({ - selector: 'ap-resourcedialog', - template: ` -
-
-
- {{ - icon - }} -
-
-

{{ data['title'] }}

-
-
- - -
-
-
- - Resource Name - - @if(inpname.invalid && (inpname.dirty || inpname.touched)) { - Please enter a waypoint name - } - -
-
- - Resource Description - - -
- @if(data['type'] === 'waypoint') { -
- - Signal K Type - - @for(i of resourceTypeList; track i) { - - {{ i.name }} - - } - - -
- } @if(data['position'][0]) { -
-
-
Lat:
-
-
-
-
Lon:
-
-
-
- } -
-
-
- -
- - -
-
-
- `, - styles: [ - ` - ._ap-resource { - font-family: arial; - min-width: 300px; - } - .ap-confirm-icon { - min-width: 35px; - max-width: 35px; - color: darkorange; - text-align: left; - } - - @media only screen and (min-device-width: 768px) and (max-device-width: 1024px), - only screen and (min-width: 800px) { - .ap-confirm-icon { - min-width: 25%; - max-width: 25%; - } - } - ` - ] -}) -export class ResourceDialog implements OnInit { - public icon: string; - - public resourceTypeList = [ - { type: '', name: 'Waypoint', icon: './assets/img/marker-yellow.png' }, - { - type: 'pseudoAtoN', - name: 'Pseudo AtoN', - icon: './assets/img/marker-red.png' - } - ]; - - constructor( - public app: AppInfo, - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) - public data: { - title: string; - name: string; - comment: string; - type?: string; - } - ) {} - - //** lifecycle: events ** - ngOnInit() { - this.data['name'] = this.data['name'] || ''; - this.data['comment'] = this.data['comment'] || ''; - this.data['title'] = this.data['title'] || ''; - this.data['position'] = this.data['position'] || [null, null]; - this.data['addMode'] = this.data['addMode'] || false; - this.data['type'] = this.data['type'] || 'waypoint'; - this.data['skType'] = this.data['skType'] || ''; - - this.icon = - this.data['type'] === 'route' - ? 'directions' - : this.data['type'] === 'region' - ? '360' - : this.data['type'] === 'note' - ? 'local_offer' - : this.data['addMode'] - ? 'add_location' - : 'edit_location'; - } -} - -/********* AISPropertiesModal ********** - data: { - title: "" title text, - target: "" vessel, - id: vessel id - } -***********************************/ -@Component({ - selector: 'ap-ais-modal', - template: ` -
- - - directions_boat - - - {{ data.title }} - - - - - - - - -
-
-
Name:
-
{{ vInfo.name }}
-
-
-
MMSI:
-
{{ vInfo.mmsi }}
-
- @if(vInfo.shipType) { -
-
Type:
-
{{ vInfo.shipType }}
-
- } @if(vInfo.flag) { -
-
Flag:
-
{{ vInfo.flag }}
-
- } @if(vInfo.port) { -
-
Port:
-
{{ vInfo.port }}
-
- } @if(vInfo.callsign) { -
-
Call sign:
-
{{ vInfo.callsign }}
-
- } @if(vInfo.length) { -
-
Dimensions:
-
- {{ vInfo.length }} x {{ vInfo.beam }} -
-
- } @if(vInfo.draft) { -
-
Draft:
-
{{ vInfo.draft }}
-
- } @if(vInfo.height) { -
-
Height:
-
{{ vInfo.height }}
-
- } @if(vInfo.state) { -
-
State:
-
{{ vInfo.state }}
-
- } @if(vInfo.destination) { -
-
Destination:
-
{{ vInfo.destination }}
-
- } @if(vInfo.eta) { -
-
ETA:
-
- {{ vInfo.eta.toLocaleString() }} -
-
- } -
-
-
-
- `, - styles: [ - ` - ._ap-ais { - font-family: arial; - } - ._ap-ais .key-label { - width: 150px; - font-weight: bold; - } - ` - ] -}) -export class AISPropertiesModal implements OnInit { - public vInfo = { - name: null, - mmsi: null, - callsign: null, - length: null, - beam: null, - draft: null, - height: null, - shipType: null, - destination: null, - eta: null, - state: null, - flag: null, - port: null - }; - - constructor( - public app: AppInfo, - private sk: SignalKClient, - public modalRef: MatBottomSheetRef, - @Inject(MAT_BOTTOM_SHEET_DATA) - public data: { - title: string; - target: SKVessel; - id: string; - } - ) {} - - ngOnInit() { - this.getVesselInfo(); - } - - formatDegrees(val: number) { - return val - ? `${Convert.radiansToDegrees(val).toFixed(1)} ${String.fromCharCode( - 186 - )}` - : '0.0'; - } - - formatKnots(val: number) { - return val ? `${Convert.msecToKnots(val).toFixed(1)} kn` : '0.0'; - } - - private getVesselInfo() { - let path: string; - if (!this.data.id) { - path = 'vessels/self'; - } else { - path = this.data.id.split('.').join('/'); - } - - this.sk.api.get(path).subscribe((v) => { - if (typeof v['name'] !== 'undefined') { - this.vInfo.name = v['name']; - } - if (typeof v['mmsi'] !== 'undefined') { - this.vInfo.mmsi = v['mmsi']; - } - if (typeof v['flag'] !== 'undefined') { - this.vInfo.flag = v['flag']; - } - if (typeof v['port'] !== 'undefined') { - this.vInfo.port = v['port']; - } - if (typeof v['communication'] !== 'undefined') { - if (typeof v['communication']['callsignVhf'] !== 'undefined') { - this.vInfo.callsign = v['communication']['callsignVhf']; - } - } - if (typeof v['navigation'] !== 'undefined') { - if (typeof v['navigation']['destination'] !== 'undefined') { - if ( - typeof v['navigation']['destination']['commonName'] !== 'undefined' - ) { - this.vInfo.destination = - v['navigation']['destination']['commonName']['value']; - } - if (typeof v['navigation']['destination']['eta'] !== 'undefined') { - this.vInfo.eta = new Date( - v['navigation']['destination']['eta']['value'] - ).toUTCString(); - } - } - if ( - typeof v['navigation']['state'] !== 'undefined' && - typeof v['navigation']['state']['value'] !== 'undefined' - ) { - this.vInfo.state = v['navigation']['state']['value']; - } - } - if (typeof v['design'] !== 'undefined') { - if ( - typeof v['design']['length'] !== 'undefined' && - v['design']['length']['value']['overall'] - ) { - this.vInfo.length = v['design']['length']['value']['overall']; - } - if (typeof v['design']['beam'] !== 'undefined') { - this.vInfo.beam = v['design']['beam']['value']; - } - if ( - typeof v['design']['draft'] !== 'undefined' && - v['design']['draft']['value'] - ) { - if (typeof v['design']['draft']['value']['maximum'] !== 'undefined') { - this.vInfo.draft = `${v['design']['draft']['value']['maximum']} (max)`; - } else if ( - typeof v['design']['draft']['value']['current'] !== 'undefined' - ) { - this.vInfo.draft = `${v['design']['draft']['value']['current']} (current)`; - } - } - if (typeof v['design']['airHeight'] !== 'undefined') { - this.vInfo.height = v['design']['airHeight']['value']; - } - if (typeof v['design']['aisShipType'] !== 'undefined') { - this.vInfo.shipType = v['design']['aisShipType']['value']['name']; - } - } - }); - } -} - -/********* AtoNPropertiesModal ********** - data: { - title: "" title text, - target: "" aid to navigation - icon: - } -***********************************/ -@Component({ - selector: 'ap-aton-modal', - template: ` -
- - - {{ data.icon }} - - - {{ data.title }} - - - - - - - - -
-
-
Name:
-
{{ data.target.name }}
-
-
-
MMSI:
-
{{ data.target.mmsi }}
-
-
-
Type:
-
{{ data.target.type.name }}
-
- - @if(showProperties) { - - } -
-
-
-
- `, - styles: [ - ` - ._ap-aton { - font-family: arial; - min-width: 300px; - } - ._ap-aton .key-label { - width: 150px; - font-weight: bold; - } - ` - ] -}) -export class AtoNPropertiesModal implements OnInit { - protected showProperties = true; - protected properties: { [key: string]: string | number | null }; - - constructor( - private sk: SignalKClient, - private app: AppInfo, - public modalRef: MatBottomSheetRef, - @Inject(MAT_BOTTOM_SHEET_DATA) - public data: { - title: string; - target: SKAtoN; - id: string; - icon: string; - type: 'aton' | 'sar' | 'meteo'; - } - ) {} - - ngOnInit() { - this.getAtoNInfo(); - } - - toggleProperties() { - this.showProperties = !this.showProperties; - } - - // fetch object information - private getAtoNInfo() { - if (!this.data.id) { - return; - } - const path = this.data.id.split('.').join('/'); - - this.sk.api.get(path).subscribe((v) => { - if (this.data.type === 'meteo') { - this.properties = this.parseMeteo(v); - } else { - this.properties = this.parseAtoN(v); - } - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private parseMeteo(data: any) { - const res = {}; - - if (data.navigation && data.navigation.position) { - res['navigation.position'] = data.navigation.position.value; - } - const bk = data.environment.observation ?? data.environment; - const pk = data.environment.observation - ? 'environment.observation' - : 'environment'; - if (bk) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Object.keys(bk).forEach((k: any) => { - const pathRoot = `${pk}.${k}`; - const g = bk[k]; - if (g.meta) { - res[pathRoot] = this.app.formatValueForDisplay( - g.value, - g.meta.units ? g.meta.units : '', - k.indexOf('level') !== -1 // depth values - ); - } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - Object.entries(g).forEach((i: any) => { - const key = `${pathRoot}.${i[0]}`; - res[key] = this.app.formatValueForDisplay( - i[1].value, - i[1].meta && i[1].meta.units ? i[1].meta.units : '', - i[0].indexOf('level') !== -1 // depth values - ); - }); - } - }); - } - return res; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private parseAtoN(data: any) { - const res = {}; - if (data.navigation && data.navigation.position) { - res['navigation.position'] = data.navigation.position.value; - } - return Object.assign(res, this.data.target.properties); - } -} - -/********* AircraftPropertiesModal ********** - data: { - title: "" title text, - target: "" aid to navigation - } -***********************************/ -@Component({ - selector: 'ap-aircraft-modal', - template: ` -
- - - airplanemode_active - - - {{ data.title }} - - - - - - - - -
-
-
Name:
-
{{ data.target.name }}
-
-
-
MMSI:
-
{{ data.target.mmsi }}
-
-
-
Call sign:
-
{{ data.target.callsign }}
-
- - @if(showProperties) { - - } -
-
-
-
- `, - styles: [ - ` - ._ap-aircraft { - font-family: arial; - min-width: 300px; - } - ._ap-aircraft .key-label { - width: 150px; - font-weight: bold; - } - ` - ] -}) -export class AircraftPropertiesModal { - protected showProperties = true; - protected properties: { [key: string]: string | number | null }; - - constructor( - private sk: SignalKClient, - private app: AppInfo, - public modalRef: MatBottomSheetRef, - @Inject(MAT_BOTTOM_SHEET_DATA) - public data: { - title: string; - target: SKAircraft; - id: string; - icon: string; - } - ) {} - - ngOnInit() { - this.getAircraftInfo(); - } - - // fetch object information - private getAircraftInfo() { - if (!this.data.id) { - return; - } - const path = this.data.id.split('.').join('/'); - - this.sk.api.get(path).subscribe((v) => { - this.properties = this.parseAircraft(v); - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private parseAircraft(data: any) { - const res = {}; - - if (data.navigation && data.navigation.position) { - res['navigation.position'] = data.navigation.position.value; - } - return res; - } - - toggleProperties() { - this.showProperties = !this.showProperties; - } -} - -/********* ActiveResourcePropertiesModal ********** - data: { - title: "" title text, - type: 'dest' | 'route' resource type, - resource: "" active resource info - skres: pointer to SKResources service - } -***********************************/ -@Component({ - selector: 'ap-dest-modal', - template: ` -
-
- - - @if(showClearButton) { - - } - - - {{ data.title }} - - - - - -
- -
-
- @for(pt of points; track pt; let i = $index) { - - -
- -
-
- @if(selIndex === i) { - flag - } -
-
-
-
Name:
-
-
- - @if(pointMeta[i].description) { -
-
Desc:
-
-
- } - -
-
- square_foot -
-
- -   - -
-
- -
-
- @if(data.type === 'route') { - drag_indicator - } -
-
-
-
- } -
-
-
- `, - styles: [ - ` - ._ap-dest { - font-family: arial; - min-width: 300px; - } - ._ap-dest .key-label { - width: 50px; - font-weight: bold; - } - ._ap-dest .selected { - background-color: silver; - } - .point-drop-placeholder { - background: #ccc; - border: dotted 3px #999; - min-height: 60px; - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); - } - .cdk-drag-preview { - box-sizing: border-box; - border-radius: 4px; - box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), - 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); - } - ` - ] -}) -export class ActiveResourcePropertiesModal implements OnInit { - protected points: Array = []; - protected pointMeta: Array<{ name: string; description: string }> = []; - protected legs: { bearing: string; distance: string }[] = []; - protected selIndex = -1; - protected clearButtonText = 'Clear'; - protected showClearButton = false; - - constructor( - public app: AppInfo, - public modalRef: MatBottomSheetRef, - @Inject(MAT_BOTTOM_SHEET_DATA) - public data: { - title: string; - type: string; - resource: SKWaypoint | SKRoute; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - skres: any; - } - ) {} - - ngOnInit() { - if ( - this.data.resource[1].feature && - this.data.resource[1].feature.geometry.coordinates - ) { - if (this.data.type === 'route') { - this.points = [].concat( - this.data.resource[1].feature.geometry.coordinates - ); - this.legs = this.getLegs(); - - this.data.title = this.data.resource[1].name - ? `${this.data.resource[1].name} Points` - : 'Route Points'; - - if (this.data.resource[0] === this.app.data.activeRoute) { - this.selIndex = this.app.data.navData.pointIndex; - this.showClearButton = true; - } - this.pointMeta = this.getPointMeta(); - } - } - } - - getLegs() { - const pos = this.app.data.vessels.self.position; - return GeoUtils.routeLegs(this.points, pos).map((l) => { - return { - bearing: this.app.formatValueForDisplay(l.bearing, 'deg'), - distance: this.app.formatValueForDisplay(l.distance, 'm') - }; - }); - } - - getPointMeta() { - if ( - this.data.resource[1].feature.properties.coordinatesMeta && - Array.isArray(this.data.resource[1].feature.properties.coordinatesMeta) - ) { - const pointsMeta = - this.data.resource[1].feature.properties.coordinatesMeta; - let idx = 0; - return pointsMeta.map((pt) => { - idx++; - if (pt.href) { - const id = pt.href.split('/').slice(-1); - const wpt = this.data.skres.fromCache('waypoints', id[0]); - return wpt - ? { - name: `* ${wpt[1].name}`, - description: `* ${wpt[1].description}` - } - : { - name: '!wpt reference!', - description: '' - }; - } else { - return { - name: pt.name ?? `RtePt-${('000' + String(idx)).slice(-3)}`, - description: pt.description ?? `` - }; - } - }); - } else { - let idx = 0; - return this.points.map(() => { - return { - name: `RtePt-${('000' + String(++idx)).slice(-3)}`, - description: '' - }; - }); - } - } - - selectPoint(idx: number) { - if (this.points.length < 2 || this.selIndex < 0) { - return; - } - this.selIndex = idx; - if (this.data.skres) { - this.data.skres.coursePointIndex(this.selIndex); - } - } - - drop(e: CdkDragDrop<{ previousIndex: number; currentIndex: number }>) { - if (this.data.type === 'route') { - const selPosition = this.points[this.selIndex]; - moveItemInArray(this.points, e.previousIndex, e.currentIndex); - this.legs = this.getLegs(); - if (this.data.resource[1].feature.properties.coordinatesMeta) { - moveItemInArray( - this.data.resource[1].feature.properties.coordinatesMeta, - e.previousIndex, - e.currentIndex - ); - } - this.pointMeta = this.getPointMeta(); - - this.updateFlag(selPosition); - this.data.skres.updateRouteCoords( - this.data.resource[0], - this.points, - this.data.resource[1].feature.properties.coordinatesMeta - ); - } - } - - updateFlag(selPosition: Position) { - if (!selPosition) { - return; - } - let idx = 0; - this.points.forEach((p: Position) => { - if (p[0] === selPosition[0] && p[1] === selPosition[1]) { - this.selIndex = idx; - } - idx++; - }); - } - - close() { - this.modalRef.dismiss(); - } - - // ** deactivate route / clear destination - deactivate() { - this.modalRef.dismiss(true); - } -} - -/********* ChartInfoDialog ********** - data: -***********************************/ -@Component({ - selector: 'ap-chartproperties', - template: ` -
- - {{ isLocal(data['url']) }} - Chart Properties - - - - - -
-
-
Name:
-
{{ data['name'] }}
-
-
-
Description:
-
{{ data['description'] }}
-
-
-
Scale:
-
{{ data['scale'] }}
-
-
-
Zoom:
-
-
- Min: - {{ data['minZoom'] }}, - Max: - {{ data['maxZoom'] }} -
-
-
- @if(data['bounds']) { -
-
Bounds:
-
-
- -
- - -
-
- -
- - -
-
-
- } -
-
Format:
-
{{ data['format'] }}
-
-
-
Type:
-
- {{ data['type'] }} -
-
-
-
Layers:
-
{{ data['layers'] }}
-
-
-
URL:
-
- {{ data['url'] }} -
-
-
-
-
- `, - styles: [ - ` - ._ap-chartinfo { - font-family: arial; - min-width: 300px; - } - .ap-confirm-icon { - min-width: 35px; - max-width: 35px; - color: darkorange; - text-align: left; - } - - ._ap-chartinfo .key-label { - width: 150px; - font-weight: 500; - } - - @media only screen and (min-device-width: 768px) and (max-device-width: 1024px), - only screen and (min-width: 800px) { - .ap-confirm-icon { - min-width: 25%; - max-width: 25%; - } - } - ` - ] -}) -export class ChartInfoDialog { - public icon: string; - - constructor( - public app: AppInfo, - public dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: SKChart - ) {} - - isLocal(url: string) { - return url && url.indexOf('signalk') !== -1 ? 'map' : 'language'; - } -} - -/********* TracksModal ********** - data: { - title: "" title text - skres: SKTrack - } -***********************************/ -@Component({ - selector: 'ap-tracks-modal', - template: ` -
- - - - - - - - {{ data.title }} - - - - - - @for(trk of trackList; track trk; let idx = $index) { - - -
-
- -
-
-
- {{ trk[1].feature?.properties?.name }} -
-
- {{ trk[1].feature?.properties?.description }} -
-
-
- -
-
-
-
- } -
- `, - styles: [ - ` - ._ap-tracks { - font-family: arial; - min-width: 300px; - } - ._ap-tracks .key-label { - font-weight: 500; - } - ._ap-tracks .key-desc { - font-style: italic; - } - ` - ] -}) -export class TracksModal implements OnInit { - public trackList: Array<[string, SKTrack]>; - public selTrk = []; - - constructor( - public app: AppInfo, - private cdr: ChangeDetectorRef, - private sk: SignalKClient, - public modalRef: MatBottomSheetRef, - @Inject(MAT_BOTTOM_SHEET_DATA) - public data: { - title: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - skres: any; - } - ) {} - - ngOnInit() { - if (this.data.title === 'undefined') { - this.data['title'] = 'Tracks'; - } - this.getTracks(); - } - - closeModal() { - this.modalRef.dismiss(); - } - - getTracks() { - this.sk.api.get(this.app.skApiVersion, '/resources/tracks').subscribe( - (trks) => { - this.trackList = Object.entries(trks).map((trk: [string, SKTrack]) => { - trk[1]['feature']['id'] = trk[0].toString(); - delete trk[1]['$source']; - delete trk[1]['timestamp']; - return trk; - }); - this.selTrk = []; - for (let i = 0; i < this.trackList.length; i++) { - this.selTrk.push( - this.app.config.selections.tracks.includes(this.trackList[i][0]) - ? true - : false - ); - } - this.cdr.detectChanges(); - }, - () => { - this.trackList = []; - this.selTrk = []; - this.cdr.detectChanges(); - } - ); - } - - handleDelete(id: string) { - if (!this.data.skres) { - return; - } - this.trackList = this.trackList.filter((t) => { - return t[0] === id ? false : true; - }); - this.data.skres.showTrackDelete(id).subscribe((ok) => { - if (ok) { - const i = this.app.config.selections.tracks.indexOf(id); - if (i !== -1) { - this.app.config.selections.tracks.splice(i, 1); - } - this.data.skres.deleteResource('tracks', id); - setTimeout(this.getTracks.bind(this), 2000); - this.app.saveConfig(); - } else { - this.getTracks(); - } - }); - } - - handleCheck(checked: boolean, id: string, idx: number) { - this.selTrk[idx] = checked; - if (checked) { - this.app.config.selections.tracks.push(id); - } else { - const i = this.app.config.selections.tracks.indexOf(id); - if (i !== -1) { - this.app.config.selections.tracks.splice(i, 1); - } - } - this.app.saveConfig(); - this.updateTracks(); - } - - clearSelections() { - this.selTrk = []; - for (let i = 0; i < this.trackList.length; i++) { - this.selTrk[i] = false; - } - this.app.config.selections.tracks = []; - this.app.saveConfig(); - this.updateTracks(); - } - - updateTracks() { - this.app.data.tracks = this.trackList - .map((trk) => { - return trk[1]; - }) - .filter((t) => { - return this.app.config.selections.tracks.includes(t.feature.id) - ? true - : false; - }); - if (this.data.skres) { - this.data.skres.trackSelected(); - } - } -} - -/********* ResourceSetModal ********** - * Fetches ResouorceSets from server for selection - data: { - path: "" resource path - skres: SKResource - } -***********************************/ -@Component({ - selector: 'ap-resourceset-modal', - template: ` -
- - - - - - - - {{ title }} - - - - - - @for(res of resList; track res; let idx = $index) { - - -
-
- -
-
-
- {{ res.name }} -
-
- {{ res.description }} -
-
-
-
-
-
- } -
- `, - styles: [ - ` - ._ap-resource-set { - font-family: arial; - min-width: 300px; - } - ._ap-resource-set .key-label { - font-weight: 500; - } - ._ap-resource-set .key-desc { - font-style: italic; - } - ` - ] -}) -export class ResourceSetModal implements OnInit { - public resList: Array; - public selRes = []; - public title = 'Resources: '; - public isResourceSet = false; - - constructor( - public app: AppInfo, - private cdr: ChangeDetectorRef, - private sk: SignalKClient, - public modalRef: MatBottomSheetRef, - @Inject(MAT_BOTTOM_SHEET_DATA) - public data: { - path: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - skres: any; - } - ) {} - - ngOnInit() { - if (this.data.path !== 'undefined') { - this.title += this.data.path; - } - this.getItems(); - } - - closeModal() { - this.modalRef.dismiss(); - } - - getItems() { - this.sk.api - .get(this.app.skApiVersion, `resources/${this.data.path}`) - .subscribe( - (resSet) => { - this.resList = this.data.skres.processItems(resSet); - this.selRes = []; - if ( - this.resList.length !== 0 && - this.resList[0].type === 'ResourceSet' - ) { - this.isResourceSet = true; - for (let i = 0; i < this.resList.length; i++) { - this.selRes.push( - this.app.config.selections.resourceSets[ - this.data.path - ].includes(this.resList[i].id) - ? true - : false - ); - } - } - this.cdr.detectChanges(); - }, - () => { - this.resList = []; - this.selRes = []; - this.cdr.detectChanges(); - } - ); - } - - handleCheck(checked: boolean, id: string, idx: number) { - if (!this.isResourceSet) { - return; - } - this.selRes[idx] = checked; - if (checked) { - this.app.config.selections.resourceSets[this.data.path].push(id); - } else { - const i = - this.app.config.selections.resourceSets[this.data.path].indexOf(id); - if (i !== -1) { - this.app.config.selections.resourceSets[this.data.path].splice(i, 1); - } - } - this.app.saveConfig(); - this.updateItems(); - this.data.skres.resourceSelected(); - } - - clearSelections() { - if (!this.isResourceSet) { - return; - } - this.selRes = []; - for (let i = 0; i < this.resList.length; i++) { - this.selRes[i] = false; - } - this.app.config.selections.resourceSets[this.data.path] = []; - this.app.saveConfig(); - this.updateItems(); - this.data.skres.resourceSelected(); - } - - updateItems() { - this.app.data.resourceSets[this.data.path] = this.resList.filter((t) => { - return this.app.config.selections.resourceSets[this.data.path].includes( - t.id - ) - ? true - : false; - }); - } -} - -/********* ResourceSetFeatureModal ********** - * Displays information about a ResourceSet feature - data: { - id: string - skres: SKResourceSet; - } -***********************************/ -@Component({ - selector: 'ap-resourceset-feature-modal', - template: ` -
- - - - {{ title }} - - - - - - - - - - - -
- `, - styles: [ - ` - ._ap-resource-set-feature { - font-family: arial; - min-width: 300px; - } - ._ap-resource-set-feature .key-label { - font-weight: 500; - } - ._ap-resource-set-feature .key-desc { - font-style: italic; - } - ` - ] -}) -export class ResourceSetFeatureModal implements OnInit { - protected properties = {}; - protected title = 'ResourceSet Feature: '; - - constructor( - public app: AppInfo, - private cdr: ChangeDetectorRef, - public modalRef: MatBottomSheetRef, - @Inject(MAT_BOTTOM_SHEET_DATA) - public data: { - id: string; - skres: SKResourceSet; - } - ) {} - - ngOnInit() { - this.parse(); - } - - closeModal() { - this.modalRef.dismiss(); - } - - parse() { - const t = this.data.id.split('.'); - const fIndex = Number(t[t.length - 1]); - const features = this.data.skres.values.features; - const feature = fIndex < features.length ? features[fIndex] : features[0]; - - this.title = feature.properties.name ?? 'Feature'; - this.properties = { - name: feature.properties.name ?? '', - description: feature.properties.description ?? '', - 'position.latitude': feature.geometry.coordinates[1], - 'position.longitude': feature.geometry.coordinates[0], - 'resourceset.name': this.data.skres.name, - 'resourceset.description': this.data.skres.description, - 'resourceset.collection': t[1] - }; - } -} diff --git a/src/app/modules/skresources/resources.module.ts b/src/app/modules/skresources/resources.module.ts deleted file mode 100644 index e4f5952e6..000000000 --- a/src/app/modules/skresources/resources.module.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { HttpClientModule } from '@angular/common/http'; -import { FormsModule } from '@angular/forms'; - -import { DragDropModule } from '@angular/cdk/drag-drop'; - -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatSelectModule } from '@angular/material/select'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { MatListModule } from '@angular/material/list'; -import { MatSlideToggleModule } from '@angular/material/slide-toggle'; -import { MatSliderModule } from '@angular/material/slider'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { ScrollingModule } from '@angular/cdk/scrolling'; - -import { RouteListComponent } from './lists/routelist'; -import { WaypointListComponent } from './lists/waypointlist'; -import { ChartListComponent, ChartLayers } from './lists/chartlist'; -import { NoteListComponent } from './lists/notelist'; -import { AISListComponent } from './lists/aislist'; - -import { - SignalKDetailsComponent, - FileInputComponent -} from 'src/app/lib/components'; -import { CommonDialogs } from 'src/app/lib/components/dialogs'; -import { PipesModule } from 'src/app/lib/pipes'; - -import { - ResourceDialog, - AISPropertiesModal, - AtoNPropertiesModal, - AircraftPropertiesModal, - ActiveResourcePropertiesModal, - ChartInfoDialog, - TracksModal, - ResourceSetModal, - ResourceSetFeatureModal -} from './resource-dialogs'; -import { ResourceImportDialog } from './sets/resource-upload-dialog'; - -@NgModule({ - imports: [ - CommonModule, - HttpClientModule, - FormsModule, - MatDialogModule, - MatCheckboxModule, - MatCardModule, - MatListModule, - MatSelectModule, - MatButtonModule, - MatIconModule, - MatTooltipModule, - MatBottomSheetModule, - MatSliderModule, - MatSlideToggleModule, - ScrollingModule, - MatFormFieldModule, - MatInputModule, - MatToolbarModule, - DragDropModule, - CommonDialogs, - PipesModule, - SignalKDetailsComponent, - FileInputComponent - ], - declarations: [ - RouteListComponent, - WaypointListComponent, - ChartListComponent, - AISListComponent, - NoteListComponent, - ResourceDialog, - AISPropertiesModal, - AtoNPropertiesModal, - AircraftPropertiesModal, - ActiveResourcePropertiesModal, - ChartInfoDialog, - ChartLayers, - TracksModal, - ResourceSetModal, - ResourceSetFeatureModal, - ResourceImportDialog - ], - exports: [ - RouteListComponent, - WaypointListComponent, - ChartListComponent, - AISListComponent, - NoteListComponent, - ResourceDialog, - AISPropertiesModal, - AtoNPropertiesModal, - AircraftPropertiesModal, - ActiveResourcePropertiesModal, - ChartInfoDialog, - ChartLayers, - TracksModal, - ResourceSetModal, - ResourceSetFeatureModal, - ResourceImportDialog - ] -}) -export class SignalKResourcesModule {} - -export * from './resources.service'; -export * from './resource-dialogs'; -export * from './resource-classes'; - -export * from './sets/resource-set'; -export * from './sets/resource-sets.service'; -export * from './sets/resource-upload-dialog'; diff --git a/src/app/modules/skresources/resources.service.ts b/src/app/modules/skresources/resources.service.ts index 1e78ffa92..0220b0066 100644 --- a/src/app/modules/skresources/resources.service.ts +++ b/src/app/modules/skresources/resources.service.ts @@ -11,8 +11,10 @@ import { GeoUtils } from 'src/app/lib/geoutils'; import { Convert } from 'src/app/lib/convert'; import { LoginDialog } from 'src/app/lib/components/dialogs'; -import { NoteDialog, RelatedNotesDialog } from './notes'; -import { ResourceDialog } from './resource-dialogs'; +import { NoteDialog, RelatedNotesDialog } from '.'; +import { ResourceDialog } from './components/resource-dialog'; +import { SKResourceSet } from '.'; + import { SKChart, SKRoute, @@ -39,7 +41,6 @@ import { Regions } from 'src/app/types'; import { PathValue } from '@signalk/server-api'; -import { SKResourceSet } from './resources.module'; // ** Signal K resource operations @Injectable({ providedIn: 'root' }) diff --git a/src/app/modules/skresources/sets/resource-set.ts b/src/app/modules/skresources/resourceset-class.ts similarity index 100% rename from src/app/modules/skresources/sets/resource-set.ts rename to src/app/modules/skresources/resourceset-class.ts diff --git a/src/app/modules/skresources/sets/resource-sets.service.ts b/src/app/modules/skresources/resourceset-service.ts similarity index 98% rename from src/app/modules/skresources/sets/resource-sets.service.ts rename to src/app/modules/skresources/resourceset-service.ts index 93a9e8c2e..c530ae992 100644 --- a/src/app/modules/skresources/sets/resource-sets.service.ts +++ b/src/app/modules/skresources/resourceset-service.ts @@ -3,7 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { Subject, Observable } from 'rxjs'; import { SignalKClient } from 'signalk-client-angular'; import { AppInfo } from 'src/app/app.info'; -import { SKResourceSet } from './resource-set'; +import { SKResourceSet } from './resourceset-class'; import { ResourceSet, CustomResources } from 'src/app/types'; // ** Signal K custom / other resource(s) operations diff --git a/src/app/modules/skstream/skstream.module.ts b/src/app/modules/skstream/skstream.module.ts deleted file mode 100644 index 88374affc..000000000 --- a/src/app/modules/skstream/skstream.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -/***************************** - * Signal K Stream Module - *****************************/ -import { NgModule } from '@angular/core'; - -@NgModule({ - imports: [], - declarations: [], - exports: [], - providers: [] -}) -export class SKStreamModule {} - -export * from './skstream.service'; -export * from './skstream.facade'; diff --git a/src/app/modules/weather/index.ts b/src/app/modules/weather/index.ts new file mode 100644 index 000000000..c14851a33 --- /dev/null +++ b/src/app/modules/weather/index.ts @@ -0,0 +1 @@ +export * from './weather-forecast-modal'; diff --git a/src/app/modules/experiments/weather/components/weather-data.component.ts b/src/app/modules/weather/weather-data.component.ts similarity index 89% rename from src/app/modules/experiments/weather/components/weather-data.component.ts rename to src/app/modules/weather/weather-data.component.ts index b6272e287..c0107f8ba 100644 --- a/src/app/modules/experiments/weather/components/weather-data.component.ts +++ b/src/app/modules/weather/weather-data.component.ts @@ -1,5 +1,17 @@ import { Component, Input } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatBottomSheetModule } from '@angular/material/bottom-sheet'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatStepperModule } from '@angular/material/stepper'; +import { MatProgressBar } from '@angular/material/progress-bar'; + export interface WeatherData { description?: string; time?: string; @@ -23,6 +35,20 @@ export interface WeatherData { /********* Weather Data viewer component ****************/ @Component({ selector: 'weather-data', + standalone: true, + imports: [ + MatCardModule, + MatListModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + MatBottomSheetModule, + MatFormFieldModule, + MatInputModule, + MatToolbarModule, + MatStepperModule, + MatProgressBar + ], template: `
diff --git a/src/app/modules/experiments/weather/weather-forecast.ts b/src/app/modules/weather/weather-forecast-modal.ts similarity index 88% rename from src/app/modules/experiments/weather/weather-forecast.ts rename to src/app/modules/weather/weather-forecast-modal.ts index 87a4dba74..99c4f9028 100644 --- a/src/app/modules/experiments/weather/weather-forecast.ts +++ b/src/app/modules/weather/weather-forecast-modal.ts @@ -2,14 +2,25 @@ ********************************/ import { Component, OnInit, Inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule } from '@angular/material/list'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatStepperModule } from '@angular/material/stepper'; +import { MatProgressBar } from '@angular/material/progress-bar'; import { + MatBottomSheetModule, MatBottomSheetRef, MAT_BOTTOM_SHEET_DATA } from '@angular/material/bottom-sheet'; import { AppInfo } from 'src/app/app.info'; import { SignalKClient } from 'signalk-client-angular'; import { Convert } from 'src/app/lib/convert'; -import { WeatherData } from './components/weather-data.component'; +import { WeatherData, WeatherDataComponent } from './weather-data.component'; /********* WeatherForecastModal ********** data: { @@ -18,6 +29,21 @@ import { WeatherData } from './components/weather-data.component'; ***********************************/ @Component({ selector: 'weather-forecast-modal', + standalone: true, + imports: [ + MatCardModule, + MatListModule, + MatButtonModule, + MatIconModule, + MatTooltipModule, + MatBottomSheetModule, + MatFormFieldModule, + MatInputModule, + MatToolbarModule, + MatStepperModule, + MatProgressBar, + WeatherDataComponent + ], template: `
diff --git a/src/app/types/index.d.ts b/src/app/types/index.d.ts index dbe234417..b4f33fe2e 100644 --- a/src/app/types/index.d.ts +++ b/src/app/types/index.d.ts @@ -124,6 +124,8 @@ export interface FBAppConfig { }; positionFormat: 'XY' | 'SHDd' | 'HDd' | 'DMdH' | 'HDMS' | 'DHMS'; aisTargets: string[]; + aisTargetTypes: number[]; + aisFilterByShipType: boolean; aisWindApparent: boolean; aisWindMinZoom: number; aisShowTrack: boolean; diff --git a/src/assets/help/img/ais_shiptypes.png b/src/assets/help/img/ais_shiptypes.png new file mode 100644 index 0000000000000000000000000000000000000000..fccdf2ab5dd4d08e0ef942b399ba2da4b381e088 GIT binary patch literal 31928 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
- +
- Vessel with updated data (<6 mins since last update) + Vessel with stale data (>6 mins since last update)
- +
-
- Vessel with stale data (>6 mins since last update) +
Vessel marked as "Buddy"
+
+
+
+ +
+
Unspecified (AIS type 10-19)
+
+
+
+
+
Wing in Ground (AIS type 20-29)
+
+
+
+ +
+
Pleasure (AIS type 30-39)
+
+
+
+ +
+
High Speed Vessel (AIS type 40-49)
@@ -1556,9 +1578,9 @@

Vessels

- +
-
Vessel marked as "Buddy"
+
Other (AIS type 90-99)
Freeboard provides the ability to de-clutter the screen of vessels in @@ -1599,10 +1621,25 @@
To Select Vessels:
directions_boat Vessels. + +
+ From this screen you can filter and select the vessels you wish to + display on the map OR
+ by selecting Filter by Vessel type vessels can be displayed + based on their AIS Ship Type. +
+
+
+ +
  • diff --git a/src/assets/img/ais_buddy.png b/src/assets/img/ais_buddy.png index eb54a4fab8aaeaa2f3b1ab34f50fd2530676bb3f..d15d2924f9e4dcb0aa5bfb467bc82c35661cf4e4 100644 GIT binary patch delta 271 zcmV+q0r39M0iFVo7=H)`0002scRlz3000DYLP=Bz2nYy#2xN!=007lVL_t(YiS3j< z4#F@Dg&)u*8(r9tkSaxD>(B#mfE<94nZBQh$R4#B!A2?-5#ys2Xq- z1ZXR2f#TrvorM6T21)a8cLs2_JBIfJzGC`5o)@szx69|;neX+5J{@}j0HEo5y6kKK zH)t;aR})}(!0$616yOYSwW}=$l5m2bZ6Yd^_wv~u8rw${pzb+UZF@mN6P(nl2e%?O VTNe`^=Kufz00>D%PDHLkV1fsrZKD7H delta 191 zcmbQobe?g7L_G^L0|SFlq{3++#aZAHSy@dl7$DGBlmzRS?ybmY%|O&~wa)5S4F;_}=J8wDE-c$ghTBOTh=J=_wQ zCHMUI5n*|(WFn%q@8VpRNh;-&RPug^Z`*vpV$p)^Jrnjk@{qW4?S;TG=B*9t3%LF` n=yC)r&6)OLrseFf&8&KDxsnYhbP0l+XkKe(pf^ diff --git a/src/assets/img/ais_cargo.png b/src/assets/img/ais_cargo.png index d15d2924f9e4dcb0aa5bfb467bc82c35661cf4e4..e4b8ece9f0e2dd4497da116ec725501014f56abd 100644 GIT binary patch delta 170 zcmV;b09F5<0@eYLG=GgrL_t(YiDO_O5wJPwIgzS|RPdh^#Z0tv1z8rrf`^vD^q*m- z;eQ4af{Bqt!x6mn^F-+%TycN_G^85{DIqq9P=f~shzO=Z5k#md2I6Ym@?==RfHipV ziIZ#riH1^U0f}JL0vcF=nM^4L$R+51xEPToD=|d{vH|Go|0ClNCkYTL`$?)oM*#)^ Y0N;KhA=twPM*si-07*qoM6N<$g6OkDi~s-t delta 242 zcmV(B#mfE<94nZBQiBA>$j2L*j;#1Q3KshDdvs2ox@`>wftE|Mai_ z7amk`5I&qZ~F?)T#u1hBY2r^czSNEKg!tG%?MTy&kb+K5$eWoudUZLDlQqq!A%MCLoWpTze qgt-(y5LkYODW>hgWNB6xW`;`=Ax{=GJcD_f(UotQM$a&KA)d`CmSFM2zxe%BZ@G~eTl=vZ)Xuys15+opgEIgG7?2_ z5kVAja2rdCqc%T%j1z$D)cm3zFqi~mO|4NMwOm_?zRS~NC|Z+x1Ff0b!&3zkU-!X8 z8pfaTah)UpfS=|0H%|-5O{WP+7QqaHq%SEKBw&@-J4Rv`bl*Z~(cX}qV{bA}?EZf` bwqWHAt5q!O29}Vs00000NkvXXu0mjfTbo@t delta 205 zcmV;;05boL0{H=uG=H#3L_t(YiDO_O5il_@IgqM{Bv7Bl@Sha*Ok~=lKobkt7}z2h z7#J8-8B`ldbP#c%K8xZ1|Cjaup}{0T6T@Mc*tj4!4u}TCTA+YUkb{9Efhw**4<1s2 zi5MT?jZ8urkDvwhSq$%y`3;SzDYQ20-+qcLU|?WiXsE9zNk*Jx3rIDTA`3_bqZUx# z0`z1`RSOy#8yOm>F=&!%30{C!M04N}r_2IehTxMM1w;b?(il|apn&3E00000NkvXX Hu0mjfoE=dD diff --git a/src/assets/img/ais_tanker.png b/src/assets/img/ais_tanker.png index 8c1bc21b2f1ff2ad852a3ff23ab694395de2158f..d0f031e5817df7cdbf5b6c02955cc1512fc6725f 100644 GIT binary patch delta 198 zcmV;%06G7S0`dWnG=Hf{L_t(YiS3oK4Z|Q1MPE>4QDy`;F0w_&NSQ?{zmM&gV;NUZF`=tNPcibh&B|k02Wr3P>mq6td953)K z1s(`+S0$??PqbZ#FLJGscp$)K9-G?I_j!`#2MkU#S^MqM7XSbN07*qoM6N<$f~JsH AsQ>@~ delta 225 zcmV<703QGH0gVEXG=IcNL_t(YiS3lJ4Z<)GMZXP=J0fKODoc!zjxi{ifE`%DVjtE( zUV;v)a1>D_f(UotQM$a&KA)d`CmSFM2zxe%BZ@G~eTl=vZ)Xuys15+opgEIgG7?2_ z5kVAja2rdCqc%T%j1z$D)cm3zFqi~mO|4NMwOm_?zRS~NC|Z+x1Ff0b!&3zkU-!X8 z8pfaTah)UpfS=|0H%|-5O{WP+7QqaHq%SEKBw&@-J4Rv`bl*Z~(cX}qV{bA}?EZf` bwqWHAt5q!O29}Vs00000NkvXXu0mjfRdii3 diff --git a/src/index.html b/src/index.html index b52747230..808c1638f 100644 --- a/src/index.html +++ b/src/index.html @@ -27,12 +27,6 @@ - diff --git a/tsconfig.json b/tsconfig.json index f6b73653b..713f1b257 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,8 +4,8 @@ "baseUrl": "./", "outDir": "./dist/out-tsc", "sourceMap": true, + "esModuleInterop": true, "declaration": false, - "downlevelIteration": true, "experimentalDecorators": true, "module": "es2022", "moduleResolution": "node",