diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2df5d14b..4c476100 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -43,3 +43,7 @@ For the moment, it does not work with docker compose. But if you install the clo
- open another shell.
- define the required environment variables:`eval $(gcloud beta emulators datastore --data-dir=MY_DATA_DIR env-init)`
- you can then run the application locally in this shell with `npm run dev`
+
+## Before you submit your PR:
+
+run this command to format/lint/test: `npx nx check`
diff --git a/apps/fxc-front/src/app/components/2d/path-element.ts b/apps/fxc-front/src/app/components/2d/path-element.ts
index 6317ca23..27c59f81 100644
--- a/apps/fxc-front/src/app/components/2d/path-element.ts
+++ b/apps/fxc-front/src/app/components/2d/path-element.ts
@@ -13,12 +13,12 @@ import { FaiSectors } from '../../gm/fai-sectors';
import { addAltitude } from '../../logic/elevation';
import { getCurrentUrl, pushCurrentState } from '../../logic/history';
import { drawRoute } from '../../logic/messages';
-import { CircuitType, Score } from '../../logic/score/scorer';
+import { CircuitType, getCircuitType, Score } from '../../logic/score/scorer';
import { setDistance, setEnabled, setRoute, setScore } from '../../redux/planner-slice';
import { RootState, store } from '../../redux/store';
import { PlannerElement } from './planner-element';
-import { OptimizationResult, optimize, ScoringRules, ScoringTrack } from 'optimizer';
-import { LeagueCode } from '../../logic/score/league/leagues';
+import { getOptimizer, ScoringTrack } from 'optimizer';
+import { getScoringRules } from '../../logic/score/league/leagues';
// Route color by circuit type.
const ROUTE_STROKE_COLORS = {
@@ -225,10 +225,10 @@ export class PathElement extends connect(store)(LitElement) {
this.closingSector.addListener('rightclick', (e) => this.appendToPath(e.latLng));
}
- if (score.closingRadius) {
+ if (score.closingRadiusM) {
const center = points[score.indexes[0]];
this.closingSector.center = center;
- this.closingSector.radius = score.closingRadius;
+ this.closingSector.radius = score.closingRadiusM;
this.closingSector.update();
this.closingSector.setMap(this.map);
} else {
@@ -252,67 +252,26 @@ export class PathElement extends connect(store)(LitElement) {
private computeScore(points: LatLon[]): Score {
const track: ScoringTrack = {
- points: points.map((point, i) => {
- return {
- ...point,
- alt: 0,
- timeSec: i * 60,
- };
- }),
- minTimeSec: new Date().getTime() / 1000,
+ points: points.map((point, i) => ({ ...point, alt: 0, timeSec: i * 60 })),
+ startTimeSec: Math.round(new Date().getTime() / 1000),
};
- const result = optimize({ track }, this.getLeague()).next().value;
- const score = new Score({
- circuit: this.getCircuitType(result),
- distance: result.lengthKm * 1000,
+ const result = getOptimizer({ track }, getScoringRules(this.league)).next().value;
+ return new Score({
+ circuit: getCircuitType(result.circuit),
+ distanceM: result.lengthKm * 1000,
multiplier: result.multiplier,
- closingRadius: result.closingRadius ? result.closingRadius * 1000 : null,
+ closingRadiusM: result.closingRadius ? result.closingRadius * 1000 : null,
indexes: result.solutionIndices,
+ points: result.score,
});
- // force the score as computed because of an unwanted side effect in constructor.
- score.forcePoints(result.score);
- return score;
- }
-
- private getLeague(): ScoringRules {
- switch (this.league as LeagueCode) {
- case 'czl':
- return ScoringRules.CzechLocal;
- case 'cze':
- return ScoringRules.CzechEuropean;
- case 'czo':
- return ScoringRules.CzechOutsideEurope;
- case 'fr':
- return ScoringRules.FederationFrancaiseVolLibre;
- case 'leo':
- return ScoringRules.Leonardo;
- case 'nor':
- return ScoringRules.Norway;
- case 'ukc':
- return ScoringRules.UnitedKingdomClub;
- case 'uki':
- return ScoringRules.UnitedKingdomInternational;
- case 'ukn':
- return ScoringRules.UnitedKingdomNational;
- case 'xc':
- return ScoringRules.XContest;
- case 'xcppg':
- return ScoringRules.XContestPPG;
- case 'wxc':
- return ScoringRules.WorldXC;
- }
- }
-
- private getCircuitType(result: OptimizationResult) {
- return result.circuit as unknown as CircuitType;
}
// Sends a message to the iframe host with the changes.
private postScoreToHost(score: Score) {
let kms = '';
let circuit = '';
- if (score.distance && window.parent) {
- kms = (score.distance / 1000).toFixed(1);
+ if (score.distanceM && window.parent) {
+ kms = (score.distanceM / 1000).toFixed(1);
circuit = CIRCUIT_SHORT_NAME[score.circuit];
if (score.circuit == CircuitType.OpenDistance) {
circuit += score.indexes.length - 2;
diff --git a/apps/fxc-front/src/app/components/2d/planner-element.ts b/apps/fxc-front/src/app/components/2d/planner-element.ts
index 270c5a8e..65ca3958 100644
--- a/apps/fxc-front/src/app/components/2d/planner-element.ts
+++ b/apps/fxc-front/src/app/components/2d/planner-element.ts
@@ -139,7 +139,7 @@ export class PlannerElement extends connect(store)(LitElement) {
${this.score.circuit}
- ${unsafeHTML(units.formatUnit(this.score.distance / 1000, this.units.distance, undefined, 'unit'))}
+ ${unsafeHTML(units.formatUnit(this.score.distanceM / 1000, this.units.distance, undefined, 'unit'))}
diff --git a/apps/fxc-front/src/app/logic/score/league/leagues.ts b/apps/fxc-front/src/app/logic/score/league/leagues.ts
index 6eb26830..ec7f72bc 100644
--- a/apps/fxc-front/src/app/logic/score/league/leagues.ts
+++ b/apps/fxc-front/src/app/logic/score/league/leagues.ts
@@ -1,4 +1,10 @@
-export const LEAGUES: { [name: string]: string } = {
+import { ScoringRules } from 'optimizer';
+
+// allowed league codes
+export const leagueCodes = ['czl', 'cze', 'czo', 'fr', 'leo', 'nor', 'ukc', 'uki', 'ukn', 'xc', 'xcppg', 'wxc'];
+export type LeagueCode = (typeof leagueCodes)[number];
+
+export const LEAGUES: Readonly> = {
czl: 'Czech (ČPP local)',
cze: 'Czech (ČPP Europe)',
czo: 'Czech (ČPP outside Europe)',
@@ -13,7 +19,32 @@ export const LEAGUES: { [name: string]: string } = {
wxc: 'World XC Online Contest',
};
-// allowed league codes
-// ensure that all league codes defined in each League sub classes are in this
-// closed set.
-export type LeagueCode = 'czl' | 'cze' | 'czo' | 'fr' | 'leo' | 'nor' | 'ukc' | 'uki' | 'ukn' | 'xc' | 'xcppg' | 'wxc';
+export function getScoringRules(league: LeagueCode): ScoringRules {
+ switch (league) {
+ case 'czl':
+ return 'CzechLocal';
+ case 'cze':
+ return 'CzechEuropean';
+ case 'czo':
+ return 'CzechOutsideEurope';
+ case 'fr':
+ return 'FederationFrancaiseVolLibre';
+ case 'leo':
+ return 'Leonardo';
+ case 'nor':
+ return 'Norway';
+ case 'ukc':
+ return 'UnitedKingdomClub';
+ case 'uki':
+ return 'UnitedKingdomInternational';
+ case 'ukn':
+ return 'UnitedKingdomNational';
+ case 'xc':
+ return 'XContest';
+ case 'xcppg':
+ return 'XContestPPG';
+ case 'wxc':
+ return 'WorldXC';
+ }
+ throw Error('no corresponding rule for ' + league);
+}
diff --git a/apps/fxc-front/src/app/logic/score/scorer.ts b/apps/fxc-front/src/app/logic/score/scorer.ts
index a5ea9211..4c378e44 100644
--- a/apps/fxc-front/src/app/logic/score/scorer.ts
+++ b/apps/fxc-front/src/app/logic/score/scorer.ts
@@ -1,3 +1,5 @@
+import { CircuitType as OptimizerCircuitType } from 'optimizer';
+
export enum CircuitType {
OpenDistance = 'Open distance',
FlatTriangle = 'Flat triangle',
@@ -5,23 +7,24 @@ export enum CircuitType {
OutAndReturn = 'Out and return',
}
+export function getCircuitType(circuit?: OptimizerCircuitType) {
+ return circuit as unknown as CircuitType;
+}
+
export class Score {
- distance: number;
+ distanceM: number;
indexes: number[];
multiplier: number;
circuit: CircuitType;
- closingRadius: number | null;
+ closingRadiusM: number | null;
points: number;
- constructor(score: Partial>) {
- this.distance = score.distance || 0;
- this.indexes = score.indexes || [];
- this.multiplier = score.multiplier || 1;
- this.circuit = score.circuit || CircuitType.OpenDistance;
- this.closingRadius = score.closingRadius || null;
- this.points = (this.distance * this.multiplier) / 1000;
- }
- public forcePoints(points: number){
- this.points = points;
+ constructor(score: Partial) {
+ this.distanceM = score.distanceM ?? 0;
+ this.indexes = score.indexes ?? [];
+ this.multiplier = score.multiplier ?? 1;
+ this.circuit = score.circuit ?? CircuitType.OpenDistance;
+ this.closingRadiusM = score.closingRadiusM ?? null;
+ this.points = score.points ?? 0;
}
}
diff --git a/apps/fxc-front/vite.config.ts b/apps/fxc-front/vite.config.ts
index c6c47f6b..3e75a63e 100644
--- a/apps/fxc-front/vite.config.ts
+++ b/apps/fxc-front/vite.config.ts
@@ -93,7 +93,8 @@ export default defineConfig({
define: {
__BUILD_TIMESTAMP__: JSON.stringify(format(new Date(), 'yyyyMMdd.HHmm')),
__AIRSPACE_DATE__: JSON.stringify(getAirspaceDate()),
- global: {}, // required by igc-xc-score
+ // required by igc-xc-score. TODO(vicb): check how to remove this
+ global: {},
},
});
diff --git a/libs/optimizer/README.md b/libs/optimizer/README.md
index a601c817..87ff2b73 100644
--- a/libs/optimizer/README.md
+++ b/libs/optimizer/README.md
@@ -3,16 +3,17 @@
This library was generated with [Nx](https://nx.dev).
## Description
+
This library computes scores for flights using applicable rules of various XC leagues.
## Usage
The `src/lib/optimizer.ts#optimize` function computes score of a given track given by a `ScoringTrack` for a given league known by it's `LeagueCode`.
-You can specify `OptimizationOptions` to limit either the number of the iterations performed during the optimization (`OptimizationOptions.maxLoop`)
+You can specify `OptimizationOptions` to limit either the number of the iterations performed during the optimization (`OptimizationOptions.maxLoop`)
or the maximum duration in milliseconds allowed for the optimization.
-The `optimize` function is a generator function. It returns an `Iterator`. You should call the `next()`
-method of this iterator to get the current `IteratorResult`. The `value` property of the `IteratorResult`
+The `optimize` function is a generator function. It returns an `Iterator`. You should call the `next()`
+method of this iterator to get the current `IteratorResult`. The `value` property of the `IteratorResult`
gives the current `OptimizationResult` and the `done` property of the `IteratorResult` indicates if the optimization is terminated or not.
If the `done` property is false, you should call again the `next()` method of the iterator so that you get another result that should be a better
diff --git a/libs/optimizer/src/index.ts b/libs/optimizer/src/index.ts
index bf970e16..c35af768 100644
--- a/libs/optimizer/src/index.ts
+++ b/libs/optimizer/src/index.ts
@@ -1,10 +1,10 @@
-export { optimize } from './lib/optimizer';
+export { getOptimizer } from './lib/optimizer';
export type {
LatLonAltTime,
- OptimizedCircuitType,
+ CircuitType,
ScoringTrack,
OptimizationResult,
OptimizationOptions,
OptimizationRequest,
} from './lib/optimizer';
-export { ScoringRules } from './lib/scoringRules';
+export type { ScoringRules } from './lib/scoringRules';
diff --git a/libs/optimizer/src/lib/fixtures/optimizer.fixtures.ts b/libs/optimizer/src/lib/fixtures/optimizer.fixtures.ts
index cb14c33b..19bff255 100644
--- a/libs/optimizer/src/lib/fixtures/optimizer.fixtures.ts
+++ b/libs/optimizer/src/lib/fixtures/optimizer.fixtures.ts
@@ -1,7 +1,7 @@
-import { OptimizationRequest, OptimizationResult, OptimizedCircuitType } from '../optimizer';
+import { OptimizationRequest, OptimizationResult, CircuitType } from '../optimizer';
import { computeDestinationPoint, getGreatCircleBearing, getPreciseDistance } from 'geolib';
import { createSegments } from '../utils/createSegments';
-import { concatTracks } from '../utils/concatTracks';
+import { mergeTracks } from '../utils/mergeTracks';
import { ScoringRules } from '../scoringRules';
/**
@@ -14,9 +14,9 @@ import { ScoringRules } from '../scoringRules';
* expectedResult: the expected OptimizationResult that should be returned by the optimizer
*/
export type OptimizerFixture = {
- givenRequest: OptimizationRequest;
- givenRules: ScoringRules;
- expectedResult: Omit;
+ request: OptimizationRequest;
+ rules: ScoringRules;
+ expectedResult: Omit;
};
/**
@@ -24,10 +24,10 @@ export type OptimizerFixture = {
*/
export function createEmptyTrackFixture(): OptimizerFixture {
return {
- givenRequest: {
- track: { points: [], minTimeSec: 0 },
+ request: {
+ track: { points: [], startTimeSec: 0 },
},
- givenRules: ScoringRules.FederationFrancaiseVolLibre,
+ rules: 'FederationFrancaiseVolLibre',
expectedResult: {
score: 0,
lengthKm: 0,
@@ -45,11 +45,11 @@ export type LatLon = {
const START_TIME_SEC = Math.round(new Date().getTime() / 1000);
/**
- * @returns a fixture for a free distance track and it's expected score
- * @param from LatLon of the starting point of the free distance
- * @param to LatLon of the ending point of the free distance
+ * @param from LatLon of the starting point
+ * @param to LatLon of the ending point
* @param nbSegments number of segments to create between the two points
* @param givenRules the ScoringRules for computing the score
+ * @returns a fixture for a free distance track and it's expected score
*/
export function createFreeDistanceFixture(
from: LatLon,
@@ -60,7 +60,7 @@ export function createFreeDistanceFixture(
const multiplier = getFreeDistanceMultiplier(givenRules);
const distance = getPreciseDistance(from, to) / 1000;
return {
- givenRequest: {
+ request: {
track: createSegments(
{ ...from, alt: 0, timeSec: 0 },
{ ...to, alt: 0, timeSec: 60 },
@@ -68,12 +68,12 @@ export function createFreeDistanceFixture(
nbSegments,
),
},
- givenRules,
+ rules: givenRules,
expectedResult: {
score: distance * multiplier,
lengthKm: distance,
multiplier,
- circuit: OptimizedCircuitType.OpenDistance,
+ circuit: CircuitType.OpenDistance,
optimal: true,
},
};
@@ -81,12 +81,12 @@ export function createFreeDistanceFixture(
/**
*
- * @returns a fixture for a free distance track with one intermediate bypass point and it's expected score
- * @param from LatLon of the starting point of the free distance
- * @param intermediate LatLon of the intermediate bypass point
- * @param to LatLon of the ending point of the free distance
+ * @param from LatLon of the starting point
+ * @param intermediate LatLon of the intermediate turn point
+ * @param to LatLon of the ending point
* @param nbSegments number of segments to create between each given points
* @param givenRules the ScoringRules for computing the score
+ * @returns a fixture for a free distance track with one intermediate turn point point and it's expected score
*/
export function createFreeDistance1PointFixture(
from: LatLon,
@@ -98,8 +98,8 @@ export function createFreeDistance1PointFixture(
const distance = (getPreciseDistance(from, intermediate) + getPreciseDistance(intermediate, to)) / 1000;
const multiplier = getFreeDistanceMultiplier(givenRules);
return {
- givenRequest: {
- track: concatTracks(
+ request: {
+ track: mergeTracks(
createSegments(
{ ...from, alt: 0, timeSec: 0 },
{ ...intermediate, alt: 0, timeSec: 60 },
@@ -114,12 +114,12 @@ export function createFreeDistance1PointFixture(
),
),
},
- givenRules,
+ rules: givenRules,
expectedResult: {
score: distance * multiplier,
lengthKm: distance,
multiplier,
- circuit: OptimizedCircuitType.OpenDistance,
+ circuit: CircuitType.OpenDistance,
optimal: true,
},
};
@@ -127,57 +127,57 @@ export function createFreeDistance1PointFixture(
/**
*
- * @returns a fixture for a free distance track with two intermediate bypass points and it's expected score
- * @param from LatLon of the starting point of the free distance
- * @param intermediate1 LatLon of the first intermediate bypass point
- * @param intermediate2 LatLon of the second intermediate bypass point
- * @param to LatLon of the ending point of the free distance
+ * @param from LatLon of the starting point
+ * @param turnPoint1 LatLon of the first intermediate turn point
+ * @param turnPoint2 LatLon of the second intermediate turn point
+ * @param to LatLon of the ending point
* @param nbSegments number of segments to create between each given points
* @param givenRules the ScoringRules for computing the score
+ * @returns a fixture for a free distance track with two intermediate turn points and it's expected score
*/
export function createFreeDistance2PointsFixture(
from: LatLon,
- intermediate1: LatLon,
- intermediate2: LatLon,
+ turnPoint1: LatLon,
+ turnPoint2: LatLon,
to: LatLon,
nbSegments: number,
givenRules: ScoringRules,
): OptimizerFixture {
const distance =
- (getPreciseDistance(from, intermediate1) +
- getPreciseDistance(intermediate1, intermediate2) +
- getPreciseDistance(intermediate2, to)) /
+ (getPreciseDistance(from, turnPoint1) +
+ getPreciseDistance(turnPoint1, turnPoint2) +
+ getPreciseDistance(turnPoint2, to)) /
1000;
const multiplier = getFreeDistanceMultiplier(givenRules);
return {
- givenRequest: {
- track: concatTracks(
+ request: {
+ track: mergeTracks(
createSegments(
{ ...from, alt: 0, timeSec: 0 },
- { ...intermediate1, alt: 0, timeSec: 60 },
+ { ...turnPoint1, alt: 0, timeSec: 60 },
START_TIME_SEC,
nbSegments,
),
createSegments(
- { ...intermediate1, alt: 0, timeSec: 60 },
- { ...intermediate2, alt: 0, timeSec: 120 },
+ { ...turnPoint1, alt: 0, timeSec: 60 },
+ { ...turnPoint2, alt: 0, timeSec: 120 },
START_TIME_SEC,
nbSegments,
),
createSegments(
- { ...intermediate2, alt: 0, timeSec: 120 },
+ { ...turnPoint2, alt: 0, timeSec: 120 },
{ ...to, alt: 0, timeSec: 180 },
START_TIME_SEC,
nbSegments,
),
),
},
- givenRules,
+ rules: givenRules,
expectedResult: {
score: distance * multiplier,
lengthKm: distance,
multiplier,
- circuit: OptimizedCircuitType.OpenDistance,
+ circuit: CircuitType.OpenDistance,
optimal: true,
},
};
@@ -185,66 +185,66 @@ export function createFreeDistance2PointsFixture(
/**
*
- * @returns a fixture for a free distance track with three intermediate bypass points and it's expected score
- * @param from LatLon of the starting point of the free distance
- * @param intermediate1 LatLon of the first intermediate bypass point
- * @param intermediate2 LatLon of the second intermediate bypass point
- * @param intermediate3 LatLon of the third intermediate bypass point
- * @param to LatLon of the ending point of the free distance
+ * @param from LatLon of the starting point
+ * @param turnPoint1 LatLon of the first intermediate turn point
+ * @param turnPoint2 LatLon of the second intermediate turn point
+ * @param turnPoint3 LatLon of the third intermediate turn point
+ * @param to LatLon of the ending point
* @param nbSegments number of segments to create between each given points
* @param givenRules the ScoringRules for computing the score
+ * @returns a fixture for a free distance track with three intermediate turn points and it's expected score
*/
export function createFreeDistance3PointsFixture(
from: LatLon,
- intermediate1: LatLon,
- intermediate2: LatLon,
- intermediate3: LatLon,
+ turnPoint1: LatLon,
+ turnPoint2: LatLon,
+ turnPoint3: LatLon,
to: LatLon,
nbSegments: number,
givenRules: ScoringRules,
): OptimizerFixture {
const distance =
- (getPreciseDistance(from, intermediate1) +
- getPreciseDistance(intermediate1, intermediate2) +
- getPreciseDistance(intermediate2, intermediate3) +
- getPreciseDistance(intermediate3, to)) /
+ (getPreciseDistance(from, turnPoint1) +
+ getPreciseDistance(turnPoint1, turnPoint2) +
+ getPreciseDistance(turnPoint2, turnPoint3) +
+ getPreciseDistance(turnPoint3, to)) /
1000;
const multiplier = getFreeDistanceMultiplier(givenRules);
return {
- givenRequest: {
- track: concatTracks(
+ request: {
+ track: mergeTracks(
createSegments(
{ ...from, alt: 0, timeSec: 0 },
- { ...intermediate1, alt: 0, timeSec: 60 },
+ { ...turnPoint1, alt: 0, timeSec: 60 },
START_TIME_SEC,
nbSegments,
),
createSegments(
- { ...intermediate1, alt: 0, timeSec: 60 },
- { ...intermediate2, alt: 0, timeSec: 120 },
+ { ...turnPoint1, alt: 0, timeSec: 60 },
+ { ...turnPoint2, alt: 0, timeSec: 120 },
START_TIME_SEC,
nbSegments,
),
createSegments(
- { ...intermediate2, alt: 0, timeSec: 120 },
- { ...intermediate3, alt: 0, timeSec: 180 },
+ { ...turnPoint2, alt: 0, timeSec: 120 },
+ { ...turnPoint3, alt: 0, timeSec: 180 },
START_TIME_SEC,
nbSegments,
),
createSegments(
- { ...intermediate3, alt: 0, timeSec: 180 },
+ { ...turnPoint3, alt: 0, timeSec: 180 },
{ ...to, alt: 0, timeSec: 240 },
START_TIME_SEC,
nbSegments,
),
),
},
- givenRules,
+ rules: givenRules,
expectedResult: {
score: distance * multiplier,
lengthKm: distance,
multiplier,
- circuit: OptimizedCircuitType.OpenDistance,
+ circuit: CircuitType.OpenDistance,
optimal: true,
},
};
@@ -252,53 +252,53 @@ export function createFreeDistance3PointsFixture(
/**
*
- * @returns a fixture for a flat triangle track and it's expected score
* @param start LatLon of the starting point of the flat triangle (first point of the triangle)
- * @param p1 LatLon of the second point of the triangle
- * @param p2 LatLon of the third point of the triangle
+ * @param turnPoint1 LatLon of the second point of the triangle
+ * @param turnPoint2 LatLon of the third point of the triangle
* @param nbSegments number of segments to create between each given points
* @param givenRules the ScoringRules for computing the score
+ * @returns a fixture for a flat triangle track and it's expected score
*/
export function createClosedFlatTriangleFixture(
start: LatLon,
- p1: LatLon,
- p2: LatLon,
+ turnPoint1: LatLon,
+ turnPoint2: LatLon,
nbSegments: number,
givenRules: ScoringRules,
): OptimizerFixture {
- if (isFAI(start, p1, p2)) {
+ if (isFAI(start, turnPoint1, turnPoint2)) {
throw new Error('invalid test data: not a flat triangle');
}
const multiplier = getFlatTriangleMultiplier(givenRules);
- return createTriangleFixture(start, p1, p2, nbSegments, givenRules, multiplier, OptimizedCircuitType.FlatTriangle);
+ return createTriangleFixture(start, turnPoint1, turnPoint2, nbSegments, givenRules, multiplier, CircuitType.FlatTriangle);
}
/**
*
- * @returns a fixture for a FAI triangle track and it's expected score
* @param start LatLon of the starting point of the flat triangle (first point of the triangle)
- * @param p1 LatLon of the second point of the triangle
+ * @param turnPoint1 LatLon of the second point of the triangle
* @param nbSegments number of segments to create between each given points
* @param givenRules the ScoringRules for computing the score
+ * @returns a fixture for a FAI triangle track and it's expected score
*
* The third point of the triangle is computed so that the triangle is equilateral
*/
export function createClosedFaiTriangleFixture(
start: LatLon,
- p1: LatLon,
+ turnPoint1: LatLon,
nbSegments: number,
givenRules: ScoringRules,
): OptimizerFixture {
- const distance1 = getPreciseDistance(start, p1);
- const bearingStartToP1 = getGreatCircleBearing(start, p1);
+ const distance1 = getPreciseDistance(start, turnPoint1);
+ const bearingStartToP1 = getGreatCircleBearing(start, turnPoint1);
// The third point 'p2' is at 'distance1' from 'p1' on a line which makes a 60° angle with the line 'start'->'p1'
const equilateralPoint = computeDestinationPoint(start, distance1, bearingStartToP1 + 60);
const p2 = { lat: equilateralPoint.latitude, lon: equilateralPoint.longitude };
- if (!isFAI(start, p1, p2)) {
+ if (!isFAI(start, turnPoint1, p2)) {
throw new Error('invalid test data: not a FAI triangle');
}
const multiplier = getFaiTriangleMultiplier(givenRules);
- return createTriangleFixture(start, p1, p2, nbSegments, givenRules, multiplier, OptimizedCircuitType.FaiTriangle);
+ return createTriangleFixture(start, turnPoint1, p2, nbSegments, givenRules, multiplier, CircuitType.FaiTriangle);
}
/**
@@ -312,13 +312,13 @@ export function createClosedFaiTriangleFixtureWithSmallCycle(
): OptimizerFixture {
const standardFixture = createClosedFaiTriangleFixture(start, p1, nbIntervals, givenRules);
return {
- givenRequest: {
- ...standardFixture.givenRequest,
+ request: {
+ ...standardFixture.request,
options: {
maxCycleDurationMs: 1,
},
},
- givenRules,
+ rules: givenRules,
expectedResult: standardFixture.expectedResult,
};
}
@@ -334,81 +334,80 @@ export function createClosedFaiTriangleFixtureWithSmallLoop(
): OptimizerFixture {
const standardFixture = createClosedFaiTriangleFixture(start, p1, nbIntervals, givenRules);
return {
- givenRequest: {
- ...standardFixture.givenRequest,
+ request: {
+ ...standardFixture.request,
options: {
maxNumCycles: 10,
},
},
- givenRules,
+ rules: givenRules,
expectedResult: standardFixture.expectedResult,
};
}
-
function getFreeDistanceMultiplier(scoringRules: ScoringRules) {
switch (scoringRules) {
- case ScoringRules.CzechLocal:
- case ScoringRules.CzechEuropean:
- case ScoringRules.FederationFrancaiseVolLibre:
- case ScoringRules.Norway:
- case ScoringRules.UnitedKingdomClub:
- case ScoringRules.UnitedKingdomInternational:
- case ScoringRules.UnitedKingdomNational:
- case ScoringRules.XContest:
- case ScoringRules.XContestPPG:
- case ScoringRules.WorldXC:
+ case 'CzechLocal':
+ case 'CzechEuropean':
+ case 'FederationFrancaiseVolLibre':
+ case 'Norway':
+ case 'UnitedKingdomClub':
+ case 'UnitedKingdomInternational':
+ case 'UnitedKingdomNational':
+ case 'XContest':
+ case 'XContestPPG':
+ case 'WorldXC':
return 1;
- case ScoringRules.CzechOutsideEurope:
+ case 'CzechOutsideEurope':
return 0.8;
- case ScoringRules.Leonardo:
+ case 'Leonardo':
return 1.5;
}
}
function getFlatTriangleMultiplier(scoringRules: ScoringRules) {
switch (scoringRules) {
- case ScoringRules.CzechEuropean:
- case ScoringRules.CzechOutsideEurope:
- case ScoringRules.FederationFrancaiseVolLibre:
- case ScoringRules.UnitedKingdomInternational:
+ case 'CzechEuropean':
+ case 'CzechOutsideEurope':
+ case 'FederationFrancaiseVolLibre':
+ case 'UnitedKingdomInternational':
return 1.2;
- case ScoringRules.Leonardo:
- case ScoringRules.WorldXC:
+ case 'Leonardo':
+ case 'WorldXC':
return 1.75;
- case ScoringRules.XContest:
+ case 'XContest':
return 1.4;
- case ScoringRules.Norway:
- case ScoringRules.UnitedKingdomClub:
- case ScoringRules.UnitedKingdomNational:
+ case 'Norway':
+ case 'UnitedKingdomClub':
+ case 'UnitedKingdomNational':
return 1.7;
- case ScoringRules.CzechLocal:
+ case 'CzechLocal':
return 1.8;
- case ScoringRules.XContestPPG:
+ case 'XContestPPG':
return 2;
}
}
function getFaiTriangleMultiplier(scoringRules: ScoringRules) {
switch (scoringRules) {
- case ScoringRules.CzechEuropean:
- case ScoringRules.CzechOutsideEurope:
- case ScoringRules.FederationFrancaiseVolLibre:
+ case 'CzechEuropean':
+ case 'CzechOutsideEurope':
+ case 'FederationFrancaiseVolLibre':
return 1.4;
- case ScoringRules.Leonardo:
- case ScoringRules.UnitedKingdomClub:
- case ScoringRules.UnitedKingdomNational:
- case ScoringRules.WorldXC:
+ case 'Leonardo':
+ case 'UnitedKingdomClub':
+ case 'UnitedKingdomNational':
+ case 'WorldXC':
return 2;
- case ScoringRules.XContest:
+ case 'XContest':
return 1.6;
- case ScoringRules.CzechLocal:
+ case 'CzechLocal':
return 2.2;
- case ScoringRules.Norway:
+ case 'Norway':
return 2.4;
- case ScoringRules.UnitedKingdomInternational:
+ case 'UnitedKingdomInternational':
return 1.5;
- case ScoringRules.XContestPPG:
+ case 'XContestPPG':
return 4;
}
}
@@ -430,22 +429,22 @@ function createTriangleFixture(
nbSegments: number,
givenRules: ScoringRules,
multiplier: number,
- circuit: OptimizedCircuitType,
+ circuit: CircuitType,
): OptimizerFixture {
const distance1 = getPreciseDistance(start, p1);
const distance2 = getPreciseDistance(p1, p2);
const distance3 = getPreciseDistance(p2, start);
const lengthKm = (distance1 + distance2 + distance3) / 1000;
- const expectedScore = (lengthKm * multiplier);
+ const expectedScore = lengthKm * multiplier;
return {
- givenRequest: {
- track: concatTracks(
+ request: {
+ track: mergeTracks(
createSegments({ ...start, alt: 0, timeSec: 0 }, { ...p1, alt: 0, timeSec: 60 }, START_TIME_SEC, nbSegments),
createSegments({ ...p1, alt: 0, timeSec: 60 }, { ...p2, alt: 0, timeSec: 120 }, START_TIME_SEC, nbSegments),
createSegments({ ...p2, alt: 0, timeSec: 120 }, { ...start, alt: 0, timeSec: 180 }, START_TIME_SEC, nbSegments),
),
},
- givenRules,
+ rules: givenRules,
expectedResult: {
score: expectedScore,
lengthKm,
diff --git a/libs/optimizer/src/lib/optimizer.spec.ts b/libs/optimizer/src/lib/optimizer.spec.ts
index da2b2e3d..447c3888 100644
--- a/libs/optimizer/src/lib/optimizer.spec.ts
+++ b/libs/optimizer/src/lib/optimizer.spec.ts
@@ -1,4 +1,4 @@
-import { OptimizationResult, optimize } from './optimizer';
+import { OptimizationResult, getOptimizer } from './optimizer';
import {
createClosedFaiTriangleFixture,
createClosedFaiTriangleFixtureWithSmallCycle,
@@ -11,7 +11,7 @@ import {
createFreeDistanceFixture,
OptimizerFixture,
} from './fixtures/optimizer.fixtures';
-import { ScoringRules } from './scoringRules';
+import { scoringRulesNames } from './scoringRules';
describe('optimizer', () => {
describe('given an empty request', () => {
@@ -20,25 +20,12 @@ describe('optimizer', () => {
expectOptimizationIsAsExpected(fixture);
});
});
- [
- ScoringRules.CzechEuropean,
- ScoringRules.CzechLocal,
- ScoringRules.CzechOutsideEurope,
- ScoringRules.FederationFrancaiseVolLibre,
- ScoringRules.Leonardo,
- ScoringRules.Norway,
- ScoringRules.UnitedKingdomClub,
- ScoringRules.UnitedKingdomInternational,
- ScoringRules.UnitedKingdomNational,
- ScoringRules.XContest,
- ScoringRules.XContestPPG,
- ScoringRules.WorldXC,
- ].forEach((rules) => {
- describe(ScoringRules[rules] + ' rules', () => {
+ scoringRulesNames.forEach((rules) => {
+ describe(`${rules} rules`, () => {
const oneSegmentPerBranch = 1;
const tenSegmentsPerBranch = 10;
[oneSegmentPerBranch, tenSegmentsPerBranch].forEach((nbSegmentsPerBranch) => {
- describe('given a free distance request (' + nbSegmentsPerBranch + ' segments(s)/branch)', () => {
+ describe(`given a free distance request (${nbSegmentsPerBranch} segments/branch)`, () => {
const fixture = createFreeDistanceFixture(
{ lat: 45, lon: 5 },
{ lat: 45, lon: 6 },
@@ -50,58 +37,49 @@ describe('optimizer', () => {
});
});
- describe(
- 'given a free distance with 1 intermediate point request (' + nbSegmentsPerBranch + ' segment(s)/branch)',
- () => {
- const fixture = createFreeDistance1PointFixture(
- { lat: 45, lon: 5 },
- { lat: 45, lon: 6 },
- { lat: 46, lon: 6 },
- nbSegmentsPerBranch,
- rules,
- );
- it('should return the expected score', () => {
- expectOptimizationIsAsExpected(fixture);
- });
- },
- );
+ describe(`given a free distance with 1 intermediate point request (${nbSegmentsPerBranch} segment/branch)`, () => {
+ const fixture = createFreeDistance1PointFixture(
+ { lat: 45, lon: 5 },
+ { lat: 45, lon: 6 },
+ { lat: 46, lon: 6 },
+ nbSegmentsPerBranch,
+ rules,
+ );
+ it('should return the expected score', () => {
+ expectOptimizationIsAsExpected(fixture);
+ });
+ });
- describe(
- 'given a free distance with 2 intermediate points request (' + nbSegmentsPerBranch + ' segment(s)/branch)',
- () => {
- const fixture = createFreeDistance2PointsFixture(
- { lat: 45, lon: 5 },
- { lat: 45, lon: 6 },
- { lat: 46, lon: 6 },
- { lat: 46, lon: 5 },
- nbSegmentsPerBranch,
- rules,
- );
- it('should return the expected score (' + nbSegmentsPerBranch + ' segment(s)/branch)', () => {
- expectOptimizationIsAsExpected(fixture);
- });
- },
- );
+ describe(`given a free distance with 2 intermediate points request (${nbSegmentsPerBranch} segment/branch)`, () => {
+ const fixture = createFreeDistance2PointsFixture(
+ { lat: 45, lon: 5 },
+ { lat: 45, lon: 6 },
+ { lat: 46, lon: 6 },
+ { lat: 46, lon: 5 },
+ nbSegmentsPerBranch,
+ rules,
+ );
+ it('should return the expected score (' + nbSegmentsPerBranch + ' segment/branch)', () => {
+ expectOptimizationIsAsExpected(fixture);
+ });
+ });
- describe(
- 'given a free distance with 3 intermediate points request (' + nbSegmentsPerBranch + ' segment(s)/branch)',
- () => {
- const fixture = createFreeDistance3PointsFixture(
- { lat: 45, lon: 5 },
- { lat: 45, lon: 6 },
- { lat: 46, lon: 6 },
- { lat: 46, lon: 5 },
- { lat: 47, lon: 5 },
- nbSegmentsPerBranch,
- rules,
- );
- it('should return the expected score', () => {
- expectOptimizationIsAsExpected(fixture);
- });
- },
- );
+ describe(`given a free distance with 3 intermediate points request (${nbSegmentsPerBranch} segment/branch)`, () => {
+ const fixture = createFreeDistance3PointsFixture(
+ { lat: 45, lon: 5 },
+ { lat: 45, lon: 6 },
+ { lat: 46, lon: 6 },
+ { lat: 46, lon: 5 },
+ { lat: 47, lon: 5 },
+ nbSegmentsPerBranch,
+ rules,
+ );
+ it('should return the expected score', () => {
+ expectOptimizationIsAsExpected(fixture);
+ });
+ });
- describe('given a closed flat triangle request (' + nbSegmentsPerBranch + ' segment(s)/branch)', () => {
+ describe(`given a closed flat triangle request (${nbSegmentsPerBranch} segment/branch)`, () => {
const fixture = createClosedFlatTriangleFixture(
{ lat: 45, lon: 5 },
{ lat: 45, lon: 6 },
@@ -114,7 +92,7 @@ describe('optimizer', () => {
});
});
- describe('given a closed FAI triangle request (' + nbSegmentsPerBranch + ' segment(s)/branch)', () => {
+ describe(`given a closed FAI triangle request (${nbSegmentsPerBranch} segment/branch)`, () => {
const fixture = createClosedFaiTriangleFixture(
{ lat: 45, lon: 5 },
{ lat: 45, lon: 6 },
@@ -134,7 +112,7 @@ describe('optimizer', () => {
{ lat: 45, lon: 5 },
{ lat: 45, lon: 6 },
9,
- ScoringRules.FederationFrancaiseVolLibre,
+ 'FederationFrancaiseVolLibre',
);
it('should return the expected score', () => {
expectOptimizationIsAsExpected(fixture);
@@ -146,15 +124,17 @@ describe('optimizer', () => {
{ lat: 45, lon: 5 },
{ lat: 45, lon: 6 },
9,
- ScoringRules.FederationFrancaiseVolLibre,
+ 'FederationFrancaiseVolLibre',
);
it('should return the expected score', () => {
expectOptimizationIsAsExpected(fixture);
});
});
+ // TODO: IsAsExpected does not really describe the behavior. Something with expect(optimize(...)).toHaveScore(...);
+ // should be better
function expectOptimizationIsAsExpected(fixture: OptimizerFixture) {
- const optimization = optimize(fixture.givenRequest, fixture.givenRules);
+ const optimization = getOptimizer(fixture.request, fixture.rules);
let currentResult: IteratorResult,
done = false;
while (!done) {
@@ -167,7 +147,7 @@ describe('optimizer', () => {
expect(currentResult.value.circuit).toEqual(fixture.expectedResult.circuit);
currentResult.value.solutionIndices.forEach((index) => {
expect(index).toBeGreaterThanOrEqual(0);
- expect(index).toBeLessThan(fixture.givenRequest.track.points.length);
+ expect(index).toBeLessThan(fixture.request.track.points.length);
});
}
});
diff --git a/libs/optimizer/src/lib/optimizer.ts b/libs/optimizer/src/lib/optimizer.ts
index e0c31c0d..5042f546 100644
--- a/libs/optimizer/src/lib/optimizer.ts
+++ b/libs/optimizer/src/lib/optimizer.ts
@@ -1,12 +1,13 @@
import { Solution, solver } from 'igc-xc-score';
import { BRecord, IGCFile } from 'igc-parser';
import { createSegments } from './utils/createSegments';
-import { concatTracks } from './utils/concatTracks';
+import { mergeTracks } from './utils/mergeTracks';
import { ScoringRules, scoringRules } from './scoringRules';
import { getDistance } from 'geolib';
// When the track has not enough points (<5), we build a new one by adding interpolated points between existing ones.
-// This constant sets the number of segments to build between two existing points.
+// see this issue https://github.com/mmomtchev/igc-xc-score/issues/231
+const MIN_POINTS = 5;
const NUM_SEGMENTS_BETWEEN_POINTS = 2;
// For adding interpolated points, this constant adjusts the proximity of the points to the starting point of
@@ -14,72 +15,86 @@ const NUM_SEGMENTS_BETWEEN_POINTS = 2;
// points returned by the solver are as close as possible (or may be equal) to one of the original points of the track.
const DISTRIBUTION_FACTOR_FOR_ADDED_POINTS = 1e-5;
+// TODO: all console.xxx statements are commented. In the future, we should use a logging library
-/**
- * lat: array of latitudes
- * lon: array of longitudes
- * alt: array of altitudes
- * timeSec: array of time in seconds elapsed since the beginning of the track
- */
export interface LatLonAltTime {
alt: number;
lat: number;
lon: number;
- timeSec: number
+ /**
+ * time in seconds elapsed since the beginning of the track (see ScoringTrack.startTimeSec)
+ */
+ timeSec: number;
}
-/**
- * points: the points that describe the track
- * minTimeSec: beginning of the track (seconds elapsed since 01-01-1970T00:00:00.000)
- */
export interface ScoringTrack {
+ /**
+ * the points that describe the track
+ */
points: LatLonAltTime[];
- minTimeSec: number;
+ /**
+ * Timestamp in seconds
+ * the "timeSec" values in LatLonAltTime's are offsets according to this timestamp.
+ */
+ startTimeSec: number;
}
-/**
- * maxCycleDurationMs: maximum duration in milliseconds for an optimization round trip. `
- * If undefined, calculation duration is unbounded.
- * maxNumCycles: maximum number of iterations allowed for an optimization round trip.
- * If undefined, number of allowed iterations is unbounded
- */
export interface OptimizationOptions {
+ /**
+ * maximum duration in milliseconds for an optimization round trip.
+ * If undefined, calculation duration is unbounded.
+ */
maxCycleDurationMs?: number;
+ /**
+ * maximum number of iterations allowed for an optimization round trip.
+ * If undefined, number of allowed iterations is unbounded
+ */
maxNumCycles?: number;
}
/**
* optimize function argument
- * track: the ScoringTrack to optimize
- * options: the OptimizationOptions for the computation
*/
export interface OptimizationRequest {
track: ScoringTrack;
options?: OptimizationOptions;
}
-export enum OptimizedCircuitType {
+export enum CircuitType {
OpenDistance = 'Open distance',
FlatTriangle = 'Flat triangle',
FaiTriangle = 'Fai triangle',
OutAndReturn = 'Out and return',
}
-/**
- * score: the score for the track in the given league
- * lengthKm: the length of the optimized track in kms
- * multiplier: multiplier for computing score. score = lengthKm * multiplier
- * circuit: type of the optimized track
- * closingRadius: if applicable, distance in m for closing the circuit
- * optimal: the result is optimal (no need to get a next result of Iterator)
- */
export interface OptimizationResult {
+ /**
+ * the score for the track in the given league
+ */
score: number;
+ /**
+ * the length of the optimized track in kms
+ */
lengthKm: number;
+ /**
+ * multiplier for computing score. score = lengthKm * multiplier
+ */
multiplier: number;
- circuit?: OptimizedCircuitType;
+ /**
+ * type of the optimized track
+ */
+ circuit?: CircuitType;
+ /**
+ * if applicable, distance in m for closing the circuit
+ */
closingRadius?: number;
- solutionIndices: number[],
+ /**
+ * indices of solutions points in ScoringTrack.points array
+ */
+ solutionIndices: number[];
+ /**
+ * the result is optimal (no need to get a next result of Iterator)
+ */
optimal: boolean;
}
@@ -87,39 +102,40 @@ const ZERO_SCORE: OptimizationResult = {
score: 0,
lengthKm: 0,
multiplier: 0,
- circuit: undefined,
- closingRadius: undefined,
solutionIndices: [],
optimal: true,
};
-const MIN_POINTS = 5;
-
/**
- * computes the score for the flight
- * @param request the OptimizationRequest
- * @param league the LeagueCode of the league rules to follow
+ * returns an iterative optimizer that computes iteratively the score for the flight. At each iteration, the score
+ * should be a better solutions.
+ * @param request the OptimizationRequest. if request.options is undefined, then there will be one iteration, and the result
+ * will be the best solution
+ * @param rules the ScoringRules to apply for computation
* @return an Iterator over the successive OptimizationResult
* @see README.md
*/
-export function* optimize(request: OptimizationRequest, league: ScoringRules): Iterator {
+export function* getOptimizer(
+ request: OptimizationRequest,
+ rules: ScoringRules,
+): Iterator {
if (request.track.points.length == 0) {
- console.warn('Empty track received in optimization request. Returns a 0 score');
+ // console.warn('Empty track received in optimization request. Returns a 0 score');
return ZERO_SCORE;
}
const originalTrack = request.track;
const solverTrack = buildValidTrackForSolver(originalTrack);
const flight = toIgcFile(solverTrack);
- const scoringRules = toScoringRules(league);
- const options = toOptions(request.options);
- const solutionIterator = solver(flight, scoringRules || {}, options);
+ const solverScoringRules = scoringRules.get(rules);
+ const options = toSolverOptions(request.options);
+ const solutionIterator = solver(flight, solverScoringRules || {}, options);
while (true) {
const solution = solutionIterator.next();
if (solution.done) {
- console.debug("solution", JSON.stringify(solution.value, undefined, 2));
- return toResult(solution.value, originalTrack, solverTrack);
+ console.debug('solution', JSON.stringify(solution.value, undefined, 2));
+ return toOptimizationResult(solution.value, originalTrack, solverTrack);
}
- yield toResult(solution.value, originalTrack, solverTrack);
+ yield toOptimizationResult(solution.value, originalTrack, solverTrack);
}
}
@@ -131,35 +147,34 @@ function buildValidTrackForSolver(track: ScoringTrack) {
if (track.points.length >= MIN_POINTS) {
return track;
}
- console.debug('not enough points (%s) in track. Interpolate intermediate points', track.points.length);
- let newTrack: ScoringTrack = track;
- while (newTrack.points.length < MIN_POINTS) {
+ // console.debug(`not enough points (${track.points.length}) in track. Interpolate intermediate points`);
+ track = deepCopy(track);
+ while (track.points.length < MIN_POINTS) {
const segments: ScoringTrack[] = [];
- for (let i = 1; i < newTrack.points.length; i++) {
+ for (let i = 1; i < track.points.length; i++) {
// split each segment of the track into two segments
segments.push(
createSegments(
- newTrack.points[i - 1],
- newTrack.points[i],
- track.minTimeSec,
+ track.points[i - 1],
+ track.points[i],
+ track.startTimeSec,
NUM_SEGMENTS_BETWEEN_POINTS,
DISTRIBUTION_FACTOR_FOR_ADDED_POINTS,
),
);
}
- newTrack = concatTracks(...segments);
+ track = mergeTracks(...segments);
}
- console.debug('new track has', newTrack.points.length, 'points');
- return newTrack;
+ // console.debug(`new track has ${newTrack.points.length} points`);
+ return track;
}
-
/**
- * build a fake igc file from a track, so that the solver can use it
+ * create an igc file from a track
* @param track the source track
*/
function toIgcFile(track: ScoringTrack): IGCFile {
- const fixes = track.points.map(point => {
+ const fixes = track.points.map((point): BRecord => {
const timeMilliseconds = point.timeSec * 1000;
return {
timestamp: timeMilliseconds,
@@ -172,84 +187,89 @@ function toIgcFile(track: ScoringTrack): IGCFile {
extensions: {},
fixAccuracy: null,
enl: null,
- } as BRecord;
+ };
});
// we ignore some properties of the igc-file, as they are not required for the computation
// @ts-ignore
return {
- date: new Date(track.minTimeSec * 1000).toISOString(),
+ date: new Date(track.startTimeSec * 1000).toISOString(),
fixes: fixes,
};
}
-function toScoringRules(league: ScoringRules) {
- return scoringRules.get(league);
-}
-
type SolverOptions = { maxloop?: number; maxcycle?: number };
-function toOptions(options?: OptimizationOptions): SolverOptions {
+function toSolverOptions(options?: OptimizationOptions): SolverOptions {
return {
maxcycle: options?.maxCycleDurationMs,
maxloop: options?.maxNumCycles,
};
}
-function toResult(solution: Solution, originalTrack: ScoringTrack, solverTrack: ScoringTrack): OptimizationResult {
+function toOptimizationResult(solution: Solution, originalTrack: ScoringTrack, solverTrack: ScoringTrack): OptimizationResult {
return {
- score: (solution.score || 0),
- lengthKm: (solution.scoreInfo?.distance || 0),
+ score: solution.score ?? 0,
+ lengthKm: solution.scoreInfo?.distance ?? 0,
multiplier: solution.opt.scoring.multiplier,
- circuit: toCircuitType(solution.opt.scoring.code as CircuitTypeCode),
+ circuit: toCircuitType(solution.opt.scoring.code),
closingRadius: getClosingRadius(solution),
- solutionIndices: getIndices(solution,originalTrack,solverTrack),
+ solutionIndices: getIndices(solution, originalTrack, solverTrack),
optimal: solution.optimal || false,
};
}
function getClosingRadius(solution: Solution) {
- // @ts-ignore
+ // @ts-ignore : closingDistanceFixed is not exposed by library
const closingDistanceFixed: number | undefined = solution.opt.scoring?.closingDistanceFixed;
- // @ts-ignore
+ // @ts-ignore : closingDistanceRelative is not exposed by library
const closingDistanceRelativeRatio: number | undefined = solution.opt.scoring?.closingDistanceRelative;
- const closingDistanceRelative = solution.scoreInfo?.distance && closingDistanceRelativeRatio ?
- closingDistanceRelativeRatio * solution.scoreInfo?.distance :
- undefined;
+ const closingDistanceRelative =
+ solution.scoreInfo?.distance && closingDistanceRelativeRatio
+ ? closingDistanceRelativeRatio * solution.scoreInfo?.distance
+ : undefined;
const closingDistance = solution.scoreInfo?.cp?.d;
- if (closingDistance === undefined) {
+ if (closingDistance == null) {
return undefined;
}
- if (closingDistanceFixed !== undefined && closingDistance < closingDistanceFixed) {
+ if (closingDistanceFixed != null && closingDistance < closingDistanceFixed) {
return closingDistanceFixed;
- } else if (closingDistanceRelative !== undefined && closingDistance < closingDistanceRelative) {
+ } else if (closingDistanceRelative != null && closingDistance < closingDistanceRelative) {
return closingDistanceRelative;
}
return undefined;
}
-type CircuitTypeCode = 'od' | 'tri' | 'fai' | 'oar';
+const circuitTypeCodes = ['od' , 'tri' , 'fai' , 'oar']
+type CircuitTypeCode = (typeof circuitTypeCodes)[number];
function toCircuitType(code: CircuitTypeCode) {
switch (code) {
case 'od':
- return OptimizedCircuitType.OpenDistance;
+ return CircuitType.OpenDistance;
case 'fai':
- return OptimizedCircuitType.FaiTriangle;
+ return CircuitType.FaiTriangle;
case 'oar':
- return OptimizedCircuitType.OutAndReturn;
+ return CircuitType.OutAndReturn;
case 'tri':
- return OptimizedCircuitType.FlatTriangle;
+ return CircuitType.FlatTriangle;
}
+ throw new Error(`no CircuitType found for ${code}`);
}
-
-// return indices of solution points
+// return indices of solution points. This permit to identify the solution points in the ScoringTrack.points array
+// it contains (when applicable):
+// - the starting point
+// - the 'in' closing point
+// - the turn points
+// - the 'out' closing point
+// - the finish point
function getIndices(solution: Solution, originalTrack: ScoringTrack, solverTrack: ScoringTrack) {
const result: number[] = [];
pushInResult(getEntryPointsStartIndex(solution, originalTrack, solverTrack));
pushInResult(getClosingPointsInIndex(solution, originalTrack, solverTrack));
- solution.scoreInfo?.tp?.map((turnPoint) => turnPoint.r)
- .forEach(index => pushInResult(getPointIndex(index, originalTrack, solverTrack)));
+ solution.scoreInfo?.tp
+ ?.map((turnPoint) => turnPoint.r)
+ .forEach((index) => pushInResult(getPointIndex(index, originalTrack, solverTrack)));
pushInResult(getClosingPointsOutIndex(solution, originalTrack, solverTrack));
pushInResult(getEntryPointsFinishIndex(solution, originalTrack, solverTrack));
return result;
@@ -262,31 +282,26 @@ function getIndices(solution: Solution, originalTrack: ScoringTrack, solverTrack
}
function getEntryPointsStartIndex(solution: Solution, originalTrack: ScoringTrack, solverTrack: ScoringTrack): number {
- console.debug("getEntryPointsStartIndex", solution.scoreInfo?.ep?.start.r);
+ // console.debug('getEntryPointsStartIndex', solution.scoreInfo?.ep?.start.r);
return getPointIndex(solution.scoreInfo?.ep?.start.r, originalTrack, solverTrack);
}
function getClosingPointsInIndex(solution: Solution, originalTrack: ScoringTrack, solverTrack: ScoringTrack): number {
- console.debug("getClosingPointsInIndex", solution.scoreInfo?.cp?.in.r);
+ // console.debug('getClosingPointsInIndex', solution.scoreInfo?.cp?.in.r);
return getPointIndex(solution.scoreInfo?.cp?.in.r, originalTrack, solverTrack);
}
function getClosingPointsOutIndex(solution: Solution, originalTrack: ScoringTrack, solverTrack: ScoringTrack): number {
- console.debug("getClosingPointsOutIndex", solution.scoreInfo?.cp?.out.r);
+ // console.debug('getClosingPointsOutIndex', solution.scoreInfo?.cp?.out.r);
return getPointIndex(solution.scoreInfo?.cp?.out.r, originalTrack, solverTrack);
}
function getEntryPointsFinishIndex(solution: Solution, originalTrack: ScoringTrack, solverTrack: ScoringTrack): number {
- console.debug("getEntryPointsFinishIndex", solution.scoreInfo?.ep?.finish.r);
+ // console.debug('getEntryPointsFinishIndex', solution.scoreInfo?.ep?.finish.r);
return getPointIndex(solution.scoreInfo?.ep?.finish.r, originalTrack, solverTrack);
}
-
-function getPointIndex(
- index: number | undefined,
- originalTrack: ScoringTrack,
- solverTrack: ScoringTrack,
-): number {
+function getPointIndex(index: number | undefined, originalTrack: ScoringTrack, solverTrack: ScoringTrack): number {
if (index === undefined) {
return -1;
}
@@ -299,17 +314,12 @@ function solutionContainsValidIndices(originalTrack: ScoringTrack, solverTrack:
return originalTrack.points.length === solverTrack.points.length;
}
-
-function getIndexInOriginalTrack(
- index: number,
- originalTrack: ScoringTrack,
- solverTrack: ScoringTrack,
-): number {
+function getIndexInOriginalTrack(index: number, originalTrack: ScoringTrack, solverTrack: ScoringTrack): number {
const solutionPoint = solverTrack.points[index];
let indexInOriginalTrack = -1;
let closestDistance = Number.MAX_VALUE;
for (let i = 0; i < originalTrack.points.length; i++) {
- let point = originalTrack.points[i];
+ const point = originalTrack.points[i];
const distance = getDistance(point, solutionPoint);
if (distance < closestDistance) {
closestDistance = distance;
@@ -318,3 +328,8 @@ function getIndexInOriginalTrack(
}
return indexInOriginalTrack;
}
+
+// Not the most performant solution but is used only for slow dimension problems
+function deepCopy(source: T): T {
+ return JSON.parse(JSON.stringify(source));
+}
diff --git a/libs/optimizer/src/lib/scoringRules.ts b/libs/optimizer/src/lib/scoringRules.ts
index 88758413..cd79c88a 100644
--- a/libs/optimizer/src/lib/scoringRules.ts
+++ b/libs/optimizer/src/lib/scoringRules.ts
@@ -1,19 +1,38 @@
-import { scoringRules as xcScoreRules} from "igc-xc-score";
+import { scoringRules as xcScoreRules } from 'igc-xc-score';
-export enum ScoringRules {
- CzechLocal,
- CzechEuropean,
- CzechOutsideEurope,
- FederationFrancaiseVolLibre,
- Leonardo,
- Norway,
- UnitedKingdomClub,
- UnitedKingdomInternational,
- UnitedKingdomNational,
- XContest,
- XContestPPG,
- WorldXC,
-}
+/*
+export const trackerNames = ['inreach', 'spot', 'skylines', 'flyme', 'flymaster', 'ogn', 'zoleo', 'xcontest'] as const;
+
+// ID for the tracking devices.
+export type TrackerNames = (typeof trackerNames)[number];
+
+// How to display the tracker name.
+export const trackerDisplayNames: Readonly> = {
+ inreach: 'InReach',
+ spot: 'Spot',
+ skylines: 'Skylines',
+ flyme: 'FlyMe (XCGlobe)',
+ flymaster: 'Flymaster',
+ ogn: 'OGN',
+ zoleo: 'zoleo',
+ xcontest: 'XContest',
+};
+ */
+export const scoringRulesNames = [
+ 'CzechLocal',
+ 'CzechEuropean',
+ 'CzechOutsideEurope',
+ 'FederationFrancaiseVolLibre',
+ 'Leonardo',
+ 'Norway',
+ 'UnitedKingdomClub',
+ 'UnitedKingdomInternational',
+ 'UnitedKingdomNational',
+ 'XContest',
+ 'XContestPPG',
+ 'WorldXC',
+];
+export type ScoringRules = (typeof scoringRulesNames)[number];
const scoringBaseModel = xcScoreRules['XContest'];
const openDistanceBase = scoringBaseModel[0];
@@ -96,16 +115,16 @@ const wxcRule = [
];
export const scoringRules: Map = new Map([
- [ScoringRules.CzechEuropean, czechEuropeRule],
- [ScoringRules.CzechLocal, czechLocalRule],
- [ScoringRules.CzechOutsideEurope, czechOutEuropeRule],
- [ScoringRules.FederationFrancaiseVolLibre, xcScoreRules['FFVL']],
- [ScoringRules.Leonardo, leonardoRule],
- [ScoringRules.Norway, norwayRule],
- [ScoringRules.UnitedKingdomClub, ukXclClubRule],
- [ScoringRules.UnitedKingdomInternational, ukXclInternationalRule],
- [ScoringRules.UnitedKingdomNational, ukXclNationalRule],
- [ScoringRules.XContest, xcScoreRules['XContest']],
- [ScoringRules.XContestPPG, xContestPpgRule],
- [ScoringRules.WorldXC, wxcRule],
+ ['CzechEuropean', czechEuropeRule],
+ ['CzechLocal', czechLocalRule],
+ ['CzechOutsideEurope', czechOutEuropeRule],
+ ['FederationFrancaiseVolLibre', xcScoreRules['FFVL']],
+ ['Leonardo', leonardoRule],
+ ['Norway', norwayRule],
+ ['UnitedKingdomClub', ukXclClubRule],
+ ['UnitedKingdomInternational', ukXclInternationalRule],
+ ['UnitedKingdomNational', ukXclNationalRule],
+ ['XContest', xcScoreRules['XContest']],
+ ['XContestPPG', xContestPpgRule],
+ ['WorldXC', wxcRule],
]);
diff --git a/libs/optimizer/src/lib/utils/createSegments.ts b/libs/optimizer/src/lib/utils/createSegments.ts
index 5ba9c7dc..0b69d0d6 100644
--- a/libs/optimizer/src/lib/utils/createSegments.ts
+++ b/libs/optimizer/src/lib/utils/createSegments.ts
@@ -1,6 +1,5 @@
import { LatLonAltTime, ScoringTrack } from '../optimizer';
-
/**
* Create segments between 2 points.
* Added points are computed by a linear interpolation
@@ -18,32 +17,28 @@ export function createSegments(
to: LatLonAltTime,
minTimeSec: number,
nbSegments: number,
- distributionFactor: number = 1,
+ distributionFactor = 1,
): ScoringTrack {
- const result: ScoringTrack = { points: [], minTimeSec };
+ const result: ScoringTrack = { points: [], startTimeSec: minTimeSec };
- appendToResult(from);
+ result.points.push(from);
if (nbSegments > 1) {
appendIntermediatePoints();
}
- appendToResult(to);
+ result.points.push(to);
return result;
- function appendToResult(p: LatLonAltTime) {
- result.points.push(p);
- }
-
function appendIntermediatePoints() {
const deltaLat = ((to.lat - from.lat) * distributionFactor) / nbSegments;
const deltaLon = ((to.lon - from.lon) * distributionFactor) / nbSegments;
const deltaAlt = ((to.alt - from.alt) * distributionFactor) / nbSegments;
const deltaTimeSec = ((to.timeSec - from.timeSec) * distributionFactor) / nbSegments;
for (let index = 1; index < nbSegments; index++) {
- appendToResult({
+ result.points.push({
lat: from.lat + deltaLat * index,
lon: from.lon + deltaLon * index,
- alt: from.alt + deltaAlt * index,
+ alt: Math.round(from.alt + deltaAlt * index),
timeSec: from.timeSec + deltaTimeSec * index,
});
}
diff --git a/libs/optimizer/src/lib/utils/concatTracks.ts b/libs/optimizer/src/lib/utils/mergeTracks.ts
similarity index 79%
rename from libs/optimizer/src/lib/utils/concatTracks.ts
rename to libs/optimizer/src/lib/utils/mergeTracks.ts
index 33b94060..b1a09b05 100644
--- a/libs/optimizer/src/lib/utils/concatTracks.ts
+++ b/libs/optimizer/src/lib/utils/mergeTracks.ts
@@ -6,15 +6,16 @@ import { ScoringTrack } from '../optimizer';
* @param tracks the tracks to concatenate
* @return a track
*/
-export function concatTracks(...tracks: ScoringTrack[]): ScoringTrack {
+export function mergeTracks(...tracks: ScoringTrack[]): ScoringTrack {
const concatenated: ScoringTrack = {
points: [],
- minTimeSec: 0,
+ startTimeSec: 0,
};
for (const track of tracks) {
const skipFirstPoint =
- concatenated.points.at(-1)?.lat === track.points[0].lat && concatenated.points.at(-1)?.lon === track.points[0].lon;
+ concatenated.points.at(-1)?.lat === track.points[0].lat &&
+ concatenated.points.at(-1)?.lon === track.points[0].lon;
const arrayToConcat = skipFirstPoint ? track.points.slice(1) : track.points;
concatenated.points = concatenated.points.concat(arrayToConcat);
}