Enhance find my device map controls

This commit is contained in:
seo
2026-06-17 15:17:20 +09:00
parent 6c30305845
commit 84ffd0f01e
2 changed files with 379 additions and 46 deletions
+11 -3
View File
@@ -14,7 +14,9 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태,
- GPS 정확도 원 표시 - GPS 정확도 원 표시
- 사용자 지정 기간 이동 경로 표시 - 사용자 지정 기간 이동 경로 표시
- 지도 타입 전환, 교통/거리뷰 레이어 토글 - 지도 타입 전환, 교통/거리뷰 레이어 토글
- 이동 기록 요약, 경로 전체 보기, 경로 재생 - 이동 기록 요약, 경로 전체 보기, 경로 재생, 시간 슬라이더
- 속도 기반 경로 색상, GPS 튐 보정 토글, 체류 지점 자동 표시
- 현재 위치 기준 500m/1km 반경 표시와 선택 지점 Naver 지도 열기
- 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시 - 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시
- 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서 정보를 섹션별로 표시 - 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서 정보를 섹션별로 표시
- 자동 위치 갱신 웹훅 제어 - 자동 위치 갱신 웹훅 제어
@@ -43,9 +45,15 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태,
Naver Map에는 전체 경로선을 먼저 그리고 시작/종료 마커와 최대 180개의 중간 포인트를 표시합니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞추며, 종료 지점 정보창을 먼저 열어 최근 위치를 바로 확인할 수 있게 합니다. Naver Map에는 전체 경로선을 먼저 그리고 시작/종료 마커와 최대 180개의 중간 포인트를 표시합니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞추며, 종료 지점 정보창을 먼저 열어 최근 위치를 바로 확인할 수 있게 합니다.
지도 기능은 NAVER 지도 API v3의 기본 컨트롤, 지도 유형, `TrafficLayer`, `StreetLayer`, `Polyline`, `PointingIcon`, `fitBounds`를 활용합니다. 화면 상단 지도 도구에서 일반/위성 지도 전환, 교통 레이어, 거리뷰 가능 구역 레이어, 경로 전체 보기, 경로 재생 제어합니다. 지도 기능은 NAVER 지도 API v3의 기본 컨트롤, 지도 유형, `TrafficLayer`, `StreetLayer`, `Polyline`, `PointingIcon`, `Circle`, `fitBounds`를 활용합니다. 지도 아래 도구 패널에서 일반/위성 지도 전환, 교통 레이어, 거리뷰 가능 구역 레이어, 경로 전체 보기, 경로 재생, GPS 튐 보정, Naver 지도 열기를 제어합니다.
히스토리 요약 패널은 이동 거리, 기록 시간, 평균 속도, 최대 기록 공백, 지점 수를 표시합니다. 경로선 클릭으로 요약 패널을 숨기거나 다시 볼 수 있고, 각 히스토리 마커 정보창에는 기록 시각 시작점 기준 누적 거리를 함께 표시합니다. 히스토리 요약 패널은 이동 거리, 기록 시간, 평균 속도, 최대 기록 공백, 지점 수, 체류 지점 수, 현재 위치 반경 정보를 표시합니다. 경로선 클릭으로 요약 패널을 숨기거나 다시 볼 수 있고, 각 히스토리 마커 정보창에는 기록 시각, 시작점 기준 누적 거리, 좌표를 함께 표시합니다.
경로선은 구간별 속도에 따라 정지, 도보, 차량, 고속 이동 색상으로 나뉘어 표시됩니다. 2km 이상 순간 이동하면서 180km/h를 넘는 GPS 튐은 기본적으로 제외하며, 지도 아래 `GPS 튐 보정` 버튼으로 원본 경로와 보정 경로를 전환할 수 있습니다.
기록 시간 슬라이더를 움직이면 선택 마커가 해당 시점으로 이동하고, 선택 시각과 추정 속도를 지도 아래에 표시합니다. 경로 재생도 같은 선택 마커를 사용합니다. 80m 반경 안에서 10분 이상 머문 구간은 체류 지점 마커로 묶어 표시하며, 클릭 시 체류 시간과 시작/종료 시각을 확인할 수 있습니다.
라이브 위치에는 GPS 정확도 원 외에 500m/1km 현재 위치 반경 원을 함께 표시합니다. `Naver 지도` 버튼은 히스토리 선택 지점 또는 현재 위치를 Naver 지도에서 바로 엽니다.
## 센서 표시 정책 ## 센서 표시 정책
+368 -43
View File
@@ -181,6 +181,17 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
historyStart: "시작", historyStart: "시작",
historyEnd: "종료", historyEnd: "종료",
historyPoint: "기록 지점", historyPoint: "기록 지점",
historyStay: "체류",
historyStops: "체류 지점",
historyFiltered: "GPS 튐 보정",
historyTimeline: "시간 위치",
speedLegend: "속도",
speedStill: "정지",
speedWalk: "도보",
speedDrive: "차량",
currentRadius: "현재 반경",
openNaverMap: "Naver 지도",
selectedPoint: "선택 지점",
}, },
en: { en: {
blog: "Blog", blog: "Blog",
@@ -363,6 +374,17 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
historyStart: "Start", historyStart: "Start",
historyEnd: "End", historyEnd: "End",
historyPoint: "History Point", historyPoint: "History Point",
historyStay: "Stay",
historyStops: "Stops",
historyFiltered: "GPS Filter",
historyTimeline: "Timeline",
speedLegend: "Speed",
speedStill: "Still",
speedWalk: "Walk",
speedDrive: "Drive",
currentRadius: "Current Radius",
openNaverMap: "Naver Map",
selectedPoint: "Selected Point",
}, },
}, },
mapState = { mapState = {
@@ -370,13 +392,21 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
marker: null, marker: null,
circle: null, circle: null,
trackLine: null, trackLine: null,
trackLines: [],
smallMarkers: [], smallMarkers: [],
stayMarkers: [],
historyStartMarker: null, historyStartMarker: null,
historyEndMarker: null, historyEndMarker: null,
playbackMarker: null, playbackMarker: null,
selectedMarker: null,
playbackTimer: null, playbackTimer: null,
playbackIndex: 0,
rangeCircles: [],
rawHistoryPoints: [],
historyPoints: [], historyPoints: [],
historyStats: null, historyStats: null,
stayPoints: [],
filterOutliers: true,
layers: {}, layers: {},
layerActive: {}, layerActive: {},
activeMapType: 'normal', activeMapType: 'normal',
@@ -1079,6 +1109,11 @@ function ensureMapTools() {
if (!container || document.getElementById('mapTools')) return; if (!container || document.getElementById('mapTools')) return;
container.classList.add('has-map-tools'); container.classList.add('has-map-tools');
const panel = document.createElement('div');
panel.id = 'mapControlPanel';
panel.className = 'map-control-panel settingSection';
container.insertAdjacentElement('afterend', panel);
const tools = document.createElement('div'); const tools = document.createElement('div');
tools.id = 'mapTools'; tools.id = 'mapTools';
tools.className = 'map-tools'; tools.className = 'map-tools';
@@ -1089,8 +1124,10 @@ function ensureMapTools() {
<button type="button" class="map-tool" data-map-action="street"></button> <button type="button" class="map-tool" data-map-action="street"></button>
<button type="button" class="map-tool" data-map-action="fit"></button> <button type="button" class="map-tool" data-map-action="fit"></button>
<button type="button" class="map-tool" data-map-action="play"></button> <button type="button" class="map-tool" data-map-action="play"></button>
<button type="button" class="map-tool active" data-map-action="filter"></button>
<button type="button" class="map-tool" data-map-action="naver"></button>
`; `;
container.appendChild(tools); panel.appendChild(tools);
tools.addEventListener('click', event => { tools.addEventListener('click', event => {
const button = event.target.closest('[data-map-action]'); const button = event.target.closest('[data-map-action]');
@@ -1102,7 +1139,24 @@ function ensureMapTools() {
summary.id = 'historySummaryPanel'; summary.id = 'historySummaryPanel';
summary.className = 'history-summary-panel'; summary.className = 'history-summary-panel';
summary.hidden = true; summary.hidden = true;
container.appendChild(summary); panel.appendChild(summary);
const timeline = document.createElement('div');
timeline.id = 'historyTimelinePanel';
timeline.className = 'history-timeline-panel';
timeline.hidden = true;
timeline.innerHTML = `
<div class="history-timeline-head">
<strong id="historyTimelineTitle"></strong>
<span id="historyTimelineValue">-</span>
</div>
<input id="historyTimelineSlider" type="range" min="0" max="0" value="0" step="1">
`;
panel.appendChild(timeline);
timeline.querySelector('#historyTimelineSlider')?.addEventListener('input', event => {
moveSelectedHistoryPoint(Number(event.target.value), true);
});
updateMapToolLabels(); updateMapToolLabels();
} }
@@ -1115,12 +1169,17 @@ function updateMapToolLabels() {
street: t.mapStreet, street: t.mapStreet,
fit: t.mapFit, fit: t.mapFit,
play: mapState.playbackTimer ? t.mapPause : t.mapPlay, play: mapState.playbackTimer ? t.mapPause : t.mapPlay,
filter: t.historyFiltered,
naver: t.openNaverMap,
}; };
document.querySelectorAll('[data-map-action]').forEach(button => { document.querySelectorAll('[data-map-action]').forEach(button => {
const action = button.dataset.mapAction; const action = button.dataset.mapAction;
button.textContent = labelMap[action] || action; button.textContent = labelMap[action] || action;
button.title = labelMap[action] || action; button.title = labelMap[action] || action;
}); });
document.querySelector('[data-map-action="filter"]')?.classList.toggle('active', mapState.filterOutliers);
const timelineTitle = document.getElementById('historyTimelineTitle');
if (timelineTitle) timelineTitle.textContent = t.historyTimeline;
} }
function handleMapTool(action) { function handleMapTool(action) {
@@ -1152,6 +1211,18 @@ function handleMapTool(action) {
} }
if (action === 'play') { if (action === 'play') {
toggleHistoryPlayback(); toggleHistoryPlayback();
return;
}
if (action === 'filter') {
mapState.filterOutliers = !mapState.filterOutliers;
if (mapState.rawHistoryPoints?.length) {
renderHistoryPoints(mapState.rawHistoryPoints);
}
updateMapToolLabels();
return;
}
if (action === 'naver') {
openSelectedPointInNaverMap();
} }
} }
@@ -1208,6 +1279,93 @@ function formatSpeed(kmh) {
return `${kmh.toFixed(kmh >= 10 ? 1 : 2)}km/h`; return `${kmh.toFixed(kmh >= 10 ? 1 : 2)}km/h`;
} }
function formatDateTime(value) {
const ts = toTimestamp(value);
if (!ts) return '-';
return new Date(ts).toLocaleString(langCode === 'ko' ? 'ko-KR' : 'en-US', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function speedColor(kmh) {
if (!Number.isFinite(kmh) || kmh < 2) return '#8e8e93';
if (kmh < 8) return '#34c759';
if (kmh < 35) return '#ff9500';
return '#ff3b30';
}
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;
}
function filterHistoryOutliers(points) {
if (!mapState.filterOutliers || points.length < 3) return points;
const filtered = [points[0]];
for (let i = 1; i < points.length; i++) {
const prev = filtered[filtered.length - 1];
const cur = 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;
filtered.push(cur);
}
return filtered.length >= 2 ? filtered : points;
}
function buildStayPoints(points) {
const stays = [];
let start = 0;
const radius = 80;
const minSeconds = 10 * 60;
for (let i = 1; i <= points.length; i++) {
const base = points[start];
const current = points[i];
const outside = !current || distanceMeters(base, current) > radius;
if (!outside) continue;
const end = points[Math.max(start, i - 1)];
const duration = (toTimestamp(end.time) - toTimestamp(base.time)) / 1000;
if (duration >= minSeconds) {
const slice = points.slice(start, i);
const lat = slice.reduce((sum, p) => sum + Number(p.lat), 0) / slice.length;
const lng = slice.reduce((sum, p) => sum + Number(p.lng), 0) / slice.length;
stays.push({
lat,
lng,
latlng: new naver.maps.LatLng(lat, lng),
startTime: base.time,
endTime: end.time,
duration,
points: slice.length,
});
}
start = Math.max(0, i - 1);
}
return stays;
}
function openPointInNaverMap(point) {
if (!point || !hasValidCoords(point.lat, point.lng)) return;
const lat = Number(point.lat);
const lng = Number(point.lng);
window.open(`https://map.naver.com/v5/search/${lat},${lng}`, '_blank', 'noopener');
}
function openSelectedPointInNaverMap() {
const point = mapState.historyPoints?.[mapState.playbackIndex] ||
(hasValidCoords(lastLat, lastLng) ? { lat: lastLat, lng: lastLng } : null);
openPointInNaverMap(point);
}
function buildHistoryStats(points) { function buildHistoryStats(points) {
if (!Array.isArray(points) || points.length < 2) return null; if (!Array.isArray(points) || points.length < 2) return null;
let distance = 0; let distance = 0;
@@ -1226,6 +1384,7 @@ function buildHistoryStats(points) {
maxGap, maxGap,
points: points.length, points: points.length,
averageSpeed: duration > 0 ? distance / duration * 3.6 : 0, averageSpeed: duration > 0 ? distance / duration * 3.6 : 0,
stops: mapState.stayPoints?.length || 0,
startedAt: points[0].time, startedAt: points[0].time,
endedAt: points.at(-1).time, endedAt: points.at(-1).time,
}; };
@@ -1250,6 +1409,14 @@ function renderHistorySummary() {
<span>${t.historyAverageSpeed}</span><strong>${formatSpeed(stats.averageSpeed)}</strong> <span>${t.historyAverageSpeed}</span><strong>${formatSpeed(stats.averageSpeed)}</strong>
<span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong> <span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong>
<span>${t.historyPoints}</span><strong>${formatNumber(stats.points)}</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>
</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>
</div> </div>
`; `;
} }
@@ -1266,8 +1433,21 @@ function buildHistoryInfoContent(point, type, label) {
return ` return `
<div class="history-info-window history-info-${typeClass}"> <div class="history-info-window history-info-${typeClass}">
<strong>${title}</strong> <strong>${title}</strong>
<span>${getRelativeTime(point.time)}</span> <span>${formatDateTime(point.time)} · ${getRelativeTime(point.time)}</span>
<span>${t.historyDistance}: ${index > 0 ? formatDistance(fromStart) : '-'}</span> <span>${t.historyDistance}: ${index > 0 ? formatDistance(fromStart) : '-'}</span>
<span>${Number(point.lat).toFixed(6)}, ${Number(point.lng).toFixed(6)}</span>
</div>
`;
}
function buildStayInfoContent(stay) {
const t = translations[langCode] || translations.ko;
return `
<div class="history-info-window history-info-stay">
<strong>${t.historyStay}</strong>
<span>${formatDuration(Math.round(stay.duration))}</span>
<span>${formatDateTime(stay.startTime)} - ${formatDateTime(stay.endTime)}</span>
<span>${Number(stay.lat).toFixed(6)}, ${Number(stay.lng).toFixed(6)}</span>
</div> </div>
`; `;
} }
@@ -1322,13 +1502,109 @@ function toggleHistoryPlayback() {
mapState.playbackTimer = setInterval(() => { mapState.playbackTimer = setInterval(() => {
const point = points[index]; const point = points[index];
mapState.playbackMarker.setPosition(point.latlng); moveSelectedHistoryPoint(index, false);
if (index % 8 === 0) mapState.map.panTo(point.latlng); if (index % 8 === 0) mapState.map.panTo(point.latlng);
index += 1; index += 1;
if (index >= points.length) stopHistoryPlayback(); if (index >= points.length) stopHistoryPlayback();
}, Math.max(120, Math.min(450, 10000 / points.length))); }, Math.max(120, Math.min(450, 10000 / points.length)));
updateMapToolLabels(); updateMapToolLabels();
} }
function clearTrackLines() {
if (mapState.trackLine) {
mapState.trackLine.setMap(null);
mapState.trackLine = null;
}
if (mapState.trackLines?.length) {
mapState.trackLines.forEach(line => line.setMap?.(null));
mapState.trackLines = [];
}
}
function drawSpeedTrack(points) {
clearTrackLines();
if (!points?.length || points.length < 2) return;
for (let i = 1; i < points.length; i++) {
const prev = points[i - 1];
const cur = points[i];
const seconds = Math.max(1, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
const kmh = distanceMeters(prev, cur) / seconds * 3.6;
const line = new naver.maps.Polyline({
map: mapState.map,
path: [prev.latlng, cur.latlng],
strokeColor: speedColor(kmh),
strokeOpacity: 0.88,
strokeWeight: 4,
strokeLineCap: 'round',
strokeLineJoin: 'round',
endIcon: i === points.length - 1 ? naver.maps.PointingIcon?.BLOCK_ARROW : undefined,
endIconSize: 16,
clickable: true
});
line.segmentSpeed = kmh;
naver.maps.Event.addListener(line, 'click', () => {
const panel = document.getElementById('historySummaryPanel');
if (panel) panel.hidden = !panel.hidden;
});
mapState.trackLines.push(line);
}
mapState.trackLine = mapState.trackLines.at(-1) || null;
}
function updateTimelinePanel() {
const panel = document.getElementById('historyTimelinePanel');
const slider = document.getElementById('historyTimelineSlider');
if (!panel || !slider) return;
const points = mapState.historyPoints || [];
if (!points.length) {
panel.hidden = true;
return;
}
panel.hidden = false;
slider.max = String(points.length - 1);
slider.value = String(Math.min(mapState.playbackIndex || 0, points.length - 1));
moveSelectedHistoryPoint(Number(slider.value), false, false);
}
function moveSelectedHistoryPoint(index, pan = false, syncSlider = true) {
const points = mapState.historyPoints || [];
if (!points.length) return;
const safeIndex = Math.max(0, Math.min(points.length - 1, Number.isFinite(index) ? index : 0));
const point = points[safeIndex];
mapState.playbackIndex = safeIndex;
if (!mapState.selectedMarker) {
mapState.selectedMarker = new naver.maps.Marker({
position: point.latlng,
map: mapState.map,
zIndex: 115,
icon: {
content: '<div class="history-selected-marker"></div>',
anchor: new naver.maps.Point(10, 10)
}
});
} else {
mapState.selectedMarker.setMap(mapState.map);
mapState.selectedMarker.setPosition(point.latlng);
}
if (mapState.playbackMarker) {
mapState.playbackMarker.setPosition(point.latlng);
}
if (pan) mapState.map.panTo(point.latlng);
const value = document.getElementById('historyTimelineValue');
if (value) {
const prev = points[Math.max(0, safeIndex - 1)];
const seconds = safeIndex > 0 ? Math.max(1, (toTimestamp(point.time) - toTimestamp(prev.time)) / 1000) : 0;
const kmh = safeIndex > 0 ? distanceMeters(prev, point) / seconds * 3.6 : 0;
value.textContent = `${formatDateTime(point.time)} · ${speedLabel(kmh)} ${formatSpeed(kmh)}`;
}
if (syncSlider) {
const slider = document.getElementById('historyTimelineSlider');
if (slider) slider.value = String(safeIndex);
}
}
function createMainMarker(latlng) { function createMainMarker(latlng) {
mapState.marker = new naver.maps.Marker({ mapState.marker = new naver.maps.Marker({
@@ -1343,18 +1619,44 @@ function createMainMarker(latlng) {
applyMarkerEffects(mapState.marker); applyMarkerEffects(mapState.marker);
} }
function createAccuracyCircle(latlng, accuracy = 50) { function createAccuracyCircle(latlng, accuracy = 50) {
mapState.circle = new naver.maps.Circle({ mapState.circle = new naver.maps.Circle({
map: mapState.map, map: mapState.map,
center: latlng, center: latlng,
radius: Math.max(accuracy, 10), radius: Math.max(accuracy, 10),
strokeColor: '#007AFF', strokeColor: '#007AFF',
strokeOpacity: 0.8, strokeOpacity: 0.8,
strokeWeight: 2, strokeWeight: 2,
fillColor: '#007AFF', fillColor: '#007AFF',
fillOpacity: 0.2 fillOpacity: 0.2
}); });
} updateCurrentRangeCircles(latlng);
}
function updateCurrentRangeCircles(latlng) {
if (!mapState.map || !latlng) return;
const ranges = [
{ radius: 500, strokeColor: '#34c759', fillColor: '#34c759' },
{ radius: 1000, strokeColor: '#ff9500', fillColor: '#ff9500' },
];
ranges.forEach((range, index) => {
if (!mapState.rangeCircles[index]) {
mapState.rangeCircles[index] = new naver.maps.Circle({
map: mapState.map,
center: latlng,
radius: range.radius,
strokeColor: range.strokeColor,
strokeOpacity: 0.45,
strokeWeight: 1,
fillColor: range.fillColor,
fillOpacity: 0.035
});
} else {
mapState.rangeCircles[index].setCenter(latlng);
}
});
}
function applyMarkerEffects(marker) { function applyMarkerEffects(marker) {
naver.maps.Event.addListener(marker, 'mouseover', () => { naver.maps.Event.addListener(marker, 'mouseover', () => {
@@ -1409,14 +1711,15 @@ function bounceMarker(marker) {
function resetHistoryLayer() { function resetHistoryLayer() {
stopHistoryPlayback(); stopHistoryPlayback();
if (mapState.trackLine) { clearTrackLines();
mapState.trackLine.setMap(null);
mapState.trackLine = null;
}
if (mapState.smallMarkers?.length) { if (mapState.smallMarkers?.length) {
mapState.smallMarkers.forEach(m => m.setMap?.(null)); mapState.smallMarkers.forEach(m => m.setMap?.(null));
mapState.smallMarkers = []; mapState.smallMarkers = [];
} }
if (mapState.stayMarkers?.length) {
mapState.stayMarkers.forEach(m => m.setMap?.(null));
mapState.stayMarkers = [];
}
if (mapState.historyStartMarker) { if (mapState.historyStartMarker) {
mapState.historyStartMarker.setMap(null); mapState.historyStartMarker.setMap(null);
mapState.historyStartMarker = null; mapState.historyStartMarker = null;
@@ -1432,9 +1735,15 @@ function resetHistoryLayer() {
if (mapState.playbackMarker) { if (mapState.playbackMarker) {
mapState.playbackMarker.setMap(null); mapState.playbackMarker.setMap(null);
} }
if (mapState.selectedMarker) {
mapState.selectedMarker.setMap(null);
}
mapState.rawHistoryPoints = [];
mapState.historyPoints = []; mapState.historyPoints = [];
mapState.historyStats = null; mapState.historyStats = null;
mapState.stayPoints = [];
renderHistorySummary(); renderHistorySummary();
updateTimelinePanel();
mapState.initialInfoOpened = false; mapState.initialInfoOpened = false;
mapState.historyBoundsFitted = false; mapState.historyBoundsFitted = false;
} }
@@ -1458,39 +1767,29 @@ function updateHistoryMarkers(history) {
.map(pos => ({ ...pos, latlng: new naver.maps.LatLng(pos.lat, pos.lng) })); .map(pos => ({ ...pos, latlng: new naver.maps.LatLng(pos.lat, pos.lng) }));
if (!points.length) return; if (!points.length) return;
mapState.historyPoints = points; mapState.rawHistoryPoints = points;
renderHistoryPoints(points);
}
if (mapState.trackLine) mapState.trackLine.setMap(null); function renderHistoryPoints(rawPoints) {
mapState.trackLine = new naver.maps.Polyline({ if (!mapState.map || !rawPoints?.length) return;
map: mapState.map, const points = filterHistoryOutliers(rawPoints);
path: points.map(pos => pos.latlng), mapState.historyPoints = points;
strokeColor: '#007AFF', mapState.playbackIndex = 0;
strokeOpacity: 0.85, drawSpeedTrack(points);
strokeWeight: 4, mapState.stayPoints = buildStayPoints(points);
strokeLineCap: 'round',
strokeLineJoin: 'round',
endIcon: naver.maps.PointingIcon?.BLOCK_ARROW,
endIconSize: 16,
clickable: true
});
mapState.historyStats = buildHistoryStats(points); mapState.historyStats = buildHistoryStats(points);
if (mapState.trackLine.getDistance && mapState.historyStats) {
const lineDistance = mapState.trackLine.getDistance();
if (Number.isFinite(lineDistance) && lineDistance > 0) {
mapState.historyStats.distance = lineDistance;
mapState.historyStats.averageSpeed = mapState.historyStats.duration > 0 ? lineDistance / mapState.historyStats.duration * 3.6 : 0;
}
}
renderHistorySummary(); renderHistorySummary();
naver.maps.Event.addListener(mapState.trackLine, 'click', () => { updateTimelinePanel();
const panel = document.getElementById('historySummaryPanel');
if (panel) panel.hidden = !panel.hidden;
});
if (mapState.smallMarkers?.length) { if (mapState.smallMarkers?.length) {
mapState.smallMarkers.forEach(marker => marker.setMap(null)); mapState.smallMarkers.forEach(marker => marker.setMap(null));
mapState.smallMarkers = []; mapState.smallMarkers = [];
} }
if (mapState.stayMarkers?.length) {
mapState.stayMarkers.forEach(marker => marker.setMap(null));
mapState.stayMarkers = [];
}
if (mapState.historyStartMarker) mapState.historyStartMarker.setMap(null); if (mapState.historyStartMarker) mapState.historyStartMarker.setMap(null);
if (mapState.historyEndMarker) mapState.historyEndMarker.setMap(null); if (mapState.historyEndMarker) mapState.historyEndMarker.setMap(null);
@@ -1521,6 +1820,8 @@ function updateHistoryMarkers(history) {
} else { } else {
marker.infoWindow.open(mapState.map, marker); marker.infoWindow.open(mapState.map, marker);
mapState.openInfoWindow = marker.infoWindow; mapState.openInfoWindow = marker.infoWindow;
const idx = mapState.historyPoints.findIndex(item => item.time === pos.time && item.lat === pos.lat && item.lng === pos.lng);
if (idx >= 0) moveSelectedHistoryPoint(idx, false);
} }
}); });
return marker; return marker;
@@ -1538,6 +1839,29 @@ function updateHistoryMarkers(history) {
mapState.smallMarkers.push(makeMarker(pos)); mapState.smallMarkers.push(makeMarker(pos));
}); });
mapState.stayPoints.forEach(stay => {
const marker = new naver.maps.Marker({
position: stay.latlng,
map: mapState.map,
zIndex: 80,
icon: {
content: `<div class="history-marker history-marker-stay">${formatDuration(Math.round(stay.duration))}</div>`,
anchor: new naver.maps.Point(26, 14)
}
});
marker.infoWindow = new naver.maps.InfoWindow({
content: buildStayInfoContent(stay)
});
naver.maps.Event.addListener(marker, "click", () => {
if (mapState.openInfoWindow && mapState.openInfoWindow !== marker.infoWindow) {
mapState.openInfoWindow.close();
}
marker.infoWindow.open(mapState.map, marker);
mapState.openInfoWindow = marker.infoWindow;
});
mapState.stayMarkers.push(marker);
});
if (!mapState.historyBoundsFitted) { if (!mapState.historyBoundsFitted) {
fitHistoryOrCurrentBounds(); fitHistoryOrCurrentBounds();
mapState.historyBoundsFitted = true; mapState.historyBoundsFitted = true;
@@ -1601,9 +1925,10 @@ function updateDOMWithData(data) {
} }
// 메인 마커 / 정확도 원 업데이트 // 메인 마커 / 정확도 원 업데이트
mapState.marker.setPosition(latlng); mapState.marker.setPosition(latlng);
mapState.circle.setCenter(latlng); mapState.circle.setCenter(latlng);
animateCircleRadius(Math.max(data.accuracy || 50, 10)); animateCircleRadius(Math.max(data.accuracy || 50, 10));
updateCurrentRangeCircles(latlng);
// autoPosition // autoPosition
const isChecked = document.getElementById('autoPositionSwitch').checked; const isChecked = document.getElementById('autoPositionSwitch').checked;