diff --git a/src/app/storage/defaultDashboard.ts b/src/app/storage/defaultDashboard.ts
index 6b1e650..9f9e900 100644
--- a/src/app/storage/defaultDashboard.ts
+++ b/src/app/storage/defaultDashboard.ts
@@ -70,6 +70,9 @@ export const defaultDashboard: DashboardLayout = {
width: 400,
height: 600,
},
+ config: {
+ enableTurnNames: false,
+ },
},
{
id: 'weather',
diff --git a/src/frontend/components/Settings/SettingsLayout.tsx b/src/frontend/components/Settings/SettingsLayout.tsx
index 537111e..192920e 100644
--- a/src/frontend/components/Settings/SettingsLayout.tsx
+++ b/src/frontend/components/Settings/SettingsLayout.tsx
@@ -127,9 +127,6 @@ export const SettingsLayout = () => {
Track Map
-
- Experimental
-
diff --git a/src/frontend/components/Settings/sections/TrackMapSettings.tsx b/src/frontend/components/Settings/sections/TrackMapSettings.tsx
index 8cd0380..48a7214 100644
--- a/src/frontend/components/Settings/sections/TrackMapSettings.tsx
+++ b/src/frontend/components/Settings/sections/TrackMapSettings.tsx
@@ -1,13 +1,26 @@
import { useState } from 'react';
import { BaseSettingsSection } from '../components/BaseSettingsSection';
-import { TrackMapWidgetSettings } from '../types';
import { useDashboard } from '@irdashies/context';
+import { ToggleSwitch } from '../components/ToggleSwitch';
+
+const SETTING_ID = 'map';
+
+interface TrackMapSettings {
+ enabled: boolean;
+ config: {
+ enableTurnNames: boolean;
+ };
+}
+
+const defaultConfig: TrackMapSettings['config'] = {
+ enableTurnNames: false
+};
export const TrackMapSettings = () => {
const { currentDashboard } = useDashboard();
- const [settings, setSettings] = useState({
- enabled: currentDashboard?.widgets.find(w => w.id === 'map')?.enabled ?? false,
- config: currentDashboard?.widgets.find(w => w.id === 'map')?.config ?? {},
+ const [settings, setSettings] = useState({
+ enabled: currentDashboard?.widgets.find(w => w.id === SETTING_ID)?.enabled ?? false,
+ config: currentDashboard?.widgets.find(w => w.id === SETTING_ID)?.config as TrackMapSettings['config'] ?? defaultConfig,
});
if (!currentDashboard) {
@@ -22,13 +35,27 @@ export const TrackMapSettings = () => {
onSettingsChange={setSettings}
widgetId="map"
>
-
-
This feature is experimental and may not work as expected.
-
- {/* Add specific settings controls here */}
-
- Additional settings will appear here
-
+ {(handleConfigChange) => (
+
+
+
This is still a work in progress. There are several tracks still missing, please report any issues/requests.
+
+
+
+
Enable Turn Names
+
+ Show turn numbers and names on the track map
+
+
+
handleConfigChange({
+ enableTurnNames: enabled
+ })}
+ />
+
+
+ )}
);
};
\ No newline at end of file
diff --git a/src/frontend/components/Settings/types.ts b/src/frontend/components/Settings/types.ts
index 4177045..12c8dde 100644
--- a/src/frontend/components/Settings/types.ts
+++ b/src/frontend/components/Settings/types.ts
@@ -31,7 +31,9 @@ export interface WeatherWidgetSettings extends BaseWidgetSettings {
}
export interface TrackMapWidgetSettings extends BaseWidgetSettings {
- // Add specific track map settings here
+ config: {
+ enableTurnNames: boolean;
+ };
}
export type InputWidgetSettings = BaseWidgetSettings;
diff --git a/src/frontend/components/TrackMap/TrackCanvas.stories.tsx b/src/frontend/components/TrackMap/TrackCanvas.stories.tsx
index 466d88c..a5b7122 100644
--- a/src/frontend/components/TrackMap/TrackCanvas.stories.tsx
+++ b/src/frontend/components/TrackMap/TrackCanvas.stories.tsx
@@ -2,14 +2,24 @@ import { Meta, StoryObj } from '@storybook/react-vite';
import { TrackCanvas, TrackDriver } from './TrackCanvas';
import { useEffect, useState } from 'react';
import tracks from './tracks/tracks.json';
-import { BROKEN_TRACKS } from './tracks/broken-tracks';
+import { BROKEN_TRACKS } from './tracks/brokenTracks';
export default {
component: TrackCanvas,
+ args: {
+ enableTurnNames: false,
+ debug: true,
+ },
argTypes: {
trackId: {
control: { type: 'number' },
},
+ enableTurnNames: {
+ control: { type: 'boolean' },
+ },
+ debug: {
+ control: { type: 'boolean' },
+ },
},
} as Meta;
@@ -358,7 +368,7 @@ const allTrackIds = Object.keys(tracks)
.sort((a, b) => a - b);
export const AllTracksGrid: Story = {
- render: () => {
+ render: (args) => {
const trackSize = 150;
return (
@@ -387,6 +397,8 @@ export const AllTracksGrid: Story = {
@@ -398,7 +410,7 @@ export const AllTracksGrid: Story = {
};
export const BrokenTracksGrid: Story = {
- render: () => {
+ render: (args) => {
const trackSize = 200;
return (
@@ -427,6 +439,8 @@ export const BrokenTracksGrid: Story = {
diff --git a/src/frontend/components/TrackMap/TrackCanvas.tsx b/src/frontend/components/TrackMap/TrackCanvas.tsx
index f2389e1..2645e45 100644
--- a/src/frontend/components/TrackMap/TrackCanvas.tsx
+++ b/src/frontend/components/TrackMap/TrackCanvas.tsx
@@ -2,12 +2,22 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Driver } from '@irdashies/types';
import tracks from './tracks/tracks.json';
import { getColor, getTailwindStyle } from '@irdashies/utils/colors';
-import { shouldShowTrack } from './tracks/broken-tracks';
+import { shouldShowTrack } from './tracks/brokenTracks';
import { TrackDebug } from './TrackDebug';
+import { useStartFinishLine } from './hooks/useStartFinishLine';
+import {
+ setupCanvasContext,
+ drawTrack,
+ drawStartFinishLine,
+ drawTurnNames,
+ drawDrivers,
+} from './trackDrawingUtils';
export interface TrackProps {
trackId: number;
drivers: TrackDriver[];
+ enableTurnNames?: boolean;
+ debug?: boolean;
}
export interface TrackDriver {
@@ -36,13 +46,15 @@ export interface TrackDrawing {
}[];
}
-// currently its a bit messy with the turns, so we disable them for now
-const ENABLE_TURNS = true;
-
const TRACK_DRAWING_WIDTH = 1920;
const TRACK_DRAWING_HEIGHT = 1080;
-export const TrackCanvas = ({ trackId, drivers }: TrackProps) => {
+export const TrackCanvas = ({
+ trackId,
+ drivers,
+ enableTurnNames,
+ debug,
+}: TrackProps) => {
const canvasRef = useRef(null);
const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 });
@@ -81,6 +93,12 @@ export const TrackCanvas = ({ trackId, drivers }: TrackProps) => {
return colors;
}, [drivers]);
+ // Get start/finish line calculations
+ const startFinishLine = useStartFinishLine({
+ startFinishPoint: trackDrawing?.startFinish?.point,
+ trackPathPoints: trackDrawing?.active?.trackPathPoints,
+ });
+
// Position calculation based on the percentage of the track completed
const calculatePositions = useMemo(() => {
if (
@@ -198,73 +216,38 @@ export const TrackCanvas = ({ trackId, drivers }: TrackProps) => {
const offsetX = (rect.width - TRACK_DRAWING_WIDTH * scale) / 2;
const offsetY = (rect.height - TRACK_DRAWING_HEIGHT * scale) / 2;
- // Save context state
- ctx.save();
-
- // Apply scaling and centering
- ctx.translate(offsetX, offsetY);
- ctx.scale(scale, scale);
-
- // Shadow
- ctx.shadowColor = 'black';
- ctx.shadowBlur = 2;
- ctx.shadowOffsetX = 1;
- ctx.shadowOffsetY = 1;
+ // Setup canvas context with scaling and shadow
+ setupCanvasContext(ctx, scale, offsetX, offsetY);
- // Draw track
- if (path2DObjects.inside) {
- ctx.strokeStyle = 'white';
- ctx.lineWidth = 20;
- ctx.stroke(path2DObjects.inside);
- }
-
- // Draw start/finish line
- if (path2DObjects.startFinish) {
- ctx.lineWidth = 10;
- ctx.strokeStyle = getColor('red');
- ctx.stroke(path2DObjects.startFinish);
- }
-
- // Draw turn numbers
- if (ENABLE_TURNS) {
- trackDrawing.turns?.forEach((turn) => {
- if (!turn.content || !turn.x || !turn.y) return;
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = 'white';
- ctx.font = '2rem sans-serif';
- ctx.fillText(turn.content, turn.x, turn.y);
- });
- }
-
- // Draw drivers
- Object.values(calculatePositions)
- .sort((a, b) => Number(a.isPlayer) - Number(b.isPlayer)) // draws player last to be on top
- .forEach(({ driver, position }) => {
- const color = driverColors[driver.CarIdx];
- if (!color) return;
-
- ctx.fillStyle = color.fill;
- ctx.beginPath();
- ctx.arc(position.x, position.y, 40, 0, 2 * Math.PI);
- ctx.fill();
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = color.text;
- ctx.font = '2rem sans-serif';
- ctx.fillText(driver.CarNumber, position.x, position.y);
- });
+ // Draw all elements
+ drawTrack(ctx, path2DObjects);
+ drawStartFinishLine(ctx, startFinishLine);
+ drawTurnNames(ctx, trackDrawing.turns, enableTurnNames);
+ drawDrivers(ctx, calculatePositions, driverColors);
// Restore context state
ctx.restore();
- }, [calculatePositions, path2DObjects, trackDrawing?.turns, driverColors, canvasSize]);
+ }, [
+ calculatePositions,
+ path2DObjects,
+ trackDrawing?.turns,
+ driverColors,
+ canvasSize,
+ enableTurnNames,
+ trackDrawing?.startFinish?.point,
+ trackDrawing?.active?.trackPathPoints,
+ startFinishLine,
+ ]);
// Development/Storybook mode - show debug info and canvas
- if (import.meta.env?.DEV || import.meta.env?.MODE === 'storybook') {
+ if (debug) {
return (
-
+
);
}
@@ -274,7 +257,10 @@ export const TrackCanvas = ({ trackId, drivers }: TrackProps) => {
return (
-
+
);
};
diff --git a/src/frontend/components/TrackMap/TrackDebug.tsx b/src/frontend/components/TrackMap/TrackDebug.tsx
index 32d5b0b..24853a2 100644
--- a/src/frontend/components/TrackMap/TrackDebug.tsx
+++ b/src/frontend/components/TrackMap/TrackDebug.tsx
@@ -1,5 +1,5 @@
import { TrackDrawing } from './TrackCanvas';
-import { getBrokenTrackInfo } from './tracks/broken-tracks';
+import { getBrokenTrackInfo } from './tracks/brokenTracks';
interface TrackDebugProps {
trackId: number;
diff --git a/src/frontend/components/TrackMap/TrackMap.tsx b/src/frontend/components/TrackMap/TrackMap.tsx
index 342db3b..c329c9d 100644
--- a/src/frontend/components/TrackMap/TrackMap.tsx
+++ b/src/frontend/components/TrackMap/TrackMap.tsx
@@ -1,12 +1,23 @@
import { useTrackId } from './hooks/useTrackId';
import { useDriverProgress } from './hooks/useDriverProgress';
+import { useTrackMapSettings } from './hooks/useTrackMapSettings';
import { TrackCanvas } from './TrackCanvas';
+const debug = import.meta.env.DEV || import.meta.env.MODE === 'storybook';
+
export const TrackMap = () => {
const trackId = useTrackId();
const driversTrackData = useDriverProgress();
+ const settings = useTrackMapSettings();
if (!trackId) return <>>;
- return ;
+ return (
+
+ );
};
diff --git a/src/frontend/components/TrackMap/hooks/useStartFinishLine.ts b/src/frontend/components/TrackMap/hooks/useStartFinishLine.ts
new file mode 100644
index 0000000..9a8508a
--- /dev/null
+++ b/src/frontend/components/TrackMap/hooks/useStartFinishLine.ts
@@ -0,0 +1,87 @@
+import { useMemo } from 'react';
+
+interface StartFinishPoint {
+ x: number;
+ y: number;
+}
+
+interface TrackPathPoint {
+ x: number;
+ y: number;
+}
+
+interface StartFinishLine {
+ point: StartFinishPoint;
+ perpendicular: { x: number; y: number };
+}
+
+interface UseStartFinishLineProps {
+ startFinishPoint?: { x?: number; y?: number; length?: number } | null;
+ trackPathPoints?: TrackPathPoint[];
+}
+
+export const useStartFinishLine = ({
+ startFinishPoint,
+ trackPathPoints,
+}: UseStartFinishLineProps): StartFinishLine | null => {
+ return useMemo(() => {
+ if (
+ startFinishPoint?.x === undefined ||
+ startFinishPoint?.y === undefined ||
+ !trackPathPoints
+ ) {
+ return null;
+ }
+
+ let closestIndex = 0;
+ let minDistance = Infinity;
+
+ // Find the closest point on the track path to the start/finish point
+ for (let i = 0; i < trackPathPoints.length; i++) {
+ const point = trackPathPoints[i];
+ const distance = Math.sqrt(
+ Math.pow(point.x - startFinishPoint.x, 2) +
+ Math.pow(point.y - startFinishPoint.y, 2)
+ );
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestIndex = i;
+ }
+ }
+
+ // Calculate the tangent direction at the closest point
+ const tangent = { x: 0, y: 0 };
+ if (closestIndex > 0 && closestIndex < trackPathPoints.length - 1) {
+ // Use points on either side to calculate tangent
+ const prev = trackPathPoints[closestIndex - 1];
+ const next = trackPathPoints[closestIndex + 1];
+ tangent.x = next.x - prev.x;
+ tangent.y = next.y - prev.y;
+ } else if (closestIndex === 0) {
+ // At the start, use the next point
+ const next = trackPathPoints[1];
+ tangent.x = next.x - trackPathPoints[0].x;
+ tangent.y = next.y - trackPathPoints[0].y;
+ } else {
+ // At the end, use the previous point
+ const prev = trackPathPoints[trackPathPoints.length - 2];
+ tangent.x = trackPathPoints[trackPathPoints.length - 1].x - prev.x;
+ tangent.y = trackPathPoints[trackPathPoints.length - 1].y - prev.y;
+ }
+
+ // Normalize the tangent vector
+ const tangentLength = Math.sqrt(
+ tangent.x * tangent.x + tangent.y * tangent.y
+ );
+ tangent.x /= tangentLength;
+ tangent.y /= tangentLength;
+
+ // Calculate the perpendicular vector (rotate 90 degrees)
+ const perpendicular = { x: -tangent.y, y: tangent.x };
+
+ return {
+ point: { x: startFinishPoint.x, y: startFinishPoint.y },
+ perpendicular,
+ };
+ }, [startFinishPoint, trackPathPoints]);
+};
\ No newline at end of file
diff --git a/src/frontend/components/TrackMap/hooks/useTrackMapSettings.tsx b/src/frontend/components/TrackMap/hooks/useTrackMapSettings.tsx
new file mode 100644
index 0000000..7b5d4ee
--- /dev/null
+++ b/src/frontend/components/TrackMap/hooks/useTrackMapSettings.tsx
@@ -0,0 +1,28 @@
+import { useDashboard } from '@irdashies/context';
+
+interface TrackMapSettings {
+ enabled: boolean;
+ config: {
+ enableTurnNames: boolean;
+ };
+}
+
+export const useTrackMapSettings = () => {
+ const { currentDashboard } = useDashboard();
+
+ const settings = currentDashboard?.widgets.find(
+ (widget) => widget.id === 'map'
+ )?.config;
+
+ // Add type guard to ensure settings matches expected shape
+ if (
+ settings &&
+ typeof settings === 'object' &&
+ 'enableTurnNames' in settings &&
+ typeof settings.enableTurnNames === 'boolean'
+ ) {
+ return settings as TrackMapSettings['config'];
+ }
+
+ return undefined;
+};
diff --git a/src/frontend/components/TrackMap/trackDrawingUtils.ts b/src/frontend/components/TrackMap/trackDrawingUtils.ts
new file mode 100644
index 0000000..cab1406
--- /dev/null
+++ b/src/frontend/components/TrackMap/trackDrawingUtils.ts
@@ -0,0 +1,102 @@
+import { getColor } from '@irdashies/utils/colors';
+import { TrackDrawing, TrackDriver } from './TrackCanvas';
+
+export const setupCanvasContext = (
+ ctx: CanvasRenderingContext2D,
+ scale: number,
+ offsetX: number,
+ offsetY: number
+) => {
+ ctx.save();
+ ctx.translate(offsetX, offsetY);
+ ctx.scale(scale, scale);
+
+ // Apply shadow
+ ctx.shadowColor = 'black';
+ ctx.shadowBlur = 2;
+ ctx.shadowOffsetX = 1;
+ ctx.shadowOffsetY = 1;
+};
+
+export const drawTrack = (
+ ctx: CanvasRenderingContext2D,
+ path2DObjects: { inside: Path2D | null }
+) => {
+ if (!path2DObjects.inside) return;
+
+ // Draw black outline first
+ ctx.strokeStyle = 'black';
+ ctx.lineWidth = 40;
+ ctx.stroke(path2DObjects.inside);
+
+ // Draw white track on top
+ ctx.strokeStyle = 'white';
+ ctx.lineWidth = 20;
+ ctx.stroke(path2DObjects.inside);
+};
+
+export const drawStartFinishLine = (
+ ctx: CanvasRenderingContext2D,
+ startFinishLine: { point: { x: number; y: number }; perpendicular: { x: number; y: number } } | null
+) => {
+ if (!startFinishLine) return;
+
+ const lineLength = 60; // Length of the start/finish line
+ const { point: sfPoint, perpendicular } = startFinishLine;
+
+ // Calculate the start and end points of the line
+ const startX = sfPoint.x - (perpendicular.x * lineLength) / 2;
+ const startY = sfPoint.y - (perpendicular.y * lineLength) / 2;
+ const endX = sfPoint.x + (perpendicular.x * lineLength) / 2;
+ const endY = sfPoint.y + (perpendicular.y * lineLength) / 2;
+
+ ctx.lineWidth = 20;
+ ctx.strokeStyle = getColor('red');
+ ctx.lineCap = 'square';
+
+ // Draw the perpendicular line
+ ctx.beginPath();
+ ctx.moveTo(startX, startY);
+ ctx.lineTo(endX, endY);
+ ctx.stroke();
+};
+
+export const drawTurnNames = (
+ ctx: CanvasRenderingContext2D,
+ turns: TrackDrawing['turns'],
+ enableTurnNames: boolean | undefined
+) => {
+ if (!enableTurnNames || !turns) return;
+
+ turns.forEach((turn) => {
+ if (!turn.content || !turn.x || !turn.y) return;
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = 'white';
+ ctx.font = '2rem sans-serif';
+ ctx.fillText(turn.content, turn.x, turn.y);
+ });
+};
+
+export const drawDrivers = (
+ ctx: CanvasRenderingContext2D,
+ calculatePositions: Record,
+ driverColors: Record
+) => {
+ Object.values(calculatePositions)
+ .sort((a, b) => Number(a.isPlayer) - Number(b.isPlayer)) // draws player last to be on top
+ .forEach(({ driver, position }) => {
+ const color = driverColors[driver.CarIdx];
+ if (!color) return;
+
+ ctx.fillStyle = color.fill;
+ ctx.beginPath();
+ ctx.arc(position.x, position.y, 40, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = color.text;
+ ctx.font = '2rem sans-serif';
+ ctx.fillText(driver.CarNumber, position.x, position.y);
+ });
+};
\ No newline at end of file
diff --git a/src/frontend/components/TrackMap/tracks/broken-tracks.ts b/src/frontend/components/TrackMap/tracks/brokenTracks.ts
similarity index 93%
rename from src/frontend/components/TrackMap/tracks/broken-tracks.ts
rename to src/frontend/components/TrackMap/tracks/brokenTracks.ts
index 611d0f2..86ae351 100644
--- a/src/frontend/components/TrackMap/tracks/broken-tracks.ts
+++ b/src/frontend/components/TrackMap/tracks/brokenTracks.ts
@@ -1,5 +1,3 @@
-import { TrackDrawing } from '../TrackCanvas';
-
export interface BrokenTrack {
id: number;
name: string;
@@ -67,7 +65,10 @@ export const getBrokenTrackInfo = (trackId: number): BrokenTrack | undefined =>
* In production, broken tracks are hidden
* In development/storybook, all tracks are available
*/
-export const shouldShowTrack = (trackId: number, trackDrawing: TrackDrawing): boolean => {
+export const shouldShowTrack = (
+ trackId: number,
+ trackDrawing: { startFinish?: { point?: unknown }; active?: { inside?: unknown } } | null | undefined
+): boolean => {
// In development or storybook, show all tracks (including broken ones)
if (import.meta.env?.DEV || import.meta.env?.MODE === 'storybook') {
return true;