Add home and away map analytics
This commit is contained in:
+190
-23
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user