From 48db318069bad318edc1a6a3c77c70c78febbbfb Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Fri, 8 Nov 2024 11:33:30 +1030 Subject: [PATCH] 2.12-patch1 --- CHANGELOG.md | 9 +- package.json | 2 +- src/app/app.info.ts | 2 +- .../components/anchor-watch.component.css | 1 + .../components/anchor-watch.component.html | 8 +- .../components/ais/ais-properties-modal.ts | 4 +- .../components/charts/chartlist.html | 1 + .../components/charts/chartlist.ts | 11 +- .../components/charts/wms-dialog.ts | 362 ++++++++++++++++++ src/app/modules/skresources/index.ts | 1 + .../modules/weather/weather-data.component.ts | 2 +- .../modules/weather/weather-forecast-modal.ts | 10 +- src/app/types/resources/signalk.ts | 3 +- src/assets/help/index.html | 2 +- src/styles.scss | 9 +- 15 files changed, 408 insertions(+), 19 deletions(-) create mode 100644 src/app/modules/skresources/components/charts/wms-dialog.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f1153079..f30d838e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,17 @@ # CHANGELOG: Freeboard +### v2.12.1 +- **Added**: WMS sources to the types of chart sources that can be defined. +- **Fixed**: Date / time formating in weather forecast. (#193) +- **Fixed**: Fix issue where anchor watch controls are not visible on small screens. (#198) +- **Fixed**: Vessel Call sign not displayed correctly in AIS Properties screen. (#199) + + ### v2.12.0 - **Added**: Define chart sources from within the Charts List including: WMTS, Mapbox Style and TileJSON. - **Updated**: Measure distances < 1km are displayed in meters and < 0.5NM uses depth units (#194). -- **Updated**: Ensure weather forecast times use 24 hr format. (#193). +- **Updated**: Ensure weather forecast times use 24 hr format. (#193) - **Updated**: OpenSea Map min / max zoom levels. - **Updated**: OpenLayers v10. - **Fixed**: gybeAngle null value handling. diff --git a/package.json b/package.json index 06a564208..ee8dacc78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@signalk/freeboard-sk", - "version": "2.12.0", + "version": "2.12.1", "description": "Openlayers chart plotter implementation for Signal K", "keywords": [ "signalk-webapp", diff --git a/src/app/app.info.ts b/src/app/app.info.ts index 3006e53dd..4c4b56671 100644 --- a/src/app/app.info.ts +++ b/src/app/app.info.ts @@ -160,7 +160,7 @@ export class AppInfo extends Info { this.name = 'Freeboard-SK'; this.shortName = 'Freeboard'; this.description = `Signal K Chart Plotter.`; - this.version = '2.12.0'; + this.version = '2.12.1'; this.url = 'https://github.com/signalk/freeboard-sk'; this.logo = './assets/img/app_logo.png'; diff --git a/src/app/modules/alarms/components/anchor-watch.component.css b/src/app/modules/alarms/components/anchor-watch.component.css index 53bf6a933..95da16481 100644 --- a/src/app/modules/alarms/components/anchor-watch.component.css +++ b/src/app/modules/alarms/components/anchor-watch.component.css @@ -2,6 +2,7 @@ min-width: 150px; position: relative; height: 100%; + overflow-y: scroll; } .anchorwatch .title-block { diff --git a/src/app/modules/alarms/components/anchor-watch.component.html b/src/app/modules/alarms/components/anchor-watch.component.html index b86758b6b..201eed12a 100644 --- a/src/app/modules/alarms/components/anchor-watch.component.html +++ b/src/app/modules/alarms/components/anchor-watch.component.html @@ -34,16 +34,14 @@ > {{ radius === -1 ? '--' : feet ? mToFt(radius) : radius.toFixed(0) }} - - {{ feet ? 'ft' : 'm' }} diff --git a/src/app/modules/skresources/components/ais/ais-properties-modal.ts b/src/app/modules/skresources/components/ais/ais-properties-modal.ts index 32e9e5cf8..26796f504 100644 --- a/src/app/modules/skresources/components/ais/ais-properties-modal.ts +++ b/src/app/modules/skresources/components/ais/ais-properties-modal.ts @@ -221,10 +221,10 @@ export class AISPropertiesModal implements OnInit { } if (typeof v['communication'] !== 'undefined') { if (typeof v['communication']['callsignVhf'] !== 'undefined') { - this.vInfo.callsignVhf = v['communication']['callsignVhf']; + this.vInfo.callsignVhf = v['communication']['callsignVhf']['value']; } if (typeof v['communication']['callsignHf'] !== 'undefined') { - this.vInfo.callsignHf = v['communication']['callsignHf']; + this.vInfo.callsignHf = v['communication']['callsignHf']['value']; } } if (typeof v['navigation'] !== 'undefined') { diff --git a/src/app/modules/skresources/components/charts/chartlist.html b/src/app/modules/skresources/components/charts/chartlist.html index 2b6b37b7b..470993464 100644 --- a/src/app/modules/skresources/components/charts/chartlist.html +++ b/src/app/modules/skresources/components/charts/chartlist.html @@ -1,6 +1,7 @@
+ diff --git a/src/app/modules/skresources/components/charts/chartlist.ts b/src/app/modules/skresources/components/charts/chartlist.ts index 28119dd0b..7608a27e4 100644 --- a/src/app/modules/skresources/components/charts/chartlist.ts +++ b/src/app/modules/skresources/components/charts/chartlist.ts @@ -30,6 +30,7 @@ import { FBCharts, FBChart, FBResourceSelect } from 'src/app/types'; import { forkJoin, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { WMTSDialog } from './wmts-dialog'; +import { WMSDialog } from './wms-dialog'; import { JsonMapSourceDialog } from './jsonmapsource-dialog'; import { SignalKClient } from 'signalk-client-angular'; @@ -310,8 +311,8 @@ export class ChartListComponent { this.delete.emit({ id: id }); } - public addChartSource(type: 'wmts' | 'json') { - let dref: MatDialogRef; + public addChartSource(type: 'wms' | 'wmts' | 'json') { + let dref: MatDialogRef; if (type === 'wmts') { dref = this.dialog.open(WMTSDialog, { @@ -319,6 +320,12 @@ export class ChartListComponent { data: {} }); } + if (type === 'wms') { + dref = this.dialog.open(WMSDialog, { + disableClose: true, + data: {} + }); + } if (type === 'json') { dref = this.dialog.open(JsonMapSourceDialog, { disableClose: true, diff --git a/src/app/modules/skresources/components/charts/wms-dialog.ts b/src/app/modules/skresources/components/charts/wms-dialog.ts new file mode 100644 index 000000000..ad8c14868 --- /dev/null +++ b/src/app/modules/skresources/components/charts/wms-dialog.ts @@ -0,0 +1,362 @@ +import { Component, Inject } from '@angular/core'; +import { + MatDialogModule, + MatDialogRef, + MAT_DIALOG_DATA +} from '@angular/material/dialog'; +import { MatTreeModule, MatTreeNestedDataSource } from '@angular/material/tree'; +import { NestedTreeControl } from '@angular/cdk/tree'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { BehaviorSubject, of as observableOf } from 'rxjs'; +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 { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatInputModule } from '@angular/material/input'; +import { AppInfo } from 'src/app/app.info'; +import { SKChart } from 'src/app/modules/skresources/resource-classes'; +import { PipesModule } from 'src/app/lib/pipes'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { parseString } from 'xml2js'; +import { ChartProvider } from 'src/app/types'; + +interface LayerNode { + name: string; + title?: string; + description: string; + children?: LayerNode[]; + selected: boolean; + parent: LayerNode; +} + +/********* WMSDialog ********** + data: +***********************************/ +@Component({ + selector: 'wms-dialog', + standalone: true, + imports: [ + MatTreeModule, + MatCheckboxModule, + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + MatDialogModule, + MatProgressBarModule, + MatInputModule, + PipesModule + ], + template: ` +
+ + public + Add WMS Source + + + + + + @if (true) { + + WMS host. + + @if (txturl) { + + } + Enter url of the WMS host. + @if (txturl.invalid) { + WMS host is required! + } + + } @if (isFetching) { + + } @else { @if (errorMsg) { + Error retrieving capabilities from server! + } @else { +
+ + + + {{ node.name }} + + + +
+ + + {{ node.name }} + +
+
+ +
+
+
+
+ } } +
+ + + +
+ `, + styles: [ + ` + ._ap-wms { + } + ._ap-wms .key-label { + width: 150px; + font-weight: 500; + } + .wms-tree .mat-nested-tree-node div[role='group'] { + padding-left: 20px; + } + .wms-tree div[role='group'] > .mat-tree-node { + padding-left: 20px; + } + .wms-tree .tree-invisible { + display: none; + } + ` + ] +}) +export class WMSDialog { + protected isFetching = false; + protected fetchError = false; + protected errorMsg = ''; + protected selections: Array = []; + protected wmsBase: ChartProvider; + protected wmsSources: { [key: string]: ChartProvider } = {}; + protected hostUrl = ''; + + protected layerNodes: LayerNode[] = []; + nestedTreeControl: NestedTreeControl; + nestedDataSource: MatTreeNestedDataSource; + dataChange: BehaviorSubject = new BehaviorSubject( + [] + ); + private _getChildren = (node: LayerNode) => { + return observableOf(node.children); + }; + hasNestedChild = (_: number, nodeData: LayerNode) => { + return nodeData.children && nodeData.children.length !== 0; + }; + + constructor( + public app: AppInfo, + public dialogRef: MatDialogRef, + private http: HttpClient, + @Inject(MAT_DIALOG_DATA) public data: SKChart + ) { + this.nestedTreeControl = new NestedTreeControl( + this._getChildren + ); + this.nestedDataSource = new MatTreeNestedDataSource(); + + this.dataChange.subscribe((data) => (this.nestedDataSource.data = data)); + + this.dataChange.next([]); + } + + toggleSelection(checked: boolean, node: LayerNode) { + this.handleSelection(checked, node); + this.parseSelections(); + } + + private handleSelection(checked: boolean, node: LayerNode) { + node.selected = checked; + if (node.children) { + node.children.forEach((child) => { + this.handleSelection(checked, child); + }); + } + this.checkAllParents(node); + } + + private checkAllParents(node: LayerNode) { + if (node.parent) { + const descendants = this.nestedTreeControl.getDescendants(node.parent); + node.parent.selected = descendants.every((child) => child.selected); + this.checkAllParents(node.parent); + } + } + + private parseSelections() { + this.selections = []; + this.wmsSources = {}; + this.layerNodes.forEach((l: LayerNode) => { + this.getSelections(l); + }); + } + + private buildSource(l: LayerNode): ChartProvider { + const s = Object.assign({}, this.wmsBase); + s.name = l.name; + s.description = l.description; + s.layers = [l.name]; + return s; + } + + private getSelections(node: LayerNode) { + const selNode = (n: LayerNode) => { + if (n.selected) { + if (!this.selections.includes(n.name)) { + this.selections.push(n.name); + this.wmsSources[n.name] = this.buildSource(n); + } else { + if ( + this.wmsSources[n.name].description === this.wmsSources[n.name].name + ) { + this.wmsSources[n.name].description = n.description; + } + } + } + }; + selNode(node); + const descendants = this.nestedTreeControl.getDescendants(node); + descendants + .filter((l) => l.selected && !this.selections.includes(l.name)) + .forEach((l) => selNode(l)); + } + + handleSave() { + this.dialogRef.close(Object.values(this.wmsSources)); + } + + /** Make requests to WMS server */ + wmsGetCapabilities(wmsHost: string) { + this.selections = []; + this.errorMsg = ''; + + const url = wmsHost + `?request=getcapabilities&service=wms`; + this.isFetching = true; + this.http.get(url, { responseType: 'text' }).subscribe( + (res: string) => { + this.isFetching = false; + this.layerNodes = []; + if (res.indexOf(' { + this.isFetching = false; + this.fetchError = true; + this.errorMsg = err.message; + } + ); + } + + /** Parse WMSCapabilities.xml */ + parseCapabilities(xml: string, urlBase: string) { + this.wmsBase = { + name: '', + description: '', + type: 'WMS', + url: urlBase, + layers: [] + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parseString(xml, (err: Error, result: any) => { + if (err) { + this.errorMsg = 'ERROR parsing XML!'; + console.log('EROR parsing XML!', err); + } else { + if (!result.WMS_Capabilities && !result.WMT_MS_Capabilities) { + this.errorMsg = 'ERROR Unable to get Capabilities!'; + console.log('ERROR Unable to get Capabilities!', err); + } else { + this.getWMSLayers(result); + } + } + }); + } + + /** Retrieve the available layers from WMS Capabilities metadata */ + getWMSLayers( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json: { [key: string]: any } + ): ChartProvider[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let rootNode: any; + if (json.WMS_Capabilities) { + rootNode = json.WMS_Capabilities; + } else if (json.WMT_MS_Capabilities) { + rootNode = json.WMT_MS_Capabilities; + } + + if (!rootNode.Capability[0].Layer) { + return; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + rootNode.Capability[0].Layer.forEach((layer: any) => { + this.parselayer(layer, this.layerNodes); + }); + this.layerNodes.sort((a, b) => (a.name < b.name ? -1 : 1)); + this.dataChange.next(this.layerNodes); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parselayer(layer: any, cList: LayerNode[], parent: LayerNode = null) { + const node: LayerNode = { + name: layer['Name'] ? layer['Name'][0] : '', + description: layer['Abstract'] ? layer['Abstract'][0] : '', + selected: false, + parent: parent + }; + if (layer['Title']) { + node.title = layer['Title'][0]; + } + if (layer.Layer) { + node.children = []; + layer.Layer.forEach((l) => this.parselayer(l, node.children)); + } + cList.push(node); + } +} diff --git a/src/app/modules/skresources/index.ts b/src/app/modules/skresources/index.ts index a7b51a06d..bbe0de424 100644 --- a/src/app/modules/skresources/index.ts +++ b/src/app/modules/skresources/index.ts @@ -19,6 +19,7 @@ export * from './components/ais/aircraft-properties-modal'; export * from './components/charts/chartlist'; export * from './components/charts/chart-properties-dialog'; export * from './components/charts/wmts-dialog'; +export * from './components/charts/wms-dialog'; export * from './components/charts/jsonmapsource-dialog'; export * from './components/tracks/track-list-modal'; diff --git a/src/app/modules/weather/weather-data.component.ts b/src/app/modules/weather/weather-data.component.ts index a011042ff..07f36aadc 100644 --- a/src/app/modules/weather/weather-data.component.ts +++ b/src/app/modules/weather/weather-data.component.ts @@ -75,7 +75,7 @@ export interface WeatherData {
Time:
- {{ item.time.split(':').slice(0, 2).join(':') }} + {{ item.time }}
@if(item.description) { diff --git a/src/app/modules/weather/weather-forecast-modal.ts b/src/app/modules/weather/weather-forecast-modal.ts index b75ca4c51..14844a437 100644 --- a/src/app/modules/weather/weather-forecast-modal.ts +++ b/src/app/modules/weather/weather-forecast-modal.ts @@ -69,7 +69,7 @@ import { WeatherData, WeatherDataComponent } from './weather-data.component'; } @else { @if(!forecasts || forecasts.length === 0) {
{{ errorText }}
} @else { - + } } `, @@ -121,6 +121,8 @@ export class WeatherForecastModal implements OnInit { return val ? `${Convert.msecToKnots(val).toFixed(1)} kn` : '0.0'; } + private dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + private getForecast() { let path = '/meteo/freeboard-sk/forecasts'; if (this.app.data.weather.hasApi && this.app.data.vessels.self.position) { @@ -138,7 +140,11 @@ export class WeatherForecastModal implements OnInit { const forecastData: WeatherData = { wind: {} }; forecastData.description = v['description'] ?? ''; const d = new Date(v['date']); - forecastData.time = d ? `${d.getHours()}:${d.getMinutes()}:00` : ''; + forecastData.time = d + ? `${this.dayNames[d.getDay()]} ${d.getHours()}:${( + '00' + d.getMinutes() + ).slice(-2)}` + : ''; if (typeof v.outside?.temperature !== 'undefined') { forecastData.temperature = diff --git a/src/app/types/resources/signalk.ts b/src/app/types/resources/signalk.ts index eb420c3a8..62120bf13 100644 --- a/src/app/types/resources/signalk.ts +++ b/src/app/types/resources/signalk.ts @@ -74,8 +74,9 @@ export interface ChartResource { export interface ChartProvider { identifier?: string; name: string; + title?: string; description: string; - type: 'WMTS' | 'mapboxstyle'; + type: 'tilejson' | 'WMS' | 'WMTS' | 'mapboxstyle'; url: string; layers?: string[]; bounds?: number[]; diff --git a/src/assets/help/index.html b/src/assets/help/index.html index c7d0113cb..334fd40ac 100644 --- a/src/assets/help/index.html +++ b/src/assets/help/index.html @@ -1024,7 +1024,7 @@

layers Resources / Layers

  • Add Chart Source: Click add to add a new chart source - (i.e. WMTS, TileJSON, Mapbox Style). See the + (i.e. WMTS, WMS, TileJSON, Mapbox Style). See the diff --git a/src/styles.scss b/src/styles.scss index 59820ce22..78ec99926 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -62,8 +62,13 @@ b { font-weight: 500; } } /*xs*/ -@media only screen and (max-width: 599px), -only screen and (min-width: 600px) and (max-width: 959px), +@media only screen and (max-width: 599px) { + .weather-data .mat-horizontal-content-container { + padding: 0 !important; + } +} + +@media only screen and (min-width: 600px) and (max-width: 959px), /*md*/ screen and (min-width: 960px) and (max-width: 1279px), /*lg*/