Harden findmydevice GPS jitter filtering

This commit is contained in:
seo
2026-06-18 04:24:31 +09:00
parent 08bd2ab4a4
commit 25fe661cc1
3 changed files with 117 additions and 27 deletions
+105 -24
View File
@@ -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() {