From 3b6ef31ebca23ff58ec8a7f2bb4c47f8593ea4b3 Mon Sep 17 00:00:00 2001 From: panaaj <38519157+panaaj@users.noreply.github.com> Date: Thu, 24 Oct 2024 16:14:33 +1030 Subject: [PATCH] fixes --- CHANGELOG.md | 9 + helper/weather/openweather.ts | 6 +- helper/weather/weather-service.ts | 6 +- package.json | 6 +- src/app/app.component.html | 2 + src/app/app.info.ts | 19 +- src/app/app.messages.ts | 28 +- src/app/modules/map/fb-map.component.ts | 92 +++++-- src/app/modules/map/mapconfig.ts | 2 +- .../lib/resources/layer-aiswind.component.ts | 23 +- .../lib/resources/layer-charts.component.ts | 7 +- .../map/ol/lib/vectorLayerStyleFactory.ts | 22 +- .../components/charts/chartlist.html | 27 ++ .../components/charts/chartlist.ts | 77 +++++- .../components/charts/jsonmapsource-dialog.ts | 237 ++++++++++++++++ .../components/charts/wmts-dialog.ts | 257 ++++++++++++++++++ src/app/modules/skresources/index.ts | 2 + .../modules/skresources/resource-classes.ts | 3 +- .../modules/skresources/resources.service.ts | 21 +- .../modules/weather/weather-forecast-modal.ts | 3 +- src/app/types/resources/signalk.ts | 14 + src/assets/help/img/chart_list.png | Bin 16840 -> 25551 bytes src/assets/help/index.html | 11 + 23 files changed, 778 insertions(+), 96 deletions(-) create mode 100644 src/app/modules/skresources/components/charts/jsonmapsource-dialog.ts create mode 100644 src/app/modules/skresources/components/charts/wmts-dialog.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 292bf545..7f115307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # CHANGELOG: Freeboard +### 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**: OpenSea Map min / max zoom levels. +- **Updated**: OpenLayers v10. +- **Fixed**: gybeAngle null value handling. + ### v2.11.5 - **Fixed**: Issue when moving waypoint. diff --git a/helper/weather/openweather.ts b/helper/weather/openweather.ts index c5c787eb..c98f4b07 100644 --- a/helper/weather/openweather.ts +++ b/helper/weather/openweather.ts @@ -117,7 +117,11 @@ export class OpenWeather implements IWeatherService { fetchData = async (position: Position): Promise => { const url = this.getUrl(position); const response = await fetch(url); - return this.parseResponse(response as OWResponse); + if ('cod' in response) { + throw new Error(response.message); + } else { + return this.parseResponse(response as OWResponse); + } }; private parseResponse = (owData: OWResponse): ParsedResponse => { diff --git a/helper/weather/weather-service.ts b/helper/weather/weather-service.ts index 5a6f519a..c4bf2791 100644 --- a/helper/weather/weather-service.ts +++ b/helper/weather/weather-service.ts @@ -380,14 +380,13 @@ const fetchWeatherData = () => { server.debug( `*** Weather: Calling service API.....(attempt: ${retryCount})` ); - server.debug(`Position: ${JSON.stringify(pos.value)}`); server.debug(`*** Weather: polling weather provider.`); weatherService .fetchData(pos.value) .then((data) => { server.debug(`*** Weather: data received....`); - //server.debug(JSON.stringify(data)); + server.debug(JSON.stringify(data)); retryCount = 0; lastFetch = Date.now(); lastWake = Date.now(); @@ -419,7 +418,8 @@ const fetchWeatherData = () => { retryInterval / 1000 } sec)` ); - server.debug(err.message); + console.log(err.message); + server.setPluginError(err.message); // sleep and retry retryTimer = setTimeout(() => fetchWeatherData(), retryInterval); }); diff --git a/package.json b/package.json index 6bf5215b..06a56420 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@signalk/freeboard-sk", - "version": "2.11.5", + "version": "2.12.0", "description": "Openlayers chart plotter implementation for Signal K", "keywords": [ "signalk-webapp", @@ -79,8 +79,8 @@ "karma-jasmine-html-reporter": "^1.5.0", "ng-packagr": "^18.1.0", "ngeohash": "^0.6.3", - "ol": "^9.0.0", - "ol-mapbox-style": "^12.3.3", + "ol": "^10.2.1", + "ol-mapbox-style": "^12.3.5", "pmtiles": "^2.7.0", "prettier": "^2.5.1", "prettier-plugin-organize-attributes": "^0.0.5", diff --git a/src/app/app.component.html b/src/app/app.component.html index 73a4d14f..221b42d7 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -36,6 +36,7 @@ save  Save to GPX + @if(app.config.resources.paths.length !== 0) {
@@ -534,6 +535,7 @@ [charts]="app.data.charts" [selectedCharts]="app.config.selections.charts" (select)="skres.chartSelected()" + (delete)="skres.showChartDelete($event)" (orderChange)="skres.chartOrder()" (refresh)="skres.getCharts()" (closed)="displayLeftMenu()" diff --git a/src/app/app.info.ts b/src/app/app.info.ts index 9489d71a..3006e53d 100644 --- a/src/app/app.info.ts +++ b/src/app/app.info.ts @@ -51,8 +51,8 @@ export const OSM = [ name: 'Sea Map', description: 'Open Sea Map', url: 'https://tiles.openseamap.org/seamark/{z}/{x}/{y}.png', - minzoom: 12, - maxzoom: 18, + minzoom: 1, + maxzoom: 24, bounds: [-180, -90, 180, 90], type: 'tilelayer' }), @@ -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.11.5'; + this.version = '2.12.0'; this.url = 'https://github.com/signalk/freeboard-sk'; this.logo = './assets/img/app_logo.png'; @@ -657,9 +657,16 @@ export class AppInfo extends Info { ? `${value.toFixed(1)} m` : `${Convert.metersToFeet(value).toFixed(1)} ft`; } else { - return this.config.units.distance !== 'ft' - ? `${(value / 1000).toFixed(1)} km` - : `${Convert.kmToNauticalMiles(value / 1000).toFixed(1)} NM`; + if (this.config.units.distance !== 'ft') { + return value < 1000 + ? `${value.toFixed(0)} m` + : `${(value / 1000).toFixed(1)} km`; + } else { + const nm = Convert.kmToNauticalMiles(value / 1000); + return nm < 0.5 + ? this.formatValueForDisplay(value, 'm', true) + : `${nm.toFixed(1)} NM`; + } } } else if (sourceUnits === 'm/s') { switch (this.config.units.speed) { diff --git a/src/app/app.messages.ts b/src/app/app.messages.ts index 2acb08a5..ec1426dc 100644 --- a/src/app/app.messages.ts +++ b/src/app/app.messages.ts @@ -1,23 +1,21 @@ const WHATS_NEW = [ - { + /*{ type: 'signalk-server-node', - title: 'AIS Vessels', + title: 'Chart Sources', message: ` - The following new features have been added: + Initial support for defining the following chart sources directly + from the Chart List: +
  • WMTS (WebMap Tile Server)
  • +
  • TileJSON
  • +
  • Mapbox Style
  • +
    + Note: This functionality requires an upcoming release of + Signal K Server.
     
    -
  • Ability to Flag vessels.
  • -
  • COG line is now displayed for AIS vessels.
  • + See HELP + for more details. ` - }, - { - type: 'signalk-server-node', - title: 'Racing Support', - message: ` - This release contains initial support for navigation.racing paths. -
     
    -
  • Display start line.
  • - ` - } + }*/ ]; export const WELCOME_MESSAGES = { diff --git a/src/app/modules/map/fb-map.component.ts b/src/app/modules/map/fb-map.component.ts index b4a7bcd4..ab943354 100644 --- a/src/app/modules/map/fb-map.component.ts +++ b/src/app/modules/map/fb-map.component.ts @@ -91,6 +91,7 @@ import { SKNotification } from 'src/app/types'; import { S57Service } from './ol/lib/s57.service'; +import { Position as SKPosition } from '@signalk/server-api'; interface IResource { id: string; @@ -1231,15 +1232,24 @@ export class FBMapComponent implements OnInit, OnDestroy { this.app.data.vessels.self.performance.beatAngle ?? Math.PI / 4 ); - const ga_deg = Convert.radiansToDegrees( - this.app.data.vessels.self.performance.gybeAngle ?? Math.PI / 9 - ); + let ga_deg: number; + let ga_diff: number; + if ( + typeof this.app.data.vessels.self.performance.gybeAngle === 'number' + ) { + ga_deg = Convert.radiansToDegrees( + this.app.data.vessels.self.performance.gybeAngle + ); + ga_diff = Math.abs(180 - ga_deg); + } - const destInTarget = - destUpwind && - Math.abs( - Angle.difference(this.app.data.navData.bearing.value, twd_deg) - ) < ba_deg; + const destInTarget = destUpwind + ? Math.abs( + Angle.difference(this.app.data.navData.bearing.value, twd_deg) + ) < ba_deg + : Math.abs( + Angle.difference(this.app.data.navData.bearing.value, twd_inv) + ) < (ga_diff ?? 0); const dtg = this.app.config.units.distance === 'm' @@ -1265,7 +1275,7 @@ export class FBMapComponent implements OnInit, OnDestroy { this.dfeat.navData.position, [bapt2.longitude, bapt2.latitude] ]; - } else { + } else if (typeof ga_deg === 'number') { const gapt1 = computeDestinationPoint( this.dfeat.navData.position, dtg, @@ -1287,30 +1297,58 @@ export class FBMapComponent implements OnInit, OnDestroy { vl.targetAngle = markLines; // vessel laylines - if (destInTarget && destUpwind) { - // Vector angles + if (destInTarget) { const hbd_deg = Angle.difference( twd_deg, this.app.data.navData.bearing.value ); - const C_RAD = Convert.degreesToRadians(ba_deg - hbd_deg); - const B_RAD = Convert.degreesToRadians(ba_deg + hbd_deg); - const A_RAD = Math.PI - (B_RAD + C_RAD); // Vector lengths - const b = (dtg * Math.sin(B_RAD)) / Math.sin(A_RAD); - const c = (dtg * Math.sin(C_RAD)) / Math.sin(A_RAD); + let b: number; + let c: number; // intersection points - const ipts = computeDestinationPoint( - this.app.data.vessels.active.position, - b, - Angle.add(twd_deg, ba_deg) - ); - const iptp = computeDestinationPoint( - this.app.data.vessels.active.position, - c, - Angle.add(twd_deg, 0 - ba_deg) - ); - + let ipts: SKPosition; + let iptp: SKPosition; + + if (destUpwind) { + // Vector angles + const C_RAD = Convert.degreesToRadians(ba_deg - hbd_deg); + const B_RAD = Convert.degreesToRadians(ba_deg + hbd_deg); + const A_RAD = Math.PI - (B_RAD + C_RAD); + b = (dtg * Math.sin(B_RAD)) / Math.sin(A_RAD); + c = (dtg * Math.sin(C_RAD)) / Math.sin(A_RAD); + // intersection points + ipts = computeDestinationPoint( + this.app.data.vessels.active.position, + b, + Angle.add(twd_deg, ba_deg) + ); + iptp = computeDestinationPoint( + this.app.data.vessels.active.position, + c, + Angle.add(twd_deg, 0 - ba_deg) + ); + } else { + // downwind + if (markLines.length !== 0 && typeof ga_diff === 'number') { + // Vector angles + const C_RAD = Convert.degreesToRadians(ga_diff - hbd_deg); + const B_RAD = Convert.degreesToRadians(ga_diff + hbd_deg); + const A_RAD = Math.PI - (B_RAD + C_RAD); + b = (dtg * Math.sin(B_RAD)) / Math.sin(A_RAD); + c = (dtg * Math.sin(C_RAD)) / Math.sin(A_RAD); + // intersection points + ipts = computeDestinationPoint( + this.app.data.vessels.active.position, + b, + Angle.add(twd_deg, ga_diff) + ); + iptp = computeDestinationPoint( + this.app.data.vessels.active.position, + c, + Angle.add(twd_deg, 0 - ga_diff) + ); + } + } vl.laylines = { port: [ [ diff --git a/src/app/modules/map/mapconfig.ts b/src/app/modules/map/mapconfig.ts index b3da92c4..e31a0c40 100644 --- a/src/app/modules/map/mapconfig.ts +++ b/src/app/modules/map/mapconfig.ts @@ -719,7 +719,7 @@ export const targetAngleStyle = new Style({ stroke: new Stroke({ color: 'gray', width: 1, - lineDash: [5, 5] + lineDash: [15, 5, 3, 5] }) }); diff --git a/src/app/modules/map/ol/lib/resources/layer-aiswind.component.ts b/src/app/modules/map/ol/lib/resources/layer-aiswind.component.ts index 67c4b73f..9e366e5d 100644 --- a/src/app/modules/map/ol/lib/resources/layer-aiswind.component.ts +++ b/src/app/modules/map/ol/lib/resources/layer-aiswind.component.ts @@ -6,8 +6,8 @@ import { SimpleChanges } from '@angular/core'; import { Feature } from 'ol'; -import { Style, Icon, Stroke } from 'ol/style'; -import { LineString, Point } from 'ol/geom'; +import { Style, Stroke } from 'ol/style'; +import { LineString } from 'ol/geom'; import { fromLonLat } from 'ol/proj'; import { MapComponent } from '../map.component'; import { AISBaseLayerComponent } from './ais-base.component'; @@ -75,10 +75,8 @@ export class AISWindLayerComponent extends AISBaseLayerComponent { } const f = new Feature(new LineString(v)); f.setId('wind-' + id); - //const s = this.buildStyle('').clone(); - //f.setStyle(this.setRotation(s, target.orientation)); f.setStyle(this.buildVectorStyle()); - this.source.addFeature(f); + this.source?.addFeature(f); } } @@ -93,19 +91,6 @@ export class AISWindLayerComponent extends AISBaseLayerComponent { }); } - /*private buildStyle(label?: string) { - return new Style({ - image: new Icon({ - src: './assets/img/ais_flag.svg', - rotateWithView: true, - scale: 0.2, - anchor: [27, 187], - anchorXUnits: 'pixels', - anchorYUnits: 'pixels' - }) - }); - }*/ - // reload all Features from this.targets override onReloadTargets() { this.extractKeys(this.targets).forEach((id) => { @@ -117,7 +102,7 @@ export class AISWindLayerComponent extends AISBaseLayerComponent { override onUpdateTargets(ids: Array) { ids.forEach((id: string) => { if (id.includes(this.targetContext)) { - const f = this.source.getFeatureById('wind-' + id) as Feature; + const f = this.source?.getFeatureById('wind-' + id) as Feature; if (this.okToRenderTarget(id)) { if (this.targets.has(id)) { if (f) { diff --git a/src/app/modules/map/ol/lib/resources/layer-charts.component.ts b/src/app/modules/map/ol/lib/resources/layer-charts.component.ts index f00b6b09..3eca59ba 100644 --- a/src/app/modules/map/ol/lib/resources/layer-charts.component.ts +++ b/src/app/modules/map/ol/lib/resources/layer-charts.component.ts @@ -192,7 +192,7 @@ export class FreeboardChartLayerComponent : charts[i][1].minZoom; const maxZ = charts[i][1].maxZoom; - if (charts[i][1].type === 'mapstyleJSON') { + if (charts[i][1].type.toLowerCase() === 'mapboxstyle') { const lg = new LayerGroup({ zIndex: this.zIndex + parseInt(i) }); @@ -208,7 +208,7 @@ export class FreeboardChartLayerComponent charts[i][1] ); layer = styleFactory.CreateLayer(); - styleFactory.ApplyStyle(layer as VectorTileLayer); + styleFactory.ApplyStyle(layer as VectorTileLayer); layer.setZIndex(this.zIndex + parseInt(i)); } else { // raster tile @@ -266,7 +266,8 @@ export class FreeboardChartLayerComponent ) { // tileJSON source = new TileJSON({ - url: charts[i][1].url + url: charts[i][1].url, + crossOrigin: 'anonymous' }); } else { // XYZ tilelayer diff --git a/src/app/modules/map/ol/lib/vectorLayerStyleFactory.ts b/src/app/modules/map/ol/lib/vectorLayerStyleFactory.ts index 7ce0df18..7cd52ea2 100644 --- a/src/app/modules/map/ol/lib/vectorLayerStyleFactory.ts +++ b/src/app/modules/map/ol/lib/vectorLayerStyleFactory.ts @@ -24,8 +24,8 @@ export abstract class VectorLayerStyler { this.MaxZ = chart.maxZoom; } - public abstract ApplyStyle(vectorLayer: VectorTileLayer); - public abstract CreateLayer(): VectorTileLayer; + public abstract ApplyStyle(vectorLayer: VectorTileLayer); + public abstract CreateLayer(): VectorTileLayer; } class S57LayerStyler extends VectorLayerStyler { @@ -33,7 +33,7 @@ class S57LayerStyler extends VectorLayerStyler { super(chart); } - public CreateLayer(): VectorTileLayer { + public CreateLayer(): VectorTileLayer { let extent: Extent = null; if (this.chart.bounds && this.chart.bounds.length > 0) { extent = transformExtent(this.chart.bounds, 'EPSG:4326', 'EPSG:3857'); @@ -41,7 +41,7 @@ class S57LayerStyler extends VectorLayerStyler { return new VectorTileLayer({ declutter: true, extent: extent }); } - public ApplyStyle(vectorLayer: VectorTileLayer) { + public ApplyStyle(vectorLayer: VectorTileLayer) { const source = new VectorTileSource({ url: this.chart.url, minZoom: this.chart.minZoom, @@ -52,7 +52,7 @@ class S57LayerStyler extends VectorLayerStyler { const style = new S57Style(this.s57service); - vectorLayer.setSource(source); + vectorLayer.setSource(source as never); vectorLayer.setPreload(0); vectorLayer.setStyle(style.getStyle); vectorLayer.setMinZoom(this.chart.minZoom); @@ -70,7 +70,7 @@ class DefaultLayerStyler extends VectorLayerStyler { super(chart); } - public CreateLayer(): VectorTileLayer { + public CreateLayer(): VectorTileLayer { return new VectorTileLayer(); } @@ -87,7 +87,7 @@ class DefaultLayerStyler extends VectorLayerStyler { }); } - public ApplyStyle(vectorLayer: VectorTileLayer) { + public ApplyStyle(vectorLayer: VectorTileLayer) { // mbtiles source const source = new VectorTileSource({ url: this.chart.url, @@ -99,7 +99,7 @@ class DefaultLayerStyler extends VectorLayerStyler { }) }); - vectorLayer.setSource(source); + vectorLayer.setSource(source as never); vectorLayer.setPreload(0); vectorLayer.setStyle(this.applyVectorStyle); vectorLayer.setMinZoom(this.MinZ); @@ -112,11 +112,11 @@ class PMLayerStyler extends DefaultLayerStyler { super(chart); } - public CreateLayer(): VectorTileLayer { + public CreateLayer(): VectorTileLayer { return new VectorTileLayer({ declutter: true }); } - public ApplyStyle(vectorLayer: VectorTileLayer) { + public ApplyStyle(vectorLayer: VectorTileLayer) { vectorLayer.set('declutter', true); const tiles = new pmtiles.PMTiles(this.chart.url); @@ -154,7 +154,7 @@ class PMLayerStyler extends DefaultLayerStyler { tileLoadFunction: loader }); - vectorLayer.setSource(source); + vectorLayer.setSource(source as never); vectorLayer.setPreload(0); vectorLayer.setStyle(this.applyVectorStyle); vectorLayer.setMinZoom(this.chart.minZoom); diff --git a/src/app/modules/skresources/components/charts/chartlist.html b/src/app/modules/skresources/components/charts/chartlist.html index 6206982e..2b6b37b7 100644 --- a/src/app/modules/skresources/components/charts/chartlist.html +++ b/src/app/modules/skresources/components/charts/chartlist.html @@ -1,4 +1,10 @@
    + + + +
    @@ -93,6 +99,15 @@ import_export Re-order + + } @@ -140,6 +155,18 @@
    + @if(r[1].source && r[1].source === 'resources-provider') { +
    + +
    + }
    + + + + + Map Server host. + + @if (txturl) { + + } + Enter url of the Map Server. + @if (txturl.invalid) { + Map server host url is required! + } + + @if (isFetching) { + + } @else { @if (errorMsg) { + Error retrieving data from server! + } @else { +
    + @if (provider) { +
    +
    +
    +
    Source:
    +
    {{ details.type }}
    +
    +
    +
    Name:
    +
    {{ details.name }}
    +
    +
    +
    Version:
    +
    {{ details.version }}
    +
    +
    +
    Layers:
    +
    + @for (l of details.layers; track l) { +
    {{ l }}
    + } +
    +
    +
    + } +
    + } } +
    + + + +
    + `, + styles: [ + ` + ._ap-mapbox { + } + ._ap-mapbox .row { + display: flex; + } + ._ap-mapbox .label { + width: 70px; + font-weight: 500; + } + ._ap-mapbox .value { + flex: 1 1 auto; + } + ` + ] +}) +export class JsonMapSourceDialog { + protected isFetching = false; + protected fetchError = false; + protected errorMsg = ''; + protected hostUrl = ''; + protected provider!: ChartProvider; + protected details: { + type: string; + name: string; + version: string; + layers: string[]; + }; + + constructor( + public app: AppInfo, + public dialogRef: MatDialogRef, + private http: HttpClient, + @Inject(MAT_DIALOG_DATA) public data: SKChart + ) {} + + handleSave() { + this.dialogRef.close([this.provider]); + } + + /** Fetch the mapbox JSON file contents */ + getJsonFile(uri: string) { + this.errorMsg = ''; + this.fetchError = false; + this.isFetching = true; + this.provider = undefined; + this.http.get(uri).subscribe( + (res: TileJson | MapboxStyle) => { + this.isFetching = false; + if (!res.name) { + this.errorMsg = 'Invalid response received!'; + } else { + const c = this.parseFileContents(res, uri); + if (c) { + this.provider = c as ChartProvider; + this.details = { + type: (res as TileJson).tilejson ? 'TileJSON' : 'Mapbox Style', + name: res.name, + version: (res as MapboxStyle).version + ? (res as MapboxStyle).version + : (res as TileJson).tilejson, + layers: (res as MapboxStyle).layers + ? (res as MapboxStyle).layers.map((l) => l.id) + : (res as TileJson).vector_layers + ? (res as TileJson).vector_layers.map((l) => l.id) + : [] + }; + } else { + this.fetchError = true; + this.errorMsg = 'Invalid file contents!'; + } + } + }, + (err: HttpErrorResponse) => { + this.isFetching = false; + this.fetchError = true; + this.errorMsg = err.message; + } + ); + } + + parseFileContents(json: TileJson | MapboxStyle, uri: string) { + if ('tilejson' in json) { + return { + name: json.name ?? '', + description: json.description ?? '', + type: 'tilejson', + url: uri + }; + } else if ('version' in json && 'sources' in json && 'layers' in json) { + return { + name: json.name ?? '', + description: '', + type: 'mapboxstyle', + url: uri + }; + } else { + return undefined; + } + } +} diff --git a/src/app/modules/skresources/components/charts/wmts-dialog.ts b/src/app/modules/skresources/components/charts/wmts-dialog.ts new file mode 100644 index 00000000..e153ce55 --- /dev/null +++ b/src/app/modules/skresources/components/charts/wmts-dialog.ts @@ -0,0 +1,257 @@ +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 { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatInputModule } from '@angular/material/input'; +import { MatListModule, MatSelectionListChange } from '@angular/material/list'; +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'; + +/********* WMTSDialog ********** + data: +***********************************/ +@Component({ + selector: 'wmts-dialog', + standalone: true, + imports: [ + MatTooltipModule, + MatIconModule, + MatCardModule, + MatButtonModule, + MatToolbarModule, + MatDialogModule, + MatProgressBarModule, + MatInputModule, + MatListModule, + PipesModule + ], + template: ` +
    + + public + Add WMTS Source + + + + + + @if (true) { + + WMTS host. + + @if (txturl) { + + } + Enter url of the WMTS host. + @if (txturl.invalid) { + WMTS host is required! + } + + } @if (isFetching) { + + } @else { @if (errorMsg) { + Error retrieving capabilities from server! + } @else { +
    + @if (wmtsLayers.length > 0) { +
    + + @for(layer of wmtsLayers; track layer; let idx = $index) { + + {{ layer.name }} + {{ layer.description }} + + } + +
    +

    + Selected: {{ wlayers.selectedOptions.selected.length }} of + {{ wmtsLayers.length }} +

    + } +
    + } } +
    + + + +
    + `, + styles: [ + ` + ._ap-wmts { + } + ._ap-wmts .key-label { + width: 150px; + font-weight: 500; + } + ` + ] +}) +export class WMTSDialog { + protected isFetching = false; + protected fetchError = false; + protected errorMsg = ''; + protected wmtsLayers: Array = []; + protected selections: Array = []; + protected selectionInfo: Array<{ name: string; description: string }> = []; + protected hostUrl = ''; + + constructor( + public app: AppInfo, + public dialogRef: MatDialogRef, + private http: HttpClient, + @Inject(MAT_DIALOG_DATA) public data: SKChart + ) {} + + handleSelection(e: MatSelectionListChange) { + this.selections = e.source.selectedOptions.selected.map((opt) => opt.value); + } + + handleSave() { + const sources: Array = this.selections.map( + (layerIdx) => this.wmtsLayers[layerIdx] + ); + this.dialogRef.close(sources); + } + + /** Make requests to WMTS server */ + wmtsGetCapabilities(wmtsHost: string) { + this.selections = []; + this.selectionInfo = []; + this.wmtsLayers = []; + this.errorMsg = ''; + + const url = wmtsHost + `?request=GetCapabilities&service=wmts`; + this.isFetching = true; + this.http.get(url, { responseType: 'text' }).subscribe( + (res: string) => { + this.isFetching = false; + if (res.indexOf(' { + this.isFetching = false; + this.fetchError = true; + this.errorMsg = err.message; + } + ); + } + + /** Parse WMTSCapabilities.xml */ + parseCapabilities(xml: string, urlBase: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parseString(xml, (err: Error, result: any) => { + if (err) { + this.errorMsg = 'ERROR parsing XML!'; + console.log('ERROR parsing XML!', err); + } else { + this.wmtsLayers = this.getWMTSLayers(result, urlBase).sort((a, b) => + a.name < b.name ? -1 : 1 + ); + } + }); + } + + /** Retrieve the available layers from WMTS Capabilities metadata */ + getWMTSLayers( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json: { [key: string]: any }, + urlBase: string + ): ChartProvider[] { + const maps: ChartProvider[] = []; + if (!json.Capabilities.Contents[0].Layer) { + return maps; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json.Capabilities.Contents[0].Layer.forEach((layer: any) => { + const ch = this.parseLayerEntry(layer, urlBase); + if (ch) { + maps.push(ch); + } + }); + return maps; + } + + /** Parse WMTS layer entry */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + parseLayerEntry(layer: any, urlBase: string): ChartProvider | null { + if ( + layer['ows:Identifier'] && + Array.isArray(layer['ows:Identifier']) && + layer['ows:Identifier'].length > 0 + ) { + const l: ChartProvider = { + name: layer['ows:Title'] ? layer['ows:Title'][0] : '', + description: layer['ows:Abstract'] ? layer['ows:Abstract'][0] : '', + type: 'WMTS', + url: `${urlBase}`, + layers: [layer['ows:Identifier'][0]] + }; + if ( + layer['ows:WGS84BoundingBox'] && + layer['ows:WGS84BoundingBox'].length > 0 + ) { + l.bounds = [ + Number( + layer['ows:WGS84BoundingBox'][0]['ows:LowerCorner'][0].split(' ')[0] + ), + Number( + layer['ows:WGS84BoundingBox'][0]['ows:LowerCorner'][0].split(' ')[1] + ), + Number( + layer['ows:WGS84BoundingBox'][0]['ows:UpperCorner'][0].split(' ')[0] + ), + Number( + layer['ows:WGS84BoundingBox'][0]['ows:UpperCorner'][0].split(' ')[1] + ) + ]; + } + if (layer['Format'] && layer['Format'].length > 0) { + const f = layer['Format'][0]; + l.format = f.indexOf('jpg') !== -1 ? 'jpg' : 'png'; + } else { + l.format = 'png'; + } + return l; + } else { + return null; + } + } +} diff --git a/src/app/modules/skresources/index.ts b/src/app/modules/skresources/index.ts index 283c971a..a7b51a06 100644 --- a/src/app/modules/skresources/index.ts +++ b/src/app/modules/skresources/index.ts @@ -18,6 +18,8 @@ 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/jsonmapsource-dialog'; export * from './components/tracks/track-list-modal'; diff --git a/src/app/modules/skresources/resource-classes.ts b/src/app/modules/skresources/resource-classes.ts index 16e955bc..061a7f10 100644 --- a/src/app/modules/skresources/resource-classes.ts +++ b/src/app/modules/skresources/resource-classes.ts @@ -127,6 +127,7 @@ export class SKChart { maxZoom = 24; type: string; url: string; + source: string; constructor(chart?: ChartResource) { this.identifier = chart?.identifier ? chart.identifier : undefined; @@ -145,7 +146,7 @@ export class SKChart { typeof chart?.scale !== 'undefined' && !isNaN(chart?.scale) ? chart.scale : this.scale; - this.url = chart?.url ? chart.url : undefined; + this.source = chart?.$source ?? undefined; } } diff --git a/src/app/modules/skresources/resources.service.ts b/src/app/modules/skresources/resources.service.ts index 2b1a24f4..bf523dd5 100644 --- a/src/app/modules/skresources/resources.service.ts +++ b/src/app/modules/skresources/resources.service.ts @@ -214,7 +214,7 @@ export class SKResources { * @param collection The resource collection to which the resource belongs e.g. routes, waypoints, etc. * @param id Resource identifier */ - public deleteResource(collection: string, id: string) { + public deleteResource(collection: string, id: string, refresh?: boolean) { this.signalk.api .delete(this.app.skApiVersion, `/resources/${collection}/${id}`) .subscribe( @@ -226,6 +226,9 @@ export class SKResources { const idx = this.app.config.selections[collection].indexOf(id); this.app.config.selections[collection].splice(idx, 1); } + if (refresh && collection === 'charts') { + this.getCharts(); + } }, (err: HttpErrorResponse) => { if (err.status && err.status === 401) { @@ -815,6 +818,22 @@ export class SKResources { } } + // ** Confirm Chart Deletion ** + showChartDelete(e: { id: string }) { + this.app + .showConfirm( + 'Do you want to delete this Chart source?\n', + 'Delete Chart:', + 'YES', + 'NO' + ) + .subscribe((result: { ok: boolean }) => { + if (result && result.ok) { + this.deleteResource('charts', e.id, true); + } + }); + } + OSMCharts = [].concat(OSM); // ** get charts from sk server getCharts(apiVersion = this.app.config.chartApi ?? 1) { diff --git a/src/app/modules/weather/weather-forecast-modal.ts b/src/app/modules/weather/weather-forecast-modal.ts index 757b0894..b75ca4c5 100644 --- a/src/app/modules/weather/weather-forecast-modal.ts +++ b/src/app/modules/weather/weather-forecast-modal.ts @@ -137,7 +137,8 @@ export class WeatherForecastModal implements OnInit { .forEach((v: any) => { const forecastData: WeatherData = { wind: {} }; forecastData.description = v['description'] ?? ''; - forecastData.time = new Date(v['date']).toLocaleTimeString() ?? ''; + const d = new Date(v['date']); + forecastData.time = d ? `${d.getHours()}:${d.getMinutes()}:00` : ''; 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 77fbe87f..eb420c3a 100644 --- a/src/app/types/resources/signalk.ts +++ b/src/app/types/resources/signalk.ts @@ -64,8 +64,22 @@ export interface ChartResource { scale?: number; url?: string; layers?: string[]; + $source?: string; //v1 tilemapUrl?: string; // replaced by url chartLayers?: string[]; // replaced by layers serverType?: string; // replaced by type } + +export interface ChartProvider { + identifier?: string; + name: string; + description: string; + type: 'WMTS' | 'mapboxstyle'; + url: string; + layers?: string[]; + bounds?: number[]; + minzoom?: number; + maxzoom?: number; + format?: string; +} diff --git a/src/assets/help/img/chart_list.png b/src/assets/help/img/chart_list.png index b6d7b338d41400e1c63e4e4bd3c307456146caea..d42bbf2648f8b7ac6113b53b53ba7d8c287f9a49 100644 GIT binary patch literal 25551 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 literal 16840 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

    8yn>vY#l+dz;4lQs2Kii}J#J2^Ys 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 Mr+79^_S(Jw2F^vIP5=M^ diff --git a/src/assets/help/index.html b/src/assets/help/index.html index 5cf2a379..c7d0113c 100644 --- a/src/assets/help/index.html +++ b/src/assets/help/index.html @@ -1021,6 +1021,17 @@

    layers Resources / Layers

    of the listed charts. The bounding rectangle of each chart will be overlayed on the current map display. +
  • + Add Chart Source: Click + add to add a new chart source + (i.e. WMTS, TileJSON, Mapbox Style). See the + + Freeboard-SK Wiki + + for more details. +
  • Properties: Click info_outline to display the