diff --git a/CHANGELOG.md b/CHANGELOG.md index 542761f6..ae3d617d 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 f02497b0..234f6ae8 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 ac31c5f3..e87a848f 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 bcffae9f..146f9c46 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 4cbaee30..4e00c52d 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 c8e7c0fa..e0c9bf77 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 d790bf36..71d3b258 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 faf95a62..9b0deeaf 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 dccac6b3..24c08428 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 48fedd40..3f7267bf 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 7f102b0d..a111871d 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 87943a79..7d6b6bc5 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 09fdcd42..f89aa322 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 00000000..a28e40a0 --- /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 00000000..1c44877b --- /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 b7aea59a..7bacd263 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 3bbe2e4b..bf998a79 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 00000000..b8779298 --- /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 00000000..85b7980a --- /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 81966435..ec6ce879 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 d164ca56..c174277a 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 0d2762cd..bdbcb873 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 6ea5665f..406ef561 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 d97ca942..6c2803a8 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 ffdf3306..33db6814 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 842a631a..fda1c886 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 00000000..05ac1c36 --- /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 00000000..697e67e3 --- /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 d4874652..056194fd 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 5eebf99e..3c74f9b7 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 04966b8b..91ba0b4b 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 00000000..283c971a --- /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 74ff93ec..00000000 --- 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 f79d5ac1..00000000 --- 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 e4f5952e..00000000 --- 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 1e78ffa9..0220b006 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 93a9e8c2..c530ae99 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 88374aff..00000000 --- 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 00000000..c14851a3 --- /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 b6272e28..c0107f8b 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 87a4dba7..99c4f902 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 dbe23441..b4f33fe2 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 00000000..fccdf2ab Binary files /dev/null and b/src/assets/help/img/ais_shiptypes.png differ diff --git a/src/assets/help/index.html b/src/assets/help/index.html index 5aa77586..0ddb45a9 100644 --- a/src/assets/help/index.html +++ b/src/assets/help/index.html @@ -1516,19 +1516,41 @@

Vessels

- +
- 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 eb54a4fa..d15d2924 100644 Binary files a/src/assets/img/ais_buddy.png and b/src/assets/img/ais_buddy.png differ diff --git a/src/assets/img/ais_cargo.png b/src/assets/img/ais_cargo.png index d15d2924..e4b8ece9 100644 Binary files a/src/assets/img/ais_cargo.png and b/src/assets/img/ais_cargo.png differ diff --git a/src/assets/img/ais_highspeed.png b/src/assets/img/ais_highspeed.png new file mode 100644 index 00000000..634e5f10 Binary files /dev/null and b/src/assets/img/ais_highspeed.png differ diff --git a/src/assets/img/ais_passenger.png b/src/assets/img/ais_passenger.png index 634e5f10..8c1bc21b 100644 Binary files a/src/assets/img/ais_passenger.png and b/src/assets/img/ais_passenger.png differ diff --git a/src/assets/img/ais_tanker.png b/src/assets/img/ais_tanker.png index 8c1bc21b..d0f031e5 100644 Binary files a/src/assets/img/ais_tanker.png and b/src/assets/img/ais_tanker.png differ diff --git a/src/index.html b/src/index.html index b5274723..808c1638 100644 --- a/src/index.html +++ b/src/index.html @@ -27,12 +27,6 @@ - diff --git a/tsconfig.json b/tsconfig.json index f6b73653..713f1b25 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",