Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@
</mat-form-field>
</div>
</div>
@if (datachartAngleRange(); as angleRangeCtrl) {
@if (angleRangeControl(); as angleRangeCtrl) {
<mat-form-field class="full-width" style="margin-top: 10px;">
<mat-label>Angle display range</mat-label>
<mat-select [formControl]="angleRangeCtrl" name="datachartAngleRange">
<mat-option [value]="null">Automatic (based on path)</mat-option>
<mat-option value="signed">Signed (-180&deg; to 180&deg;)</mat-option>
<mat-option value="direction">Compass (0&deg; to 360&deg;)</mat-option>
</mat-select>
<mat-hint>For angular paths. Choose Signed for values that go negative (e.g. wind shift).</mat-hint>
<mat-hint>Choose Signed for values that go negative (e.g. wind shift).</mat-hint>
</mat-form-field>
}
<br />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { UntypedFormControl } from '@angular/forms';
import { FormControl, UntypedFormControl } from '@angular/forms';
import { GraphDataOptionsComponent } from './graph-data-options.component';
import { DataService } from '../../core/services/data.service';
import { UnitsService } from '../../core/services/units.service';
Expand All @@ -13,9 +13,11 @@ describe('GraphDataOptionsComponent', () => {
let component: GraphDataOptionsComponent;
let fixture: ComponentFixture<GraphDataOptionsComponent>;
let pathObject: Partial<ISkPathData> | null;
let pathUnits: Record<string, string>;

beforeEach(async () => {
pathObject = null;
pathUnits = {};
await TestBed.configureTestingModule({
imports: [GraphDataOptionsComponent],
providers: [
Expand All @@ -24,6 +26,7 @@ describe('GraphDataOptionsComponent', () => {
useValue: {
getPathsAndMetaByType: () => [],
getPathObject: () => pathObject,
getPathUnitType: (path: string) => pathUnits[path] ?? null,
},
},
{
Expand Down Expand Up @@ -172,4 +175,58 @@ describe('GraphDataOptionsComponent', () => {
vi.useRealTimers();
}
});

const mountWithAngleRange = (path: string) => {
const fx = TestBed.createComponent(GraphDataOptionsComponent);
const set = fx.componentRef.setInput.bind(fx.componentRef) as (k: string, v: unknown) => void;
set('filterSelfPaths', new UntypedFormControl(false));
set('datachartPath', new UntypedFormControl(path));
set('datachartSource', new UntypedFormControl({ value: '', disabled: true }));
set('datachartAngleRange', new FormControl<'signed' | 'direction' | null>(null));
set('timeScale', new UntypedFormControl(''));
set('period', new UntypedFormControl(''));
fx.detectChanges();
return fx;
};

const showsAngleRange = (fx: ComponentFixture<GraphDataOptionsComponent>) =>
((fx.nativeElement as HTMLElement).textContent ?? '').includes('Angle display range');

it('hides the angle range for a path with a non-angular unit (#368)', () => {
pathUnits['self.propulsion.port.temperature'] = 'K';
expect(showsAngleRange(mountWithAngleRange('self.propulsion.port.temperature'))).toBe(false);
});

it('offers the angle range for a radian path (#368)', () => {
pathUnits['self.environment.wind.angleApparent'] = 'rad';
expect(showsAngleRange(mountWithAngleRange('self.environment.wind.angleApparent'))).toBe(true);
});

it('offers the angle range while the path publishes no unit (#368)', () => {
// An idle instrument publishes no metadata, and the override is then the only way to keep a
// graph angular — see resolveAngleDomain.
expect(showsAngleRange(mountWithAngleRange('self.environment.wind.angleApparent'))).toBe(true);
});

it('hides the angle range as soon as the path changes to a non-angular one (#368)', async () => {
// Behind the 300 ms path debounce the control would stay editable for a path it does not
// belong to.
vi.useFakeTimers();
try {
pathUnits['self.environment.wind.angleApparent'] = 'rad';
pathUnits['self.propulsion.port.temperature'] = 'K';
const fx = mountWithAngleRange('self.environment.wind.angleApparent');
expect(showsAngleRange(fx)).toBe(true);

fx.componentInstance.datachartPath().setValue('self.propulsion.port.temperature');
fx.detectChanges();
expect(showsAngleRange(fx)).toBe(false);

await vi.advanceTimersByTimeAsync(400);
fx.detectChanges();
expect(showsAngleRange(fx)).toBe(false);
} finally {
vi.useRealTimers();
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { DataService } from '../../core/services/data.service';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatInputModule } from '@angular/material/input';
import { IPathMetaData, ISkPathData } from '../../core/interfaces/app-interfaces';
import { debounceTime } from 'rxjs';
import { debounceTime, tap } from 'rxjs';
import { RouterLink } from '@angular/router';
import { pathRequiredValidator, pathSlotWarning } from '../../core/utils/path-validators.util';

Expand Down Expand Up @@ -38,13 +38,33 @@ export class GraphDataOptionsComponent implements OnInit {
protected pathWarning = signal<string | null>(null);
/** The path `pathSources` was last built for, so re-deriving needs a real path change. */
private _sourcesForPath: string | null = null;
/** Base unit of the selected path, or null when the server publishes none for it. */
private pathUnit = signal<string | null>(null);
protected maxDuration = computed<number>(() => this.timeScale().value === 'day' ? 365 : 60);
/**
* The angle range only changes how radian values are read, so it is offered for radian paths and
* withheld from every path with another unit. An unknown unit keeps it: metadata is missing while
* the producing instrument is idle, and the override is then the only way to keep the graph
* angular (see `resolveAngleDomain`).
*/
protected angleRangeControl = computed<FormControl<'signed' | 'direction' | null> | undefined>(() => {
const control = this.datachartAngleRange();
if (!control) return undefined;
const unit = this.pathUnit();
return unit === null || unit === 'rad' ? control : undefined;
});

ngOnInit(): void {
this.refreshNumericPaths();
this.filteredNumericPaths.set(this.numericPaths());

this.datachartPath().valueChanges.pipe(debounceTime(300), takeUntilDestroyed(this._destroyRef)).subscribe(value => {
// The unit gates whether the angle range is shown at all, so it is read ahead of the debounce:
// behind it the control stays editable for 300 ms on a path it does not belong to.
this.datachartPath().valueChanges.pipe(
tap(value => this.refreshPathUnit(value)),
debounceTime(300),
takeUntilDestroyed(this._destroyRef)
).subscribe(value => {
this.refreshNumericPaths();
const term = (value || '').toLowerCase().trim();
if (!term) {
Expand All @@ -66,6 +86,7 @@ export class GraphDataOptionsComponent implements OnInit {
this.datachartPath().updateValueAndValidity({ emitEvent: false });
const currentPath = this.datachartPath()?.value;
this.refreshPathWarning(currentPath);
this.refreshPathUnit(currentPath);
this.setPathSourcesFor(currentPath);
this.setInitFormState();
}
Expand All @@ -80,6 +101,10 @@ export class GraphDataOptionsComponent implements OnInit {
}));
}

private refreshPathUnit(path: string | null): void {
this.pathUnit.set(path ? this.data.getPathUnitType(path) : null);
}

/**
* Build the Source list for `path`, keeping the select usable when the server is not sending that
* path: its sources are unknown, so surface the stored one alongside "Any" rather than leaving an
Expand Down
Loading