Skip to content

Commit

Permalink
polish: add comments, suppress dead code and comply more strictly to …
Browse files Browse the repository at this point in the history
…solver API
  • Loading branch information
flyingtof committed Oct 10, 2023
1 parent c91f7b8 commit 1f2e3cf
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 158 deletions.
47 changes: 47 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# How to contribute.

## Required tools

- node.js
- npm
- on mac-os, you have to install xcode command line developer tools (run xcode-select --install)
- gcloud
- docker
- the IDE of your choice


## Project setup

- run `npm install`
- add default keys definitions
- `cp apps/fxc-front/src/app/keys.ts.dist apps/fxc-front/src/app/keys.ts`
- `cp libs/common/src/lib/keys.ts.dist libs/common/src/lib/keys.ts`

### Simplistic configuration

**redis server**
- `cd docker; docker compose up -d redis`

**pubsub**
- `cd docker; docker compose up -d pubsub`

**datastore**

For the moment, it does not work with docker compose. But if you install the cloud-datastore-emulator, you will have a working configuration.

***Installation***
- `gcloud components install cloud-datastore-emulator`

***run:***
- `gcloud beta emulators datastore start`

**before npm run dev:**

define the required env variables:
```
export DATASTORE_DATASET=flyxc
export DATASTORE_EMULATOR_HOST=localhost:8081
export DATASTORE_EMULATOR_HOST_PATH=localhost:8081/datastore
export DATASTORE_HOST=http://localhost:8081
export DATASTORE_PROJECT_ID=flyxc
```
38 changes: 0 additions & 38 deletions CONTRIBUTION.md

This file was deleted.

28 changes: 14 additions & 14 deletions apps/fxc-front/src/app/components/2d/planner-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import * as units from '../../logic/units';
import { decrementSpeed, incrementSpeed, setSpeed } from '../../redux/planner-slice';
import { RootState, store } from '../../redux/store';
import { scoreTrack } from '../../logic/track';
import * as common from "@flyxc/common";
import { currentTrack } from "../../redux/selectors";
import { LEAGUES } from "../../logic/score/league/leagues";
// introduce here a circular dependency. Issue to solve later
import * as common from '@flyxc/common';
import { currentLeague, currentTrack } from '../../redux/selectors';

const ICON_MINUS =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJAQMAAADaX5RTAAAABlBMVEX///9xe4e/5menAAAAE0lEQVQImWP438DQAEP7kNj/GwCK4wo9HA2mvgAAAABJRU5ErkJggg==';
Expand All @@ -37,9 +37,7 @@ export class PlannerElement extends connect(store)(LitElement) {
@state()
private isFreeDrawing = false;
@state()
private track?: common.RuntimeTrack;
@state()
private league?: string;
private track: common.RuntimeTrack | undefined;

private duration?: number;
private readonly closeHandler = () => this.dispatchEvent(new CustomEvent('close'));
Expand All @@ -57,7 +55,6 @@ export class PlannerElement extends connect(store)(LitElement) {
this.duration = ((this.distance / this.speed) * 60) / 1000;
this.isFreeDrawing = state.planner.isFreeDrawing;
this.track = currentTrack(state);
this.league = LEAGUES[state.planner.league].name ;
}

static get styles(): CSSResult {
Expand Down Expand Up @@ -148,10 +145,11 @@ export class PlannerElement extends connect(store)(LitElement) {
</div>
${when(
this.track,
() => html `
<div @click="${this.computeScore}">
<div><b>🆕<i class="las la-trophy"></i> Compute score 🆕</b></div>
</div>`
() => html` <div @click="${this.scoreTrack}">
<div>
<b>🆕<i class="las la-trophy"></i>Score🆕</b>
</div>
</div>`,
)}
<div>
<div>${this.score.circuit}</div>
Expand All @@ -160,7 +158,8 @@ export class PlannerElement extends connect(store)(LitElement) {
</div>
</div>
<div class="collapsible">
<div>Points = ${this.getMultiplier()} <br/>${this.league}</div>
<div>Points = ${this.getMultiplier()}</div>
<div>${store.getState().planner.leagueName}</div>
<div class="large">${this.score.points.toFixed(1)}</div>
</div>
<div class="collapsible">
Expand Down Expand Up @@ -277,9 +276,10 @@ export class PlannerElement extends connect(store)(LitElement) {
store.dispatch(e.deltaY > 0 ? incrementSpeed() : decrementSpeed());
}

private computeScore() {
// compute score on the current selected track
private scoreTrack() {
if (this.track) {
scoreTrack(this.track);
scoreTrack(this.track, currentLeague(store.getState()));
}
}
}
2 changes: 1 addition & 1 deletion apps/fxc-front/src/app/components/ui/main-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,8 +514,8 @@ export class TrackItems extends connect(store)(LitElement) {
pushCurrentState();
addUrlParamValues(ParamNames.groupId, ids);
el.value = '';
await menuController.close();
}
await menuController.close();
}
}

Expand Down
2 changes: 1 addition & 1 deletion apps/fxc-front/src/app/components/ui/track-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class TrackModal extends connect(store)(LitElement) {
<ion-list>
${this.tracks.map(
(track: RuntimeTrack) =>
html` <ion-item
html`<ion-item
button
lines="full"
color=${track.id == this.currentTrackId ? 'primary' : ''}
Expand Down
100 changes: 66 additions & 34 deletions apps/fxc-front/src/app/logic/score/improvedScorer.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Point } from '@flyxc/common';
import { Point, RuntimeTrack } from '@flyxc/common';
import { Point as XcScorePoint, scoringRules, Solution } from 'igc-xc-score';
import { BRecord, IGCFile } from 'igc-parser';
import { CircuitType, Score } from './scorer';
import { LeagueCode } from './league';

// use a lazy loading mechanism to avoid loading this library when not needed, because it is quite heavy and not
// required by every user.
async function lazyLoadedSolver(): Promise<typeof _solver> {
if (!_solver) {
const { solver } = await import('igc-xc-score');
Expand All @@ -18,14 +20,18 @@ let _solver: (
config?: { [key: string]: any } | undefined,
) => Iterator<Solution, Solution>;

// ScoreAndWaypoints could be more appropriate here?
export type ScoreAndRoute = { score: Score; route: Point[] };
export type ScoringTrack = { lat: number[]; lon: number[]; timeSec: number[]; minTimeSec: number };

// ScoringTrack is a subset of RuntimeTrack
// we define it for the sake of clarity and define the minimal information required to invoke the scoreTrack function
export type ScoringTrack = Pick<RuntimeTrack, 'lat' | 'lon' |'alt' | 'timeSec' | 'minTimeSec'>

export async function scoreTrack(track: ScoringTrack, leagueCode: LeagueCode): Promise<ScoreAndRoute | undefined> {
const scoringRules = getScoringRules(leagueCode);
if (scoringRules) {
const solver = await lazyLoadedSolver();
const solutions = solver(igcFile(track), scoringRules, undefined);
const solutions = solver(createIgcFile(track), scoringRules, undefined);
const solution = solutions.next().value;
return { score: toScore(solution), route: toRoute(solution) };
}
Expand All @@ -43,7 +49,6 @@ function toScore(solution: Solution): Score {
});
}

// duplicated code from apps/fxc-front/src/app/logic/track.ts
type CircuitTypeCode = 'od' | 'tri' | 'fai' | 'oar';

function toCircuitType(code: CircuitTypeCode) {
Expand All @@ -59,36 +64,46 @@ function toCircuitType(code: CircuitTypeCode) {
}
}

// end duplication

// return indices of solution points
// Pay attention to the high coupling between getIndexes and toRoute function.
// They HAVE TO use the same points in the same order
// May be a visitor pattern would be valuable here.
function getIndexes(solution: Solution) {
let currentIndex = -1;
const entryPointsStart = getEntryPointsStart(solution);
const result = entryPointsStart ? [currentIndex++] : [];
const closingPointsIn = getClosingPointsIn(solution);
if (closingPointsIn) result.push(currentIndex++);
if (closingPointsIn) {
result.push(currentIndex++);
}
solution.scoreInfo?.legs?.map((leg) => leg.start.r).forEach(() => result.push(currentIndex++));
const closingPointsOut = getClosingPointsOut(solution);
if (closingPointsOut) result.push(currentIndex++);
if (closingPointsOut) {
result.push(currentIndex++);
}
const entryPointsFinish = getEntryPointsFinish(solution);
if (entryPointsFinish) result.push(currentIndex++);
if (entryPointsFinish) {
result.push(currentIndex++);
}
return result;
}

function push(point: XcScorePoint | undefined, route: Point[]) {
if (point) route.push(getPoint(point));
}

function toRoute(solution: Solution): Point[] {
const route: Point[] = [];
push(getEntryPointsStart(solution), route);
const closingPointsIn = getClosingPointsIn(solution);
if (closingPointsIn) route.push(getPoint(closingPointsIn));
if (closingPointsIn) {
route.push(getPoint(closingPointsIn));
}
solution.scoreInfo?.legs?.map((leg) => leg.start).forEach((it) => route.push(getPoint(it)));
const closingPointsOut = getClosingPointsOut(solution);
if (closingPointsOut) route.push(getPoint(closingPointsOut));
if (closingPointsOut) {
route.push(getPoint(closingPointsOut));
}
const entryPointsFinish = getEntryPointsFinish(solution);
if (entryPointsFinish) route.push(getPoint(entryPointsFinish));
if (entryPointsFinish) {
route.push(getPoint(entryPointsFinish));
}
return route;
}

Expand All @@ -112,22 +127,35 @@ function getPoint(point: XcScorePoint): Point {
return { ...point };
}

// build a fake igc file from a track
function igcFile(track: ScoringTrack): IGCFile {
function push(point: XcScorePoint | undefined, route: Point[]) {
if (point) {
route.push(getPoint(point));
}
}

// build a fake igc file from a track, so that the solver can use it.
function createIgcFile(track: ScoringTrack): IGCFile {
const fixes: BRecord[] = [];
for (let i = 0; i < track.lon.length; i++) {
// @ts-ignore
const timeMilliseconds = track.timeSec[i]*1000;
const record: BRecord = {
timestamp: track.timeSec[i],
timestamp: timeMilliseconds,
time: new Date(timeMilliseconds).toISOString(),
latitude: track.lat[i],
longitude: track.lon[i],
valid: true,
pressureAltitude: null,
gpsAltitude: track.alt[i],
extensions: {},
fixAccuracy: null,
enl: null,
};
fixes.push(record);
}
// we ignore some properties of the igc-file, as they are not required for the computation
// @ts-ignore
return {
date: new Date(track.minTimeSec).toISOString(),
date: new Date(track.minTimeSec*1000).toISOString(),
fixes: fixes,
};
}
Expand All @@ -136,6 +164,9 @@ function getScoringRules(leagueCode: LeagueCode): object | undefined {
return leaguesScoringRules.get(leagueCode);
}

// scoring rules could have been defined individually in each League subclass, but as the definition of rules is
// tedious and error-prone, it seems more practical to define all rules here.
// The downside is that we have to define a "coupling key" (LeagueCode) in each League
const scoringBaseModel = scoringRules['XContest'];
const openDistanceBase = scoringBaseModel[0];
const freeTriangleBase = scoringBaseModel[1];
Expand Down Expand Up @@ -216,16 +247,17 @@ const wxcScoringRule = [
{ ...faiTriangleBase, multiplier: 2, closingDistanceFixed: 0.2 },
];

const leaguesScoringRules: Map<LeagueCode, object> = new Map()
.set('czl', czlScoringRule)
.set('cze', czeScoringRule)
.set('czo', czoScoringRule)
.set('fr', scoringRules['FFVL'])
.set('leo', leoScoringRule)
.set('nor', norScoringRule)
.set('ukc', ukcScoringRule)
.set('uki', ukiScoringRule)
.set('ukn', uknScoringRule)
.set('xc', scoringRules.XContest)
.set('xcppg', xcppgScoringRule)
.set('wxc', wxcScoringRule);
const leaguesScoringRules: Map<LeagueCode, object> = new Map([
['czl', czlScoringRule],
['cze', czeScoringRule],
['czo', czoScoringRule],
['fr', scoringRules['FFVL']],
['leo', leoScoringRule],
['nor', norScoringRule],
['ukc', ukcScoringRule],
['uki', ukiScoringRule],
['ukn', uknScoringRule],
['xc', scoringRules.XContest],
['xcppg', xcppgScoringRule],
['wxc', wxcScoringRule],
]);
Loading

0 comments on commit 1f2e3cf

Please sign in to comment.