Harden findmydevice GPS jitter filtering
This commit is contained in:
+105
-24
@@ -1305,16 +1305,70 @@ function speedLabel(kmh) {
|
||||
return speedBucket(kmh).label;
|
||||
}
|
||||
|
||||
const HOME_RADIUS_M = 50;
|
||||
const HOME_NEAR_RADIUS_M = 90;
|
||||
const HOME_JITTER_ABSORB_RADIUS_M = 1200;
|
||||
const HOME_JITTER_SAMPLE_SECONDS = 30 * 60;
|
||||
const TRACK_MAX_GAP_SECONDS = 30 * 60;
|
||||
const AWAY_MAX_GAP_SECONDS = 2 * 60 * 60;
|
||||
|
||||
function pointWithLatLng(point, lat, lng, extra = {}) {
|
||||
return {
|
||||
...point,
|
||||
...extra,
|
||||
lat,
|
||||
lng,
|
||||
latlng: new naver.maps.LatLng(lat, lng)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHomeJitterPoint(point) {
|
||||
if (!mapState.filterOutliers || !mapState.homeCenter || !point) return point;
|
||||
const distance = distanceFromHome(point);
|
||||
if (distance > HOME_JITTER_ABSORB_RADIUS_M) return point;
|
||||
return pointWithLatLng(point, mapState.homeCenter.lat, mapState.homeCenter.lng, {
|
||||
homeJitter: true,
|
||||
originalLat: point.lat,
|
||||
originalLng: point.lng,
|
||||
originalDistanceFromHome: distance,
|
||||
});
|
||||
}
|
||||
|
||||
function isSinglePointSpike(prev, cur, next) {
|
||||
if (!prev || !cur || !next) return false;
|
||||
const prevNext = distanceMeters(prev, next);
|
||||
if (prevNext > 180) return false;
|
||||
|
||||
const inDistance = distanceMeters(prev, cur);
|
||||
const outDistance = distanceMeters(cur, next);
|
||||
if (inDistance < 250 || outDistance < 250) return false;
|
||||
|
||||
const inSeconds = Math.max(1, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
|
||||
const outSeconds = Math.max(1, (toTimestamp(next.time) - toTimestamp(cur.time)) / 1000);
|
||||
const inKmh = inDistance / inSeconds * 3.6;
|
||||
const outKmh = outDistance / outSeconds * 3.6;
|
||||
return inKmh > 80 && outKmh > 80;
|
||||
}
|
||||
|
||||
function filterHistoryOutliers(points) {
|
||||
if (!mapState.filterOutliers || points.length < 3) return points;
|
||||
const filtered = [points[0]];
|
||||
const filtered = [normalizeHomeJitterPoint(points[0])];
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
if (i < points.length - 1 && isSinglePointSpike(points[i - 1], points[i], points[i + 1])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const prev = filtered[filtered.length - 1];
|
||||
const cur = points[i];
|
||||
const cur = normalizeHomeJitterPoint(points[i]);
|
||||
const seconds = Math.max(1, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
|
||||
const distance = distanceMeters(prev, cur);
|
||||
const kmh = distance / seconds * 3.6;
|
||||
if (distance > 2000 && kmh > 180) continue;
|
||||
if (distance > 500 && kmh > 220) continue;
|
||||
if (distance > 2000 && kmh > 120) continue;
|
||||
if (cur.accuracy && Number(cur.accuracy) > 300 && distance > 300) continue;
|
||||
if (cur.homeJitter && prev.homeJitter && seconds < HOME_JITTER_SAMPLE_SECONDS) {
|
||||
continue;
|
||||
}
|
||||
filtered.push(cur);
|
||||
}
|
||||
return filtered.length >= 2 ? filtered : points;
|
||||
@@ -1389,8 +1443,9 @@ function distanceFromHome(point) {
|
||||
|
||||
function classifyHomePoint(point) {
|
||||
const distance = distanceFromHome(point);
|
||||
if (distance <= 50) return 'home';
|
||||
if (distance <= 90) return 'near';
|
||||
if (distance <= HOME_RADIUS_M) return 'home';
|
||||
if (mapState.filterOutliers && distance <= HOME_JITTER_ABSORB_RADIUS_M) return 'home';
|
||||
if (distance <= HOME_NEAR_RADIUS_M) return 'near';
|
||||
return 'away';
|
||||
}
|
||||
|
||||
@@ -1405,9 +1460,7 @@ function buildHomeStats(points) {
|
||||
const sessions = [];
|
||||
const closeAwaySession = (returnTime = null) => {
|
||||
if (!currentAway) return;
|
||||
const endTime = returnTime || points.at(-1).time;
|
||||
currentAway.returnTime = returnTime;
|
||||
currentAway.duration = Math.max(0, (toTimestamp(endTime) - toTimestamp(currentAway.departureTime)) / 1000);
|
||||
if (currentAway.duration >= minAwaySeconds) {
|
||||
sessions.push(currentAway);
|
||||
if (returnTime) lastReturn = returnTime;
|
||||
@@ -1420,19 +1473,29 @@ function buildHomeStats(points) {
|
||||
const cur = points[i];
|
||||
const duration = Math.max(0, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
|
||||
if (!duration) continue;
|
||||
const isLargeGap = duration > AWAY_MAX_GAP_SECONDS;
|
||||
|
||||
const prevState = classifyHomePoint(prev);
|
||||
const curState = classifyHomePoint(cur);
|
||||
const state = prevState === curState ? curState : (curState === 'home' ? 'home' : (curState === 'away' ? 'away' : 'near'));
|
||||
const state = prevState === 'away' && curState === 'away'
|
||||
? 'away'
|
||||
: (prevState === 'home' || curState === 'home' ? 'home' : 'near');
|
||||
|
||||
if (state === 'home') {
|
||||
homeSeconds += duration;
|
||||
closeAwaySession(cur.time);
|
||||
} else if (state === 'away') {
|
||||
if (isLargeGap) {
|
||||
closeAwaySession(null);
|
||||
unstableSeconds += duration;
|
||||
continue;
|
||||
}
|
||||
if (!currentAway) {
|
||||
currentAway = { departureTime: prev.time, returnTime: null, duration: 0 };
|
||||
}
|
||||
currentAway.duration += duration;
|
||||
} else {
|
||||
closeAwaySession(null);
|
||||
unstableSeconds += duration;
|
||||
}
|
||||
}
|
||||
@@ -1473,9 +1536,11 @@ function buildHistoryStats(points) {
|
||||
let distance = 0;
|
||||
let maxGap = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
distance += distanceMeters(points[i - 1], points[i]);
|
||||
const gap = Math.max(0, (toTimestamp(points[i].time) - toTimestamp(points[i - 1].time)) / 1000);
|
||||
maxGap = Math.max(maxGap, gap);
|
||||
if (gap <= TRACK_MAX_GAP_SECONDS) {
|
||||
distance += distanceMeters(points[i - 1], points[i]);
|
||||
}
|
||||
}
|
||||
const startedAt = toTimestamp(points[0].time);
|
||||
const endedAt = toTimestamp(points.at(-1).time);
|
||||
@@ -1518,7 +1583,7 @@ function renderHistorySummary() {
|
||||
<span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong>
|
||||
<span>${t.historyPoints}</span><strong>${stats.rawPoints && stats.rawPoints !== stats.renderPoints ? `${formatNumber(stats.renderPoints)} / ${formatNumber(stats.rawPoints)}` : formatNumber(stats.points)}</strong>
|
||||
<span>${t.historyStops}</span><strong>${formatNumber(stats.stops || 0)}</strong>
|
||||
<span>${t.homeRadius}</span><strong>50m</strong>
|
||||
<span>${t.homeRadius}</span><strong>${mapState.filterOutliers ? '50m + GPS 1.2km' : '50m'}</strong>
|
||||
<span>${t.homeStay}</span><strong>${home ? formatDuration(Math.round(home.homeSeconds)) : '-'}</strong>
|
||||
<span>${t.awayTime}</span><strong>${home ? formatDuration(Math.round(home.awaySeconds)) : '-'}</strong>
|
||||
<span>${t.awaySessions}</span><strong>${home ? formatNumber(sessions.length) : '-'}</strong>
|
||||
@@ -1640,21 +1705,37 @@ function drawSpeedTrack(points) {
|
||||
clearTrackLines();
|
||||
if (!points?.length || points.length < 2) return;
|
||||
|
||||
const path = points.map(point => point.latlng);
|
||||
const line = new naver.maps.Polyline({
|
||||
map: mapState.map,
|
||||
path,
|
||||
strokeColor: '#007aff',
|
||||
strokeOpacity: 0.86,
|
||||
strokeWeight: 5,
|
||||
strokeLineCap: 'round',
|
||||
strokeLineJoin: 'round',
|
||||
endIcon: naver.maps.PointingIcon?.BLOCK_ARROW,
|
||||
endIconSize: 16,
|
||||
clickable: false
|
||||
const segments = [];
|
||||
let segment = [points[0]];
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const cur = points[i];
|
||||
const gap = Math.max(0, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
|
||||
if (gap > TRACK_MAX_GAP_SECONDS) {
|
||||
if (segment.length >= 2) segments.push(segment);
|
||||
segment = [cur];
|
||||
continue;
|
||||
}
|
||||
segment.push(cur);
|
||||
}
|
||||
if (segment.length >= 2) segments.push(segment);
|
||||
|
||||
segments.forEach((items, index) => {
|
||||
const line = new naver.maps.Polyline({
|
||||
map: mapState.map,
|
||||
path: items.map(point => point.latlng),
|
||||
strokeColor: '#007aff',
|
||||
strokeOpacity: 0.86,
|
||||
strokeWeight: 5,
|
||||
strokeLineCap: 'round',
|
||||
strokeLineJoin: 'round',
|
||||
endIcon: index === segments.length - 1 ? naver.maps.PointingIcon?.BLOCK_ARROW : undefined,
|
||||
endIconSize: 16,
|
||||
clickable: false
|
||||
});
|
||||
mapState.trackLines.push(line);
|
||||
if (!mapState.trackLine) mapState.trackLine = line;
|
||||
});
|
||||
mapState.trackLines.push(line);
|
||||
mapState.trackLine = line;
|
||||
}
|
||||
|
||||
function updateTimelinePanel() {
|
||||
|
||||
Reference in New Issue
Block a user