F1Manager-Calc/components/Calculator/Calculator.jsx
2023-06-10 12:35:26 +08:00

716 lines
30 KiB
JavaScript

import {Delete, OpenInNew} from "@mui/icons-material";
import {
Button,
Chip,
Container,
Divider,
FormControl,
Grid,
InputLabel,
MenuItem,
Paper,
Select,
Slider,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography
} from "@mui/material";
import {DataGrid} from "@mui/x-data-grid";
import * as Sentry from "@sentry/nextjs";
import dynamic from "next/dynamic";
import Image from "next/image";
import {useSnackbar} from "notistack";
import {useState} from "react";
import {useDispatch} from "react-redux";
import {BiasParams, CarSetupParams} from "../../consts/params";
import {AllPossibleSetups, FeedbackColorForMUI} from "../../consts/setup";
import {tracks} from "../../consts/tracks";
import {validateSetupArray} from "../../consts/validator";
import {updateSlot} from "../../libs/reducers/configReducer";
import {arrayFloatEqual, biasToSetup, eps, nearestSetup, randomSetup, setupToBias} from "../../libs/setup";
import {ClearFeedbackDialog} from "./ClearFeedbackDialog";
import {MuiOtpInput} from "mui-one-time-password-input";
import {HtmlTooltip} from "../Tooltip";
const trackMap = {};
tracks.map(x => trackMap[x.id] = x);
const shortAlphabet = "ogdb+-u12345 ";
const feedbackShortMapping = {
"optimal": "o",
"great": "g",
"good": "d",
"bad": "b",
"bad+": "+",
"bad-": "-",
"unknown": "u",
}
const feedbackShortUnmapping = {
"o": "optimal",
"g": "great",
"d": "good",
"b": "bad",
"+": "bad+",
"-": "bad-",
"u": "unknown",
"1": "optimal",
"2": "great",
"3": "good",
"4": "bad",
"5": "unknown",
" ": "unknown",
}
export function Calculator({ slot }) {
const { enqueueSnackbar } = useSnackbar();
const dispatch = useDispatch();
const [carSetupList, setCarSetupList] = useState([]);
const [possibleSetups, setPossibleSetups] = useState(AllPossibleSetups);
const [openClearFeedback, setOpenClearFeedback] = useState(false);
const update = (payload) => dispatch(updateSlot({
id: slot.id, payload
}));
let {
track, carSetup,
prevCarSetup, feedback, previousRuns,
} = slot;
if (
slot.id && !(
validateSetupArray(carSetup) &&
validateSetupArray(prevCarSetup) &&
feedback && track && previousRuns
)
) {
update({
carSetup: [0.5, 0.5, 0.5, 0.5, 0.5],
prevCarSetup: [0.5, 0.5, 0.5, 0.5, 0.5],
feedback: [[], [], [], [], []],
track: "XX",
previousRuns: [],
});
}
const biasParam = setupToBias(carSetup);
const prevBiasParam = setupToBias(prevCarSetup);
const isValidSetup = CarSetupParams.map(p => {
try {
const val = carSetup ? carSetup[p.index] : 0.5;
if (val < -1e-6 || val >= 1+1e-6) {
return false;
}
const roundValue = val * (p.max - p.min) / p.step;
return Math.abs(Math.round(roundValue) - roundValue) <= 1e-6;
} catch {
Sentry.captureMessage("invalid car setup found: " + JSON.stringify(carSetup));
update({
carSetup: [0.5, 0.5, 0.5, 0.5, 0.5],
prevCarSetup: [0.5, 0.5, 0.5, 0.5, 0.5],
feedback: [[], [], [], [], []],
track: "XX",
previousRuns: [],
});
}
});
const createFeedback = (idx, biasValue, v) => {
const matchedRuns = previousRuns.filter(x => (
x.track === track &&
arrayFloatEqual(x.carSetup, carSetup)
))
if (v === "unknown") {
// setLastCarSetup(carSetup)
update({
previousRuns: matchedRuns.length ? (
previousRuns.map(x => x.id === matchedRuns[0].id ? {
...x,
["feedback_" + idx]: null,
} : x)
) : ([{
track,
carSetup: JSON.parse(JSON.stringify(carSetup)),
["feedback_" + idx]: null,
id: +new Date(),
}, ...previousRuns]),
feedback: feedback.map((x, i) => idx === i ? x.filter(x => x.value !== biasValue): x),
});
} else {
// setLastCarSetup(carSetup)
update({
previousRuns: matchedRuns.length ? (
previousRuns.map(x => x.id === matchedRuns[0].id ? {
...x,
["feedback_" + idx]: {
value: biasValue,
timestamp: +new Date(),
feedback: v
},
} : x)
) : ([{
track,
carSetup: JSON.parse(JSON.stringify(carSetup)),
["feedback_" + idx]: {
value: biasValue,
timestamp: +new Date(),
feedback: v
},
id: +new Date(),
}, ...previousRuns]),
feedback: feedback.map((x, i) => idx === i ? [
...x.filter(x => x.value !== biasValue), {
value: biasValue,
timestamp: +new Date(),
feedback: v
}
]: x),
});
}
// if (v === "optimal" && Object.keys(preset).length) {
// axios.post(`/api/report`, {
// track,
// value: biasValue,
// feedback: v,
// index: row.index,
// });
// }
}
const currentTrack = trackMap[track];
const currentFeedbacks = [0, 1, 2, 3, 4].map(i => {
for(const fb of feedback[i]) {
if (fb.value === biasParam[i]) {
return fb.feedback;
}
}
return "unknown";
})
const currentShortFeedbacks = currentFeedbacks.map(x => feedbackShortMapping[x])
const setShortFeedbacks = (_val) => {
const val = _val.toLowerCase();
[0, 1, 2, 3, 4].map(i => {
if (val[i] && (currentShortFeedbacks[i] !== val[i])) {
createFeedback(i, biasParam[i], feedbackShortUnmapping[val[i]])
}
})
}
const setCarSetup = (carSetup) => {
update({ carSetup });
}
const findNearest = () => {
update({
prevCarSetup: carSetup,
});
const {setup, possibleSetups, lowestRuleBreak, possibleSetupList} = nearestSetup(biasParam, feedback);
if (setup) {
if (lowestRuleBreak > 0) {
enqueueSnackbar(
'Unable to find a valid setup matching all feedbacks. This is the closest we could get.',
{ variant: "warning" }
);
setPossibleSetups(0);
} else {
setPossibleSetups(possibleSetups);
}
setCarSetup(setup);
setCarSetupList(possibleSetupList);
} else {
enqueueSnackbar(
'Unable to find a valid setup matching all feedbacks. Try deleting some feedbacks.',
{ variant: "error" }
);
}
}
const clearFeedbacks = () => {
update({ feedback: [[], [], [], [], []] });
setPossibleSetups(AllPossibleSetups);
}
const loadPreset = () => setCarSetup(currentTrack.setup);
return (
<Container disableGutters maxWidth={false} key={slot.slotNaming}>
<Divider variant="fullWidth" />
<ClearFeedbackDialog clear={() => {
clearFeedbacks();
loadPreset();
}} isOpen={openClearFeedback} setIsOpen={setOpenClearFeedback} />
<Container maxWidth={false} component="main" sx={{ p: 0, pt: 2 }} style={{ paddingLeft: 0, paddingRight: 0 }}>
<Grid container spacing={2}>
<Grid item xs={12} lg={6}>
<TableContainer component={Paper}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
<TableCell colSpan={3} sx={{ textAlign: 'right' }}>
<Grid direction="row-reverse" container spacing={1}>
<Grid item>
<Button variant="contained" color="secondary" onClick={loadPreset}>Load Preset</Button>
</Grid>
<Grid item>
<Typography sx={{ color: "#777", display: "inline-block", verticalAlign: "middle" }}>Track Select:</Typography>
<FormControl variant="standard" sx={{ ml: 3, display: "inline-block", verticalAlign: "middle", mr: 3 }}>
<Select
label="Track"
value={track}
sx={{ width: "100%" }}
onChange={(e) => {
const selectedTrack = e.target.value;
if (trackMap.hasOwnProperty(selectedTrack)) {
update({track: e.target.value});
setOpenClearFeedback(true);
}
}}
>
{tracks.map(t => <MenuItem key={t.id} value={t.id}>
<Image src={require(`../../assets/flags/${t.id}.svg`)} width={24} height={20} alt={t.country} style={{ display: 'inline-block' }} />
<Typography sx={{ m: 0, ml: 1, display: 'inline-block' }}> {t.name}, {t.country}</Typography>
</MenuItem>)}
</Select>
</FormControl>
</Grid>
</Grid>
</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ width: 160, fontSize: 18 }}><b>Setup</b></TableCell>
<TableCell sx={{ minWidth: 360, fontSize: 18 }}><b>Values</b></TableCell>
<TableCell sx={{ fontSize: 18, textAlign: 'right' }}><b>Compare</b></TableCell>
</TableRow>
</TableHead>
<TableBody>
{
CarSetupParams.map(row => {
let carSetupDiff = carSetup[row.index] - prevCarSetup[row.index];
if (Math.abs(carSetupDiff) < eps) {
carSetupDiff = 0;
}
return (
<TableRow key={row.name}>
<TableCell sx={{ fontSize: 16 }}><b>{row.name}</b></TableCell>
<TableCell>
<Slider
marks
color={
(carSetup[row.index] > 1 || carSetup[row.index] < 0) ?
"error" : (isValidSetup[row.index] ? "primary" : "warning")
}
step={row.step / (row.max - row.min)}
max={Math.max(1, carSetup[row.index])}
min={Math.min(0, carSetup[row.index])}
valueLabelFormat={v => row.render(v * (row.max - row.min) + row.min)}
valueLabelDisplay="on"
value={carSetup[row.index]}
onChange={(e, value) => {
const setup = carSetup.map((x, idx) => idx === row.index ? (
Math.round(value * 560) / 560
): x);
setCarSetup(setup)
}}
/>
</TableCell>
<TableCell sx={{ fontSize: 16, textAlign: 'right' }}>
<Typography sx={{ color: carSetupDiff > 0 ? "#ff6383" : carSetupDiff < 0 ? "#76ff03" : "white" }}>{carSetupDiff > 0 ? "▲" : carSetupDiff < 0 ? "▼" : ""} {
row.render(carSetup[row.index] * (row.max - row.min) + row.min)
}</Typography>
<Typography sx={{ color: "#777" }}>Prev: {
row.render(prevCarSetup[row.index] * (row.max - row.min) + row.min)
}</Typography>
</TableCell>
</TableRow>
)
})
}
</TableBody>
<TableHead>
<TableRow>
<TableCell colSpan={3} sx={{ textAlign: 'right' }}>
<Stack direction="row-reverse" spacing={1}>
<Button variant="contained" onClick={findNearest}>Find Setup</Button>
<Button variant="contained" color="secondary" onClick={loadPreset}>Preset</Button>
<Button variant="contained" color="secondary" onClick={
() => setCarSetup([0.5, 0.5, 0.5, 0.5, 0.5])
}>Reset</Button>
<Button variant="contained" color="error" onClick={
() => setCarSetup(randomSetup())
}>Random</Button>
<div style={{ padding: 5 }}>
<Typography sx={{ color: "#777" }}>{possibleSetups} Possibilities</Typography>
</div>
</Stack>
</TableCell>
</TableRow>
</TableHead>
</Table>
</TableContainer>
</Grid>
<Grid item xs={12} lg={6}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell colSpan={3} sx={{ textAlign: 'left' }}>
<Stack spacing={1} direction="row-reverse">
{/*
<Button variant="contained" color="error" onClick={
() => {
clearAll()
}
}>Clear History</Button>
*/}
<Button
variant="contained" color="warning" onClick={
() => {
clearFeedbacks()
}
}>Clear Feedbacks</Button>
<div style={{ flex: 1 }}>
<HtmlTooltip
title={
<div>
<Typography color="inherit">Quick Input</Typography>
<em>Use <b>these shortcuts</b> to input the feedbacks quicker.</em>
<table>
<thead>
<tr><th>short</th><th>meaning</th><th>number</th></tr>
</thead>
<tbody>
<tr><th>o</th><td><u>o</u>ptimal</td><th>1</th></tr>
<tr><th>g</th><td><u>g</u>reat</td><th>2</th></tr>
<tr><th>d</th><td>goo<u>d</u></td><th>3</th></tr>
<tr><th>b</th><td><u>b</u>ad</td><th>4</th></tr>
<tr><th>u</th><td><u>u</u>nknown</td><th>5</th></tr>
</tbody>
</table>
<span>Additionally, +/- for bad(+)/(-).</span>
</div>
}
>
<MuiOtpInput
TextFieldsProps={{ size: 'small', placeholder: '-', sx: {p: 0} }}
style={{ maxWidth: 300 }}
length={5}
value={currentShortFeedbacks}
onChange={v => setShortFeedbacks(v)}
validateChar={(ch, idx) => {
return shortAlphabet.indexOf(ch.toLowerCase()) !== -1;
}}
/>
</HtmlTooltip>
</div>
</Stack>
</TableCell>
</TableRow>
<TableRow>
<TableCell sx={{ width: 120, fontSize: 18 }}><b>Feedback</b></TableCell>
<TableCell sx={{ minWidth: 360, fontSize: 18 }}><b>Bias</b></TableCell>
<TableCell sx={{ minWidth: 120, width: 120, fontSize: 18 }}>Value</TableCell>
</TableRow>
</TableHead>
<TableBody>
{
BiasParams.map(row => {
let feedbacks = JSON.parse(JSON.stringify(feedback[row.index]));
const biasValue = biasParam[row.index];
const k = row.name + ":" + slot.slotNaming;
return [(
<TableRow key={k}>
<TableCell sx={{ pt: 0, pb: 0, pl: 1, pr: 1, borderBottom: '1px dashed rgba(81, 81, 81, .6)' }}>
<FormControl variant="standard" sx={{ m: 1, minWidth: 120 }}>
<InputLabel id="demo-simple-select-standard-label">{row.name}</InputLabel>
<Select
labelId="demo-simple-select-standard-label"
component="label"
label={row.name}
value={currentFeedbacks[row.index]}
disabled={!isValidSetup.every(x => x)}
onChange={(e) => {
createFeedback(row.index, biasValue, e.target.value)
}}
>
<MenuItem value='optimal'>Optimal</MenuItem>
<MenuItem value='great'>Great</MenuItem>
<MenuItem value='good'>Good</MenuItem>
<MenuItem value='bad'>Bad</MenuItem>
<MenuItem value='bad+'>Bad (Too High)</MenuItem>
<MenuItem value='bad-'>Bad (Too Low)</MenuItem>
</Select>
</FormControl>
</TableCell>
<TableCell sx={{ pt: 3, pb: 0, pl: 1, pr: 1, borderBottom: '1px dashed rgba(81, 81, 81, .6)' }}>
<Slider
max={1}
step={0.000001}
min={0}
valueLabelFormat={v => v.toFixed(6)}
valueLabelDisplay="on"
value={biasParam[row.index]}
onChange={(e, value) => {
const bias = biasParam.map((x, idx) => idx === row.index ? value : x);
setCarSetup(biasToSetup(bias))
}}
/>
</TableCell>
<TableCell sx={{ pt: 0, pb: 0, pl: 1, pr: 1, borderBottom: '1px dashed rgba(81, 81, 81, .6)' }}>
<FormControl>
<TextField
label={row.name}
type="number"
value={biasParam[row.index].toFixed(6)}
variant="standard"
InputLabelProps={{
shrink: true,
}}
inputProps={{ inputMode: 'numeric', pattern: '[0-9.]*', step: 0.001 }}
onChange={
(e) => {
const val = e.target.value;
const nVal = Number(val);
if (0 <= nVal && nVal <= 1) {
const b = biasParam.map((x, idx) => idx === row.index ? nVal : x);
setCarSetup(biasToSetup(b))
}
}
}
/>
</FormControl>
</TableCell>
</TableRow>
),(
<TableRow key={`${k}_2`}>
<TableCell colSpan={3} sx={{ padding: "0 2px" }}>
<Grid container spacing={1} style={{ minHeight: 40 }}>
{
feedbacks.sort(
(x, y) => x.value - y.value
).map((f, _idx) => (
<Grid
item
key={_idx}
>
<Chip
label={`${f.value.toFixed(4)}: ${f.feedback}`}
color={FeedbackColorForMUI[f.feedback]}
onClick={() => {
const bias = biasParam.map((x, idx) => idx === row.index ? f.value : x);
setCarSetup(biasToSetup(bias))
}}
onDelete={() => {
update({
feedback: feedback.map((x, idx) => idx === row.index ?
x.filter(x => x.value !== f.value) : x
)
})
}}
/>
</Grid>
))
}
</Grid>
</TableCell>
</TableRow>
)];
}).flat()
}
</TableBody>
</Table>
</TableContainer>
</Grid>
<Grid item xs={12} lg={12} sx={{ mt: 3 }}>
<div style={{ display: 'flex', height: '100%' }}>
<div style={{ flexGrow: 1 }}>
<DataGrid
autoHeight
rows={[
{
arr: prevCarSetup,
biasParams: setupToBias(prevCarSetup),
diff: 0,
id: 0,
},
...carSetupList.map((x, id) => {
const biasParams = setupToBias(x.arr);
return {...x, biasParams, id: id + 1}
})
]}
columns={[
{
field: 'id', headerName: 'Setup #',
renderCell : ({ row, value }) =>
<Button variant="contained" color={value ? "info" : "secondary"} sx={{ pt: 0.2, pb: 0.2, minWidth: 80 }} onClick={
() => {
setCarSetup(row.arr);
}
}>{value ? "#" + value : "PRV"}</Button>
},
{
field: 'diff', headerName: '%',
valueGetter: ({ value }) => value.toFixed(1) + "%",
},
...CarSetupParams.map(param => {
const idx = param.index;
return {
field: 'arr_' + idx,
headerName: param.name,
valueGetter: ({ row }) => row.arr ? row.arr[idx] : 0,
renderCell: ({ row }) => {
const value = row.arr ? row.arr[idx] : 0;
const carSetupDiff = value - (prevCarSetup ? prevCarSetup[idx] : 0);
return <Typography sx={{
fontSize: 13, p: 0.5, textAlign: "center",
color: carSetupDiff > 0 ? "#ff6383" : carSetupDiff < 0 ? "#76ff03" : "white" }}
>
{carSetupDiff > 0 ? "▲" : carSetupDiff < 0 ? "▼" : ""} {
param.render(value * (param.max - param.min) + param.min)
}</Typography>
},
}
}),
{
field: 'arr', headerName: '⇒',
width: 8,
renderCell: () => "⇒",
},
...BiasParams.map(param => {
const idx = param.index;
return {
field: 'biasArr_' + idx,
headerName: param.name,
valueGetter: ({ row }) => row.biasParams ? row.biasParams[idx] : 0,
renderCell: ({ row }) => {
const value = row.biasParams ? row.biasParams[idx] : 0;
const carSetupDiff = value - (prevBiasParam ? prevBiasParam[idx] : 0);
return <Typography sx={{
fontSize: 13, p: 0.5, textAlign: "center",
color: carSetupDiff > 0 ? "#ff6383" : carSetupDiff < 0 ? "#76ff03" : "white" }}
>
{value.toFixed(4)} {carSetupDiff > 0 ? "▲" : carSetupDiff < 0 ? "▼" : ""}
</Typography>
},
}
})
]}
density="compact"
rowsPerPageOptions={[10, 20, 40, 100, 200]}
/>
</div>
</div>
</Grid>
<Grid item xs={12} lg={12} sx={{ mt: 5 }}>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell><b>Previous Runs</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>FWA</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>RWA</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>ARD</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>TC</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>TO</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>Oversteer</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>Braking</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>Cornering</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>Traction</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}><b>Straights</b></TableCell>
<TableCell sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}></TableCell>
</TableRow>
</TableHead>
<TableBody>
{
previousRuns.map(x => {
return (
<TableRow key={x.id}>
<TableCell sx={{ fontSize: 14, p: 0.5, pl: 2 }}>
<Image src={require(`../../assets/flags/${x.track}.svg`)} width={24} height={20} alt={x.track} style={{ display: 'inline-block' }} />
<Typography sx={{ m: 0, ml: 1, display: 'inline-block' }}>{trackMap[x.track]?.name}</Typography>
</TableCell>
{
[0, 1, 2, 3, 4].map(idx => (
<TableCell key={idx} sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}>{
CarSetupParams[idx].render(
x.carSetup[idx] * (
CarSetupParams[idx].max - CarSetupParams[idx].min
) + CarSetupParams[idx].min
)
}</TableCell>
))
}
{
[0, 1, 2, 3, 4].map(idx => (
<TableCell key={idx} sx={{ fontSize: 14, p: 0.5, textAlign: "center" }}>
{
x["feedback_" + idx] && (
<Chip
label={`${x["feedback_" + idx].value.toFixed(4)}: ${x["feedback_" + idx].feedback}`}
color={FeedbackColorForMUI[x["feedback_" + idx].feedback]}
/>
)
}
</TableCell>
))
}
<TableCell sx={{ textAlign: 'right', p: 0.5, pr: 2 }}>
<Stack spacing={1} direction="row-reverse">
<Button variant="contained" color="info" sx={{ minWidth: 32, p: 1 }} onClick={
() => {
setCarSetup(x.carSetup);
}
}>
<OpenInNew />
</Button>
<Button variant="contained" color="error" sx={{ minWidth: 32, p: 1 }} onClick={
() => {
update({
previousRuns: previousRuns.filter(r => r.id !== x.id)
})
}
}>
<Delete />
</Button>
</Stack>
</TableCell>
</TableRow>
)
})
}
</TableBody>
</Table>
</TableContainer>
</Grid>
</Grid>
</Container>
</Container>
)
}
export default dynamic(() => Promise.resolve(Calculator), {
ssr: false,
});