Add home and away map analytics

This commit is contained in:
seo
2026-06-17 15:25:45 +09:00
parent 84ffd0f01e
commit 689b8013b9
2 changed files with 197 additions and 27 deletions
+190 -23
View File
@@ -188,8 +188,26 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
speedLegend: "속도",
speedStill: "정지",
speedWalk: "도보",
speedFastWalk: "빠른 도보",
speedSlowRide: "저속 이동",
speedCityDrive: "시내 차량",
speedHighway: "고속 차량",
speedExtreme: "비정상 고속",
speedDrive: "차량",
currentRadius: "현재 반경",
homeRadius: "집 반경",
homeStay: "집 체류",
awayTime: "외출 시간",
awaySessions: "외출 횟수",
lastReturn: "마지막 복귀",
currentPlaceStatus: "현재 상태",
atHome: "집",
awayNow: "외출 중",
nearHome: "집 근처",
unstableLocation: "판정 불안정",
departure: "출발",
returnHome: "복귀",
awaySessionTitle: "외출 기록",
openNaverMap: "Naver 지도",
selectedPoint: "선택 지점",
},
@@ -381,8 +399,26 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
speedLegend: "Speed",
speedStill: "Still",
speedWalk: "Walk",
speedFastWalk: "Fast Walk",
speedSlowRide: "Slow Ride",
speedCityDrive: "City Drive",
speedHighway: "Highway",
speedExtreme: "Extreme",
speedDrive: "Drive",
currentRadius: "Current Radius",
homeRadius: "Home Radius",
homeStay: "Home Stay",
awayTime: "Away Time",
awaySessions: "Away Trips",
lastReturn: "Last Return",
currentPlaceStatus: "Current Status",
atHome: "Home",
awayNow: "Away",
nearHome: "Near Home",
unstableLocation: "Unstable",
departure: "Departure",
returnHome: "Return",
awaySessionTitle: "Away Sessions",
openNaverMap: "Naver Map",
selectedPoint: "Selected Point",
},
@@ -402,10 +438,13 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
playbackTimer: null,
playbackIndex: 0,
rangeCircles: [],
homeCircle: null,
homeCenter: null,
rawHistoryPoints: [],
historyPoints: [],
historyStats: null,
stayPoints: [],
homeStats: null,
filterOutliers: true,
layers: {},
layerActive: {},
@@ -1094,6 +1133,7 @@ function initMap(latlng, accuracy = 50) {
createMainMarker(latlng);
createAccuracyCircle(latlng, accuracy);
updateHomeCircle();
initMapLayers();
ensureMapTools();
}
@@ -1279,6 +1319,17 @@ function formatSpeed(kmh) {
return `${kmh.toFixed(kmh >= 10 ? 1 : 2)}km/h`;
}
function speedBucket(kmh) {
const t = translations[langCode] || translations.ko;
if (!Number.isFinite(kmh) || kmh < 1) return { key: 'still', label: t.speedStill, color: '#8e8e93' };
if (kmh < 5) return { key: 'walk', label: t.speedWalk, color: '#34c759' };
if (kmh < 8) return { key: 'fastWalk', label: t.speedFastWalk, color: '#00c7be' };
if (kmh < 20) return { key: 'slowRide', label: t.speedSlowRide, color: '#5ac8fa' };
if (kmh < 60) return { key: 'cityDrive', label: t.speedCityDrive, color: '#ff9500' };
if (kmh < 110) return { key: 'highway', label: t.speedHighway, color: '#ff3b30' };
return { key: 'extreme', label: t.speedExtreme, color: '#af52de' };
}
function formatDateTime(value) {
const ts = toTimestamp(value);
if (!ts) return '-';
@@ -1291,17 +1342,11 @@ function formatDateTime(value) {
}
function speedColor(kmh) {
if (!Number.isFinite(kmh) || kmh < 2) return '#8e8e93';
if (kmh < 8) return '#34c759';
if (kmh < 35) return '#ff9500';
return '#ff3b30';
return speedBucket(kmh).color;
}
function speedLabel(kmh) {
const t = translations[langCode] || translations.ko;
if (!Number.isFinite(kmh) || kmh < 2) return t.speedStill;
if (kmh < 8) return t.speedWalk;
return t.speedDrive;
return speedBucket(kmh).label;
}
function filterHistoryOutliers(points) {
@@ -1353,6 +1398,107 @@ function buildStayPoints(points) {
return stays;
}
function updateHomeCenter(lat, lng) {
if (!hasValidCoords(lat, lng)) return;
mapState.homeCenter = {
lat: Number(lat),
lng: Number(lng),
latlng: new naver.maps.LatLng(Number(lat), Number(lng))
};
updateHomeCircle();
}
function updateHomeCircle() {
if (!mapState.map || !mapState.homeCenter) return;
if (!mapState.homeCircle) {
mapState.homeCircle = new naver.maps.Circle({
map: mapState.map,
center: mapState.homeCenter.latlng,
radius: 50,
strokeColor: '#5856d6',
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: '#5856d6',
fillOpacity: 0.08
});
} else {
mapState.homeCircle.setCenter(mapState.homeCenter.latlng);
}
}
function distanceFromHome(point) {
if (!mapState.homeCenter || !point) return Infinity;
return distanceMeters(mapState.homeCenter, point);
}
function classifyHomePoint(point) {
const distance = distanceFromHome(point);
if (distance <= 50) return 'home';
if (distance <= 90) return 'near';
return 'away';
}
function buildHomeStats(points) {
if (!mapState.homeCenter || !Array.isArray(points) || points.length < 2) return null;
const t = translations[langCode] || translations.ko;
const minAwaySeconds = 5 * 60;
let homeSeconds = 0;
let unstableSeconds = 0;
let currentAway = null;
let lastReturn = null;
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;
}
currentAway = null;
};
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const cur = points[i];
const duration = Math.max(0, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
if (!duration) continue;
const prevState = classifyHomePoint(prev);
const curState = classifyHomePoint(cur);
const state = prevState === curState ? curState : (curState === 'home' ? 'home' : (curState === 'away' ? 'away' : 'near'));
if (state === 'home') {
homeSeconds += duration;
closeAwaySession(cur.time);
} else if (state === 'away') {
if (!currentAway) {
currentAway = { departureTime: prev.time, returnTime: null, duration: 0 };
}
} else {
unstableSeconds += duration;
}
}
if (currentAway) {
closeAwaySession(null);
}
const lastState = classifyHomePoint(points.at(-1));
const status = lastState === 'home' ? t.atHome : (lastState === 'away' ? t.awayNow : t.nearHome);
const awaySeconds = sessions.reduce((sum, session) => sum + session.duration, 0);
return {
homeSeconds,
awaySeconds,
unstableSeconds,
sessions,
lastReturn,
status,
};
}
function openPointInNaverMap(point) {
if (!point || !hasValidCoords(point.lat, point.lng)) return;
const lat = Number(point.lat);
@@ -1385,6 +1531,7 @@ function buildHistoryStats(points) {
points: points.length,
averageSpeed: duration > 0 ? distance / duration * 3.6 : 0,
stops: mapState.stayPoints?.length || 0,
home: mapState.homeStats,
startedAt: points[0].time,
endedAt: points.at(-1).time,
};
@@ -1400,6 +1547,9 @@ function renderHistorySummary() {
return;
}
const t = translations[langCode] || translations.ko;
const home = stats.home;
const sessions = home?.sessions || [];
const recentSessions = sessions.slice(-3).reverse();
panel.hidden = false;
panel.innerHTML = `
<div class="history-summary-title">${t.historySummary}</div>
@@ -1410,14 +1560,27 @@ function renderHistorySummary() {
<span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong>
<span>${t.historyPoints}</span><strong>${formatNumber(stats.points)}</strong>
<span>${t.historyStops}</span><strong>${formatNumber(stats.stops || 0)}</strong>
<span>${t.currentRadius}</span><strong>500m / 1km</strong>
<span>${t.homeRadius}</span><strong>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>
<span>${t.lastReturn}</span><strong>${home?.lastReturn ? formatDateTime(home.lastReturn) : '-'}</strong>
<span>${t.currentPlaceStatus}</span><strong>${home?.status || '-'}</strong>
</div>
<div class="speed-legend">
<span><i style="--legend-color:#8e8e93"></i>${t.speedStill}</span>
<span><i style="--legend-color:#34c759"></i>${t.speedWalk}</span>
<span><i style="--legend-color:#ff9500"></i>${t.speedDrive}</span>
<span><i style="--legend-color:#ff3b30"></i>35km/h+</span>
${[0, 3, 6, 12, 35, 80, 130].map(kmh => {
const bucket = speedBucket(kmh);
return `<span><i style="--legend-color:${bucket.color}"></i>${bucket.label}</span>`;
}).join('')}
</div>
${recentSessions.length ? `
<div class="away-session-list">
<strong>${t.awaySessionTitle}</strong>
${recentSessions.map(session => `
<span>${t.departure} ${formatDateTime(session.departureTime)} · ${session.returnTime ? `${t.returnHome} ${formatDateTime(session.returnTime)}` : t.awayNow} · ${formatDuration(Math.round(session.duration))}</span>
`).join('')}
</div>
` : ''}
`;
}
@@ -1742,6 +1905,7 @@ function resetHistoryLayer() {
mapState.historyPoints = [];
mapState.historyStats = null;
mapState.stayPoints = [];
mapState.homeStats = null;
renderHistorySummary();
updateTimelinePanel();
mapState.initialInfoOpened = false;
@@ -1778,6 +1942,7 @@ function renderHistoryPoints(rawPoints) {
mapState.playbackIndex = 0;
drawSpeedTrack(points);
mapState.stayPoints = buildStayPoints(points);
mapState.homeStats = buildHomeStats(points);
mapState.historyStats = buildHistoryStats(points);
renderHistorySummary();
updateTimelinePanel();
@@ -1884,7 +2049,7 @@ function renderHistoryPoints(rawPoints) {
}
}
function updateDOMWithData(data) {
function updateDOMWithData(data) {
// ⚑ 히스토리 모드: 값은 "-"로, 지도엔 히스토리만 반영
if (typeof _overrideRange !== 'undefined' && _overrideRange) {
resetDisplayValues();
@@ -1896,12 +2061,13 @@ function updateDOMWithData(data) {
: (data.latitude && data.longitude ? new naver.maps.LatLng(data.latitude, data.longitude) : null);
// 지도 초기화 (최초 1회)
if (!mapState.map && baseLatLng) {
initMap(baseLatLng, data.accuracy || 50);
}
// 히스토리 궤적/포인트 업데이트
if (data.history?.length > 0) {
if (!mapState.map && baseLatLng) {
initMap(baseLatLng, data.accuracy || 50);
}
updateHomeCenter(data.latitude, data.longitude);
// 히스토리 궤적/포인트 업데이트
if (data.history?.length > 0) {
updateHistoryMarkers(data.history);
}
@@ -1913,9 +2079,10 @@ function updateDOMWithData(data) {
}
// ── 라이브 모드 기존 로직 ──
const latlng = new naver.maps.LatLng(data.latitude, data.longitude);
// DOM 업데이트
const latlng = new naver.maps.LatLng(data.latitude, data.longitude);
updateHomeCenter(data.latitude, data.longitude);
// DOM 업데이트
updateDOM(data);
updateActivitySection(data);