signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
1,030 lines (1,029 loc) • 40.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CourseApi = exports.COURSE_API_INITIAL_DELTA_COUNT = exports.COURSE_API_V1_DELTA_COUNT = exports.COURSE_API_V2_DELTA_COUNT = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
const debug_1 = require("../../debug");
const debug = (0, debug_1.createDebug)('signalk-server:api:course');
const lodash_1 = __importDefault(require("lodash"));
const signalk_schema_1 = require("@signalk/signalk-schema");
const server_api_1 = require("@signalk/server-api");
const { Location, RoutePoint, VesselPosition } = server_api_1.COURSE_POINT_TYPES;
const geolib_1 = require("geolib");
const __1 = require("../");
const store_1 = require("../../serverstate/store");
const api_schema_builder_1 = require("api-schema-builder");
const openApi_json_1 = __importDefault(require("./openApi.json"));
const config_1 = require("../../config/config");
const COURSE_API_SCHEMA = (0, api_schema_builder_1.buildSchemaSync)(openApi_json_1.default);
const SIGNALK_API_PATH = `/signalk/v2/api`;
const COURSE_API_PATH = `${SIGNALK_API_PATH}/vessels/self/navigation/course`;
const API_CMD_SRC = {
$source: 'courseApi',
type: 'API'
};
exports.COURSE_API_V2_DELTA_COUNT = 13;
exports.COURSE_API_V1_DELTA_COUNT = 8;
exports.COURSE_API_INITIAL_DELTA_COUNT = exports.COURSE_API_V1_DELTA_COUNT * 2 + exports.COURSE_API_V2_DELTA_COUNT;
const NO_COURSE_INFO = {
startTime: null,
targetArrivalTime: null,
arrivalCircle: 0,
activeRoute: null,
nextPoint: null,
previousPoint: null
};
class CourseApi {
app;
resourcesApi;
courseInfo = NO_COURSE_INFO;
store;
cmdSource = null; // source which set the destination
unsubscribes = [];
constructor(app, resourcesApi) {
this.app = app;
this.resourcesApi = resourcesApi;
this.store = new store_1.Store(app, 'course');
this.parseSettings();
}
async start() {
return new Promise(async (resolve) => {
this.initCourseRoutes();
let storeData;
try {
storeData = await this.store.read();
debug('Found persisted course data');
this.courseInfo = await this.validateCourseInfo(storeData);
this.cmdSource = this.courseInfo.nextPoint ? API_CMD_SRC : null;
}
catch (_error) {
debug('No persisted course data (using default)');
}
debug('** courseInfo **', this.courseInfo, '** cmdSource **', this.cmdSource);
if (this.courseInfo.nextPoint) {
this.emitCourseInfo(true);
}
this.app.subscriptionmanager?.subscribe({
context: 'vessels.self',
subscribe: [
{
path: 'navigation.courseRhumbline.nextPoint.position',
period: 500
},
{
path: 'navigation.courseGreatCircle.nextPoint.position',
period: 500
}
]
}, this.unsubscribes, (err) => {
console.log(`Course API: Subscribe failed: ${err}`);
}, (msg) => {
this.processV1DestinationDeltas(msg);
});
this.app.subscriptionmanager?.subscribe({
context: 'vessels.self',
subscribe: [
{
path: 'resources.routes.*',
period: 500
},
{
path: 'resources.waypoints.*',
period: 500
}
]
}, this.unsubscribes, (err) => {
console.log(`Course API: Subscribe failed: ${err}`);
}, (msg) => {
this.processResourceDeltas(msg);
});
resolve();
});
}
/**
* Resource delta message processing to ensure update made to
* 1. waypoint referenced as the current destination OR
* 2. active route
* are reflected in course deltas.
*/
async processResourceDeltas(delta) {
let h;
if (this.courseInfo.activeRoute?.href) {
h = this.courseInfo.activeRoute?.href.split('/').slice(-3);
}
else if (this.courseInfo.nextPoint?.href) {
h = this.courseInfo.nextPoint?.href.split('/').slice(-3);
}
else {
return;
}
const ref = h ? h.join('.') : undefined;
const refType = h ? h[1] : undefined;
for (const update of delta.updates) {
if ((0, server_api_1.hasValues)(update)) {
for (const pathValue of update.values) {
if (ref === pathValue.path) {
if (refType === 'routes') {
if (this.courseInfo.activeRoute) {
const rte = await this.getRoute(this.courseInfo.activeRoute.href);
if (rte) {
this.courseInfo.activeRoute.name = rte.name;
this.courseInfo.activeRoute.pointTotal =
rte.feature.geometry.coordinates.length;
const pointIndex = this.parsePointIndex(this.courseInfo.activeRoute.pointIndex, rte);
this.courseInfo.nextPoint = {
type: RoutePoint,
position: this.getRoutePoint(rte, pointIndex, !!this.courseInfo.activeRoute.reverse)
};
this.emitCourseInfo();
}
}
}
else {
const r = await this.resourcesApi.getResource(refType, h[2]);
if (r && (0, geolib_1.isValidCoordinate)(r.feature.geometry.coordinates)) {
;
this.courseInfo.nextPoint.position = {
latitude: r.feature.geometry.coordinates[1],
longitude: r.feature.geometry.coordinates[0]
};
this.emitCourseInfo();
}
}
}
}
}
}
}
// parse server settings
parseSettings() {
const defaultSettings = {
apiOnly: false
};
if (!('courseApi' in this.app.config.settings)) {
debug('***** Applying Default Settings ********');
this.app.config.settings.courseApi = defaultSettings;
}
if (this.app.config.settings.courseApi &&
typeof this.app.config.settings.courseApi.apiOnly === 'undefined') {
debug('***** Applying missing apiOnly attribute to Settings ********');
this.app.config.settings.courseApi.apiOnly = false;
}
debug('** Parsed App Settings ***', this.app.config.settings);
debug('** Applied cmdSource ***', this.cmdSource);
}
// write to server settings file
saveSettings() {
(0, config_1.writeSettingsFile)(this.app, this.app.config.settings, () => debug('***SETTINGS SAVED***'));
}
/** Process deltas for <destination>.nextPoint data
* Note: Delta source cannot override destination isAPIset by API!
* Destination is set when:
* 1. There is no current destination
* 2. msg source matches current Destination source
* 3. Destination Position is changed.
*/
async processV1DestinationDeltas(delta) {
if (!Array.isArray(delta.updates) ||
this.isAPICmdSource() ||
(!this.cmdSource && this.app.config.settings.courseApi?.apiOnly)) {
return;
}
delta.updates.forEach((update) => {
if ((0, server_api_1.hasValues)(update)) {
update.values.forEach((pathValue) => {
if (update.source &&
update.source.type &&
['NMEA0183', 'NMEA2000'].includes(update.source.type)) {
this.parseStreamValue({
type: update.source.type,
$source: update.$source || (0, signalk_schema_1.getSourceId)(update.source),
msg: update.source.type === 'NMEA0183'
? `${update.source.sentence}`
: `${update.source.pgn}`,
path: pathValue.path
}, pathValue.value);
}
});
}
});
}
/** Test for valid Signal K position */
isValidPosition(position) {
return (typeof position?.latitude === 'number' &&
typeof position?.latitude === 'number' &&
position?.latitude >= -90 &&
position?.latitude <= 90 &&
position?.longitude >= -180 &&
position?.longitude <= 180);
}
/** Process stream value and take action
* @param cmdSource Object describing the source of the update
* @param pos Destination location value in the update
*/
async parseStreamValue(cmdSource, pos) {
if (!this.cmdSource) {
// New source
if (!this.isValidPosition(pos)) {
return;
}
debug('parseStreamValue:', 'Setting Destination...');
const result = await this.setDestination({ position: pos }, cmdSource);
debug('parseStreamValue: Source set...', this.cmdSource);
if (result) {
this.emitCourseInfo();
return;
}
}
if (this.isCurrentCmdSource(cmdSource)) {
if (!this.isValidPosition(pos)) {
debug('parseStreamValue:', 'No or invalid position... Clear Destination...');
this.clearDestination();
return;
}
if (this.courseInfo.nextPoint?.position?.latitude !== pos.latitude ||
this.courseInfo.nextPoint?.position?.longitude !== pos.longitude) {
debug('parseStreamValue:', 'Position changed... Updating Destination...');
const result = await this.setDestination({ position: pos }, cmdSource);
if (result) {
this.emitCourseInfo();
}
}
}
}
/** Get course (exposed to plugins) */
async getCourse() {
debug(`** getCourse()`);
return this.courseInfo;
}
/** Clear destination / route (exposed to plugins) */
async clearDestination(persistState) {
this.courseInfo = {
...NO_COURSE_INFO,
arrivalCircle: this.courseInfo.arrivalCircle
};
this.cmdSource = null;
this.emitCourseInfo(!persistState);
}
/** Set course (exposed to plugins)
* @param dest Setting to null clears the current destination
*/
async destination(dest) {
debug(`** destination(${dest})`);
if (!dest) {
throw new Error('No destination information supplied!');
}
const result = await this.setDestination(dest);
if (result) {
this.emitCourseInfo();
}
}
/** Set / clear route (exposed to plugins)
* @param dest Setting to null clears the current destination
*/
async activeRoute(dest) {
debug(`** activeRoute(${dest})`);
if (!dest) {
throw new Error('No route information supplied!');
}
const result = await this.activateRoute(dest);
if (result) {
this.emitCourseInfo();
}
}
getVesselPosition() {
return lodash_1.default.get(this.app.signalk.self, 'navigation.position');
}
async validateCourseInfo(info) {
if (!hasAllProperties(info, ['activeRoute', 'nextPoint', 'previousPoint'])) {
debug(`** Error: Loaded course data is invalid!! **`);
return NO_COURSE_INFO;
}
if ((await this.isValidRouteCourse(info)) ||
(await this.isValidWaypointCourse(info))) {
return info;
}
return NO_COURSE_INFO;
}
async isValidRouteCourse(info) {
if (!info?.activeRoute?.href) {
return false;
}
const activeRoute = info.activeRoute;
const route = await this.getRoute(activeRoute.href);
return (route?.feature !== undefined &&
activeRoute.pointIndex >= 0 &&
activeRoute.pointIndex < route.feature.geometry.coordinates.length);
}
async isValidWaypointCourse(info) {
if (!info?.nextPoint?.href) {
return false;
}
const parsedHref = this.parseHref(info.nextPoint.href);
if (!parsedHref) {
return false;
}
const wpt = (await this.resourcesApi.getResource(parsedHref.type, parsedHref.id));
return wpt?.feature !== undefined;
}
updateAllowed(request) {
return this.app.securityStrategy.shouldAllowPut(request, 'vessels.self', null, 'navigation.course');
}
initCourseRoutes() {
debug(`** Initialise ${COURSE_API_PATH} path handlers **`);
// Return current course information
this.app.get(`${COURSE_API_PATH}`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
res.json(this.courseInfo);
});
// Return course api config
this.app.get(`${COURSE_API_PATH}/_config`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
res.json(this.app.config.settings.courseApi);
});
// Set apiOnly mode
this.app.post(`${COURSE_API_PATH}/_config/apiOnly`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
try {
if (this.app.config.settings.courseApi) {
this.app.config.settings.courseApi.apiOnly = true;
}
else {
this.app.config.settings.courseApi = { apiOnly: true };
}
if (!this.isAPICmdSource()) {
this.clearDestination(true);
}
this.saveSettings();
res.status(200).json(__1.Responses.ok);
}
catch {
res.status(400).json(__1.Responses.invalid);
}
});
// Clear apiOnly mode
this.app.delete(`${COURSE_API_PATH}/_config/apiOnly`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
try {
if (this.app.config.settings.courseApi) {
this.app.config.settings.courseApi.apiOnly = false;
}
else {
this.app.config.settings.courseApi = { apiOnly: false };
}
this.saveSettings();
res.status(200).json(__1.Responses.ok);
}
catch {
res.status(400).json(__1.Responses.invalid);
}
});
// course metadata
this.app.get(`${COURSE_API_PATH}/arrivalCircle/meta`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
res.json({
arrivalCircle: {
description: 'The circle which indicates arrival when vessel position is within its radius.',
units: 'm'
}
});
});
this.app.put(`${COURSE_API_PATH}/arrivalCircle`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
if (this.isValidArrivalCircle(req.body.value)) {
this.courseInfo.arrivalCircle = req.body.value;
this.emitCourseInfo(false, 'arrivalCircle');
res.status(200).json(__1.Responses.ok);
}
else {
res.status(400).json(__1.Responses.invalid);
}
});
this.app.put(`${COURSE_API_PATH}/restart`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
if (!this.courseInfo.nextPoint) {
res.status(400).json({
state: 'FAILED',
statusCode: 400,
message: `No active destination!`
});
return;
}
// set previousPoint to vessel position
try {
const position = this.getVesselPosition();
if (position && position.value) {
this.courseInfo.previousPoint = {
position: position.value,
type: VesselPosition
};
this.emitCourseInfo(false, 'previousPoint');
res.status(200).json(__1.Responses.ok);
}
else {
res.status(400).json({
state: 'FAILED',
statusCode: 400,
message: `Vessel position unavailable!`
});
}
}
catch (_err) {
res.status(400).json({
state: 'FAILED',
statusCode: 400,
message: `Vessel position unavailable!`
});
}
});
this.app.put(`${COURSE_API_PATH}/targetArrivalTime`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
if (req.body.value === null || this.isValidIsoTime(req.body.value)) {
this.courseInfo.targetArrivalTime = req.body.value;
this.emitCourseInfo(false, 'targetArrivalTime');
res.status(200).json(__1.Responses.ok);
}
else {
res.status(400).json(__1.Responses.invalid);
}
});
// clear / cancel course
this.app.delete(`${COURSE_API_PATH}`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
this.clearDestination(true);
res.status(200).json(__1.Responses.ok);
});
// set destination
this.app.put(`${COURSE_API_PATH}/destination`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
const endpoint = COURSE_API_SCHEMA[`${COURSE_API_PATH}/destination`].put;
if (!endpoint.body.validate(req.body)) {
res.status(400).json(endpoint.body.errors);
return;
}
try {
const result = await this.setDestination(req.body);
if (result) {
this.emitCourseInfo();
res.status(200).json(__1.Responses.ok);
}
else {
res.status(400).json(__1.Responses.invalid);
}
}
catch (error) {
res.status(400).json({
state: 'FAILED',
statusCode: 400,
message: error.message
});
}
});
// set activeRoute
this.app.put(`${COURSE_API_PATH}/activeRoute`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
try {
const result = await this.activateRoute(req.body);
debug(this.courseInfo);
if (result) {
this.emitCourseInfo();
res.status(200).json(__1.Responses.ok);
}
else {
res.status(400).json(__1.Responses.invalid);
}
}
catch (error) {
res.status(400).json({
state: 'FAILED',
statusCode: 400,
message: error.message
});
}
});
this.app.put(`${COURSE_API_PATH}/activeRoute/:action`, async (req, res) => {
debug(`** ${req.method} ${req.path}, ${req.params.action}`);
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
return;
}
// fetch active route data
if (!this.courseInfo.activeRoute) {
res.status(400).json(__1.Responses.invalid);
return;
}
const rte = await this.getRoute(this.courseInfo.activeRoute.href);
if (!rte) {
res.status(400).json(__1.Responses.invalid);
return;
}
if (req.params.action === 'nextPoint') {
if (typeof this.courseInfo.activeRoute.pointIndex === 'number') {
if (!req.body.value || typeof req.body.value !== 'number') {
req.body.value = 1;
}
this.courseInfo.activeRoute.pointIndex = this.parsePointIndex(this.courseInfo.activeRoute.pointIndex + req.body.value, rte);
}
else {
res.status(400).json(__1.Responses.invalid);
return;
}
}
if (req.params.action === 'pointIndex') {
if (typeof req.body.value === 'number') {
this.courseInfo.activeRoute.pointIndex = this.parsePointIndex(req.body.value, rte);
}
else {
res.status(400).json(__1.Responses.invalid);
return;
}
}
// reverse direction from current point
if (req.params.action === 'reverse') {
if (typeof req.body.pointIndex === 'number') {
this.courseInfo.activeRoute.pointIndex = req.body.pointIndex;
}
else {
this.courseInfo.activeRoute.pointIndex = this.calcReversedIndex(this.courseInfo.activeRoute);
}
this.courseInfo.activeRoute.reverse =
!this.courseInfo.activeRoute.reverse;
}
if (req.params.action === 'refresh') {
this.courseInfo.activeRoute.pointTotal =
rte.feature.geometry.coordinates.length;
let idx = -1;
for (let i = 0; i < rte.feature.geometry.coordinates.length; i++) {
if (rte.feature.geometry.coordinates[i][0] ===
this.courseInfo.nextPoint?.position?.longitude &&
rte.feature.geometry.coordinates[i][1] ===
this.courseInfo.nextPoint.position?.latitude) {
idx = i;
}
}
if (idx !== -1) {
this.courseInfo.activeRoute.pointIndex = idx;
}
this.emitCourseInfo();
res.status(200).json(__1.Responses.ok);
return;
}
// set new destination
this.courseInfo.nextPoint = {
position: this.getRoutePoint(rte, this.courseInfo.activeRoute.pointIndex, this.courseInfo.activeRoute.reverse),
type: RoutePoint
};
// set previousPoint
if (this.courseInfo.activeRoute.pointIndex === 0) {
try {
const position = this.getVesselPosition();
if (position && position.value) {
this.courseInfo.previousPoint = {
position: position.value,
type: VesselPosition
};
}
else {
res.status(400).json(__1.Responses.invalid);
return false;
}
}
catch (_err) {
console.log(`** Course API: Unable to retrieve vessel position!`);
res.status(400).json(__1.Responses.invalid);
return false;
}
}
else {
this.courseInfo.previousPoint = {
position: this.getRoutePoint(rte, this.courseInfo.activeRoute.pointIndex - 1, this.courseInfo.activeRoute.reverse),
type: RoutePoint
};
}
this.emitCourseInfo();
res.status(200).json(__1.Responses.ok);
});
}
calcReversedIndex(activeRoute) {
return (activeRoute.pointTotal -
1 -
activeRoute.pointIndex);
}
async activateRoute(route, src = API_CMD_SRC) {
const { href, reverse } = route;
let rte;
if (href) {
rte = await this.getRoute(href);
if (!rte) {
throw new Error(`** Could not retrieve route information for ${route.href}`);
}
if (!Array.isArray(rte.feature?.geometry?.coordinates)) {
throw new Error(`Invalid route coordinate data! (${route.href})`);
}
}
else {
throw new Error('Route information not supplied!');
}
const newCourse = { ...this.courseInfo };
const pointIndex = this.parsePointIndex(route.pointIndex, rte);
const activeRoute = {
href,
name: rte.name,
reverse: !!reverse,
pointIndex,
pointTotal: rte.feature.geometry.coordinates.length
};
newCourse.activeRoute = activeRoute;
newCourse.nextPoint = {
type: RoutePoint,
position: this.getRoutePoint(rte, pointIndex, !!reverse)
};
newCourse.startTime = new Date().toISOString();
if (this.isValidArrivalCircle(route.arrivalCircle)) {
newCourse.arrivalCircle = route.arrivalCircle;
}
// set previousPoint
if (activeRoute.pointIndex === 0) {
try {
const position = this.getVesselPosition();
if (position && position.value) {
newCourse.previousPoint = {
position: position.value,
type: VesselPosition
};
}
else {
throw new Error(`Error: Unable to retrieve vessel position!`);
}
}
catch (_err) {
throw new Error(`Error: Unable to retrieve vessel position!`);
}
}
else {
newCourse.previousPoint = {
position: this.getRoutePoint(rte, activeRoute.pointIndex - 1, activeRoute.reverse),
type: RoutePoint
};
}
if (this.isSourceChange(src)) {
this.clearDestination(true);
}
this.courseInfo = newCourse;
this.cmdSource = src;
return true;
}
async setDestination(dest, src = API_CMD_SRC) {
const newCourse = { ...this.courseInfo };
newCourse.startTime = new Date().toISOString();
if (this.isValidArrivalCircle(dest.arrivalCircle)) {
newCourse.arrivalCircle = dest.arrivalCircle;
}
if ('href' in dest) {
const typedHref = this.parseHref(dest.href);
if (typedHref) {
debug(`fetching ${JSON.stringify(typedHref)}`);
// fetch waypoint resource details
try {
const r = (await this.resourcesApi.getResource(typedHref.type, typedHref.id));
if ((0, geolib_1.isValidCoordinate)(r.feature.geometry.coordinates)) {
newCourse.nextPoint = {
position: {
latitude: r.feature.geometry.coordinates[1],
longitude: r.feature.geometry.coordinates[0]
},
href: dest.href,
type: r.type ?? 'Waypoint'
};
newCourse.activeRoute = null;
}
else {
throw new Error(`Invalid waypoint coordinate data! (${dest.href})`);
}
}
catch (_err) {
throw new Error(`Error retrieving and validating ${dest.href}`);
}
}
else {
throw new Error(`Invalid href! (${dest.href})`);
}
}
else if ('position' in dest) {
if (this.isValidPosition(dest.position)) {
newCourse.nextPoint = {
position: dest.position,
type: Location
};
}
else {
throw new Error(`Error: position is not valid`);
}
}
else {
throw new Error(`Destination not provided!`);
}
// clear activeRoute
newCourse.activeRoute = null;
// set previousPoint
try {
const position = this.getVesselPosition();
if (position && position.value) {
newCourse.previousPoint = {
position: position.value,
type: VesselPosition
};
}
else {
throw new Error(`Error: navigation.position.value is undefined! (${position})`);
}
}
catch (_err) {
throw new Error(`Error: Unable to retrieve vessel position!`);
}
if (this.isSourceChange(src)) {
this.clearDestination(true);
}
this.courseInfo = newCourse;
this.cmdSource = src;
return true;
}
isValidArrivalCircle(value) {
return typeof value === 'number' && value >= 0;
}
isValidIsoTime(value) {
return !value
? false
: /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)((-(\d{2}):(\d{2})|Z))$/.test(value);
}
parsePointIndex(index, rte) {
if (typeof index !== 'number' || !rte) {
return 0;
}
if (!rte.feature?.geometry?.coordinates) {
return 0;
}
if (!Array.isArray(rte.feature?.geometry?.coordinates)) {
return 0;
}
if (index < 0) {
return 0;
}
if (index > rte.feature?.geometry?.coordinates.length - 1) {
return rte.feature?.geometry?.coordinates.length - 1;
}
return index;
}
parseHref(href) {
if (!href) {
return undefined;
}
const ref = href.split('/').slice(-3);
if (ref.length < 3) {
return undefined;
}
if (ref[0] !== 'resources') {
return undefined;
}
return {
type: ref[1],
id: ref[2]
};
}
getRoutePoint(rte, index, reverse) {
const pos = reverse
? rte.feature.geometry.coordinates[rte.feature.geometry.coordinates.length - (index + 1)]
: rte.feature.geometry.coordinates[index];
const result = {
latitude: pos[1],
longitude: pos[0]
};
if (pos.length === 3) {
result.altitude = pos[2];
}
return result;
}
getRoutePoints(rte) {
const pts = rte.feature.geometry.coordinates.map((pt) => {
return {
position: {
latitude: pt[1],
longitude: pt[0]
}
};
});
return pts;
}
async getRoute(href) {
const h = this.parseHref(href);
if (h) {
try {
return (await this.resourcesApi.getResource(h.type, h.id));
}
catch (_err) {
debug(`** Unable to fetch resource: ${h.type}, ${h.id}`);
return undefined;
}
}
else {
debug(`** Unable to parse href: ${href}`);
return undefined;
}
}
buildDeltaMsg(paths) {
const values = [];
const navPath = 'navigation.course';
if (paths.length === 0 ||
(paths && (paths.includes('activeRoute') || paths.includes('nextPoint')))) {
values.push({
path: `${navPath}.startTime`,
value: this.courseInfo.startTime
});
}
if (paths.length === 0 || (paths && paths.includes('targetArrivalTime'))) {
values.push({
path: `${navPath}.targetArrivalTime`,
value: this.courseInfo.targetArrivalTime
});
}
if (paths.length === 0 || (paths && paths.includes('activeRoute'))) {
values.push({
path: `${navPath}.activeRoute`,
value: this.courseInfo.activeRoute
});
}
if (paths.length === 0 || (paths && paths.includes('arrivalCircle'))) {
values.push({
path: `${navPath}.arrivalCircle`,
value: this.courseInfo.arrivalCircle
});
}
if (paths.length === 0 || (paths && paths.includes('previousPoint'))) {
values.push({
path: `${navPath}.previousPoint`,
value: this.courseInfo.previousPoint
});
}
return {
updates: [
{
values
}
]
};
}
buildV1DeltaMsg(paths) {
const values = [];
const navGC = 'navigation.courseGreatCircle';
const navRL = 'navigation.courseRhumbline';
if (paths.length === 0 || (paths && paths.includes('activeRoute'))) {
values.push({
path: `${navGC}.activeRoute.href`,
value: this.courseInfo.activeRoute?.href ?? null
});
values.push({
path: `${navRL}.activeRoute.href`,
value: this.courseInfo.activeRoute?.href ?? null
});
values.push({
path: `${navGC}.activeRoute.startTime`,
value: this.courseInfo.startTime
});
values.push({
path: `${navRL}.activeRoute.startTime`,
value: this.courseInfo.startTime
});
}
if (paths.length === 0 || (paths && paths.includes('nextPoint'))) {
values.push({
path: `${navGC}.nextPoint.value.href`,
value: this.courseInfo.nextPoint?.href ?? null
});
values.push({
path: `${navRL}.nextPoint.value.href`,
value: this.courseInfo.nextPoint?.href ?? null
});
values.push({
path: `${navGC}.nextPoint.value.type`,
value: this.courseInfo.nextPoint?.type ?? null
});
values.push({
path: `${navRL}.nextPoint.value.type`,
value: this.courseInfo.nextPoint?.type ?? null
});
values.push({
path: `${navGC}.nextPoint.position`,
value: this.courseInfo.nextPoint?.position ?? null
});
values.push({
path: `${navRL}.nextPoint.position`,
value: this.courseInfo.nextPoint?.position ?? null
});
}
if (paths.length === 0 || (paths && paths.includes('arrivalCircle'))) {
values.push({
path: `${navGC}.nextPoint.arrivalCircle`,
value: this.courseInfo.arrivalCircle
});
values.push({
path: `${navRL}.nextPoint.arrivalCircle`,
value: this.courseInfo.arrivalCircle
});
}
if (paths.length === 0 || (paths && paths.includes('previousPoint'))) {
values.push({
path: `${navGC}.previousPoint.position`,
value: this.courseInfo.previousPoint?.position ?? null
});
values.push({
path: `${navRL}.previousPoint.position`,
value: this.courseInfo.previousPoint?.position ?? null
});
values.push({
path: `${navGC}.previousPoint.value.type`,
value: this.courseInfo.previousPoint?.type ?? null
});
values.push({
path: `${navRL}.previousPoint.value.type`,
value: this.courseInfo.previousPoint?.type ?? null
});
}
return {
updates: [
{
values: values
}
]
};
}
emitCourseInfo(noSave, ...paths) {
this.app.handleMessage(API_CMD_SRC.$source, this.buildV1DeltaMsg(paths), server_api_1.SKVersion.v1);
const v2Delta = this.buildDeltaMsg(paths);
v2Delta.updates[0].$source = API_CMD_SRC.$source;
v2Delta.updates.push({
$source: this.cmdSource ? this.cmdSource.$source : API_CMD_SRC.$source,
values: [
{
path: `navigation.course.nextPoint`,
value: this.courseInfo.nextPoint
}
]
});
this.app.handleMessage('N/A', //no-op as updates already have $source
v2Delta, server_api_1.SKVersion.v2);
const p = typeof noSave === 'undefined' ? this.isAPICmdSource() : !noSave;
if (p) {
debug('*** persisting state **');
this.store.write(this.courseInfo).catch((error) => {
console.log('Course API: Unable to persist destination details!');
debug(error);
});
}
}
isAPICmdSource = () => this.cmdSource?.type === API_CMD_SRC.type;
isSourceChange = (newSource) => this.cmdSource !== null &&
(this.cmdSource.type !== newSource.type ||
this.cmdSource.$source !== newSource.$source);
isCurrentCmdSource = (cmdSource) => this.cmdSource?.type === cmdSource.type &&
this.cmdSource?.$source === cmdSource.$source &&
this.cmdSource?.path === cmdSource.path &&
this.cmdSource?.msg === cmdSource.msg;
}
exports.CourseApi = CourseApi;
const hasAllProperties = (info, propNames) => {
return !propNames.find((propName) => !(propName in info));
};