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*/