├── src ├── assets │ ├── .gitkeep │ ├── img │ │ ├── note.png │ │ ├── anchor.png │ │ ├── app_logo.png │ │ ├── alarms │ │ │ └── meteo.png │ │ ├── anchor-limit.png │ │ ├── ap │ │ │ └── autopilot.png │ │ ├── sar_active.png │ │ ├── vessels │ │ │ ├── self.png │ │ │ ├── self_blur.png │ │ │ ├── self_fixed.png │ │ │ ├── ais_flag.svg │ │ │ ├── ais_buddy.svg │ │ │ ├── ais_cargo.svg │ │ │ ├── ais_other.svg │ │ │ ├── ais_active.svg │ │ │ ├── ais_tanker.svg │ │ │ ├── ais_special.svg │ │ │ ├── ais_inactive.svg │ │ │ ├── ais_highspeed.svg │ │ │ ├── ais_passenger.svg │ │ │ └── ais_self.svg │ │ ├── anchor-radius.png │ │ ├── aircraft_active.png │ │ ├── aircraft_inactive.png │ │ ├── weather_station.png │ │ ├── waypoints │ │ │ ├── waypoint.png │ │ │ ├── marker-blue.png │ │ │ ├── marker-green.png │ │ │ ├── start-pin.svg │ │ │ └── start-boat.svg │ │ ├── anchor-radius-raised.png │ │ ├── ob │ │ │ ├── alarm-emergency-iec.svg │ │ │ ├── heading-h-up.svg │ │ │ ├── cent-off-iec.svg │ │ │ ├── warning-acknowledged-iec.svg │ │ │ ├── alarm-acknowledged-iec.svg │ │ │ ├── warning-rectified-iec.svg │ │ │ ├── alarm-arrival.svg │ │ │ ├── alerts-active.svg │ │ │ ├── heading-n-up.svg │ │ │ ├── alarm-fire.svg │ │ │ ├── cent-iec.svg │ │ │ ├── warning-unack-iec.svg │ │ │ ├── alarm-aground.svg │ │ │ ├── navigation-route.svg │ │ │ ├── alert-list.svg │ │ │ ├── sound-unavailable-fill.svg │ │ │ ├── chart-display-settings-iec.svg │ │ │ ├── warning-silenced-iec.svg │ │ │ ├── alarm-unack-iec.svg │ │ │ ├── alarm-mob.svg │ │ │ ├── alarm-abandon.svg │ │ │ ├── route-planning.svg │ │ │ ├── alarm-depth.svg │ │ │ ├── route-export.svg │ │ │ ├── route-import.svg │ │ │ ├── command-autopilot.svg │ │ │ ├── sound-high-fill.svg │ │ │ ├── sound-off-fill.svg │ │ │ ├── alarm-silenced-iec.svg │ │ │ └── route.svg │ │ ├── wind │ │ │ ├── awa.svg │ │ │ └── twd.svg │ │ └── atons │ │ │ ├── basestation.svg │ │ │ ├── real-starboard.svg │ │ │ ├── virtual-starboard.svg │ │ │ ├── real-port.svg │ │ │ └── virtual-port.svg │ ├── sound │ │ ├── ding.mp3 │ │ └── woop.mp3 │ ├── help │ │ ├── favicon.ico │ │ ├── img │ │ │ ├── alarms.png │ │ │ ├── navdata.png │ │ │ ├── screen.png │ │ │ ├── ais_list.png │ │ │ ├── autopilot.png │ │ │ ├── playback.png │ │ │ ├── anchor_alarm.png │ │ │ ├── anchor_watch.png │ │ │ ├── trail2route.png │ │ │ ├── vessel_lines.png │ │ │ ├── ais_properties.png │ │ │ ├── ais_shiptypes.png │ │ │ ├── anchor_circle.png │ │ │ ├── anchor_watch_2.png │ │ │ ├── anchor_watch_3.png │ │ │ ├── settings_paths.png │ │ │ ├── anchor_alarm_ack.png │ │ │ ├── anchor_watch_rode.png │ │ │ ├── course_settings.png │ │ │ ├── settings_signalk.png │ │ │ ├── anchor_alarm_muted.png │ │ │ ├── anchor_watch_adjust.png │ │ │ ├── settings_resources_layers.png │ │ │ ├── ais-flag.svg │ │ │ └── command-autopilot.svg │ │ └── css │ │ │ ├── MaterialIcons-Regular.ttf │ │ │ ├── help.css │ │ │ └── material-icons.css │ ├── icons │ │ ├── favicon.ico │ │ ├── startup.png │ │ ├── icon-72x72.png │ │ ├── startup_ls.png │ │ ├── icon-192x192.png │ │ ├── ms-icon-144x144.png │ │ ├── ms-icon-150x150.png │ │ ├── ms-icon-310x310.png │ │ ├── startup_small.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-144x144.png │ │ └── apple-icon-180x180.png │ └── s57 │ │ └── rastersymbols-day.png ├── app │ ├── modules │ │ ├── map │ │ │ ├── ol │ │ │ │ └── lib │ │ │ │ │ ├── map.component.scss │ │ │ │ │ ├── themes.ts │ │ │ │ │ ├── content.component.ts │ │ │ │ │ ├── models.ts │ │ │ │ │ ├── control.component.ts │ │ │ │ │ ├── map.service.ts │ │ │ │ │ ├── controls.directive.ts │ │ │ │ │ ├── interactions.directive.ts │ │ │ │ │ ├── overlay.component.ts │ │ │ │ │ └── util.ts │ │ │ ├── index.ts │ │ │ ├── popovers │ │ │ │ ├── index.ts │ │ │ │ ├── chartlist-popover.component.ts │ │ │ │ └── featurelist-popover.component.ts │ │ │ └── fb-map.component.css │ │ ├── weather │ │ │ └── index.ts │ │ ├── experiments │ │ │ ├── index.ts │ │ │ └── experiments.component.ts │ │ ├── course │ │ │ └── index.ts │ │ ├── gpx │ │ │ ├── index.ts │ │ │ ├── gpxsave-dialog.css │ │ │ └── gpxload-dialog.css │ │ ├── autopilot │ │ │ ├── index.ts │ │ │ └── autopilot.component.css │ │ ├── settings │ │ │ ├── index.ts │ │ │ └── components │ │ │ │ ├── settings-dialog.css │ │ │ │ └── signalk-preferredpaths.component.css │ │ ├── icons │ │ │ ├── index.ts │ │ │ ├── openbridge.ts │ │ │ ├── waypoints.ts │ │ │ ├── poi.ts │ │ │ ├── vessels.ts │ │ │ └── atons.ts │ │ ├── alarms │ │ │ ├── index.ts │ │ │ └── components │ │ │ │ ├── anchor-watch.component.css │ │ │ │ ├── alert-list.component.css │ │ │ │ ├── nsew-buttons.component.ts │ │ │ │ └── timer-button.component.ts │ │ ├── skresources │ │ │ ├── components │ │ │ │ ├── notes │ │ │ │ │ ├── notes.css │ │ │ │ │ ├── safe.pipe.ts │ │ │ │ │ └── relatednotes-dialog.ts │ │ │ │ ├── resourcesets │ │ │ │ │ └── resource-upload-dialog.css │ │ │ │ ├── resourcelist.css │ │ │ │ ├── signalk-details.component.css │ │ │ │ └── routes │ │ │ │ │ ├── build-route.component.css │ │ │ │ │ └── nextpoint.component.ts │ │ │ ├── custom-resource-classes.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── buddies │ │ │ └── buddy.service.ts │ ├── lib │ │ ├── pipes │ │ │ ├── index.ts │ │ │ ├── reverse.pipe.ts │ │ │ └── coords.pipe.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── localstorage.service.ts │ │ │ └── wakelock.service.ts │ │ └── components │ │ │ ├── dialogs │ │ │ ├── index.ts │ │ │ ├── geojson │ │ │ │ └── geojson-dialog.css │ │ │ ├── errorlist-dialog.ts │ │ │ └── trail2route-dialog.html │ │ │ ├── index.ts │ │ │ ├── dial-text.css │ │ │ ├── country-flags.component.ts │ │ │ ├── pob-button.component.ts │ │ │ ├── file-input.component.html │ │ │ ├── wpt-button.component.ts │ │ │ ├── file-input.component.css │ │ │ ├── autopilot-button.component.ts │ │ │ └── mfb-container.component.ts │ ├── app.component.spec.ts │ ├── types │ │ ├── resources │ │ │ ├── geojson.ts │ │ │ ├── freeboard.ts │ │ │ ├── custom.ts │ │ │ └── signalk.ts │ │ └── stream.ts │ └── app.messages.ts ├── favicon.ico ├── tsconfig.app.json ├── tsconfig.spec.json ├── main.ts ├── manifest.json ├── browserconfig.xml └── styles.scss ├── .gitignore ├── .prettierrc ├── helper └── lib │ ├── index.d.ts │ └── fetch.ts ├── font_resources └── material │ ├── KFOmCnqEu92Fr1Mu4mxK.woff2 │ ├── MaterialIcons-Regular.ttf │ ├── KFOmCnqEu92Fr1Mu4WxKOzY.woff2 │ ├── KFOmCnqEu92Fr1Mu5mxKOzY.woff2 │ ├── KFOmCnqEu92Fr1Mu72xKOzY.woff2 │ ├── KFOmCnqEu92Fr1Mu7GxKOzY.woff2 │ ├── KFOmCnqEu92Fr1Mu7WxKOzY.woff2 │ ├── KFOmCnqEu92Fr1Mu7mxKOzY.woff2 │ ├── KFOlCnqEu92Fr1MmEU9fBBc4.woff2 │ ├── KFOlCnqEu92Fr1MmSU5fBBc4.woff2 │ ├── MaterialIconsRound-Regular.otf │ ├── MaterialIconsSharp-Regular.otf │ ├── MaterialIconsTwoTone-Regular.otf │ ├── KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2 │ ├── KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2 │ ├── MaterialIconsOutlined-Regular.otf │ └── material-icons.css ├── .eslintignore ├── tsconfig.spec.json ├── tsconfig.worker.json ├── tsconfig-helper.json ├── .eslintrc ├── .npmignore ├── CHANGELOG.md ├── tsconfig.app.json ├── webpack.config.js ├── .github └── workflows │ └── main.yaml └── tsconfig.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/map.component.scss: -------------------------------------------------------------------------------- 1 | @use 'ol/ol'; 2 | -------------------------------------------------------------------------------- /src/app/modules/weather/index.ts: -------------------------------------------------------------------------------- 1 | export * from './weather-forecast-modal'; 2 | -------------------------------------------------------------------------------- /src/app/modules/experiments/index.ts: -------------------------------------------------------------------------------- 1 | export * from './experiments.component'; 2 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/app/lib/pipes/index.ts: -------------------------------------------------------------------------------- 1 | export * from './coords.pipe'; 2 | export * from './reverse.pipe'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /public 2 | /dist 3 | /plugin 4 | node_modules 5 | package-lock.json 6 | .angular 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /src/assets/img/note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/note.png -------------------------------------------------------------------------------- /helper/lib/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'geojson-validation'; 2 | 3 | declare module 'openweather-apis'; 4 | -------------------------------------------------------------------------------- /src/app/modules/course/index.ts: -------------------------------------------------------------------------------- 1 | export * from './course-settings'; 2 | export * from './course.service'; 3 | -------------------------------------------------------------------------------- /src/app/modules/gpx/index.ts: -------------------------------------------------------------------------------- 1 | export * from './gpxload-dialog'; 2 | export * from './gpxsave-dialog'; 3 | -------------------------------------------------------------------------------- /src/assets/img/anchor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/anchor.png -------------------------------------------------------------------------------- /src/assets/sound/ding.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/sound/ding.mp3 -------------------------------------------------------------------------------- /src/assets/sound/woop.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/sound/woop.mp3 -------------------------------------------------------------------------------- /src/assets/help/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/favicon.ico -------------------------------------------------------------------------------- /src/assets/icons/startup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/startup.png -------------------------------------------------------------------------------- /src/assets/img/app_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/app_logo.png -------------------------------------------------------------------------------- /src/app/modules/autopilot/index.ts: -------------------------------------------------------------------------------- 1 | export * from './autopilot.component'; 2 | export * from './autopilot.service'; 3 | -------------------------------------------------------------------------------- /src/app/modules/map/index.ts: -------------------------------------------------------------------------------- 1 | export * from './popovers'; 2 | export { FBMapComponent } from './fb-map.component'; 3 | -------------------------------------------------------------------------------- /src/assets/help/img/alarms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/alarms.png -------------------------------------------------------------------------------- /src/assets/help/img/navdata.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/navdata.png -------------------------------------------------------------------------------- /src/assets/help/img/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/screen.png -------------------------------------------------------------------------------- /src/assets/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/icon-72x72.png -------------------------------------------------------------------------------- /src/assets/icons/startup_ls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/startup_ls.png -------------------------------------------------------------------------------- /src/assets/img/alarms/meteo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/alarms/meteo.png -------------------------------------------------------------------------------- /src/assets/img/anchor-limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/anchor-limit.png -------------------------------------------------------------------------------- /src/assets/img/ap/autopilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/ap/autopilot.png -------------------------------------------------------------------------------- /src/assets/img/sar_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/sar_active.png -------------------------------------------------------------------------------- /src/assets/img/vessels/self.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/vessels/self.png -------------------------------------------------------------------------------- /src/app/modules/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './settings.facade'; 2 | export * from './components/settings-dialog'; 3 | -------------------------------------------------------------------------------- /src/assets/help/img/ais_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/ais_list.png -------------------------------------------------------------------------------- /src/assets/help/img/autopilot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/autopilot.png -------------------------------------------------------------------------------- /src/assets/help/img/playback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/playback.png -------------------------------------------------------------------------------- /src/assets/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/assets/img/anchor-radius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/anchor-radius.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_alarm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_alarm.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_watch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_watch.png -------------------------------------------------------------------------------- /src/assets/help/img/trail2route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/trail2route.png -------------------------------------------------------------------------------- /src/assets/help/img/vessel_lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/vessel_lines.png -------------------------------------------------------------------------------- /src/assets/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/ms-icon-150x150.png -------------------------------------------------------------------------------- /src/assets/icons/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/ms-icon-310x310.png -------------------------------------------------------------------------------- /src/assets/icons/startup_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/startup_small.png -------------------------------------------------------------------------------- /src/assets/img/aircraft_active.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/aircraft_active.png -------------------------------------------------------------------------------- /src/assets/img/aircraft_inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/aircraft_inactive.png -------------------------------------------------------------------------------- /src/assets/img/vessels/self_blur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/vessels/self_blur.png -------------------------------------------------------------------------------- /src/assets/img/weather_station.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/weather_station.png -------------------------------------------------------------------------------- /src/assets/s57/rastersymbols-day.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/s57/rastersymbols-day.png -------------------------------------------------------------------------------- /src/assets/help/img/ais_properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/ais_properties.png -------------------------------------------------------------------------------- /src/assets/help/img/ais_shiptypes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/ais_shiptypes.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_circle.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_watch_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_watch_2.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_watch_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_watch_3.png -------------------------------------------------------------------------------- /src/assets/help/img/settings_paths.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/settings_paths.png -------------------------------------------------------------------------------- /src/assets/img/vessels/self_fixed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/vessels/self_fixed.png -------------------------------------------------------------------------------- /src/assets/img/waypoints/waypoint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/waypoints/waypoint.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_alarm_ack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_alarm_ack.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_watch_rode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_watch_rode.png -------------------------------------------------------------------------------- /src/assets/help/img/course_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/course_settings.png -------------------------------------------------------------------------------- /src/assets/help/img/settings_signalk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/settings_signalk.png -------------------------------------------------------------------------------- /src/assets/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /src/assets/icons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/apple-icon-144x144.png -------------------------------------------------------------------------------- /src/assets/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/assets/img/anchor-radius-raised.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/anchor-radius-raised.png -------------------------------------------------------------------------------- /src/assets/img/waypoints/marker-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/waypoints/marker-blue.png -------------------------------------------------------------------------------- /src/assets/img/waypoints/marker-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/img/waypoints/marker-green.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_alarm_muted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_alarm_muted.png -------------------------------------------------------------------------------- /src/assets/help/img/anchor_watch_adjust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/anchor_watch_adjust.png -------------------------------------------------------------------------------- /src/assets/help/css/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/css/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /font_resources/material/KFOmCnqEu92Fr1Mu4mxK.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOmCnqEu92Fr1Mu4mxK.woff2 -------------------------------------------------------------------------------- /font_resources/material/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /src/assets/help/img/settings_resources_layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/src/assets/help/img/settings_resources_layers.png -------------------------------------------------------------------------------- /font_resources/material/KFOmCnqEu92Fr1Mu4WxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOmCnqEu92Fr1Mu4WxKOzY.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOmCnqEu92Fr1Mu5mxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOmCnqEu92Fr1Mu5mxKOzY.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOmCnqEu92Fr1Mu72xKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOmCnqEu92Fr1Mu72xKOzY.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOmCnqEu92Fr1Mu7GxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOmCnqEu92Fr1Mu7GxKOzY.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOmCnqEu92Fr1Mu7WxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOmCnqEu92Fr1Mu7WxKOzY.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOmCnqEu92Fr1Mu7mxKOzY.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOmCnqEu92Fr1Mu7mxKOzY.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmEU9fBBc4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmEU9fBBc4.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmSU5fBBc4.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmSU5fBBc4.woff2 -------------------------------------------------------------------------------- /font_resources/material/MaterialIconsRound-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/MaterialIconsRound-Regular.otf -------------------------------------------------------------------------------- /font_resources/material/MaterialIconsSharp-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/MaterialIconsSharp-Regular.otf -------------------------------------------------------------------------------- /font_resources/material/MaterialIconsTwoTone-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/MaterialIconsTwoTone-Regular.otf -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmEU9fABc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmEU9fBxc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmEU9fCBc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmEU9fCRc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmEU9fChc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmEU9fCxc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmSU5fABc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmSU5fBxc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmSU5fCBc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmSU5fCRc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmSU5fChc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/KFOlCnqEu92Fr1MmSU5fCxc4EsA.woff2 -------------------------------------------------------------------------------- /font_resources/material/MaterialIconsOutlined-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SignalK/freeboard-sk/HEAD/font_resources/material/MaterialIconsOutlined-Regular.otf -------------------------------------------------------------------------------- /src/app/modules/icons/index.ts: -------------------------------------------------------------------------------- 1 | export * from './app.icons'; 2 | export * from './atons'; 3 | export * from './poi'; 4 | export * from './vessels'; 5 | export * from './waypoints'; 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | public 6 | # other folders 7 | e2e 8 | 9 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": ["src/test.ts", "**/*.spec.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-emergency-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/themes.ts: -------------------------------------------------------------------------------- 1 | // Map Themes 2 | 3 | export const LightTheme = { 4 | labelText: { 5 | color: '#333' 6 | } 7 | }; 8 | 9 | export const DarkTheme = { 10 | labelText: { 11 | color: 'yellow' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine" 7 | ] 8 | }, 9 | "include": [ 10 | "src/**/*.spec.ts", 11 | "src/**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/app/lib/services/index.ts: -------------------------------------------------------------------------------- 1 | // ** app state services ** 2 | export * from './info.service'; 3 | export * from './state.service'; 4 | 5 | // ** app storage services ** 6 | export * from './localstorage.service'; 7 | export * from './indexeddb'; 8 | 9 | export * from './wakelock.service'; 10 | -------------------------------------------------------------------------------- /src/assets/img/ob/heading-h-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/lib/components/dialogs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common-dialogs'; 2 | export * from './errorlist-dialog'; 3 | export * from './playback-dialog'; 4 | export * from './geojson/geojson-dialog'; 5 | export * from './trail2route-dialog'; 6 | export * from './multiselectlist-dialog'; 7 | export * from './singleselectlist-dialog'; 8 | -------------------------------------------------------------------------------- /src/app/modules/alarms/index.ts: -------------------------------------------------------------------------------- 1 | export * from './notification-manager'; 2 | export * from './components/alert-list.component'; 3 | export * from './components/alert.component'; 4 | export * from './components/alert-properties-modal'; 5 | 6 | export * from './components/anchor-watch.component'; 7 | export * from './anchor.service'; 8 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/content.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, ElementRef } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'ol-map > ol-content', 5 | template: '', 6 | standalone: false 7 | }) 8 | export class ContentComponent { 9 | constructor(public elementRef: ElementRef) {} 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/img/ob/cent-off-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.worker.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/worker", 6 | "lib": [ 7 | "es2022", 8 | "webworker" 9 | ], 10 | "types": [] 11 | }, 12 | "include": [ 13 | "src/**/*.worker.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig-helper.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./plugin", 5 | "types": [], 6 | "resolveJsonModule": true, 7 | "sourceMap": false, 8 | "module": "commonjs", 9 | "moduleResolution": "node" 10 | }, 11 | "include": ["helper/**/*", "helper/openApi.json"], 12 | "exclude": ["src","projects"] 13 | } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrapApplication } from '@angular/platform-browser'; 2 | import { 3 | provideHttpClient, 4 | withInterceptorsFromDi 5 | } from '@angular/common/http'; 6 | import { AppComponent } from './app/app.component'; 7 | 8 | bootstrapApplication(AppComponent, { 9 | providers: [provideHttpClient(withInterceptorsFromDi())] 10 | }).catch((e) => console.log(e)); 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended" 10 | ], 11 | "rules": { 12 | // Override our default settings just for this directory 13 | "eqeqeq": "warn" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/lib/pipes/reverse.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | 3 | @Pipe({ 4 | name: 'reverse' 5 | }) 6 | export class ReversePipe implements PipeTransform { 7 | //constructor() {} 8 | 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | public transform(value: Array): Array { 11 | return value.slice().reverse(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .browserslistrc 2 | .eslintrc 3 | .eslintignore 4 | .gitignore 5 | .prettierrc 6 | angular.json 7 | package-lock.json 8 | package.json 9 | projects 10 | src 11 | helper 12 | font_resources 13 | tsconfig.json 14 | tsconfig.app.json 15 | tsconfig-helper.json 16 | tsconfig.spec.json 17 | tsconfig.worker.json 18 | webpack.config.js 19 | CHANGELOG.md 20 | .angular 21 | .github 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG: Freeboard 2 | 3 | ### v2.19.3 4 | 5 | - **New**: Added support for submitting tile seeding jobs to `charts-plugin`. 6 | - **New**: Added Regions list so they can now be managed similarly to routes & waypoints. 7 | - **Updated**: Regions and Charts can now be added to resource groups. 8 | - **Updated**: Resource groups to control behaviour of resource types when none have been included in a group. 9 | -------------------------------------------------------------------------------- /src/assets/img/ob/warning-acknowledged-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": ["node"] 6 | }, 7 | "files": [ 8 | "src/main.ts" 9 | ], 10 | "include": [ 11 | "src/**/*.d.ts" 12 | ], 13 | "exclude": [ 14 | "src/test.ts", 15 | "src/**/*.spec.ts", 16 | "helper/**/*", 17 | "helper/openApi.json", 18 | "src/**/*.worker.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/modules/skresources/components/notes/notes.css: -------------------------------------------------------------------------------- 1 | .draft-watermark { 2 | color:lightgrey; 3 | font-size:120px; 4 | transform:rotate(300deg); 5 | -webkit-transform:rotate(300deg); 6 | position: absolute; 7 | z-index: 0; 8 | left: 25%; 9 | bottom: 0; 10 | opacity: .4; 11 | } 12 | 13 | 14 | :host ::ng-deep .angular-editor-textarea b { 15 | font-weight: bold; 16 | } 17 | 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = { 4 | resolve: { 5 | fallback: { 6 | //"buffer": require.resolve("buffer/") 7 | "buffer": false 8 | } 9 | }/*, 10 | plugins: [ 11 | new webpack.ProvidePlugin({ 12 | process: 'process/browser', 13 | Buffer: ['buffer', 'Buffer'], 14 | }) 15 | ]*/ 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/lib/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dialogs'; 2 | export * from './dial-text'; 3 | export * from './file-input.component'; 4 | export * from './pip.component'; 5 | export * from './measurements.component'; 6 | export * from './country-flags.component'; 7 | export * from './mfb-container.component'; 8 | export * from './pob-button.component'; 9 | export * from './wpt-button.component'; 10 | export * from './interact-help.component'; 11 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Freeboard", 3 | "short_name": "Freeboard", 4 | "theme_color": "#1976d2", 5 | "background_color": "#fafafa", 6 | "display": "fullscreen", 7 | "orientation": "portrait", 8 | "scope": "/@signalk/freeboard-sk/", 9 | "start_url": "index.html", 10 | "icons": [ 11 | { 12 | "src": "assets/icons/icon-72x72.png", 13 | "sizes": "72x72", 14 | "type": "image/png" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/app/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './alarms'; 2 | export * from './autopilot'; 3 | export * from './buddies/buddy.service'; 4 | export * from './course'; 5 | export * from './gpx'; 6 | export * from './map'; 7 | export * from './settings'; 8 | export * from './skresources'; 9 | export * from './weather'; 10 | 11 | export * from './skstream/skstream.service'; 12 | export * from './skstream/skstream.facade'; 13 | 14 | export * from './experiments'; 15 | -------------------------------------------------------------------------------- /src/app/modules/map/popovers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './popover.component'; 2 | export * from './resource-popover.component'; 3 | export * from './compass.component'; 4 | export * from './vessel-popover.component'; 5 | export * from './featurelist-popover.component'; 6 | export * from './chartlist-popover.component'; 7 | export * from './aircraft-popover.component'; 8 | export * from './alarm-popover.component'; 9 | export * from './aton-popover.component'; 10 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-acknowledged-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/img/ob/warning-rectified-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | #ffffff 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-arrival.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/lib/components/dial-text.css: -------------------------------------------------------------------------------- 1 | .dial-text { 2 | padding: 0 10px; 3 | height: 96px; 4 | font-family: roboto; 5 | border: transparent 1px solid; 6 | } 7 | 8 | .dial-text-title { 9 | font-size: 12pt; 10 | font-weight: 500; 11 | } 12 | 13 | .dial-text-subtitle { 14 | font-size: 8pt; 15 | font-weight: 500; 16 | } 17 | 18 | .dial-text-value { 19 | font-size: 22pt; 20 | font-weight: 500; 21 | } 22 | 23 | .dial-text-units { 24 | font-size: 10pt; 25 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | 4 | describe('AppComponent', () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [AppComponent] 8 | }).compileComponents(); 9 | }); 10 | 11 | it('should create the app', () => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /src/app/modules/skresources/components/resourcesets/resource-upload-dialog.css: -------------------------------------------------------------------------------- 1 | .resourceload-dialog { 2 | font-family: roboto; 3 | position: relative; width: 100%; 4 | } 5 | 6 | .card-group { 7 | padding: 5px 5% 5px 5%; 8 | } 9 | 10 | 11 | /* phone */ 12 | @media only screen and (min-width : 400px) { 13 | } 14 | 15 | /* tablet */ 16 | @media only screen and (min-width : 475px) { 17 | } 18 | 19 | /* large */ 20 | @media only screen and (min-width : 800px) { 21 | .card-group { 22 | padding: 5px 5% 5px 5%; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/modules/alarms/components/anchor-watch.component.css: -------------------------------------------------------------------------------- 1 | .anchorwatch { 2 | min-width: 150px; 3 | position: relative; 4 | height: 100%; 5 | overflow-y: scroll; 6 | } 7 | 8 | .anchorwatch .title-block { 9 | padding-left: 5px; 10 | display: flex; 11 | } 12 | 13 | .anchorwatch .title-block .title-text { 14 | font-family: roboto; 15 | font-size: 14pt; 16 | font-weight: 500; 17 | } 18 | 19 | .anchorwatch .content { 20 | overflow-y: auto; 21 | position: absolute; 22 | left: 0; 23 | right: 0; 24 | bottom: 0; 25 | top: 50px; 26 | } -------------------------------------------------------------------------------- /src/assets/img/ob/alerts-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/ob/heading-n-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-fire.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/img/ob/cent-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/help/img/ais-flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | red-flag 6 | 9 | 11 | -------------------------------------------------------------------------------- /src/assets/img/ob/warning-unack-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-aground.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/ob/navigation-route.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/lib/components/country-flags.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, effect, input, signal } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'country-flag', 5 | imports: [], 6 | template: ` 7 | @if (showFlag()) { 8 | 9 | } 10 | ` 11 | }) 12 | export class CountryFlagComponent { 13 | protected flagIcon: string; 14 | protected showFlag = signal(true); 15 | 16 | public mmsi = input(); 17 | public host = input(''); 18 | 19 | constructor() { 20 | effect(() => { 21 | this.flagIcon = `${this.host()}/signalk/v2/api/resources/flags/mmsi/${this.mmsi()}`; 22 | }); 23 | } 24 | 25 | /** 26 | * Handle flag image error 27 | */ 28 | imgError() { 29 | this.showFlag.set(false); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Build Application 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [22.x] 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Cache node modules 14 | uses: actions/cache@v4 15 | with: 16 | path: ~/.npm 17 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 18 | restore-keys: | 19 | ${{ runner.os }}-node- 20 | - name: Node ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: npm install 25 | run: | 26 | npm i 27 | - name: npm run build 28 | run: | 29 | npm run build:all 30 | 31 | -------------------------------------------------------------------------------- /src/assets/help/css/help.css: -------------------------------------------------------------------------------- 1 | li.nobullet { list-style-type: none !important; } 2 | 3 | .top-bar { 4 | background: rgba(200,200,200,.6); 5 | color: #333333 6 | } 7 | 8 | .content-container { 9 | position: absolute; 10 | top:45px; 11 | left: 0; 12 | right: 0; 13 | bottom: 0; 14 | display: flex; 15 | flex-wrap: nowrap; 16 | } 17 | 18 | .content-left { 19 | min-width: 150px; 20 | max-width: 30%; 21 | overflow-y: auto; 22 | display: block; 23 | } 24 | 25 | .content-right { 26 | flex: 1 1 auto; 27 | overflow-y: auto; 28 | } 29 | 30 | tr { 31 | background: white !important; 32 | } 33 | 34 | tr td { 35 | padding: 1px 5px 1px 5px !important; 36 | } 37 | 38 | @media only screen and (max-width: 40.0625em) { 39 | .content-left { 40 | display: none; 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_flag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | flag 4 | 5 | layer 1 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/img/ob/alert-list.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/modules/skresources/components/resourcelist.css: -------------------------------------------------------------------------------- 1 | .resourcelist { 2 | min-width: 180px; 3 | position: relative; 4 | height: 100%; 5 | } 6 | 7 | .resourcelist .title-block { 8 | padding-left: 5px; 9 | display: flex; 10 | } 11 | 12 | .resourcelist .title-block .title-text { 13 | font-family: roboto; 14 | font-size: 14pt; 15 | font-weight: 500; 16 | } 17 | 18 | .resourcelist .resources { 19 | overflow-y: auto; 20 | position: absolute; 21 | left: 0; 22 | right: 0; 23 | bottom: 0; 24 | top: 125px; 25 | } 26 | 27 | .resourcelist .resources.infolayers, 28 | .resourcelist .resources.vessels, 29 | .resourcelist .resources.charts 30 | { 31 | top: 160px; 32 | } 33 | 34 | .resourcelist .vscroller { 35 | position: absolute; 36 | top: 0px; 37 | bottom: 0; 38 | left: 0; 39 | right: 0; 40 | border-top: silver 1px outset; 41 | } -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/models.ts: -------------------------------------------------------------------------------- 1 | import { Map } from 'ol'; 2 | import { MapService } from './map.service'; 3 | 4 | /* enum types */ 5 | 6 | export enum LayerType { 7 | IMAGE, 8 | TILE, 9 | VECTOR_TILE, 10 | VECTOR 11 | } 12 | 13 | export enum SourceType { 14 | BINGMAPS, 15 | CARTODB, 16 | CLUSTER, 17 | IMAGE, 18 | IMAGEARCGISREST, 19 | IMAGECANVAS, 20 | IMAGEMAPGUIDE, 21 | IMAGESTATIC, 22 | IMAGEVECTOR, 23 | IMAGEWMS, 24 | OSM, 25 | RASTER, 26 | STAMEN, 27 | TILEARCGISREST, 28 | TILEDEBUG, 29 | TILEIMAGE, 30 | TILEJSON, 31 | TILEUTFGRID, 32 | TILEWMS, 33 | URLTILE, 34 | VECTOR, 35 | VECTORTILE, 36 | WMTS, 37 | XYZ, 38 | ZOOMIFY 39 | } 40 | 41 | /* interface types */ 42 | export type Coordinate = [number, number, number?]; 43 | 44 | export type Extent = [number, number, number, number]; 45 | 46 | export interface MapReadyEvent { 47 | map: Map; 48 | mapService: MapService; 49 | } 50 | -------------------------------------------------------------------------------- /src/assets/img/ob/sound-unavailable-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/lib/components/pob-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { MatIconModule } from '@angular/material/icon'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatTooltipModule } from '@angular/material/tooltip'; 5 | import { NotificationManager } from 'src/app/modules'; 6 | 7 | @Component({ 8 | selector: 'pob-button', 9 | imports: [MatIconModule, MatButtonModule, MatTooltipModule], 10 | template: ` 11 | 20 | `, 21 | styles: [] 22 | }) 23 | export class POBButtonComponent { 24 | constructor(private notiMgr: NotificationManager) {} 25 | 26 | protected raiseAlarm() { 27 | this.notiMgr.raiseServerAlarm('mob', 'Person Overboard!'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/img/ob/chart-display-settings-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "strict": false, 6 | "noImplicitOverride": true, 7 | "noPropertyAccessFromIndexSignature": false, 8 | "noImplicitReturns": false, 9 | "noFallthroughCasesInSwitch": true, 10 | "skipLibCheck": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "experimentalDecorators": true, 14 | "moduleResolution": "bundler", 15 | "importHelpers": true, 16 | "target": "ES2022", 17 | "module": "ES2022", 18 | "baseUrl": "./", 19 | "sourceMap": true, 20 | "declaration": false, 21 | "downlevelIteration": true, 22 | "typeRoots": [ 23 | "node_modules/@types" 24 | ], 25 | "lib": [ 26 | "es2018", 27 | "es2020", 28 | "es2022", 29 | "dom" 30 | ], 31 | "paths": {} 32 | }, 33 | "angularCompilerOptions": { 34 | "fullTemplateTypeCheck": true, 35 | "strictInjectionParameters": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/app/lib/components/file-input.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 22 |
23 | @if (preview && avatar) { 24 |
25 | 26 | @if (avatar) { 27 | 28 | } 29 |
30 | } 31 |
32 | -------------------------------------------------------------------------------- /src/app/lib/components/wpt-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, input } from '@angular/core'; 2 | import { MatIconModule } from '@angular/material/icon'; 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatTooltipModule } from '@angular/material/tooltip'; 5 | import { SKResourceService } from 'src/app/modules'; 6 | import { Position } from 'src/app/types'; 7 | 8 | @Component({ 9 | selector: 'wpt-button', 10 | imports: [MatIconModule, MatButtonModule, MatTooltipModule], 11 | template: ` 12 | 22 | `, 23 | styles: [] 24 | }) 25 | export class WptButtonComponent { 26 | protected position = input([0, 0]); 27 | protected active = input(false); 28 | 29 | constructor(private skres: SKResourceService) {} 30 | 31 | protected dropWaypoint() { 32 | this.skres.newWaypointAt(this.position()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/app/modules/icons/openbridge.ts: -------------------------------------------------------------------------------- 1 | // OpenBridge Icons 2 | 3 | import { AppIconSet } from './app.icons'; 4 | 5 | export const OpenBridgeIcons: AppIconSet = { 6 | path: './assets/img/ob', 7 | files: [ 8 | 'cent-iec.svg', 9 | 'cent-off-iec.svg', 10 | 'chart-display-settings-iec.svg', 11 | 'heading-h-up.svg', 12 | 'heading-n-up.svg', 13 | 'navigation-route.svg', 14 | 'route.svg', 15 | 'route-export.svg', 16 | 'route-import.svg', 17 | 'route-planning.svg', 18 | 'sound-high-fill.svg', 19 | 'sound-off-fill.svg', 20 | 'sound-unavailable-fill.svg', 21 | 'alert-list.svg', 22 | 'alerts-active.svg', 23 | 'alarm-mob.svg', 24 | 'alarm-abandon.svg', 25 | 'alarm-aground.svg', 26 | 'alarm-arrival.svg', 27 | 'alarm-depth.svg', 28 | 'alarm-fire.svg', 29 | 'alarm-cpa.svg', 30 | 'alarm-silenced.svg', 31 | 'alarm-unack-iec.svg', 32 | 'alarm-acknowledged-iec.svg', 33 | 'alarm-emergency-iec.svg', 34 | 'warning-acknowledged-iec.svg', 35 | 'warning-unack-iec.svg', 36 | 'warning-silenced-iec.svg', 37 | 'warning-unack-iec.svg', 38 | 'command-autopilot.svg' 39 | ] 40 | }; 41 | -------------------------------------------------------------------------------- /src/assets/img/ob/warning-silenced-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/types/resources/geojson.ts: -------------------------------------------------------------------------------- 1 | export type Position = [number, number, number?]; // [lon,lat, alt] 2 | export type LineString = Position[]; 3 | export type MultiLineString = LineString[]; 4 | export type Polygon = MultiLineString; 5 | export type MultiPolygon = Polygon[]; 6 | 7 | interface Feature { 8 | type: 'Feature'; 9 | properties?: { [key: string]: any }; 10 | id?: string; 11 | } 12 | 13 | export interface PointFeature extends Feature { 14 | geometry: { 15 | type: 'Point'; 16 | coordinates: Position; 17 | }; 18 | } 19 | 20 | export interface LineStringFeature extends Feature { 21 | geometry: { 22 | type: 'LineString'; 23 | coordinates: LineString; 24 | }; 25 | } 26 | 27 | export interface MultiLineStringFeature extends Feature { 28 | geometry: { 29 | type: 'MultiLineString'; 30 | coordinates: MultiLineString; 31 | }; 32 | } 33 | 34 | export interface PolygonFeature extends Feature { 35 | geometry: { 36 | type: 'Polygon'; 37 | coordinates: Polygon; 38 | }; 39 | } 40 | 41 | export interface MultiPolygonFeature { 42 | geometry: { 43 | type: 'MultiPolygon'; 44 | coordinates: MultiPolygon; 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /src/app/modules/buddies/buddy.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { SignalKClient } from 'signalk-client-angular'; 3 | import { AppFacade } from 'src/app/app.facade'; 4 | 5 | interface BuddyProperties { 6 | urn: string; 7 | name: string; 8 | } 9 | 10 | const BUDDIES_URI = '/resources/buddies'; 11 | 12 | // ** Signal K Buddies operations 13 | @Injectable({ providedIn: 'root' }) 14 | export class Buddies { 15 | constructor( 16 | public signalk: SignalKClient, 17 | public app: AppFacade 18 | ) {} 19 | 20 | list() { 21 | return this.signalk.api.get(this.app.skApiVersion, `${BUDDIES_URI}`); 22 | } 23 | 24 | add(urn: string, name: string) { 25 | return this.signalk.api.post(this.app.skApiVersion, `${BUDDIES_URI}`, { 26 | urn: urn, 27 | name: name 28 | }); 29 | } 30 | 31 | update(urn: string, name: string) { 32 | return this.signalk.api.put( 33 | this.app.skApiVersion, 34 | `${BUDDIES_URI}/${urn}`, 35 | { name: name } 36 | ); 37 | } 38 | 39 | remove(urn: string) { 40 | return this.signalk.api.delete( 41 | this.app.skApiVersion, 42 | `${BUDDIES_URI}/${urn}` 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-unack-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-mob.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/modules/map/fb-map.component.css: -------------------------------------------------------------------------------- 1 | 2 | .mapButton { 3 | position: fixed; 4 | z-index: 4000; 5 | } 6 | 7 | .control { 8 | display: flex; 9 | align-items: center; 10 | justify-content: flex-start; 11 | margin: 20px; 12 | } 13 | 14 | .ol-mouse-position { 15 | width: 100%; 16 | top: unset; 17 | bottom: 10px; 18 | left: unset; 19 | right:unset; 20 | text-align: center; 21 | color: rgb(30,30,30); 22 | font-family: roboto; 23 | } 24 | 25 | .ol-zoom { 26 | position: fixed; 27 | top:10px; 28 | left:60px; 29 | color: rgb(30,30,30); 30 | font-family:roboto; 31 | background-color: transparent; 32 | height: 45px; 33 | } 34 | 35 | ::ng-deep .ol-dragbox { 36 | background-color: rgba(255,255,255,0.4) !important; 37 | border-color: rgba(100,150,0,1) !important; 38 | border-style: solid; 39 | border-width: 1px; 40 | } 41 | 42 | .nosmall { 43 | display: block; 44 | } 45 | 46 | @media only screen and (max-width: 650px) { 47 | } 48 | 49 | @media only screen and (max-width: 400px) { 50 | .nosmall { 51 | display: none; 52 | } 53 | .ol-mouse-position { 54 | bottom: 2px; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/lib/components/file-input.component.css: -------------------------------------------------------------------------------- 1 | .ap-file-input { 2 | display: -webkit-box; 3 | display: -moz-box; 4 | display: -ms-flexbox; 5 | display: -webkit-flex; 6 | display: inline-flex; 7 | -moz-flex-direction: row; 8 | -ms-flex-direction: row; 9 | -webkit-flex-direction: row; 10 | flex-direction: row; 11 | flex-wrap: nowrap; 12 | justify-content: flex-start; 13 | align-content: stretch; 14 | width:100%; 15 | } 16 | #ctrl { 17 | cursor: pointer; 18 | } 19 | #ctrl label { 20 | cursor: pointer; 21 | } 22 | #apfileinput { 23 | position: absolute; 24 | top: 0; 25 | left: -9000px; 26 | 27 | width: 0.1px; 28 | height: 0.1px; 29 | opacity: 0; 30 | overflow: hidden; 31 | z-index: -1; 32 | } 33 | 34 | .disabled { 35 | color:rgba(33,33,33,.5) !important; 36 | border-color:rgba(33,33,33,.5) !important; 37 | cursor: none !important; 38 | } 39 | 40 | #avatar { 41 | background-color: inherit; 42 | border-radius: 5px; 43 | height: 42px; 44 | } 45 | #avatar>img { 46 | width: 42px; 47 | height: 42px; 48 | } 49 | #avatar>button { 50 | background-color: inherit; 51 | border: 0; 52 | color: red; 53 | } 54 | 55 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-abandon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/ob/route-planning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-depth.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/control.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | ElementRef, 4 | ChangeDetectionStrategy, 5 | ChangeDetectorRef, 6 | OnDestroy, 7 | OnInit 8 | } from '@angular/core'; 9 | import { Control } from 'ol/control'; 10 | import { MapComponent } from './map.component'; 11 | 12 | @Component({ 13 | selector: 'ol-map > ol-control', 14 | template: '', 15 | changeDetection: ChangeDetectionStrategy.OnPush, 16 | standalone: false 17 | }) 18 | export class ControlComponent implements OnInit, OnDestroy { 19 | protected control: Control; 20 | public element: HTMLElement; 21 | 22 | constructor( 23 | protected changeDetectorRef: ChangeDetectorRef, 24 | protected elementRef: ElementRef, 25 | protected mapComponent: MapComponent 26 | ) { 27 | this.changeDetectorRef.detach(); 28 | } 29 | 30 | ngOnInit() { 31 | if (this.elementRef.nativeElement) { 32 | this.element = this.elementRef.nativeElement; 33 | this.control = new Control({ element: this.element }); 34 | this.mapComponent.getMap().addControl(this.control); 35 | } 36 | } 37 | 38 | ngOnDestroy() { 39 | if (this.control) { 40 | this.mapComponent.getMap().removeControl(this.control); 41 | this.control = null; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/app/modules/settings/components/settings-dialog.css: -------------------------------------------------------------------------------- 1 | :host ::ng-deep .mat-dialog-content { 2 | padding: 0 4px; 3 | } 4 | 5 | .settings .setting-group-title { 6 | font-weight: bold; 7 | font-size: 10pt; 8 | font-family: Arial, Helvetica, sans-serif; 9 | padding: 10px 0 10px 0; 10 | } 11 | .setting-card-row { 12 | display: flex; 13 | flex-wrap: wrap; 14 | } 15 | .settings .setting-card-row-item { 16 | padding-right: 15px; 17 | flex: 1 1 auto; 18 | } 19 | 20 | 21 | /*ipads*/ 22 | @media only screen 23 | and (min-device-width : 768px) 24 | and (max-device-width : 1024px), 25 | /*desktop / laptop */ 26 | only screen and (min-width : 800px) { 27 | } 28 | /*xs*/ 29 | @media only screen and (max-width: 599px) { 30 | .settings .setting-group { 31 | padding: 5px 5% 5px 5%; 32 | } 33 | } 34 | @media only screen and (max-height: 500px) { 35 | } 36 | /*sm*/ 37 | @media only screen and (min-width: 600px) and (max-width: 959px), 38 | /*md*/ 39 | screen and (min-width: 960px) and (max-width: 1279px), 40 | /*lg*/ 41 | screen and (min-width: 1280px) and (max-width: 1919px), 42 | /*xl*/ 43 | screen and (min-width: 1920px) and (max-width: 5000px) { 44 | .settings .setting-group { 45 | padding: 5px 5% 5px 5%; 46 | } 47 | } -------------------------------------------------------------------------------- /src/app/modules/icons/waypoints.ts: -------------------------------------------------------------------------------- 1 | // Waypoint Icons 2 | 3 | import { AppIconSet } from './app.icons'; 4 | import { AtoNsType1 } from './atons'; 5 | 6 | const WaypointMarkerIcons: AppIconSet = { 7 | path: './assets/img/waypoints', 8 | scale: 1, 9 | anchor: [10.5, 25], 10 | files: ['waypoint.png', 'marker-blue.png', 'marker-green.png'] 11 | }; 12 | 13 | export const WaypointIcons: AppIconSet = { 14 | path: './assets/img/waypoints', 15 | scale: 1, 16 | anchor: [12, 24], 17 | files: [ 18 | 'start-pin.svg', 19 | 'start-boat.svg', 20 | 'whale.svg', 21 | 'pob.svg', 22 | 'pseudoaton.svg' 23 | ] 24 | }; 25 | 26 | /** 27 | * @description Build MapIcon definitions for use by MapImageRegistry 28 | */ 29 | export const getWaypointDefs = () => { 30 | const waypointList = {}; 31 | 32 | const addToList = (list: AppIconSet) => { 33 | list.files.forEach((file: string) => { 34 | const id = file.slice(0, file.indexOf('.')); 35 | waypointList[id] = { 36 | path: `${list.path}/${file}`, 37 | scale: list.scale, 38 | anchor: list.anchor 39 | }; 40 | }); 41 | }; 42 | addToList(WaypointMarkerIcons); 43 | addToList(WaypointIcons); 44 | addToList(AtoNsType1); 45 | waypointList['default'] = waypointList['waypoint']; 46 | return waypointList; 47 | }; 48 | -------------------------------------------------------------------------------- /src/assets/help/css/material-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(https://example.com/MaterialIcons-Regular.eot); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url(./MaterialIconsRound-Regular.otf) format('otf'), 9 | url(./MaterialIconsOutlined-Regular.otf) format('otf'), 10 | url(./MaterialIconsSharp-Regular.otf) format('otf'), 11 | url(./MaterialIconsTwoTone-Regular.otf) format('otf'), 12 | url(./MaterialIcons-Regular.ttf) format('truetype'); 13 | } 14 | 15 | .material-icons { 16 | font-family: 'Material Icons'; 17 | font-weight: normal; 18 | font-style: normal; 19 | font-size: 24px; /* Preferred icon size */ 20 | display: inline-block; 21 | line-height: 1; 22 | text-transform: none; 23 | letter-spacing: normal; 24 | word-wrap: normal; 25 | white-space: nowrap; 26 | direction: ltr; 27 | 28 | /* Support for all WebKit browsers. */ 29 | -webkit-font-smoothing: antialiased; 30 | /* Support for Safari and Chrome. */ 31 | text-rendering: optimizeLegibility; 32 | 33 | /* Support for Firefox. */ 34 | -moz-osx-font-smoothing: grayscale; 35 | 36 | /* Support for IE. */ 37 | font-feature-settings: 'liga'; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /font_resources/material/material-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url(https://example.com/MaterialIcons-Regular.eot); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url(./MaterialIconsRound-Regular.otf) format('otf'), 9 | url(./MaterialIconsOutlined-Regular.otf) format('otf'), 10 | url(./MaterialIconsSharp-Regular.otf) format('otf'), 11 | url(./MaterialIconsTwoTone-Regular.otf) format('otf'), 12 | url(./MaterialIcons-Regular.ttf) format('truetype'); 13 | } 14 | 15 | .material-icons { 16 | font-family: 'Material Icons'; 17 | font-weight: normal; 18 | font-style: normal; 19 | font-size: 24px; /* Preferred icon size */ 20 | display: inline-block; 21 | line-height: 1; 22 | text-transform: none; 23 | letter-spacing: normal; 24 | word-wrap: normal; 25 | white-space: nowrap; 26 | direction: ltr; 27 | 28 | /* Support for all WebKit browsers. */ 29 | -webkit-font-smoothing: antialiased; 30 | /* Support for Safari and Chrome. */ 31 | text-rendering: optimizeLegibility; 32 | 33 | /* Support for Firefox. */ 34 | -moz-osx-font-smoothing: grayscale; 35 | 36 | /* Support for IE. */ 37 | font-feature-settings: 'liga'; 38 | } 39 | 40 | -------------------------------------------------------------------------------- /src/app/lib/components/dialogs/geojson/geojson-dialog.css: -------------------------------------------------------------------------------- 1 | .geojsonload-dialog { 2 | font-family: roboto; 3 | position: relative; width: 100%; 4 | } 5 | 6 | .dialog-icon { 7 | display: none; 8 | } 9 | 10 | .card-group-title { 11 | font-weight: bold; 12 | font-size: 10pt; 13 | font-family: Arial, Helvetica, sans-serif; 14 | padding: 10px 0 10px 0; 15 | } 16 | .card-group-row { 17 | display: flex; 18 | flex-wrap: wrap; 19 | } 20 | .card-group-row-item { 21 | padding-right: 15px; 22 | max-width: 60%; 23 | } 24 | 25 | .pnlRow { 26 | display:flex; 27 | flex-wrap:wrap; 28 | width:100%; 29 | } 30 | 31 | .pnlGeoType { 32 | flex: unset; 33 | } 34 | 35 | .pnlArrow { 36 | width:30px; 37 | text-align:center; 38 | } 39 | 40 | .pnlSKType { 41 | width: unset; 42 | padding-left:3px; 43 | padding-right:0; 44 | flex: 1 1 auto; 45 | } 46 | 47 | 48 | /* phone */ 49 | @media only screen and (min-width : 400px) { 50 | } 51 | 52 | /* tablet */ 53 | @media only screen and (min-width : 475px) { 54 | .dialog-content { 55 | min-width: 400px; 56 | } 57 | .dialog-icon { 58 | display: inline; 59 | } 60 | } 61 | 62 | /* large */ 63 | @media only screen and (min-width : 800px) { 64 | } 65 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_buddy.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-buddy 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_cargo.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-cargo 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_other.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-other 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_active.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-active 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_tanker.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-tanker 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_special.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-special 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_inactive.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-inactive 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_highspeed.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-highspeed 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_passenger.svg: -------------------------------------------------------------------------------- 1 | 4 | ais-passenger 5 | 15 | 16 | 18 | 22 | 23 | 25 | 28 | 29 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/app/modules/icons/poi.ts: -------------------------------------------------------------------------------- 1 | // POI Icons 2 | 3 | import { AppIconSet } from './app.icons'; 4 | 5 | const DefaultNoteIcon = { 6 | path: './assets/img/note.png', 7 | scale: 1, 8 | anchor: [5, 3] 9 | }; 10 | 11 | export const PoiIcons: AppIconSet = { 12 | path: './assets/img/poi', 13 | scale: 0.65, 14 | anchor: [1, 37], 15 | files: [ 16 | 'anchorage.svg', 17 | 'boatramp.svg', 18 | 'bridge.svg', 19 | 'business.svg', 20 | 'dam.svg', 21 | 'ferry.svg', 22 | 'hazard.svg', 23 | 'inlet.svg', 24 | 'lock.svg', 25 | 'marina.svg', 26 | 'dock.svg', 27 | 'turning-basin.svg', 28 | 'radio-call-point.svg', 29 | 'transhipment-dock.svg', 30 | 'notice-to-mariners.svg', 31 | 'navigation-structure.svg', 32 | 'fuel.svg', 33 | 'tunnel.svg', 34 | 'waterway-guage.svg' 35 | ] 36 | }; 37 | 38 | /** 39 | * @description Build MapIcon definitions for use by MapImageRegistry 40 | */ 41 | export const getPoiDefs = () => { 42 | const poiList = {}; 43 | 44 | const addToList = (list: AppIconSet) => { 45 | list.files.forEach((file: string) => { 46 | const id = file.slice(0, file.indexOf('.')); 47 | poiList[id] = { 48 | path: `${list.path}/${file}`, 49 | scale: list.scale, 50 | anchor: list.anchor 51 | }; 52 | }); 53 | }; 54 | poiList['default'] = DefaultNoteIcon; 55 | addToList(PoiIcons); 56 | 57 | return poiList; 58 | }; 59 | -------------------------------------------------------------------------------- /src/assets/img/ob/route-export.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/img/ob/route-import.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/lib/components/autopilot-button.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, input } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { MatIconModule } from '@angular/material/icon'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatTooltipModule } from '@angular/material/tooltip'; 6 | import { AppFacade } from 'src/app/app.facade'; 7 | 8 | @Component({ 9 | selector: 'autopilot-button', 10 | imports: [CommonModule, MatIconModule, MatButtonModule, MatTooltipModule], 11 | template: ` 12 | 28 | `, 29 | styles: [] 30 | }) 31 | export class AutopilotButtonComponent { 32 | protected active = input(false); 33 | 34 | constructor(protected app: AppFacade) {} 35 | 36 | handleClick() { 37 | this.app.uiCtrl.update((current) => { 38 | const show = !current.autopilotConsole; 39 | return Object.assign({}, current, { autopilotConsole: show }); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/lib/components/mfb-container.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, input } from '@angular/core'; 2 | 3 | import { AppFacade } from 'src/app/app.facade'; 4 | import { MFBAction } from 'src/app/types'; 5 | import { POBButtonComponent } from './pob-button.component'; 6 | import { WptButtonComponent } from './wpt-button.component'; 7 | import { AutopilotButtonComponent } from './autopilot-button.component'; 8 | 9 | @Component({ 10 | selector: 'mfb-container', 11 | standalone: true, 12 | imports: [POBButtonComponent, WptButtonComponent, AutopilotButtonComponent], 13 | template: ` 14 |
15 | @if (action() === 'wpt') { 16 | 20 | } 21 | @if (action() === 'pob') { 22 | 23 | } 24 | @if (action() === 'autopilot') { 25 | 31 | } 32 |
33 | `, 34 | styles: [ 35 | ` 36 | .mfb-container { 37 | position: absolute; 38 | bottom: 23px; 39 | right: 5px; 40 | z-index: 5000; 41 | } 42 | ` 43 | ] 44 | }) 45 | export class MFBContainerComponent { 46 | protected action = input(); 47 | constructor(protected app: AppFacade) {} 48 | } 49 | -------------------------------------------------------------------------------- /src/app/types/resources/freeboard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SKRoute, 3 | SKWaypoint, 4 | SKNote, 5 | SKChart, 6 | SKRegion, 7 | SKTrack, 8 | SKVessel 9 | } from 'src/app/modules/skresources/resource-classes'; 10 | 11 | import { 12 | SKInfoLayer, 13 | SKResourceSet 14 | } from 'src/app/modules/skresources/custom-resource-classes'; 15 | 16 | export type FBRoutes = Array; 17 | export type FBRoute = [string, SKRoute, boolean?]; 18 | 19 | export type FBWaypoints = Array; 20 | export type FBWaypoint = [string, SKWaypoint, boolean?]; 21 | 22 | export type FBNotes = Array; 23 | export type FBNote = [string, SKNote, boolean?]; 24 | 25 | export type FBRegions = Array; 26 | export type FBRegion = [string, SKRegion, boolean?]; 27 | 28 | export type FBCharts = Array; 29 | export type FBChart = [string, SKChart, boolean?]; 30 | 31 | export type FBTracks = Array; 32 | export type FBTrack = [string, SKTrack, boolean?]; 33 | 34 | export type FBVessels = Array; 35 | export type FBVessel = [string, SKVessel, boolean?]; 36 | 37 | export type FBResourceSets = Array; 38 | export type FBResourceSet = [string, SKResourceSet, boolean?]; 39 | 40 | export type FBInfoLayers = Array; 41 | export type FBInfoLayer = [string, SKInfoLayer, boolean?]; 42 | 43 | export type FBResource = 44 | | FBRoute 45 | | FBWaypoint 46 | | FBNote 47 | | FBRegion 48 | | FBChart 49 | | FBTrack; 50 | 51 | export type FBResourceSelect = { 52 | id: string; 53 | value?: boolean; 54 | type?: string; 55 | isGroup?: boolean; 56 | }; 57 | -------------------------------------------------------------------------------- /src/app/modules/skresources/custom-resource-classes.ts: -------------------------------------------------------------------------------- 1 | // **** CUSTOM RESOURCE CLASSES ********** 2 | import { ResourceSet, CustomStyles, InfoLayerResource } from 'src/app/types'; 3 | 4 | // ** Freeboard / SK ResourceSet 5 | export class SKResourceSet { 6 | id: string; 7 | name: string; 8 | description: string; 9 | values; 10 | styles: CustomStyles; 11 | type = 'ResourceSet'; 12 | 13 | constructor(resSet?: ResourceSet) { 14 | if (resSet) { 15 | this.id = resSet.id ? resSet.id : null; 16 | this.name = resSet.name ? resSet.name : null; 17 | this.description = resSet.description ? resSet.description : null; 18 | this.styles = resSet.styles ? resSet.styles : {}; 19 | this.values = resSet.values ?? { 20 | type: 'FeatureCollection', 21 | features: [] 22 | }; 23 | } 24 | } 25 | } 26 | 27 | // ** Freeboard / SK Information Layer 28 | export class SKInfoLayer { 29 | id: string; 30 | name: string; 31 | description: string; 32 | values: any = { 33 | url: null, 34 | sourceType: null, 35 | layers: [], 36 | time: { 37 | current: null, 38 | min: null, 39 | max: null, 40 | values: [] 41 | }, 42 | opacity: 1, 43 | minZoom: 1, 44 | maxZoom: 24, 45 | refreshInterval: 0 46 | }; 47 | type = 'InfoLayer'; 48 | 49 | constructor(info?: InfoLayerResource) { 50 | if (info) { 51 | this.id = info.id ? info.id : null; 52 | this.name = info.name ? info.name : null; 53 | this.description = info.description ? info.description : null; 54 | this.values = info.values ?? this.values; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/app/modules/skresources/components/notes/safe.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { 3 | DomSanitizer, 4 | SafeHtml, 5 | SafeStyle, 6 | SafeScript, 7 | SafeUrl, 8 | SafeResourceUrl 9 | } from '@angular/platform-browser'; 10 | 11 | @Pipe({ 12 | name: 'safe', 13 | standalone: true 14 | }) 15 | export class SafePipe implements PipeTransform { 16 | constructor(protected sanitizer: DomSanitizer) {} 17 | 18 | public transform( 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | value: any, 21 | type: string 22 | ): SafeHtml | SafeStyle | SafeScript | SafeUrl | SafeResourceUrl { 23 | switch (type) { 24 | case 'html': 25 | return this.sanitizer.bypassSecurityTrustHtml(value); 26 | case 'style': 27 | return this.sanitizer.bypassSecurityTrustStyle(value); 28 | case 'script': 29 | return this.sanitizer.bypassSecurityTrustScript(value); 30 | case 'url': 31 | return this.sanitizer.bypassSecurityTrustUrl(value); 32 | case 'resourceUrl': 33 | return this.sanitizer.bypassSecurityTrustResourceUrl(value); 34 | default: 35 | throw new Error(`Invalid safe type specified: ${type}`); 36 | } 37 | } 38 | } 39 | 40 | @Pipe({ 41 | name: 'addTarget', 42 | standalone: true 43 | }) 44 | export class AddTargetPipe implements PipeTransform { 45 | constructor(protected sanitizer: DomSanitizer) {} 46 | 47 | public transform(value: string, target: string): string { 48 | if (typeof value === 'string') { 49 | const a = value.split(' 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/ob/command-autopilot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/img/ob/sound-high-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/types/resources/custom.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PointFeature, 3 | LineStringFeature, 4 | MultiLineStringFeature, 5 | PolygonFeature, 6 | MultiPolygonFeature 7 | } from './geojson'; 8 | 9 | // *** Freeboard defined RESOURCE types 10 | 11 | export type Tracks = { [id: string]: TrackResource }; 12 | 13 | export interface TrackResource { 14 | feature: MultiLineStringFeature; 15 | } 16 | 17 | export type ResourceSets = { [id: string]: ResourceSet }; 18 | 19 | export interface ResourceSet extends CustomResource { 20 | type: 'ResourceSet'; 21 | styles?: CustomStyles; 22 | values: { 23 | type: 'FeatureCollection'; 24 | features: Array< 25 | | PointFeature 26 | | LineStringFeature 27 | | MultiLineStringFeature 28 | | PolygonFeature 29 | | MultiPolygonFeature 30 | >; 31 | }; 32 | } 33 | 34 | export type InfoLayers = { [id: string]: InfoLayerResource }; 35 | 36 | export interface InfoLayerResource extends CustomResource { 37 | name: string; 38 | description: string; 39 | type: 'InfoLayer'; 40 | values: { 41 | url: string; 42 | sourceType: 'WMTS' | 'WMS'; 43 | layers: string[]; 44 | opacity: number; 45 | minZoom: number; 46 | maxZoom: number; 47 | refreshInterval?: number; 48 | }; 49 | } 50 | 51 | export type CustomResources = { [id: string]: CustomResource }; 52 | 53 | export interface CustomResource { 54 | id?: string; 55 | name?: string | null; 56 | description?: string | null; 57 | type: string; 58 | values: { [key: string]: any }; 59 | } 60 | 61 | export interface CustomStyles { 62 | default?: CustomStyle; 63 | [key: string]: CustomStyle; 64 | } 65 | 66 | export interface CustomStyle { 67 | stroke: string; 68 | fill: string; 69 | width: number; 70 | lineDash?: Array; 71 | } 72 | -------------------------------------------------------------------------------- /src/assets/img/ob/sound-off-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/img/ob/alarm-silenced-iec.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/modules/map/popovers/chartlist-popover.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | ChangeDetectionStrategy 7 | } from '@angular/core'; 8 | 9 | import { MatListModule } from '@angular/material/list'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | import { MatIconModule } from '@angular/material/icon'; 12 | import { MatTooltipModule } from '@angular/material/tooltip'; 13 | import { PopoverComponent } from './popover.component'; 14 | 15 | /*********** Chart boundaries List Popover *************** 16 | features: Array<{id: string, name: string}> - list of chart boundaries 17 | *************************************************/ 18 | @Component({ 19 | selector: 'chart-list-popover', 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | imports: [ 22 | MatListModule, 23 | MatButtonModule, 24 | MatTooltipModule, 25 | MatIconModule, 26 | PopoverComponent 27 | ], 28 | template: ` 29 | 30 | 31 | @for (c of features; track c) { 32 | 33 | map 34 | {{ c.text }} 35 | 36 | } 37 | 38 | 39 | `, 40 | styleUrls: [] 41 | }) 42 | export class ChartListPopoverComponent { 43 | protected title = 'Click to Show / Hide Chart'; 44 | @Input() features: Array<{ id: string; text: string }> = []; 45 | @Input() canClose: boolean; 46 | @Output() closed: EventEmitter = new EventEmitter(); 47 | @Output() selected: EventEmitter = new EventEmitter(); 48 | 49 | handleSelect(id: string) { 50 | this.selected.emit(id); 51 | } 52 | handleClose() { 53 | this.closed.emit(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/lib/services/localstorage.service.ts: -------------------------------------------------------------------------------- 1 | /*********************************** 2 | LocalStorage Service 3 | ************************************ 4 | Class to encapsulate window.localStorage 5 | namespace: prefixes keys with value supplied 6 | ***********************************/ 7 | 8 | import { Injectable } from '@angular/core'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class LocalStorage { 14 | private _ls = null; 15 | private _namespace = ''; 16 | 17 | constructor() { 18 | this._ls = null; 19 | try { 20 | if ('localStorage' in window && window['localStorage'] !== null) { 21 | this._ls = window.localStorage; 22 | } 23 | } catch (e) { 24 | console.warn('window.localStorage is not supported by this browser!'); 25 | } 26 | } 27 | 28 | set namespace(value) { 29 | this._namespace = value + '_'; 30 | } 31 | get namespace() { 32 | return this._namespace; 33 | } 34 | 35 | //** read data and return a JSON object to supplied key 36 | read(key) { 37 | if (this._ls && key) { 38 | try { 39 | return JSON.parse(this._ls.getItem(this._namespace + key)); 40 | } catch (e) { 41 | return this._ls.getItem(this._namespace + key); 42 | } 43 | } 44 | } 45 | 46 | //** write data as a JSON object to supplied key 47 | write(key, value = '') { 48 | value = JSON.stringify(value); 49 | if (this._ls && key) { 50 | this._ls.setItem(this._namespace + key, value); 51 | } 52 | } 53 | 54 | //** delete the supplied key 55 | delete(key) { 56 | if (this._ls && key) { 57 | return this._ls.removeItem(this._namespace + key); 58 | } 59 | } 60 | 61 | //** check supplied key exists ** 62 | exists(key) { 63 | if (this._ls && key) { 64 | return this._ls.getItem(this._namespace + key) ? true : false; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/app/modules/gpx/gpxsave-dialog.css: -------------------------------------------------------------------------------- 1 | .gpxsave-dialog { 2 | font-family: roboto; 3 | position: relative; 4 | } 5 | 6 | .dialog-content { 7 | position: relative; 8 | min-height: 300px; 9 | max-height: 400px; 10 | width: 100%; 11 | min-width: 240px; 12 | overflow-y: auto; 13 | } 14 | 15 | .dialog-icon { 16 | display: none; 17 | } 18 | 19 | .card-group { 20 | padding: 5px 5% 5px 5%; 21 | } 22 | 23 | .card-group .panel-header { 24 | padding: 0 5px; 25 | } 26 | 27 | .card-group .panel-description { 28 | text-align: right; 29 | width: 100%; 30 | padding-right: 10px; 31 | } 32 | 33 | .card-group-title { 34 | font-weight: bold; 35 | font-size: 10pt; 36 | font-family: Arial, Helvetica, sans-serif; 37 | padding: 10px 0 10px 0; 38 | } 39 | 40 | button { 41 | min-width: unset; 42 | padding: unset; 43 | } 44 | 45 | .item-row { 46 | display: flex; 47 | width: 100%; 48 | padding: 2px 0; 49 | } 50 | 51 | .item-row .name { 52 | flex: 1 1 auto; 53 | overflow: hidden; 54 | display: -webkit-box; 55 | -webkit-box-orient: vertical; 56 | -webkit-line-clamp: 1; 57 | text-overflow:ellipsis; 58 | line-height: 2.5em; 59 | } 60 | 61 | .item-row .check { 62 | text-align: right; 63 | padding-right: 15px; 64 | } 65 | 66 | 67 | /* tablet */ 68 | @media only screen and (min-width : 475px) { 69 | button { 70 | min-width: inherit; 71 | padding: inherit; 72 | } 73 | .dialog-content { 74 | min-width: 400px; 75 | } 76 | .dialog-icon { 77 | display: inline; 78 | } 79 | } 80 | 81 | /* large */ 82 | @media only screen and (min-width : 800px) { 83 | .card-group { 84 | padding: 5px 5% 5px 5%; 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/map.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Map } from 'ol'; 3 | import BaseLayer from 'ol/layer/Base'; 4 | import { get as getProj } from 'ol/proj'; 5 | import { register } from 'ol/proj/proj4.js'; 6 | import proj4 from 'proj4'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class MapService { 12 | maps: Map[]; 13 | 14 | constructor() { 15 | this.maps = []; 16 | } 17 | 18 | /** 19 | * Retrieves all the maps 20 | */ 21 | getMaps(): Map[] { 22 | return this.maps; 23 | } 24 | 25 | /** 26 | * Returns a map object from the maps array 27 | */ 28 | getMapById(id: string): Map { 29 | let map: Map = null; 30 | for (let i = 0; i < this.maps.length; i++) { 31 | if (this.maps[i].getTarget() === id) { 32 | map = this.maps[i]; 33 | break; 34 | } 35 | } 36 | return map; 37 | } 38 | 39 | getLayerByKey(key: string, value: string): BaseLayer { 40 | let tl: BaseLayer; 41 | this.maps.forEach((map) => { 42 | map.getLayers().forEach((layer) => { 43 | if (layer.get(key) === value) { 44 | tl = layer; 45 | } 46 | }); 47 | }); 48 | return tl; 49 | } 50 | 51 | addMap(map: Map): void { 52 | this.maps.push(map); 53 | } 54 | 55 | updateSize() { 56 | this.maps.forEach((map) => { 57 | map.updateSize(); 58 | }); 59 | } 60 | 61 | addProj4(epsg: string, proj4Def: string, extent?) { 62 | let projection = getProj(epsg); 63 | if (!projection) { 64 | proj4.defs(epsg, proj4Def); 65 | register(proj4); 66 | projection = getProj(epsg); 67 | if (extent) { 68 | projection.setExtent(extent); 69 | } 70 | } 71 | if (!projection) { 72 | console.error(`Failed to register ${epsg} projection in OpenLayers`); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/app/modules/skresources/components/signalk-details.component.css: -------------------------------------------------------------------------------- 1 | /* -- nmea sentence id list --*/ 2 | .sk-details { 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | font-family: Roboto, Arial, Helvetica, sans-serif; 7 | font-size: 10pt; 8 | max-height: 100%; 9 | } 10 | .sk-details .title { 11 | background: left 1px -webkit-gradient(linear, 12 | left top, left bottom, 13 | from(rgba(0,0,0,0.18)), color-stop(0.65, transparent)) 14 | rgba(178,187,194,0.89); 15 | padding: 3px 5px 0 5px; 16 | font-weight: 500; 17 | line-height: 23px; 18 | } 19 | 20 | .sk-details .content { 21 | padding: 0; 22 | line-height: 23px; 23 | margin: 0; 24 | flex: 1 1 auto; 25 | overflow: auto; 26 | } 27 | .sk-details .item { 28 | display: flex; 29 | flex-direction: row; 30 | line-height: 26px; 31 | border-width: 0 0 0 0; 32 | border-style: solid; 33 | border-color: gray; 34 | padding-left: 5px; 35 | } 36 | .sk-details .content .item { 37 | border-width: 0 0 1px 0; 38 | } 39 | 40 | 41 | .sk-details .sectionname { 42 | text-align: left; 43 | white-space: nowrap; 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | width: 100%; 47 | font-weight: 500; 48 | } 49 | 50 | .sk-details .pathvalue { 51 | display: flex; 52 | flex-wrap: nowrap; 53 | width: 100%; 54 | } 55 | 56 | .sk-details .pathvalue .path { 57 | text-align: left; 58 | width: 60%; 59 | white-space: nowrap; 60 | overflow: hidden; 61 | text-overflow: ellipsis; 62 | font-style: italic; 63 | } 64 | 65 | .sk-details .pathvalue .value { 66 | text-align: center; 67 | white-space: nowrap; 68 | overflow: hidden; 69 | text-overflow: ellipsis; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/app/modules/autopilot/autopilot.component.css: -------------------------------------------------------------------------------- 1 | .autopilot-console { 2 | position: absolute; 3 | z-index: 6010; 4 | top: 130px; 5 | left: 50px; 6 | display: flex; 7 | flex-direction: column; 8 | font-family: Roboto, Arial, Helvetica, sans-serif; 9 | font-size: 10pt; 10 | width: 238px; 11 | height: 400px; 12 | background-image: url('src/assets/img/ap/autopilot.png'); 13 | background-color: transparent; 14 | border-radius: 115px; 15 | } 16 | .autopilot-console .title { 17 | padding: 10px 0 0 0; 18 | font-weight: 500; 19 | font-size: 14px; 20 | line-height: 23px; 21 | display: flex; 22 | color: rgb(237, 224, 221); 23 | text-align: center; 24 | } 25 | 26 | .autopilot-console .title .closer:hover { 27 | cursor: pointer; 28 | } 29 | 30 | .autopilot-console .content { 31 | padding: 0 5px; 32 | margin: 0; 33 | flex: 1 1 auto; 34 | text-align: center; 35 | } 36 | 37 | .autopilot-console .content .lcd{ 38 | padding: 5px 0; 39 | color:black; 40 | margin: 0 15px; 41 | } 42 | 43 | .autopilot-console .content .dial-text { 44 | font-family: roboto; 45 | flex: 1 1 auto; 46 | } 47 | 48 | .autopilot-console .content .dial-text-title { 49 | font-size: 10pt; 50 | font-weight: 500; 51 | flex: 1; 52 | } 53 | 54 | .autopilot-console .content .dial-text-value { 55 | font-size: 37pt; 56 | font-weight: 500; 57 | } 58 | 59 | .autopilot-console .content .dial-text-units { 60 | font-size: 10pt; 61 | } 62 | 63 | .autopilot-console .content .button-bar { 64 | display:flex; 65 | flex-wrap:nowrap; 66 | padding: 30px 0 10px 0; 67 | } 68 | 69 | .autopilot-console .content .button-bar-thin { 70 | display:flex; 71 | flex-wrap:nowrap; 72 | padding: 10px 0 0 0; 73 | } 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/app/modules/experiments/experiments.component.ts: -------------------------------------------------------------------------------- 1 | /** Experiments Components ** 2 | ********************************/ 3 | 4 | import { Component, Output, EventEmitter } from '@angular/core'; 5 | 6 | import { MatButtonModule } from '@angular/material/button'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatTooltipModule } from '@angular/material/tooltip'; 9 | import { MatMenuModule } from '@angular/material/menu'; 10 | 11 | /********* ExperimentsComponent ********/ 12 | @Component({ 13 | selector: 'fb-experiments', 14 | imports: [MatMenuModule, MatIconModule, MatButtonModule, MatTooltipModule], 15 | template: ` 16 | 17 | 26 | 27 | adb 28 | Capture Debug Info 29 | 30 | 31 | 32 |
33 | 42 |
43 | `, 44 | styles: [``] 45 | }) 46 | export class ExperimentsComponent { 47 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 48 | @Output() selected: EventEmitter = new EventEmitter(); 49 | 50 | //constructor() {} 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | handleSelect(choice: string, value?: any) { 54 | this.selected.emit({ choice: choice, value: value }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/controls.directive.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Directive, Input, OnInit } from '@angular/core'; 2 | import { 3 | Attribution, 4 | FullScreen, 5 | Rotate, 6 | ScaleLine, 7 | Zoom, 8 | ZoomSlider, 9 | ZoomToExtent 10 | } from 'ol/control'; 11 | import { MapComponent } from './map.component'; 12 | 13 | @Directive({ 14 | selector: 'ol-map > [olControls]', 15 | standalone: false 16 | }) 17 | export class ControlsDirective implements OnInit { 18 | private controls = []; 19 | private readonly controlList = { 20 | attribution: Attribution, 21 | fullscreen: FullScreen, 22 | rotate: Rotate, 23 | scaleline: ScaleLine, 24 | zoom: Zoom, 25 | zoomslider: ZoomSlider, 26 | zoomtoextent: ZoomToExtent 27 | }; 28 | 29 | constructor( 30 | protected changeDetectorRef: ChangeDetectorRef, 31 | protected mapComponent: MapComponent 32 | ) { 33 | this.changeDetectorRef.detach(); 34 | } 35 | 36 | @Input() 37 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 38 | set olControls(value: any[]) { 39 | this.controls = value; 40 | this.setControls(); 41 | } 42 | 43 | ngOnInit() { 44 | this.setControls(); 45 | } 46 | 47 | setControls() { 48 | const map = this.mapComponent.getMap(); 49 | if (undefined !== map) { 50 | map.getControls().clear(); 51 | if (!this.controls || this.controls.length < 0) { 52 | return; 53 | } 54 | for (const config of this.controls) { 55 | this.addControl(map, config); 56 | } 57 | this.changeDetectorRef.detectChanges(); 58 | } 59 | } 60 | 61 | private addControl(map, controlConfig) { 62 | if (!this.controlList[controlConfig.name]) { 63 | console.warn(`Unknown control ${controlConfig.name}`); 64 | return; 65 | } 66 | const newControl = new this.controlList[controlConfig.name]( 67 | controlConfig.options 68 | ); 69 | map.addControl(newControl); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/modules/settings/components/signalk-preferredpaths.component.css: -------------------------------------------------------------------------------- 1 | /* -- nmea sentence id list --*/ 2 | .sk-details { 3 | position: relative; 4 | display: flex; 5 | flex-direction: column; 6 | font-family: Roboto, Arial, Helvetica, sans-serif; 7 | font-size: 10pt; 8 | max-height: 100%; 9 | width: 60vw; 10 | max-width: 300px; 11 | } 12 | .sk-details .title { 13 | background: left 1px -webkit-gradient(linear, 14 | left top, left bottom, 15 | from(rgba(0,0,0,0.18)), color-stop(0.65, transparent)) 16 | rgba(178,187,194,0.89); 17 | padding: 3px 5px 0 5px; 18 | margin-top:3px; 19 | font-weight: 500; 20 | line-height: 23px; 21 | color: black; 22 | } 23 | 24 | .sk-details .content { 25 | padding: 0; 26 | line-height: 23px; 27 | margin: 0; 28 | flex: 1 1 auto; 29 | overflow: auto; 30 | } 31 | .sk-details .item { 32 | display: flex; 33 | flex-direction: row; 34 | line-height: 26px; 35 | border-width: 0 0 0 0; 36 | border-style: solid; 37 | border-color: gray; 38 | padding-left: 5px; 39 | } 40 | .sk-details .content .item { 41 | border-width: 0 0 1px 0; 42 | } 43 | 44 | 45 | .sk-details .sectionname { 46 | text-align: left; 47 | white-space: nowrap; 48 | overflow: hidden; 49 | text-overflow: ellipsis; 50 | width: 100%; 51 | font-weight: 500; 52 | } 53 | 54 | .sk-details .pathvalue { 55 | display: flex; 56 | flex-wrap: nowrap; 57 | width: 100%; 58 | background-color: white; 59 | } 60 | 61 | .sk-details .pathvalue .path { 62 | text-align: left; 63 | white-space: nowrap; 64 | overflow: hidden; 65 | text-overflow: ellipsis; 66 | font-style: italic; 67 | } 68 | 69 | .sk-details .pathvalue .value { 70 | text-align: center; 71 | white-space: nowrap; 72 | overflow: hidden; 73 | text-overflow: ellipsis; 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/app/modules/gpx/gpxload-dialog.css: -------------------------------------------------------------------------------- 1 | .gpxload-dialog { 2 | font-family: roboto; 3 | position: relative; 4 | } 5 | 6 | .mat-expansion-panel-body { 7 | padding: 0 12px 6px; 8 | } 9 | 10 | .card-group-title { 11 | font-weight: bold; 12 | font-size: 10pt; 13 | font-family: Arial, Helvetica, sans-serif; 14 | padding: 10px 0 10px 0; 15 | } 16 | 17 | .item-row { 18 | display: flex; 19 | width: 100%; 20 | padding: 2px 0; 21 | } 22 | 23 | .item-row .name { 24 | flex: 1 1 auto; 25 | overflow: hidden; 26 | display: -webkit-box; 27 | -webkit-box-orient: vertical; 28 | -webkit-line-clamp: 1; 29 | text-overflow:ellipsis; 30 | line-height: 2.5em; 31 | } 32 | 33 | .item-row .check { 34 | text-align: right; 35 | padding-right: 15px; 36 | } 37 | 38 | 39 | .pnlSelectedItems { 40 | display: none; 41 | line-height: 2.9em; 42 | } 43 | 44 | .pnlSelect { 45 | flex: 1 1 auto; 46 | padding-left:3px; 47 | padding-right:3px; 48 | text-align:right; 49 | } 50 | 51 | .txtSelect { 52 | display: none; 53 | } 54 | 55 | button { 56 | min-width: unset; 57 | padding: unset; 58 | } 59 | 60 | /* phone */ 61 | @media only screen and (min-width : 400px) { 62 | .pnlSelectedItems { 63 | display: inline; 64 | } 65 | } 66 | 67 | /* tablet */ 68 | @media only screen and (min-width : 475px) { 69 | .pnlSelectedItems { 70 | display: inline; 71 | line-height: 2.9em; 72 | } 73 | .txtSelect { 74 | display: inline; 75 | } 76 | .pnlSelect { 77 | flex: 1 1 auto; 78 | padding-left:20px; 79 | padding-right:15px; 80 | text-align:right; 81 | } 82 | button { 83 | min-width: inherit; 84 | padding: inherit; 85 | } 86 | } 87 | 88 | /* large */ 89 | @media only screen and (min-width : 800px) { 90 | } 91 | -------------------------------------------------------------------------------- /src/app/modules/skresources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './resources.service'; 2 | export * from './resource-classes'; 3 | export * from './custom-resource-classes'; 4 | export * from './custom-resources-service'; 5 | 6 | export * from './components/active-resource-dialog'; 7 | export * from './components/signalk-details.component'; 8 | 9 | export * from './components/notes/notelist'; 10 | export * from './components/notes/note-dialog'; 11 | export * from './components/notes/relatednotes-dialog'; 12 | 13 | export * from './components/regions/regionlist'; 14 | export * from './components/regions/region-dialog'; 15 | 16 | export * from './components/ais/aislist'; 17 | export * from './components/ais/ais-properties-modal'; 18 | export * from './components/ais/aton-properties-modal'; 19 | export * from './components/ais/aircraft-properties-modal'; 20 | 21 | export * from './components/charts/chartlist'; 22 | export * from './components/charts/chart-properties-dialog'; 23 | export * from './components/charts/wmts-dialog'; 24 | export * from './components/charts/wms-dialog'; 25 | export * from './components/charts/jsonmapsource-dialog'; 26 | 27 | export * from './components/tracks/tracklist'; 28 | export * from './components/tracks/track-dialog'; 29 | 30 | export * from './components/routes/routelist'; 31 | export * from './components/routes/route-dialog'; 32 | export * from './components/routes/build-route.component'; 33 | export { RouteNextPointComponent } from './components/routes/nextpoint.component'; 34 | 35 | export * from './components/waypoints/waypointlist'; 36 | export * from './components/waypoints/waypoint-dialog'; 37 | 38 | export * from './components/groups/grouplist'; 39 | 40 | export * from './components/resourcesets/resourceset-list-modal'; 41 | export * from './components/resourcesets/resourceset-feature-properties-modal'; 42 | export * from './components/resourcesets/resource-upload-dialog'; 43 | 44 | export * from './components/infolayers/infolayerlist'; 45 | export * from './components/infolayers/infolayer-properties-dialog'; 46 | -------------------------------------------------------------------------------- /src/assets/img/wind/awa.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 48 | -------------------------------------------------------------------------------- /src/assets/img/wind/twd.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 48 | -------------------------------------------------------------------------------- /src/assets/img/waypoints/start-pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 36 | 41 | 46 | 51 | 52 | -------------------------------------------------------------------------------- /src/app/lib/components/dialogs/errorlist-dialog.ts: -------------------------------------------------------------------------------- 1 | import { Component, Inject } from '@angular/core'; 2 | import { 3 | MatDialogModule, 4 | MatDialogRef, 5 | MAT_DIALOG_DATA 6 | } from '@angular/material/dialog'; 7 | import { MatIconModule } from '@angular/material/icon'; 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { MatListModule } from '@angular/material/list'; 10 | 11 | /********* ErrorListDialog ************ 12 | data: { 13 | errorList: "" text to display, 14 | buttonText"" button text, 15 | } 16 | ***********************************/ 17 | @Component({ 18 | selector: 'ap-errorlistdialog', 19 | imports: [MatDialogModule, MatIconModule, MatButtonModule, MatListModule], 20 | template: ` 21 |
22 |
23 |

24 | warning 25 |  {{ data.errorList?.length }} Errors Encountered 26 |

27 |
28 | 29 | 30 | @for (err of data.errorList; track err) { 31 | 32 | {{ err.message }} 33 | Status: {{ err.status }} 34 | 35 | } 36 | 37 | 38 | 39 | 42 | 43 |
44 | `, 45 | styles: [ 46 | ` 47 | ._ap-errlist { 48 | min-width: 150px; 49 | } 50 | ` 51 | ], 52 | standalone: true 53 | }) 54 | export class ErrorListDialog { 55 | public image = null; 56 | 57 | constructor( 58 | public dialogRef: MatDialogRef, 59 | @Inject(MAT_DIALOG_DATA) public data 60 | ) {} 61 | 62 | //** lifecycle: events ** 63 | ngOnInit() { 64 | this.data.buttonText = this.data.buttonText || 'OK'; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/app/modules/alarms/components/alert-list.component.css: -------------------------------------------------------------------------------- 1 | .alert-list-main { 2 | position: fixed; 3 | z-index: 6100; 4 | --hA: 100vh; 5 | --lA: 100vw; 6 | top: calc((var(--hA)* .1)); 7 | left: calc((var(--lA)* .1)); 8 | display: flex; 9 | flex-direction: column; 10 | font-family: Roboto, Arial, Helvetica, sans-serif; 11 | width: 90vw; 12 | max-width: 650px; 13 | height: 40vh; 14 | border: 2px gray solid; 15 | } 16 | .alert-list-main .title { 17 | padding: 3px 5px 0 5px; 18 | font-weight: 500; 19 | font-size: 14px; 20 | line-height: 23px; 21 | display: flex; 22 | } 23 | 24 | .alert-list-main .content { 25 | padding: 0 5px; 26 | line-height: 23px; 27 | margin: 0; 28 | flex: 1 1 auto; 29 | overflow: hidden; 30 | text-align: center; 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | 35 | .alert-list-container { 36 | display: inline-block; 37 | vertical-align: top; 38 | overflow-y: auto; 39 | flex: 1 1 auto; 40 | } 41 | 42 | .alert-list { 43 | border: solid 1px rgba(200, 200, 200, .3); 44 | min-height: 99%; 45 | border-radius: 4px; 46 | overflow: hidden; 47 | display: block; 48 | } 49 | 50 | .alert-box { 51 | padding: 15px 10px; 52 | border-bottom: solid 1px #ccc; 53 | display: flex; 54 | flex-direction: row; 55 | align-items: center; 56 | justify-content: space-between; 57 | box-sizing: border-box; 58 | width:100%; 59 | } 60 | 61 | .alert-text { 62 | flex:2; 63 | text-align:left; 64 | overflow: hidden; 65 | display: -webkit-box; 66 | -webkit-box-orient: vertical; 67 | -webkit-line-clamp: 3; 68 | line-clamp: 3; 69 | text-overflow:ellipsis; 70 | } 71 | 72 | .red-text { 73 | color: red; 74 | } 75 | 76 | .amber-text { 77 | color: rgb(179, 101, 5); 78 | } 79 | 80 | .blink-text { 81 | animation: blink 1.0s infinite; 82 | animation-fill-mode: both; 83 | } 84 | 85 | @keyframes blink { 86 | 0% { opacity: 1 } 87 | 50% { opacity: 0 } 88 | 100% { opacity: 1 } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/interactions.directive.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectorRef, Directive, Input, OnInit } from '@angular/core'; 2 | import { 3 | DoubleClickZoom, 4 | DragPan, 5 | DragRotate, 6 | DragZoom, 7 | KeyboardPan, 8 | KeyboardZoom, 9 | MouseWheelZoom, 10 | PinchZoom 11 | } from 'ol/interaction'; 12 | import { MapComponent } from './map.component'; 13 | 14 | @Directive({ 15 | selector: 'ol-map > [olInteractions]', 16 | standalone: false 17 | }) 18 | export class InteractionsDirective implements OnInit { 19 | private interactions = []; 20 | private readonly interactionList = { 21 | dragpan: DragPan, 22 | dragrotate: DragRotate, 23 | dragzoom: DragZoom, 24 | doubleclickzoom: DoubleClickZoom, 25 | keyboardpan: KeyboardPan, 26 | keyboardzoom: KeyboardZoom, 27 | mousewheelzoom: MouseWheelZoom, 28 | pinchzoom: PinchZoom 29 | }; 30 | 31 | constructor( 32 | protected changeDetectorRef: ChangeDetectorRef, 33 | protected mapComponent: MapComponent 34 | ) { 35 | this.changeDetectorRef.detach(); 36 | } 37 | 38 | @Input() 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | set olInteractions(value: any[]) { 41 | this.interactions = value; 42 | this.setInteractions(); 43 | } 44 | 45 | ngOnInit() { 46 | this.setInteractions(); 47 | } 48 | 49 | setInteractions() { 50 | const map = this.mapComponent.getMap(); 51 | if (undefined !== map) { 52 | map.getInteractions().clear(); 53 | if (!this.interactions || this.interactions.length < 0) return; 54 | for (const config of this.interactions) { 55 | this.addInteraction(map, config); 56 | } 57 | this.changeDetectorRef.detectChanges(); 58 | } 59 | } 60 | 61 | private addInteraction(map, controlConfig) { 62 | if (!this.interactionList[controlConfig.name]) { 63 | console.error(`Unknown interaction ${controlConfig.name}`); 64 | return; 65 | } 66 | const newInteraction = new this.interactionList[controlConfig.name]( 67 | controlConfig.options 68 | ); 69 | map.addInteraction(newInteraction); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/lib/services/wakelock.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, signal } from '@angular/core'; 2 | 3 | @Injectable({ providedIn: 'root' }) 4 | export class WakeLockService { 5 | private wakeLockRef: WakeLockSentinel; 6 | private _enabled = signal(false); 7 | readonly enabled = this._enabled.asReadonly(); 8 | 9 | constructor() {} 10 | 11 | ngOnDestroy() { 12 | this.disable(); 13 | document.removeEventListener('visibilitychange', this.onVisibilityChange); 14 | } 15 | 16 | get isAvailable(): boolean { 17 | return 'wakeLock' in navigator; 18 | } 19 | 20 | toggle() { 21 | if (this._enabled) { 22 | this.disable(); 23 | } else { 24 | this.enable(); 25 | } 26 | } 27 | 28 | /** set wakelock */ 29 | async enable() { 30 | if (!this.isAvailable) { 31 | return; 32 | } 33 | if (this.wakeLockRef) { 34 | return; 35 | } 36 | try { 37 | this.wakeLockRef = await navigator.wakeLock.request('screen'); 38 | 39 | // listen for release 40 | this.wakeLockRef.addEventListener('release', () => this.handleRelease()); 41 | this._enabled.set(true); 42 | 43 | // listen for visibility change 44 | document.addEventListener('visibilitychange', () => 45 | this.onVisibilityChange() 46 | ); 47 | } catch (err) { 48 | this.wakeLockRef = null; 49 | this._enabled.set(false); 50 | } 51 | } 52 | 53 | /** release wakelock */ 54 | async disable() { 55 | if (this.wakeLockRef) { 56 | await this.wakeLockRef.release(); 57 | this.wakeLockRef.removeEventListener('release', () => 58 | this.handleRelease() 59 | ); 60 | this.wakeLockRef = null; 61 | } 62 | } 63 | 64 | /** Handle release event */ 65 | private handleRelease() { 66 | this._enabled.set(false); 67 | } 68 | 69 | /** handle document visibility change event */ 70 | private async onVisibilityChange() { 71 | if (this.wakeLockRef !== null && document.visibilityState === 'visible') { 72 | this.wakeLockRef = await navigator.wakeLock.request('screen'); 73 | this._enabled.set(true); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/overlay.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeDetectionStrategy, 3 | ChangeDetectorRef, 4 | Component, 5 | ElementRef, 6 | Input, 7 | OnChanges, 8 | OnDestroy, 9 | OnInit, 10 | SimpleChanges 11 | } from '@angular/core'; 12 | import Overlay, { Options, PanIntoViewOptions } from 'ol/Overlay'; 13 | import { fromLonLat } from 'ol/proj'; 14 | import { MapComponent } from './map.component'; 15 | import { Coordinate } from './models'; 16 | 17 | @Component({ 18 | selector: 'ol-map > ol-overlay', 19 | template: '', 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | standalone: false 22 | }) 23 | export class OverlayComponent implements OnInit, OnChanges, OnDestroy { 24 | protected overlay: Overlay; 25 | public element: HTMLElement; 26 | 27 | @Input() id: number | string; 28 | @Input() className: string; 29 | @Input() offset: number[]; 30 | @Input() position: Coordinate; 31 | @Input() positioning: string; 32 | @Input() stopEvent: boolean; 33 | @Input() insertFirst: boolean; 34 | 35 | constructor( 36 | protected changeDetectorRef: ChangeDetectorRef, 37 | protected elementRef: ElementRef, 38 | protected mapComponent: MapComponent 39 | ) { 40 | this.changeDetectorRef.detach(); 41 | } 42 | 43 | ngOnInit() { 44 | if (this.elementRef.nativeElement) { 45 | this.element = this.elementRef.nativeElement; 46 | this.overlay = new Overlay(this as Options); 47 | this.mapComponent.getMap().addOverlay(this.overlay); 48 | if (this.position) { 49 | this.overlay.setPosition(fromLonLat(this.position)); 50 | } 51 | } 52 | } 53 | 54 | ngOnDestroy() { 55 | if (this.overlay) { 56 | this.mapComponent.getMap().removeOverlay(this.overlay); 57 | this.overlay = null; 58 | } 59 | } 60 | 61 | ngOnChanges(changes: SimpleChanges) { 62 | if (this.overlay && changes.position) { 63 | this.overlay.setPosition(fromLonLat(changes.position.currentValue)); 64 | } 65 | } 66 | 67 | panIntoView(panIntoViewOptions: PanIntoViewOptions) { 68 | this.overlay.panIntoView(panIntoViewOptions); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/assets/img/ob/route.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/modules/icons/vessels.ts: -------------------------------------------------------------------------------- 1 | // Vessel Icons 2 | 3 | import { AppIconSet } from './app.icons'; 4 | 5 | const VesselSelfIcons: AppIconSet = { 6 | path: './assets/img/vessels', 7 | scale: 0.75, 8 | anchor: [9.5, 22.5], 9 | files: ['ais_self.svg'] 10 | }; 11 | 12 | export const VesselAisIcons: AppIconSet = { 13 | path: './assets/img/vessels', 14 | scale: 1.0, 15 | anchor: [17, 16], 16 | files: [ 17 | 'ais_active.svg', 18 | 'ais_buddy.svg', 19 | 'ais_cargo.svg', 20 | 'ais_flag.svg', 21 | 'ais_highspeed.svg', 22 | 'ais_inactive.svg', 23 | 'ais_other.svg', 24 | 'ais_passenger.svg', 25 | 'ais_special.svg', 26 | 'ais_tanker.svg', 27 | 'ais_self.svg' 28 | ] 29 | }; 30 | 31 | export const AIS_TYPE_IDS = { 32 | 10: 'ais_active', 33 | 20: 'ais_active', 34 | 30: 'ais_active', 35 | 40: 'ais_highspeed', 36 | 50: 'ais_special', 37 | 60: 'ais_passenger', 38 | 70: 'ais_cargo', 39 | 80: 'ais_tanker', 40 | 90: 'ais_other', 41 | default: 'ais_active', 42 | inactive: 'ais_inactive', 43 | focused: 'ais_self', 44 | buddy: 'ais_buddy' 45 | }; 46 | 47 | export const AIS_MOORED_STYLE_IDS = { 48 | // [stroke, fill] 49 | 10: ['white', '#FF00FF'], 50 | 20: ['white', '#FF00FF'], 51 | 30: ['white', '#FF00FF'], 52 | 40: ['#7F6A00', '#FFE97F'], 53 | 50: ['#000000', '#00FFFF'], 54 | 60: ['#0026FF', '#0026FF'], 55 | 70: ['#000000', '#009931'], 56 | 80: ['#7F0000', '#FF0000'], 57 | 90: ['#000000', '#808080'], 58 | default: ['white', '#FF00FF'], 59 | inactive: ['FF00DC', 'white'], 60 | buddy: ['white', '#4CFF00'] 61 | }; 62 | 63 | /** 64 | * @description Build MapIcon definitions for use by MapImageRegistry 65 | */ 66 | export const getVesselDefs = () => { 67 | const vesselList = {}; 68 | 69 | const addToList = (list: AppIconSet) => { 70 | list.files.forEach((file: string) => { 71 | const id = file.slice(0, file.indexOf('.')); 72 | vesselList[id] = { 73 | path: `${list.path}/${file}`, 74 | scale: list.scale, 75 | anchor: list.anchor 76 | }; 77 | }); 78 | }; 79 | addToList(VesselAisIcons); 80 | addToList(VesselSelfIcons); 81 | 82 | return vesselList; 83 | }; 84 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @use '../node_modules/ol/ol.css'; 3 | @use '../font_resources/material/material-roboto.css'; 4 | @use '../font_resources/material/material-icons.css'; 5 | 6 | * { -webkit-tap-highlight-color: transparent; } 7 | * { tap-highlight-color: transparent; } 8 | 9 | .app-night { 10 | filter: brightness(0.3) sepia(0.2) hue-rotate(-30deg); 11 | backdrop-filter: brightness(0.3); 12 | transition: filter 0.3s ease; 13 | } 14 | 15 | body { 16 | font-family: Roboto, "Helvetica Neue", sans-serif; 17 | background-color: white; 18 | margin: 0 0 0 0; 19 | -webkit-user-select: none; 20 | -moz-user-select: none; 21 | user-select: none; 22 | } 23 | b { font-weight: 500; } 24 | 25 | .loading { /*loading screen */ 26 | font-size:12pt; 27 | font-family: Arial, Helvetica, sans-serif; 28 | position: absolute; 29 | top: 25%; 30 | left: 0; 31 | right:0; 32 | margin-right: auto; 33 | color: black; 34 | text-align: center; 35 | } 36 | 37 | .loading img { 38 | border: 5px rgba(200,200,200,0) solid; 39 | border-radius: 100px; 40 | animation: colorchange 1s; 41 | animation-iteration-count: infinite; 42 | } 43 | 44 | .ol-scale-line { 45 | bottom: unset; 46 | top: 10px; 47 | left: 180px; 48 | background-color: transparent; 49 | font-family: 'Roboto'; 50 | } 51 | 52 | @keyframes colorchange 53 | { 54 | 0% {border:3px solid rgba(8, 85, 178, .1);} 55 | 25% {border:3px solid #7AB7FF;} 56 | 50% {border:3px solid rgba(8, 85, 178, 1);} 57 | 75% {border:3px solid #7AB7FF;} 58 | 100% {border:3px solid rgba(8, 85, 178, .1);} 59 | } 60 | 61 | .theme-page { 62 | font-family: roboto; 63 | } 64 | 65 | .theme-panel { 66 | margin: 0; 67 | color: inherit; 68 | } 69 | 70 | /*xs*/ 71 | @media only screen and (max-width: 599px) { 72 | .weather-data .mat-horizontal-content-container { 73 | padding: 0 !important; 74 | } 75 | } 76 | 77 | @media only screen and (min-width: 600px) and (max-width: 959px), 78 | /*md*/ 79 | screen and (min-width: 960px) and (max-width: 1279px), 80 | /*lg*/ 81 | screen and (min-width: 1280px) and (max-width: 1919px), 82 | /*xl*/ 83 | screen and (min-width: 1920px) and (max-width: 5000px) { 84 | 85 | } -------------------------------------------------------------------------------- /src/app/modules/skresources/components/notes/relatednotes-dialog.ts: -------------------------------------------------------------------------------- 1 | /** Related Notes Dialog Component ** 2 | ********************************/ 3 | 4 | import { Component, OnInit, Inject } from '@angular/core'; 5 | 6 | import { FormsModule } from '@angular/forms'; 7 | import { MatButtonModule } from '@angular/material/button'; 8 | import { MatCardModule } from '@angular/material/card'; 9 | import { 10 | MatDialogModule, 11 | MatDialogRef, 12 | MAT_DIALOG_DATA 13 | } from '@angular/material/dialog'; 14 | import { MatFormFieldModule } from '@angular/material/form-field'; 15 | import { MatIconModule } from '@angular/material/icon'; 16 | import { MatInputModule } from '@angular/material/input'; 17 | import { MatTooltipModule } from '@angular/material/tooltip'; 18 | import { MatToolbarModule } from '@angular/material/toolbar'; 19 | 20 | import { AddTargetPipe } from './safe.pipe'; 21 | 22 | import { AppFacade } from 'src/app/app.facade'; 23 | 24 | /********* RelatedNotesDialog ********** 25 | data: { 26 | notes: [] 27 | } 28 | ***********************************/ 29 | @Component({ 30 | selector: 'ap-relatednotesdialog', 31 | imports: [ 32 | FormsModule, 33 | MatDialogModule, 34 | MatCardModule, 35 | MatButtonModule, 36 | MatIconModule, 37 | MatTooltipModule, 38 | MatFormFieldModule, 39 | MatInputModule, 40 | MatToolbarModule, 41 | AddTargetPipe 42 | ], 43 | templateUrl: `relatednotes-dialog.html`, 44 | styleUrls: ['notes.css'] 45 | }) 46 | export class RelatedNotesDialog implements OnInit { 47 | relatedBy: string; 48 | 49 | constructor( 50 | public app: AppFacade, 51 | public dialogRef: MatDialogRef, 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | @Inject(MAT_DIALOG_DATA) public data: any 54 | ) {} 55 | 56 | ngOnInit() { 57 | this.relatedBy = 58 | this.data.relatedBy[0].toUpperCase() + this.data.relatedBy.slice(1); 59 | } 60 | 61 | openNoteUrl(url: string) { 62 | window.open(url, '_notes'); 63 | } 64 | 65 | addNote() { 66 | this.dialogRef.close({ result: true, data: 'add' }); 67 | } 68 | 69 | editNote(noteId: string) { 70 | this.dialogRef.close({ result: true, data: 'edit', id: noteId }); 71 | } 72 | 73 | deleteNote(noteId: string) { 74 | this.dialogRef.close({ result: true, data: 'delete', id: noteId }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/assets/img/atons/basestation.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 47 | 50 | 54 | 55 | 56 | 60 | -------------------------------------------------------------------------------- /src/app/modules/skresources/components/routes/build-route.component.css: -------------------------------------------------------------------------------- 1 | .rte-builder { 2 | position: fixed; 3 | z-index: 6001; 4 | --hA: 100vh; 5 | --lA: 100vw; 6 | top: calc((var(--hA)* .1)); 7 | left: calc((var(--lA)* .1)); 8 | display: flex; 9 | flex-direction: column; 10 | font-family: Roboto, Arial, Helvetica, sans-serif; 11 | width: 90vw; 12 | max-width: 450px; 13 | height: 80vh; 14 | } 15 | .rte-builder .title { 16 | padding: 3px 5px 0 5px; 17 | font-weight: 500; 18 | font-size: 14px; 19 | line-height: 23px; 20 | display: flex; 21 | } 22 | 23 | .rte-builder .content { 24 | padding: 0 5px; 25 | line-height: 23px; 26 | margin: 0; 27 | flex: 1 1 auto; 28 | overflow: hidden; 29 | text-align: center; 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | .rte-builder .wptlists { 35 | display:flex; 36 | flex: 1 1 auto; 37 | overflow: hidden; 38 | } 39 | 40 | .wptlist-container { 41 | width: 50%; 42 | max-width: 50%; 43 | display: inline-block; 44 | vertical-align: top; 45 | overflow-y: auto; 46 | flex: 1 1 auto; 47 | } 48 | 49 | .wpt-list { 50 | border: solid 1px rgba(200, 200, 200, .3); 51 | min-height: 99%; 52 | border-radius: 4px; 53 | overflow: hidden; 54 | display: block; 55 | } 56 | 57 | .wpt-box { 58 | padding: 15px 10px; 59 | border-bottom: solid 1px #ccc; 60 | display: flex; 61 | flex-direction: row; 62 | align-items: center; 63 | justify-content: space-between; 64 | box-sizing: border-box; 65 | cursor: move; 66 | } 67 | 68 | .wpt-text { 69 | text-overflow: ellipsis; 70 | overflow: hidden; 71 | white-space: pre; 72 | flex:1 1 auto; 73 | text-align:left; 74 | } 75 | 76 | .cdk-drag-preview { 77 | box-sizing: border-box; 78 | border-radius: 4px; 79 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 80 | 0 8px 10px 1px rgba(0, 0, 0, 0.14), 81 | 0 3px 14px 2px rgba(0, 0, 0, 0.12); 82 | } 83 | 84 | .cdk-drag-placeholder { 85 | opacity: 0; 86 | } 87 | 88 | .cdk-drag-animating { 89 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 90 | } 91 | 92 | .wpt-list.cdk-drop-list-dragging .example-box:not(.cdk-drag-placeholder) { 93 | transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); 94 | opacity: .5; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /src/app/lib/components/dialogs/trail2route-dialog.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | insights 4 | Vessel Trail to Route 7 | 8 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 24 | 25 | 26 |  Points: {{pointCount}} 27 |
28 |
29 | 33 | Include Trail from server 34 | 35 |
36 |
37 |
38 |
39 | 57 | 58 | 59 |
60 |
61 |
62 | 63 | 64 |
65 | 72 |
73 |
74 |
75 | -------------------------------------------------------------------------------- /src/app/modules/icons/atons.ts: -------------------------------------------------------------------------------- 1 | // AtoN Icons 2 | 3 | import { AppIconSet } from './app.icons'; 4 | import { MapIconDef } from '../map/ol/lib/map-image-registry.service'; 5 | 6 | export const AtoNsType1: AppIconSet = { 7 | path: './assets/img/atons', 8 | files: [ 9 | 'real-north.svg', 10 | 'real-south.svg', 11 | 'real-east.svg', 12 | 'real-west.svg', 13 | 'real-port.svg', 14 | 'real-starboard.svg', 15 | 'real-danger.svg', 16 | 'real-special.svg', 17 | 'virtual-north.svg', 18 | 'virtual-south.svg', 19 | 'virtual-east.svg', 20 | 'virtual-west.svg', 21 | 'virtual-port.svg', 22 | 'virtual-starboard.svg', 23 | 'virtual-danger.svg', 24 | 'virtual-special.svg' 25 | ], 26 | scale: 0.4, 27 | anchor: [23, 72] 28 | }; 29 | 30 | const AtoNsType2: AppIconSet = { 31 | path: './assets/img/atons', 32 | files: [ 33 | 'real-aton.svg', 34 | 'real-safe.svg', 35 | 'virtual-aton.svg', 36 | 'virtual-safe.svg' 37 | ], 38 | scale: 0.4, 39 | anchor: [23, 49] 40 | }; 41 | 42 | export const ATON_TYPE_IDS = { 43 | aton: 'aton', 44 | '-1': 'weatherStation', 45 | 9: 'north', 46 | 10: 'east', 47 | 11: 'south', 48 | 12: 'west', 49 | 13: 'port', 50 | 14: 'starboard', 51 | 20: 'north', 52 | 21: 'east', 53 | 22: 'south', 54 | 23: 'west', 55 | 24: 'port', 56 | 25: 'starboard', 57 | 28: 'danger', 58 | 29: 'safe', 59 | 30: 'special' 60 | }; 61 | 62 | const WeatherStation: MapIconDef = { 63 | path: './assets/img/weather_station.png', 64 | anchor: [1, 25], 65 | scale: 0.75 66 | }; 67 | 68 | /** 69 | * @description Build MapIcon definitions for use by MapImageRegistry 70 | */ 71 | export const getAtoNDefs = () => { 72 | const atonList = {}; 73 | 74 | const addToList = (list: AppIconSet) => { 75 | list.files.forEach((file: string) => { 76 | const gid = file.slice(0, file.lastIndexOf('-')); 77 | const id = file.slice(file.lastIndexOf('-') + 1, file.indexOf('.')); 78 | if (!atonList[gid]) { 79 | atonList[gid] = {}; 80 | } 81 | atonList[gid][id] = { 82 | path: `${list.path}/${file}`, 83 | scale: list.scale, 84 | anchor: list.anchor 85 | }; 86 | }); 87 | }; 88 | addToList(AtoNsType1); 89 | addToList(AtoNsType2); 90 | atonList['real']['weatherStation'] = WeatherStation; 91 | atonList['virtual']['weatherStation'] = WeatherStation; 92 | return atonList; 93 | }; 94 | -------------------------------------------------------------------------------- /src/assets/img/atons/real-starboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 42 | 43 | 44 | 52 | -------------------------------------------------------------------------------- /src/app/modules/alarms/components/nsew-buttons.component.ts: -------------------------------------------------------------------------------- 1 | /*********************************** 2 | NSEW arrow buttons component 3 | 4 | ***********************************/ 5 | import { 6 | Component, 7 | Input, 8 | Output, 9 | ChangeDetectionStrategy, 10 | ChangeDetectorRef, 11 | EventEmitter 12 | } from '@angular/core'; 13 | 14 | import { MatButtonModule } from '@angular/material/button'; 15 | import { MatIconModule } from '@angular/material/icon'; 16 | 17 | @Component({ 18 | selector: 'nsew-buttons', 19 | imports: [MatButtonModule, MatIconModule], 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | styles: [ 22 | ` 23 | .nsew { 24 | border: silver 0px solid; 25 | } 26 | .nsew .btnRow { 27 | display: flex; 28 | flex-wrap: no-wrap; 29 | } 30 | .nsew .btnDiv { 31 | width: 50px; 32 | } 33 | ` 34 | ], 35 | template: ` 36 |
37 |
38 |
39 |
40 | 43 |
44 |
45 |
46 | 47 |
48 |
49 | 52 |
53 |
54 |
55 | 58 |
59 |
60 | 61 |
62 |
63 |
64 | 67 |
68 |
69 |
70 |
71 | ` 72 | }) 73 | export class NSEWButtonsComponent { 74 | @Input() disabled: boolean; 75 | @Output() direction: EventEmitter = new EventEmitter(); 76 | 77 | constructor(private cdr: ChangeDetectorRef) {} 78 | 79 | action(value: number) { 80 | this.direction.emit(value); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/modules/alarms/components/timer-button.component.ts: -------------------------------------------------------------------------------- 1 | /*********************************** 2 | timer button component 3 | 4 | ***********************************/ 5 | import { 6 | Component, 7 | Input, 8 | Output, 9 | ChangeDetectionStrategy, 10 | ChangeDetectorRef, 11 | EventEmitter 12 | } from '@angular/core'; 13 | 14 | import { MatButtonModule } from '@angular/material/button'; 15 | import { MatIconModule } from '@angular/material/icon'; 16 | 17 | @Component({ 18 | selector: 'timer-button', 19 | imports: [MatButtonModule, MatIconModule], 20 | changeDetection: ChangeDetectionStrategy.OnPush, 21 | styles: [``], 22 | template: ` 23 |
24 | @if (cancelled) { 25 |   26 | 32 | } @else { 33 | 36 | } 37 |
38 | ` 39 | }) 40 | export class TimerButtonComponent { 41 | @Input() period = 5000; // timeout period in milliseconds 42 | @Input() label: string; 43 | @Input() icon: string; 44 | @Input() cancelledLabel: string; 45 | @Input() disabled: boolean; 46 | @Output() nextPoint: EventEmitter = new EventEmitter(); 47 | 48 | private timer: ReturnType; 49 | public timeLeft: number; 50 | public cancelled = false; 51 | 52 | constructor(private cdr: ChangeDetectorRef) {} 53 | 54 | ngOnInit() { 55 | this.timeLeft = isNaN(this.period) ? 5 : this.period / 1000; 56 | this.label = this.label ?? 'Action in '; 57 | this.cancelledLabel = this.cancelledLabel ?? 'OK'; 58 | this.timer = setInterval(() => { 59 | --this.timeLeft; 60 | if (this.timeLeft === 0) { 61 | this.disabled = true; 62 | this.action(); 63 | clearInterval(this.timer); 64 | this.timer = null; 65 | } 66 | this.cdr.detectChanges(); 67 | }, 1000); 68 | } 69 | 70 | ngOnDestroy() { 71 | if (this.timer) { 72 | clearInterval(this.timer); 73 | } 74 | } 75 | 76 | cancel() { 77 | if (this.timer) { 78 | clearInterval(this.timer); 79 | this.timer = null; 80 | } 81 | this.cancelled = true; 82 | } 83 | 84 | action() { 85 | this.nextPoint.emit(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/assets/img/atons/virtual-starboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 42 | 43 | 44 | 52 | -------------------------------------------------------------------------------- /src/app/modules/map/popovers/featurelist-popover.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | Output, 5 | EventEmitter, 6 | ChangeDetectionStrategy 7 | } from '@angular/core'; 8 | import { CommonModule } from '@angular/common'; 9 | import { MatListModule } from '@angular/material/list'; 10 | import { MatButtonModule } from '@angular/material/button'; 11 | import { MatIconModule } from '@angular/material/icon'; 12 | import { MatTooltipModule } from '@angular/material/tooltip'; 13 | import { PopoverComponent } from './popover.component'; 14 | 15 | /*********** feature List Popover *************** 16 | title: string - title text, 17 | features: Array - list of features 18 | *************************************************/ 19 | @Component({ 20 | selector: 'feature-list-popover', 21 | changeDetection: ChangeDetectionStrategy.OnPush, 22 | imports: [ 23 | CommonModule, 24 | MatListModule, 25 | MatButtonModule, 26 | MatTooltipModule, 27 | MatIconModule, 28 | PopoverComponent 29 | ], 30 | template: ` 31 | 32 | 33 | @for (f of features; track f) { 34 | 35 | @if (f.icon === 'route') { 36 | 37 | } @else if (f.icon.indexOf('sk-') === 0) { 38 | 39 | } @else { 40 | 45 | {{ f.icon }} 46 | 47 | } 48 | {{ f.text }} 49 | 50 | } 51 | 52 | 53 | `, 54 | styleUrls: [] 55 | }) 56 | export class FeatureListPopoverComponent { 57 | @Input() title: string; 58 | @Input() features: Array<{ text: string; icon: string }> = []; 59 | @Input() canClose: boolean; 60 | @Output() closed: EventEmitter = new EventEmitter(); 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 62 | @Output() selected: EventEmitter = new EventEmitter(); 63 | 64 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 65 | handleSelect(item: any) { 66 | this.selected.emit(item); 67 | } 68 | handleClose() { 69 | this.closed.emit(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/types/resources/signalk.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PointFeature, 3 | LineStringFeature, 4 | PolygonFeature, 5 | MultiPolygonFeature 6 | } from './geojson'; 7 | 8 | export type SKPosition = { 9 | latitude: number; 10 | longitude: number; 11 | altitude?: number; 12 | }; 13 | 14 | export type Routes = { [id: string]: RouteResource }; 15 | export type Waypoints = { [id: string]: WaypointResource }; 16 | export type Regions = { [id: string]: RegionResource }; 17 | export type Notes = { [id: string]: NoteResource }; 18 | export type Charts = { [id: string]: ChartResource }; 19 | 20 | export interface RouteResource { 21 | name?: string | null; 22 | description?: string | null; 23 | distance?: number | null; 24 | feature: LineStringFeature; 25 | } 26 | 27 | export interface WaypointResource { 28 | name?: string | null; 29 | description?: string | null; 30 | type?: string | null; 31 | feature: PointFeature; 32 | } 33 | 34 | export interface RegionResource { 35 | name?: string | null; 36 | description?: string | null; 37 | feature: PolygonFeature | MultiPolygonFeature; 38 | } 39 | 40 | export interface NoteResource { 41 | name?: string; 42 | description?: string; 43 | href?: string; 44 | position?: SKPosition; 45 | mimeType?: string; 46 | url?: string; 47 | // ca reports attributes 48 | group?: string; 49 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 50 | authors?: Array; 51 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 52 | properties: { [key: string]: any }; 53 | timestamp: string; 54 | source: string; 55 | } 56 | 57 | export interface ChartResource { 58 | name?: string; 59 | identifier?: string; 60 | description?: string; 61 | bounds?: Array; 62 | format?: string; 63 | minzoom?: number; 64 | maxzoom?: number; 65 | type?: string; 66 | scale?: number; 67 | url?: string; 68 | layers?: string[]; 69 | defaultOpacity?: number; 70 | proxy?: boolean; 71 | $source?: string; 72 | style?: string; 73 | //v1 74 | tilemapUrl?: string; // replaced by url 75 | chartLayers?: string[]; // replaced by layers 76 | serverType?: string; // replaced by type 77 | } 78 | 79 | export interface ChartProvider { 80 | identifier?: string; 81 | name: string; 82 | title?: string; 83 | description: string; 84 | type: 'tileJSON' | 'WMS' | 'WMTS' | 'mapstyleJSON'; 85 | url: string; 86 | layers?: string[]; 87 | bounds?: number[]; 88 | minzoom?: number; 89 | maxzoom?: number; 90 | format?: string; 91 | defaultOpacity?: number; 92 | } 93 | -------------------------------------------------------------------------------- /helper/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import * as https from 'https'; 2 | import * as url from 'url'; 3 | import * as http from 'http'; 4 | 5 | // HTTP GET 6 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 7 | export const fetch = (href: string): Promise => { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | const opt: any = url.parse(href); 10 | opt.headers = { 'User-Agent': 'Mozilla/5.0' }; 11 | 12 | const req = href.indexOf('https') !== -1 ? https : http; 13 | 14 | return new Promise((resolve, reject) => { 15 | req 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | .get(opt, (res: any) => { 18 | let data = ''; 19 | res.on('data', (chunk: string) => { 20 | data += chunk; 21 | }); 22 | res.on('end', () => { 23 | try { 24 | const json = JSON.parse(data.toString()); 25 | resolve(json); 26 | } catch (error) { 27 | reject(new Error(data)); 28 | } 29 | }); 30 | }) 31 | .on('error', (error: Error) => { 32 | reject(error); 33 | }); 34 | }); 35 | }; 36 | 37 | // HTTP POST 38 | export const post = (href: string, data: string) => { 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | const opt: any = url.parse(href); 41 | ((opt.method = 'POST'), 42 | (opt.headers = { 43 | 'Content-Type': 'application/json' 44 | })); 45 | 46 | const req = href.indexOf('https') !== -1 ? https : http; 47 | 48 | return new Promise((resolve, reject) => { 49 | const postReq = req 50 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 51 | .request(opt, (res: any) => { 52 | let resText = ''; 53 | res.on('data', (chunk: string) => { 54 | resText += chunk; 55 | }); 56 | res.on('end', () => { 57 | if (Math.floor(res.statusCode / 100) === 2) { 58 | resolve({ 59 | statusCode: res.statusCode, 60 | state: 'COMPLETED' 61 | }); 62 | } else { 63 | reject({ 64 | statusCode: res.statusCode, 65 | state: 'FAILED', 66 | message: resText 67 | }); 68 | } 69 | }); 70 | }) 71 | .on('error', (error: Error) => { 72 | reject({ 73 | statusCode: 400, 74 | state: 'FAILED', 75 | message: error.message 76 | }); 77 | }); 78 | 79 | postReq.write(data); 80 | postReq.end(); 81 | }); 82 | }; 83 | -------------------------------------------------------------------------------- /src/app/app.messages.ts: -------------------------------------------------------------------------------- 1 | const WHATS_NEW = [ 2 | { 3 | type: 'signalk-server-node', 4 | title: 'Deprecation Notice', 5 | message: ` 6 | The built-in Weather Service has been removed! 7 |
 
8 | Weather forecast functionality remains available when using 9 | Signal K server v2.16 (or later) and a weather provider plugin from the AppStore. 10 |
 
11 | See HELP 12 | for more details. 13 | ` 14 | }, 15 | { 16 | type: 'signalk-server-node', 17 | title: 'New Feature', 18 | message: ` 19 | Map Overlays 20 |
 
21 | Overlays allow map data from WMS & WMTS sources to be overlayed onto charts and can be 22 | configured to refresh at regular intervals. 23 |
 
24 | See HELP 25 | for more details. 26 | ` 27 | }, 28 | { 29 | type: 'signalk-server-node', 30 | title: 'New Feature', 31 | message: ` 32 | Hazardous Area Alarm 33 |
 
34 | Attribute a region as hazardous which will sound an alarm when the vessel enters 35 | its bounds. 36 |
 
37 | See HELP 38 | for more details. 39 | ` 40 | } 41 | ]; 42 | 43 | export const WELCOME_MESSAGES = { 44 | welcome: { 45 | title: 'Welcome to Freeboard', 46 | message: `Freeboard is your Signal K chartplotter WebApp from which 47 | you can manage routes, waypoints, notes, alarms, 48 | notifications and more.` 49 | }, 50 | 'signalk-server-node': { 51 | title: 'Server Plugins', 52 | message: `Some Freeboard features require that certain plugins are installed to service the 53 | required Signal K API paths. 54 |
 
55 | See HELP 56 | for more details.` 57 | }, 58 | experiments: { 59 | title: 'Experiments', 60 | message: ` 61 | Experiments are a means for testing out potential new features 62 | in Freeboard. 63 |
 
64 | You can enable Experiments in Settings. 65 |
 
66 | Check out HELP 67 | for more details.` 68 | }, 69 | 'whats-new': WHATS_NEW 70 | }; 71 | -------------------------------------------------------------------------------- /src/assets/img/atons/real-port.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 42 | 43 | 44 | 61 | -------------------------------------------------------------------------------- /src/assets/img/atons/virtual-port.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 23 | 42 | 43 | 44 | 61 | -------------------------------------------------------------------------------- /src/app/lib/pipes/coords.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core'; 2 | import { decimalToSexagesimal } from 'geolib'; 3 | 4 | @Pipe({ 5 | name: 'coords' 6 | }) 7 | export class CoordsPipe implements PipeTransform { 8 | private symDegree = String.fromCharCode(186); 9 | 10 | public transform(value: number, type: string, isLat?: boolean): string { 11 | const h = isLat ? (value < 0 ? 'S' : 'N') : value < 0 ? 'W' : 'E'; 12 | switch (type) { 13 | case 'SHDd': 14 | return this.toHDdSigned(value, h); 15 | case 'DMdH': 16 | return this.toDMdH(value, h); 17 | case 'HDd': 18 | return this.toHDd(value, h); 19 | case 'HDMS': 20 | return this.toHDMS(value, h); 21 | case 'DHMS': 22 | return this.toDHMS(value, h); 23 | default: 24 | return this.toXY(value); 25 | } 26 | } 27 | 28 | // returns +/-DD.dddddd 29 | private toXY(value: number, precision = 5): string { 30 | return value.toFixed(precision); 31 | } 32 | 33 | // returns H +/-D.ddddd° 34 | private toHDdSigned( 35 | value: number, 36 | hemisphere: string, 37 | precision = 5 38 | ): string { 39 | const h = ['S', 'W'].includes(hemisphere) 40 | ? `${hemisphere} -` 41 | : `${hemisphere} +`; 42 | let ddec: string = value.toFixed(precision); 43 | ddec = ddec.substring(ddec.indexOf('.')); 44 | return `${h}${Math.floor(Math.abs(value))}${ddec}${this.symDegree}`; 45 | } 46 | 47 | // returns DDD° MM.ddd' H (e.g 020° 44.56' E) 48 | private toDMdH(value: number, hemisphere: string, precision = 5): string { 49 | const D = Math.floor(Math.abs(value)) ?? 0; 50 | const d = D === 0 ? Math.abs(value) : Math.abs(value % D); 51 | const mdec = (d * 60).toFixed(precision); 52 | const pad = ['N', 'S'].includes(hemisphere) ? '00' : '000'; 53 | const s = `${(pad + D.toString()).slice(0 - pad.length)}${ 54 | this.symDegree 55 | } ${mdec}' ${hemisphere}`; 56 | return s; 57 | } 58 | 59 | // returns H D.ddddd° 60 | private toHDd(value: number, hemisphere: string, precision = 5): string { 61 | let ddec: string = value.toFixed(precision); 62 | ddec = ddec.substring(ddec.indexOf('.')); 63 | return `${hemisphere} ${Math.floor(Math.abs(value))}${ddec}${ 64 | this.symDegree 65 | }`; 66 | } 67 | 68 | // returns H D°M'S"sss 69 | private toHDMS(value: number, hemisphere: string): string { 70 | return `${hemisphere} ${decimalToSexagesimal(value)}`; 71 | } 72 | 73 | // returns DHM'S"sss 74 | private toDHMS(value: number, hemisphere: string): string { 75 | let c = decimalToSexagesimal(value); 76 | c = 77 | c.slice(0, c.indexOf(' ') - 1) + hemisphere + c.slice(c.indexOf(' ') + 1); 78 | return c; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/app/types/stream.ts: -------------------------------------------------------------------------------- 1 | /** Signal K Types */ 2 | 3 | // Notification types 4 | export enum ALARM_STATE { 5 | nominal = 'nominal', 6 | normal = 'normal', 7 | alert = 'alert', 8 | warn = 'warn', 9 | alarm = 'alarm', 10 | emergency = 'emergency' 11 | } 12 | 13 | export enum ALARM_METHOD { 14 | visual = 'visual', 15 | sound = 'sound' 16 | } 17 | 18 | export interface SKNotification { 19 | state: ALARM_STATE; 20 | method: ALARM_METHOD[]; 21 | message: string; 22 | } 23 | 24 | // Update Deltas 25 | export interface PathValue { 26 | path: string; 27 | value: object | number | string | null | Notification | boolean; 28 | } 29 | 30 | export interface ActionResult { 31 | state: 'COMPLETED' | 'PENDING' | 'FAILED'; 32 | statusCode?: number; 33 | message?: string; 34 | timestamp?: string; 35 | } 36 | 37 | export interface SKPosition { 38 | latitude: number; 39 | longitude: number; 40 | altitude?: number; 41 | } 42 | 43 | /***************** */ 44 | 45 | import { 46 | SKVessel, 47 | SKAtoN, 48 | SKAircraft, 49 | SKSaR, 50 | SKMeteo 51 | } from 'src/app/modules/skresources/resource-classes'; 52 | 53 | type AisIds = Array; 54 | 55 | interface WorkerMessageBase { 56 | action: string; 57 | playback: boolean; 58 | result: ResultPayload | PathValue; 59 | self: string; 60 | timestamp: string; 61 | } 62 | 63 | export interface ResourceDeltaSignal { 64 | path: string; 65 | value: any; 66 | sourceRef?: string; 67 | } 68 | 69 | export interface ResultPayload { 70 | self: SKVessel; 71 | aisTargets: Map; 72 | aisStatus: { 73 | updated: AisIds; 74 | stale: AisIds; 75 | expired: AisIds; 76 | }; 77 | paths: { [key: string]: string }; 78 | atons: Map; 79 | aircraft: Map; 80 | sar: Map; 81 | meteo: Map; 82 | } 83 | 84 | export class NotificationMessage implements WorkerMessageBase { 85 | action = 'notification'; 86 | playback = false; 87 | result = null; 88 | self = null; 89 | timestamp = new Date().toISOString(); 90 | sourceRef!: string; 91 | } 92 | 93 | export class UpdateMessage implements WorkerMessageBase { 94 | action: string; 95 | playback = false; 96 | result = null; 97 | timestamp: string; 98 | self = null; 99 | 100 | constructor() { 101 | this.action = 'update'; 102 | } 103 | } 104 | 105 | export class ResourceMessage extends UpdateMessage { 106 | constructor() { 107 | super(); 108 | this.action = 'resource'; 109 | } 110 | } 111 | 112 | export class TrailMessage extends UpdateMessage { 113 | constructor() { 114 | super(); 115 | this.action = 'trail'; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/app/modules/map/ol/lib/util.ts: -------------------------------------------------------------------------------- 1 | import TileLayer from 'ol/layer/Tile.js'; 2 | import OSM from 'ol/source/OSM.js'; 3 | import { getPointResolution, fromLonLat } from 'ol/proj'; 4 | 5 | import { Coordinate } from './models'; 6 | 7 | export function stringToEl(html: string) { 8 | const parser = new DOMParser(); 9 | const DOM = parser.parseFromString(html, 'text/html'); 10 | return DOM.body.firstChild; 11 | } 12 | 13 | export function defaultLayers() { 14 | return [osmLayer()]; 15 | } 16 | 17 | export function osmLayer() { 18 | return new TileLayer({ source: new OSM() }); 19 | } 20 | 21 | export function osmSource() { 22 | return new OSM(); 23 | } 24 | 25 | // Point | LineString | MultiLineString 26 | export function fromLonLatArray( 27 | coords: Array> | Array | Coordinate 28 | ) { 29 | if (!Array.isArray(coords)) { 30 | return coords; 31 | } 32 | if (typeof coords[0] === 'number') { 33 | return fromLonLat(coords as Coordinate); 34 | } else if (Array.isArray(coords[0])) { 35 | return coords.map((c) => { 36 | return fromLonLatArray(c); 37 | }); 38 | } else { 39 | return coords; 40 | } 41 | } 42 | 43 | /** DateLine Crossing: 44 | * returns true if point is in the zone for dateline transition 45 | * zoneValue: lower end of 180 to xx range within which Longitude must fall for retun value to be true 46 | **/ 47 | export function inDLCrossingZone(coord: Coordinate, zoneValue = 170) { 48 | return Math.abs(coord[0]) >= zoneValue ? true : false; 49 | } 50 | 51 | // update linestring coords for map display (including dateline crossing) 52 | export function mapifyCoords( 53 | coords: Array, 54 | zoneValue?: number 55 | ): Array { 56 | if (coords.length === 0) { 57 | return coords; 58 | } 59 | let dlCrossing = 0; 60 | const last = coords[0]; 61 | for (let i = 0; i < coords.length; i++) { 62 | if ( 63 | inDLCrossingZone(coords[i], zoneValue) || 64 | inDLCrossingZone(last, zoneValue) 65 | ) { 66 | dlCrossing = 67 | last[0] > 0 && coords[i][0] < 0 68 | ? 1 69 | : last[0] < 0 && coords[i][0] > 0 70 | ? -1 71 | : 0; 72 | if (dlCrossing === 1) { 73 | coords[i][0] = coords[i][0] + 360; 74 | } 75 | if (dlCrossing === -1) { 76 | coords[i][0] = Math.abs(coords[i][0]) - 360; 77 | } 78 | } 79 | } 80 | return coords; 81 | } 82 | 83 | // ** return adjusted radius to correctly render circle on ground at given position. 84 | export function mapifyRadius(radius: number, position: Coordinate): number { 85 | if (typeof radius === 'undefined' || typeof position === 'undefined') { 86 | return radius; 87 | } 88 | return radius / getPointResolution('EPSG:3857', 1, fromLonLat(position)); 89 | } 90 | -------------------------------------------------------------------------------- /src/app/modules/skresources/components/routes/nextpoint.component.ts: -------------------------------------------------------------------------------- 1 | /** Route NextPoint Component ** 2 | ************************/ 3 | 4 | import { 5 | Component, 6 | Input, 7 | Output, 8 | EventEmitter, 9 | ChangeDetectionStrategy 10 | } from '@angular/core'; 11 | import { MatButtonModule } from '@angular/material/button'; 12 | import { MatIconModule } from '@angular/material/icon'; 13 | import { MatTooltipModule } from '@angular/material/tooltip'; 14 | 15 | /*********** NextPoint *************** 16 | index: number - index of current point, 17 | total: number - total number of points 18 | ***********************************/ 19 | @Component({ 20 | selector: 'route-nextpoint', 21 | imports: [MatButtonModule, MatTooltipModule, MatIconModule], 22 | changeDetection: ChangeDetectionStrategy.OnPush, 23 | template: ` 24 |
28 |
29 |
30 | 40 |
41 |
42 | 52 |
53 |
54 |
55 | {{ index + 1 }} of {{ total }} 56 |
57 |
58 | `, 59 | styles: [ 60 | ` 61 | .nav-button { 62 | height: 40px; 63 | } 64 | ` 65 | ] 66 | }) 67 | export class RouteNextPointComponent { 68 | @Input() index: number; 69 | @Input() total: number; 70 | @Input() circular = false; 71 | @Output() selected: EventEmitter = new EventEmitter(); 72 | 73 | //constructor() {} 74 | 75 | changeIndex(i: number) { 76 | if (i === 1) { 77 | if (this.circular && this.index === this.total - 1) { 78 | this.selected.emit(1); 79 | } else { 80 | this.selected.emit(this.index + 1); 81 | } 82 | } else { 83 | if (this.circular && this.index === 0) { 84 | this.selected.emit(this.total - 2); 85 | } else { 86 | this.selected.emit(this.index - 1); 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/assets/img/waypoints/start-boat.svg: -------------------------------------------------------------------------------- 1 | 2 | 15 | 17 | 36 | 41 | 46 | 50 | 54 | 59 | 68 | 69 | -------------------------------------------------------------------------------- /src/assets/img/vessels/ais_self.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 19 | 38 | 42 | 65 | 66 | 67 | --------------------------------------------------------------------------------