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); }