Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an optimizer library to score tracks #203

Merged
merged 21 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# 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`
vicb marked this conversation as resolved.
Show resolved Hide resolved

### 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.
vicb marked this conversation as resolved.
Show resolved Hide resolved

**_Installation_**

- `gcloud components install cloud-datastore-emulator`

**_run the data store:_**

- `gcloud beta emulators datastore start --data-dir=MY_DATA_DIR`

**_before npm run dev:_**

- 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`

## Helpful commands

`npx nx check` runs the build, lint, and test targets for all the projects. Nice to use before uploading a PR.

`nx affected:test --all --parallel --maxParallel 10 --watch` will run the tests affected by your code changes.
48 changes: 30 additions & 18 deletions apps/fxc-front/src/app/components/2d/path-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import { FaiSectors } from '../../gm/fai-sectors';
import { addAltitude } from '../../logic/elevation';
import { getCurrentUrl, pushCurrentState } from '../../logic/history';
import { drawRoute } from '../../logic/messages';
import { LEAGUES } from '../../logic/score/league/leagues';
import { Measure } from '../../logic/score/measure';
import { CircuitType, Score } from '../../logic/score/scorer';
import { 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 { CircuitType, getOptimizer } from '@flyxc/optimizer';
import { getScoringRuleName } from '../../logic/score/league/leagues';
import type { LeagueCode } from '../../logic/score/league/leagues';
vicb marked this conversation as resolved.
Show resolved Hide resolved

// Route color by circuit type.
const ROUTE_STROKE_COLORS = {
Expand All @@ -44,7 +45,7 @@ export class PathElement extends connect(store)(LitElement) {
@state()
private enabled = false;
@state()
private league = 'xc';
private league: LeagueCode = 'xc';
@state()
private encodedRoute = '';
@state()
Expand Down Expand Up @@ -191,18 +192,14 @@ export class PathElement extends connect(store)(LitElement) {

// Optimize the route and draw the optimize lines and sectors.
private optimize(): void {
if (!this.line || this.line.getPath().getLength() < 2) {
const { line } = this;
if (!line || line.getPath().getLength() < 2 || this.doNotSyncState) {
return;
}
const line = this.line;
store.dispatch(setDistance(google.maps.geometry.spherical.computeLength(line.getPath())));

const points = this.getPathPoints();
const measure = new Measure(points);
const scores = LEAGUES[this.league].score(measure);

scores.sort((score1, score2) => score2.points - score1.points);
const score = scores[0];
const score = this.computeScore(points);
store.dispatch(setScore(score));

let optimizedPath = score.indexes.map((index) => new google.maps.LatLng(points[index].lat, points[index].lon));
Expand All @@ -212,9 +209,8 @@ export class PathElement extends connect(store)(LitElement) {
optimizedPath = [optimizedPath[1], optimizedPath[2]];
}

if (!this.optimizedLine) {
this, (this.optimizedLine = new google.maps.Polyline());
}
this.optimizedLine ??= new google.maps.Polyline();

this.optimizedLine.setOptions({
map: this.map,
path: optimizedPath,
Expand All @@ -229,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 {
Expand All @@ -254,12 +250,28 @@ export class PathElement extends connect(store)(LitElement) {
this.postScoreToHost(score);
}

private computeScore(points: LatLon[]): Score {
// TODO: limit the processing time ?
const result = getOptimizer(
{ track: { points: points.map((point, i) => ({ ...point, alt: 0, timeSec: i * 60 })) } },
getScoringRuleName(this.league),
).next().value;
return new Score({
circuit: result.circuit,
distanceM: result.lengthKm * 1000,
multiplier: result.multiplier,
closingRadiusM: result.closingRadiusM ? result.closingRadiusM * 1000 : null,
indexes: result.solutionIndices,
points: result.score,
});
}

// 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;
Expand Down
2 changes: 1 addition & 1 deletion apps/fxc-front/src/app/components/2d/planner-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class PlannerElement extends connect(store)(LitElement) {
<div>
<div>${this.score.circuit}</div>
<div class="large">
${unsafeHTML(units.formatUnit(this.score.distance / 1000, this.units.distance, undefined, 'unit'))}
${unsafeHTML(units.formatUnit(this.score.distanceM / 1000, this.units.distance, undefined, 'unit'))}
</div>
</div>
<div class="collapsible">
Expand Down
3 changes: 2 additions & 1 deletion apps/fxc-front/src/app/components/ui/about-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export class AboutModal extends LitElement {
<a href="https://github.com/mmomtchev" target="_blank">Momtchil Momtchev</a>,
<a href="https://github.com/osmeras" target="_blank">Stanislav Ošmera</a>,
<a href="https://github.com/tris" target="_blank">Tristan Horn</a>,
<a href="https://github.com/spasutto" target="_blank">Sylvain Pasutto</a>
<a href="https://github.com/spasutto" target="_blank">Sylvain Pasutto</a>,
<a href="https://github.com/flyingtof" target="_blank">flyingtof</a>
</p>

<p>
Expand Down
12 changes: 6 additions & 6 deletions apps/fxc-front/src/app/components/ui/main-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -628,18 +628,18 @@ export class TrackItems extends connect(store)(LitElement) {
(this.renderRoot.querySelector('#track') as HTMLInputElement)?.click();
}

private async handleUpload(e: Event): Promise<void> {
private async handleUpload(e: Event & { target: HTMLInputElement }): Promise<void> {
if (e.target) {
const el = e.target as HTMLInputElement;
if (el.files?.length) {
const input = e.target;
if (input.files?.length) {
const files: File[] = [];
for (let i = 0; i < el.files.length; i++) {
files.push(el.files[i]);
for (let i = 0; i < input.files.length; i++) {
files.push(input.files[i]);
}
const ids = await uploadTracks(files);
pushCurrentState();
addUrlParamValues(ParamNames.groupId, ids);
el.value = '';
input.value = '';
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions apps/fxc-front/src/app/components/ui/pref-modal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { connect } from 'pwa-helpers';

import { modalController } from '@ionic/core/components';

import { LEAGUES } from '../../logic/score/league/leagues';
import * as units from '../../logic/units';
import { setLeague } from '../../redux/planner-slice';
import { RootState, store } from '../../redux/store';
import { setAltitudeUnit, setDistanceUnit, setSpeedUnit, setVarioUnit } from '../../redux/units-slice';
import { LEAGUE_CODES, LEAGUES } from '../../logic/score/league/leagues';

@customElement('pref-modal')
export class PrefModal extends connect(store)(LitElement) {
Expand All @@ -21,7 +21,7 @@ export class PrefModal extends connect(store)(LitElement) {

constructor() {
super();
Object.getOwnPropertyNames(LEAGUES).forEach((value) => {
LEAGUE_CODES.forEach((value) => {
this.leagues.push({ value, name: LEAGUES[value].name });
});
vicb marked this conversation as resolved.
Show resolved Hide resolved
this.leagues.sort((a, b) => (a < b ? -1 : 1));
Expand Down
7 changes: 0 additions & 7 deletions apps/fxc-front/src/app/logic/score/league.ts

This file was deleted.

68 changes: 0 additions & 68 deletions apps/fxc-front/src/app/logic/score/league/czech.ts

This file was deleted.

25 changes: 0 additions & 25 deletions apps/fxc-front/src/app/logic/score/league/frcfd.ts

This file was deleted.

64 changes: 44 additions & 20 deletions apps/fxc-front/src/app/logic/score/league/leagues.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,46 @@
import { League } from '../league';
import { CzechEurope, CzechLocal, CzechOutEurope } from './czech';
import { FrCfd } from './frcfd';
import { Leonardo } from './leonardo';
import { UKXCLClub, UKXCLInternational, UKXCLNational } from './ukxcl';
import { WXC } from './wxc';
import { NorwayLeague, XContest, XContestPPG } from './xcontest';
import { ScoringRuleName } from '@flyxc/optimizer';
vicb marked this conversation as resolved.
Show resolved Hide resolved

export const LEAGUES: { [name: string]: League } = {
czl: new CzechLocal(),
cze: new CzechEurope(),
czo: new CzechOutEurope(),
fr: new FrCfd(),
leo: new Leonardo(),
nor: new NorwayLeague(),
ukc: new UKXCLClub(),
uki: new UKXCLInternational(),
ukn: new UKXCLNational(),
xc: new XContest(),
xcppg: new XContestPPG(),
wxc: new WXC(),
export const LEAGUE_CODES = [
'czl',
'cze',
'czo',
'fr',
'leo',
'nor',
'ukc',
'uki',
'ukn',
'xc',
'xcppg',
'wxc',
] as const;

export type LeagueCode = (typeof LEAGUE_CODES)[number];

interface LeagueDetails {
name: string;
ruleName: ScoringRuleName;
}

export const LEAGUES: Readonly<Record<LeagueCode, LeagueDetails>> = {
czl: { name: 'Czech (ČPP local)', ruleName: 'CzechLocal' },
cze: { name: 'Czech (ČPP Europe)', ruleName: 'CzechEurope' },
czo: { name: 'Czech (ČPP outside Europe)', ruleName: 'CzechOutsideEurope' },
fr: { name: 'France (CFD)', ruleName: 'FFVL' },
leo: { name: 'Leonardo', ruleName: 'Leonardo' },
nor: { name: 'Norway (Distanseligaen)', ruleName: 'Norway' },
ukc: { name: 'UK (XC League, Club)', ruleName: 'UKClub' },
uki: { name: 'UK (XC League, International)', ruleName: 'UKInternational' },
ukn: { name: 'UK (XC League, National)', ruleName: 'UKNational' },
xc: { name: 'XContest', ruleName: 'XContest' },
xcppg: { name: 'XContest PPG', ruleName: 'XContestPPG' },
wxc: { name: 'World XC Online Contest', ruleName: 'WorldXC' },
};

export function getScoringRuleName(leagueCode: LeagueCode): ScoringRuleName {
const ruleName = LEAGUES[leagueCode]?.ruleName;
if (ruleName == null) {
throw new Error('Unkown league code "${leagueCode}"');
}
return ruleName;
}
Loading