updates on the kalman filter and the dispatch page by time filteration updates
This commit is contained in:
@@ -60,7 +60,7 @@ import { FaCircleCheck } from 'react-icons/fa6';
|
||||
|
||||
import MapWithRoute from './mapWithRoute';
|
||||
import CircularLoader from 'components/CircularLoader';
|
||||
import { fetchDeliveries, getriderbydelivery, gettenantlocations, getTenants } from 'pages/api/api';
|
||||
import { fetchDeliveries, fetchRidersList, gettenantlocations, getTenants } from 'pages/api/api';
|
||||
import { CSVExport } from 'components/third-party/ReactTable';
|
||||
import Loader from 'components/Loader';
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
@@ -218,6 +218,205 @@ const StampCell = ({ value, formatDate, formatTime, success }) => {
|
||||
|
||||
// ==============================|| Orders Details ||============================== //
|
||||
|
||||
// Haversine distance between two [lat, lng] points in kilometers.
|
||||
function haversineKm(a, b) {
|
||||
const R = 6371; // km
|
||||
const toRad = (d) => (d * Math.PI) / 180;
|
||||
const lat1 = toRad(a[0]);
|
||||
const lat2 = toRad(b[0]);
|
||||
const dLat = toRad(b[0] - a[0]);
|
||||
const dLon = toRad(b[1] - a[1]);
|
||||
const s = Math.sin(dLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(s)));
|
||||
}
|
||||
|
||||
function kalmanSmoothGps(pings, options = {}) {
|
||||
if (!Array.isArray(pings) || pings.length === 0) return [];
|
||||
|
||||
// 1. Filter out obviously invalid coordinate pings (e.g. 0,0 or NaN)
|
||||
const cleanedPings = pings.filter(p =>
|
||||
Number.isFinite(p.lat) &&
|
||||
Number.isFinite(p.lng) &&
|
||||
(Math.abs(p.lat) > 0.1 || Math.abs(p.lng) > 0.1)
|
||||
);
|
||||
|
||||
if (cleanedPings.length === 0) return [];
|
||||
if (cleanedPings.length === 1) {
|
||||
return [{ lat: cleanedPings[0].lat, lng: cleanedPings[0].lng, logdate: cleanedPings[0].logdate, _ts: cleanedPings[0]._ts }];
|
||||
}
|
||||
|
||||
const processNoise =
|
||||
options.processNoise != null ? options.processNoise : 1e-10;
|
||||
const measurementNoise =
|
||||
options.measurementNoise != null ? options.measurementNoise : 2e-9;
|
||||
const outlierGate =
|
||||
options.outlierGate != null ? options.outlierGate : 9.0;
|
||||
const maxSpeedKmh =
|
||||
options.maxSpeedKmh != null ? options.maxSpeedKmh : 120;
|
||||
|
||||
const tsOf = (p) =>
|
||||
p._ts || (p.logdate ? new Date(p.logdate).getTime() : 0);
|
||||
|
||||
// 2. Scan forward to find the first valid starting anchor
|
||||
let startIdx = 0;
|
||||
while (startIdx < cleanedPings.length - 1) {
|
||||
const p0 = cleanedPings[startIdx];
|
||||
const p1 = cleanedPings[startIdx + 1];
|
||||
const ts0 = tsOf(p0);
|
||||
const ts1 = tsOf(p1) || ts0 + 1000;
|
||||
const dtSec = Math.max(0.001, (ts1 - ts0) / 1000);
|
||||
const km = haversineKm([p0.lat, p0.lng], [p1.lat, p1.lng]);
|
||||
const speedKmh = (km / dtSec) * 3600;
|
||||
|
||||
if (speedKmh <= maxSpeedKmh) {
|
||||
break;
|
||||
} else {
|
||||
// Speed is too high. Check if p1->p2 is normal (meaning p0 is the outlier)
|
||||
if (startIdx + 2 < cleanedPings.length) {
|
||||
const p2 = cleanedPings[startIdx + 2];
|
||||
const ts2 = tsOf(p2) || ts1 + 1000;
|
||||
const dtSec12 = Math.max(0.001, (ts2 - ts1) / 1000);
|
||||
const km12 = haversineKm([p1.lat, p1.lng], [p2.lat, p2.lng]);
|
||||
const speedKmh12 = (km12 / dtSec12) * 3600;
|
||||
|
||||
if (speedKmh12 <= maxSpeedKmh) {
|
||||
startIdx = startIdx + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
startIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Teleport filter starting from the valid anchor
|
||||
const accepted = [cleanedPings[startIdx]];
|
||||
let lastTs = tsOf(cleanedPings[startIdx]);
|
||||
for (let i = startIdx + 1; i < cleanedPings.length; i++) {
|
||||
const p = cleanedPings[i];
|
||||
const ts = tsOf(p) || lastTs + 1000;
|
||||
const dtSec = Math.max(0.001, (ts - lastTs) / 1000);
|
||||
const prev = accepted[accepted.length - 1];
|
||||
const km = haversineKm([prev.lat, prev.lng], [p.lat, p.lng]);
|
||||
const speedKmh = (km / dtSec) * 3600;
|
||||
if (speedKmh > maxSpeedKmh) continue;
|
||||
accepted.push(p);
|
||||
lastTs = ts;
|
||||
}
|
||||
|
||||
if (accepted.length < 2) {
|
||||
return accepted.map((p) => ({ lat: p.lat, lng: p.lng, logdate: p.logdate, _ts: p._ts }));
|
||||
}
|
||||
|
||||
// Run a 1D Kalman + RTS smoother over one axis. Returns smoothed
|
||||
// positions parallel to `accepted`.
|
||||
const smoothAxis = (axisKey) => {
|
||||
const N = accepted.length;
|
||||
const xPost = new Array(N);
|
||||
const pPost = new Array(N);
|
||||
const xPrior = new Array(N);
|
||||
const pPrior = new Array(N);
|
||||
const dtArr = new Array(N);
|
||||
|
||||
const ts0 = tsOf(accepted[0]);
|
||||
const ts1 = tsOf(accepted[1]);
|
||||
const dt01 = Math.max(0.1, (ts1 - ts0) / 1000);
|
||||
const v0 = (accepted[1][axisKey] - accepted[0][axisKey]) / dt01;
|
||||
xPost[0] = [accepted[0][axisKey], v0];
|
||||
pPost[0] = [measurementNoise, 0, 0, 1];
|
||||
xPrior[0] = xPost[0].slice();
|
||||
pPrior[0] = pPost[0].slice();
|
||||
dtArr[0] = 0;
|
||||
|
||||
let prevTs = ts0;
|
||||
for (let i = 1; i < N; i++) {
|
||||
const ts = tsOf(accepted[i]) || prevTs + 1000;
|
||||
const dt = Math.max(0.1, (ts - prevTs) / 1000);
|
||||
prevTs = ts;
|
||||
dtArr[i] = dt;
|
||||
|
||||
// Predict
|
||||
const [xPrev, vPrev] = xPost[i - 1];
|
||||
const xPredPos = xPrev + vPrev * dt;
|
||||
const xPredVel = vPrev;
|
||||
const [pp00, pp01, pp10, pp11] = pPost[i - 1];
|
||||
const dt2 = dt * dt;
|
||||
const dt3 = dt2 * dt;
|
||||
const dt4 = dt3 * dt;
|
||||
const np00 = pp00 + dt * (pp01 + pp10) + dt2 * pp11 + (dt4 / 4) * processNoise;
|
||||
const np01 = pp01 + dt * pp11 + (dt3 / 2) * processNoise;
|
||||
const np10 = pp10 + dt * pp11 + (dt3 / 2) * processNoise;
|
||||
const np11 = pp11 + dt2 * processNoise;
|
||||
xPrior[i] = [xPredPos, xPredVel];
|
||||
pPrior[i] = [np00, np01, np10, np11];
|
||||
|
||||
// Update
|
||||
const z = accepted[i][axisKey];
|
||||
const y = z - xPredPos;
|
||||
const S = np00 + measurementNoise;
|
||||
const mahal2 = (y * y) / S;
|
||||
if (mahal2 > outlierGate) {
|
||||
xPost[i] = [xPredPos, xPredVel];
|
||||
pPost[i] = [np00, np01, np10, np11];
|
||||
continue;
|
||||
}
|
||||
const K0 = np00 / S;
|
||||
const K1 = np10 / S;
|
||||
const newPos = xPredPos + K0 * y;
|
||||
const newVel = xPredVel + K1 * y;
|
||||
xPost[i] = [newPos, newVel];
|
||||
pPost[i] = [
|
||||
(1 - K0) * np00,
|
||||
(1 - K0) * np01,
|
||||
np10 - K1 * np00,
|
||||
np11 - K1 * np01
|
||||
];
|
||||
}
|
||||
|
||||
// RTS backward smoother
|
||||
const xSmooth = new Array(N);
|
||||
xSmooth[N - 1] = xPost[N - 1].slice();
|
||||
for (let i = N - 2; i >= 0; i--) {
|
||||
const dt = dtArr[i + 1];
|
||||
const [pp00, pp01, pp10, pp11] = pPost[i];
|
||||
const a = pp00 + dt * pp01;
|
||||
const b = pp01;
|
||||
const c = pp10 + dt * pp11;
|
||||
const d = pp11;
|
||||
const [q00, q01, q10, q11] = pPrior[i + 1];
|
||||
const det = q00 * q11 - q01 * q10;
|
||||
if (!Number.isFinite(det) || Math.abs(det) < 1e-30) {
|
||||
xSmooth[i] = xPost[i].slice();
|
||||
continue;
|
||||
}
|
||||
const inv00 = q11 / det;
|
||||
const inv01 = -q01 / det;
|
||||
const inv10 = -q10 / det;
|
||||
const inv11 = q00 / det;
|
||||
const c00 = a * inv00 + b * inv10;
|
||||
const c01 = a * inv01 + b * inv11;
|
||||
const c10 = c * inv00 + d * inv10;
|
||||
const c11 = c * inv01 + d * inv11;
|
||||
const dxPos = xSmooth[i + 1][0] - xPrior[i + 1][0];
|
||||
const dxVel = xSmooth[i + 1][1] - xPrior[i + 1][1];
|
||||
xSmooth[i] = [
|
||||
xPost[i][0] + c00 * dxPos + c01 * dxVel,
|
||||
xPost[i][1] + c10 * dxPos + c11 * dxVel
|
||||
];
|
||||
}
|
||||
|
||||
return xSmooth.map((s) => s[0]);
|
||||
};
|
||||
|
||||
const lats = smoothAxis('lat');
|
||||
const lngs = smoothAxis('lng');
|
||||
return accepted.map((p, i) => ({
|
||||
lat: lats[i],
|
||||
lng: lngs[i],
|
||||
logdate: p.logdate,
|
||||
_ts: p._ts
|
||||
}));
|
||||
}
|
||||
|
||||
export default function OrdersDetails() {
|
||||
const loadMoreRef = useRef();
|
||||
const containerRef = useRef();
|
||||
@@ -319,14 +518,35 @@ export default function OrdersDetails() {
|
||||
try {
|
||||
const res = await axios.get(`${process.env.REACT_APP_URL3}/deliveries/getdeliverylogs/?deliveryid=${id}`);
|
||||
const datas = res.data.details;
|
||||
if (datas.length != 0) {
|
||||
setRiderStart(datas[0].logdate);
|
||||
setRiderEnd(datas[datas.length - 1].logdate);
|
||||
const coData = datas.map((data) => ({ lat: data.latitude, lng: data.longitude }));
|
||||
setRiderCoordinates(coData);
|
||||
calculateTotalDistance(coData);
|
||||
setMapOpen(true);
|
||||
} else if (datas == null || !datas) {
|
||||
if (Array.isArray(datas) && datas.length !== 0) {
|
||||
// Sort chronologically by logdate
|
||||
const sorted = datas
|
||||
.map((r) => {
|
||||
const ts = r?.logdate ? dayjs(r.logdate) : null;
|
||||
return {
|
||||
lat: parseFloat(r?.latitude ?? r?.lat),
|
||||
lng: parseFloat(r?.longitude ?? r?.lng ?? r?.lon),
|
||||
logdate: r?.logdate,
|
||||
_ts: ts && ts.isValid() ? ts.valueOf() : Number.MAX_SAFE_INTEGER
|
||||
};
|
||||
})
|
||||
.filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lng))
|
||||
.sort((a, b) => a._ts - b._ts);
|
||||
|
||||
if (sorted.length !== 0) {
|
||||
setRiderStart(sorted[0].logdate);
|
||||
setRiderEnd(sorted[sorted.length - 1].logdate);
|
||||
|
||||
// Apply Kalman filter
|
||||
const smoothed = kalmanSmoothGps(sorted);
|
||||
const coData = smoothed.map((data) => ({ lat: data.lat, lng: data.lng }));
|
||||
setRiderCoordinates(coData);
|
||||
calculateTotalDistance(coData);
|
||||
setMapOpen(true);
|
||||
} else {
|
||||
opentoast('No Valid Logs Found', 'error', 2000);
|
||||
}
|
||||
} else {
|
||||
opentoast('No Logs Found ', 'error', 2000);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -416,9 +636,9 @@ export default function OrdersDetails() {
|
||||
isError: getriderbydeliveryIsError,
|
||||
error: getriderbydeliveryError
|
||||
} = useQuery({
|
||||
queryKey: ['getriderbydelivery', startdate, enddate, appId, tenantid, locationid],
|
||||
queryFn: () => getriderbydelivery(startdate, enddate, appId, tenantid, locationid),
|
||||
enabled: appId != 0
|
||||
queryKey: ['fetchRidersList', appId],
|
||||
queryFn: fetchRidersList,
|
||||
enabled: appId !== 0
|
||||
});
|
||||
|
||||
const {
|
||||
|
||||
Reference in New Issue
Block a user