Skip to content

Commit 3e224b4

Browse files
Add position component (#527)
1 parent f9666d1 commit 3e224b4

File tree

7 files changed

+291
-1
lines changed

7 files changed

+291
-1
lines changed

src/app/core/components/dashboard/dashboard.component.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { WidgetGaugeNgLinearComponent } from '../../../widgets/widget-gauge-ng-l
2424
import { WidgetGaugeNgRadialComponent } from '../../../widgets/widget-gauge-ng-radial/widget-gauge-ng-radial.component';
2525
import { WidgetSteelGaugeComponent } from '../../../widgets/widget-gauge-steel/widget-gauge-steel.component';
2626
import { WidgetIframeComponent } from '../../../widgets/widget-iframe/widget-iframe.component';
27+
import { WidgetPositionComponent } from '../../../widgets/widget-position/widget-position.component';
2728
import { WidgetRaceTimerComponent } from '../../../widgets/widget-race-timer/widget-race-timer.component';
2829
import { WidgetSimpleLinearComponent } from '../../../widgets/widget-simple-linear/widget-simple-linear.component';
2930
import { WidgetTutorialComponent } from '../../../widgets/widget-tutorial/widget-tutorial.component';
@@ -72,7 +73,8 @@ export class DashboardComponent implements AfterViewInit, OnDestroy{
7273
WidgetRaceTimerComponent,
7374
WidgetIframeComponent,
7475
WidgetTutorialComponent,
75-
WidgetWindComponent
76+
WidgetWindComponent,
77+
WidgetPositionComponent,
7678
]);
7779

7880
effect(() => {

src/app/core/services/widget.service.ts

+8
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ export class WidgetService {
150150
category: 'Components',
151151
selector: 'widget-tutorial',
152152
componentClassName: 'WidgetTutorialComponent',
153+
},
154+
{
155+
name: 'Position',
156+
description: 'Displays latitude and longitude for location tracking and navigation.',
157+
icon: 'positionWidget',
158+
category: 'Components',
159+
selector: 'widget-position',
160+
componentClassName: 'WidgetPositionComponent',
153161
}
154162
];
155163

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<widget-host [(config)]="widgetProperties.config" [id]="widgetProperties.uuid" (configChange)="updateConfig($event)">
2+
<div class="textGenericWrapper" (onResize)="onResized($event)">
3+
<canvas id="canvasBG" class="canvas-size" #canvasBG></canvas>
4+
<canvas id="canvasValue" class="canvas-size" #canvasEl></canvas>
5+
</div>
6+
</widget-host>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.textGenericWrapper {
2+
position: relative;
3+
width: 100%;
4+
height: 100%;
5+
}
6+
7+
.canvas-size {
8+
position: absolute;
9+
top: 0;
10+
left: 0;
11+
z-index: inherit;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
2+
3+
import { WidgetPositionComponent } from './widget-position.component';
4+
5+
describe('WidgetPositionComponent', () => {
6+
let component: WidgetPositionComponent;
7+
let fixture: ComponentFixture<WidgetPositionComponent>;
8+
9+
beforeEach(waitForAsync(() => {
10+
TestBed.configureTestingModule({
11+
imports: [WidgetPositionComponent]
12+
})
13+
.compileComponents();
14+
}));
15+
16+
beforeEach(() => {
17+
fixture = TestBed.createComponent(WidgetPositionComponent);
18+
component = fixture.componentInstance;
19+
fixture.detectChanges();
20+
});
21+
22+
it('should be created', () => {
23+
expect(component).toBeTruthy();
24+
});
25+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
2+
import { BaseWidgetComponent } from '../../core/utils/base-widget.component';
3+
import { WidgetHostComponent } from '../../core/components/widget-host/widget-host.component';
4+
import { IWidgetSvcConfig } from '../../core/interfaces/widgets-interface';
5+
import { NgxResizeObserverModule } from 'ngx-resize-observer';
6+
7+
@Component({
8+
selector: 'widget-position',
9+
templateUrl: './widget-position.component.html',
10+
styleUrls: ['./widget-position.component.scss'],
11+
standalone: true,
12+
imports: [ WidgetHostComponent, NgxResizeObserverModule ]
13+
})
14+
15+
export class WidgetPositionComponent extends BaseWidgetComponent implements OnInit, OnDestroy {
16+
@ViewChild('canvasEl', {static: true, read: ElementRef}) canvasEl: ElementRef;
17+
@ViewChild('canvasBG', {static: true, read: ElementRef}) canvasBG: ElementRef;
18+
latPos = 0;
19+
longPos = 0;
20+
labelColor: string = undefined;
21+
valueColor: string = undefined;
22+
currentValueLength = 0; // length (in characters) of value text to be displayed.
23+
valueFontSize = 1;
24+
canvasValCtx: CanvasRenderingContext2D;
25+
canvasBGCtx: CanvasRenderingContext2D;
26+
private readonly fontString = 'Roboto';
27+
28+
constructor() {
29+
super();
30+
this.defaultConfig = {
31+
displayName: 'Position',
32+
filterSelfPaths: true,
33+
paths: {
34+
'longPath': {
35+
description: 'Longitude',
36+
path: 'self.navigation.position.longitude',
37+
source: 'default',
38+
pathType: 'number',
39+
isPathConfigurable: true,
40+
convertUnitTo: 'longitudeMin',
41+
showPathSkUnitsFilter: true,
42+
pathSkUnitsFilter: null,
43+
sampleTime: 500
44+
},
45+
'latPath': {
46+
description: 'Latitude',
47+
path: 'self.navigation.position.latitude',
48+
source: 'default',
49+
pathType: 'number',
50+
isPathConfigurable: true,
51+
convertUnitTo: 'latitudeMin',
52+
showPathSkUnitsFilter: true,
53+
pathSkUnitsFilter: null,
54+
sampleTime: 500
55+
}
56+
},
57+
color: 'white',
58+
enableTimeout: false,
59+
dataTimeout: 5
60+
};
61+
}
62+
63+
ngOnInit() {
64+
this.validateConfig();
65+
this.startWidget();
66+
67+
}
68+
69+
protected startWidget(): void {
70+
this.canvasValCtx = this.canvasEl.nativeElement.getContext('2d');
71+
this.canvasBGCtx = this.canvasBG.nativeElement.getContext('2d');
72+
this.getColors(this.widgetProperties.config.color);
73+
this.observeDataStream('longPath', newValue => {
74+
this.longPos = newValue.data.value;
75+
this.updateCanvas();
76+
});
77+
this.observeDataStream('latPath', newValue => {
78+
this.latPos = newValue.data.value;
79+
this.updateCanvas();
80+
});
81+
this.updateCanvasBG();
82+
}
83+
84+
ngOnDestroy() {
85+
this.unsubscribeDataStream();
86+
}
87+
88+
protected updateConfig(config: IWidgetSvcConfig): void {
89+
this.widgetProperties.config = config;
90+
this.startWidget();
91+
this.updateCanvas();
92+
this.updateCanvasBG();
93+
}
94+
95+
96+
private getColors(color: string): void {
97+
switch (color) {
98+
case 'white':
99+
this.labelColor = this.theme.whiteDim;
100+
this.valueColor = this.theme.white;
101+
break;
102+
case 'blue':
103+
this.labelColor = this.theme.blueDim;
104+
this.valueColor = this.theme.blue;
105+
break;
106+
case 'green':
107+
this.labelColor = this.theme.greenDim;
108+
this.valueColor = this.theme.green;
109+
break;
110+
case 'pink':
111+
this.labelColor = this.theme.pinkDim;
112+
this.valueColor = this.theme.pink;
113+
break;
114+
case 'orange':
115+
this.labelColor = this.theme.orangeDim;
116+
this.valueColor = this.theme.orange;
117+
break;
118+
case 'purple':
119+
this.labelColor = this.theme.purpleDim;
120+
this.valueColor = this.theme.purple;
121+
break;
122+
case 'grey':
123+
this.labelColor = this.theme.greyDim;
124+
this.valueColor = this.theme.grey;
125+
break;
126+
case 'yellow':
127+
this.labelColor = this.theme.yellowDim;
128+
this.valueColor = this.theme.yellow;
129+
break;
130+
default:
131+
this.labelColor = this.theme.whiteDim;
132+
this.valueColor = this.theme.white;
133+
break;
134+
}
135+
}
136+
137+
protected onResized(event: ResizeObserverEntry) {
138+
if (event.contentRect.height < 50) { return; }
139+
if (event.contentRect.width < 50) { return; }
140+
if ((this.canvasEl.nativeElement.width !== Math.floor(event.contentRect.width))
141+
|| (this.canvasEl.nativeElement.height !== Math.floor(event.contentRect.height))) {
142+
this.canvasEl.nativeElement.width = Math.floor(event.contentRect.width);
143+
this.canvasEl.nativeElement.height = Math.floor(event.contentRect.height);
144+
this.canvasBG.nativeElement.width = Math.floor(event.contentRect.width);
145+
this.canvasBG.nativeElement.height = Math.floor(event.contentRect.height);
146+
this.currentValueLength = 0; // will force resetting the font size
147+
this.updateCanvasBG();
148+
this.updateCanvas();
149+
}
150+
}
151+
152+
153+
/* ******************************************************************************************* */
154+
/* Canvas */
155+
/* ******************************************************************************************* */
156+
// TODO: Better canvas scaling
157+
// see https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas
158+
updateCanvas() {
159+
if (this.canvasValCtx) {
160+
this.canvasValCtx.clearRect(0, 0, this.canvasEl.nativeElement.width, this.canvasEl.nativeElement.height);
161+
this.drawValue();
162+
}
163+
}
164+
165+
private updateCanvasBG() {
166+
if (this.canvasBGCtx) {
167+
this.canvasBGCtx.clearRect(0, 0, this.canvasBG.nativeElement.width, this.canvasBG.nativeElement.height);
168+
this.drawTitle();
169+
}
170+
}
171+
172+
private drawValue() {
173+
const maxTextWidth = Math.floor(this.canvasEl.nativeElement.width * 0.85);
174+
const maxTextHeight = Math.floor(this.canvasEl.nativeElement.height * 0.85) / 2; // we use two lines
175+
const latPosText = this.latPos.toString();
176+
const longPosText = this.longPos.toString();
177+
let longestString: string;
178+
if (latPosText.length > longPosText.length) {
179+
longestString = latPosText;
180+
} else {
181+
longestString = longPosText;
182+
}
183+
// check if length of string has changed since last time.
184+
if (this.currentValueLength !== longestString.length) {
185+
this.currentValueLength = longestString.length;
186+
this.valueFontSize = this.calculateFontSize(longestString, maxTextWidth, maxTextHeight, this.canvasValCtx);
187+
}
188+
const center = this.canvasEl.nativeElement.width / 2;
189+
const middle = this.canvasEl.nativeElement.height / 2;
190+
const fs = this.valueFontSize / 2;
191+
this.canvasValCtx.textAlign = 'center';
192+
this.canvasValCtx.textBaseline = 'middle';
193+
this.canvasValCtx.fillStyle = this.valueColor;
194+
this.canvasValCtx.font = `bold ${this.valueFontSize}px ${this.fontString}`;
195+
this.canvasValCtx.fillText(latPosText, center, middle - fs, maxTextWidth);
196+
this.canvasValCtx.fillText(longPosText, center, middle + fs, maxTextWidth);
197+
}
198+
199+
private drawTitle() {
200+
const maxTextWidth = Math.floor(this.canvasBG.nativeElement.width * 0.94);
201+
const maxTextHeight = Math.floor(this.canvasBG.nativeElement.height * 0.1);
202+
if (this.widgetProperties.config.displayName === null) { return; }
203+
this.canvasBGCtx.font = 'bold ' + this.calculateFontSize(this.widgetProperties.config.displayName,
204+
maxTextWidth, maxTextHeight, this.canvasBGCtx).toString() + 'px ' + `${this.fontString}`;
205+
this.canvasBGCtx.textAlign = 'left';
206+
this.canvasBGCtx.textBaseline = 'top';
207+
this.canvasBGCtx.fillStyle = this.labelColor;
208+
209+
this.canvasBGCtx.fillText(
210+
this.widgetProperties.config.displayName,
211+
this.canvasBG.nativeElement.width * 0.03,
212+
this.canvasBG.nativeElement.height * 0.03,
213+
maxTextWidth
214+
);
215+
}
216+
217+
private calculateFontSize(text: string, maxWidth: number, maxHeight: number, ctx: CanvasRenderingContext2D): number {
218+
let minFontSize = 1;
219+
let maxFontSize = maxHeight;
220+
let fontSize = maxFontSize;
221+
222+
while (minFontSize <= maxFontSize) {
223+
fontSize = Math.floor((minFontSize + maxFontSize) / 2);
224+
ctx.font = `bold ${fontSize}px ${this.fontString}`;
225+
const measure = ctx.measureText(text).width;
226+
if (measure > maxWidth) {
227+
maxFontSize = fontSize - 1;
228+
} else {
229+
minFontSize = fontSize + 1;
230+
}
231+
}
232+
return maxFontSize;
233+
}
234+
}

src/assets/svg/icons.svg

+3
Loading

0 commit comments

Comments
 (0)