Enhance Naver map history tools
This commit is contained in:
@@ -13,6 +13,8 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태,
|
|||||||
- 현재 위치 지도 표시
|
- 현재 위치 지도 표시
|
||||||
- GPS 정확도 원 표시
|
- GPS 정확도 원 표시
|
||||||
- 사용자 지정 기간 이동 경로 표시
|
- 사용자 지정 기간 이동 경로 표시
|
||||||
|
- 지도 타입 전환, 교통/거리뷰 레이어 토글
|
||||||
|
- 이동 기록 요약, 경로 전체 보기, 경로 재생
|
||||||
- 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시
|
- 배터리, 충전 상태, 배터리 건강, 온도, 전력, 활동 상태, 걸음 수, 이동 거리 표시
|
||||||
- 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서 정보를 섹션별로 표시
|
- 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서 정보를 섹션별로 표시
|
||||||
- 자동 위치 갱신 웹훅 제어
|
- 자동 위치 갱신 웹훅 제어
|
||||||
@@ -41,6 +43,10 @@ Home Assistant에 기록된 Galaxy Z Fold7의 위치, 배터리, 활동 상태,
|
|||||||
|
|
||||||
Naver Map에는 전체 경로선을 먼저 그리고 시작/종료 마커와 최대 60개의 중간 포인트만 표시합니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞추며, 종료 지점 정보창을 먼저 열어 최근 위치를 바로 확인할 수 있게 합니다.
|
Naver Map에는 전체 경로선을 먼저 그리고 시작/종료 마커와 최대 60개의 중간 포인트만 표시합니다. 기록 조회 시 지도는 경로 전체가 보이도록 자동으로 범위를 맞추며, 종료 지점 정보창을 먼저 열어 최근 위치를 바로 확인할 수 있게 합니다.
|
||||||
|
|
||||||
|
지도 기능은 NAVER 지도 API v3의 기본 컨트롤, 지도 유형, `TrafficLayer`, `StreetLayer`, `Polyline`, `PointingIcon`, `fitBounds`를 활용합니다. 화면 상단 지도 도구에서 일반/위성 지도 전환, 교통 레이어, 거리뷰 가능 구역 레이어, 경로 전체 보기, 경로 재생을 제어합니다.
|
||||||
|
|
||||||
|
히스토리 요약 패널은 이동 거리, 기록 시간, 평균 속도, 최대 기록 공백, 지점 수를 표시합니다. 경로선 클릭으로 요약 패널을 숨기거나 다시 볼 수 있고, 각 히스토리 마커 정보창에는 기록 시각과 시작점 기준 누적 거리를 함께 표시합니다.
|
||||||
|
|
||||||
## 센서 표시 정책
|
## 센서 표시 정책
|
||||||
|
|
||||||
실제 Fold7에서 확인된 값 기준으로 의미 있는 섹션만 유지합니다. 현재 표시 대상은 배터리, 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서입니다.
|
실제 Fold7에서 확인된 값 기준으로 의미 있는 섹션만 유지합니다. 현재 표시 대상은 배터리, 네트워크, 기기, 화면/오디오, 건강/활동, 저장공간/데이터, 알림/통화, 센서입니다.
|
||||||
|
|||||||
+342
-6
@@ -165,6 +165,22 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
|||||||
history: "기록",
|
history: "기록",
|
||||||
startTime: "시작",
|
startTime: "시작",
|
||||||
endTime: "종료",
|
endTime: "종료",
|
||||||
|
mapNormal: "일반",
|
||||||
|
mapSatellite: "위성",
|
||||||
|
mapTraffic: "교통",
|
||||||
|
mapStreet: "거리뷰",
|
||||||
|
mapFit: "전체",
|
||||||
|
mapPlay: "재생",
|
||||||
|
mapPause: "정지",
|
||||||
|
historySummary: "기록 요약",
|
||||||
|
historyDistance: "이동 거리",
|
||||||
|
historyDuration: "기록 시간",
|
||||||
|
historyPoints: "지점",
|
||||||
|
historyAverageSpeed: "평균 속도",
|
||||||
|
historyMaxGap: "최대 공백",
|
||||||
|
historyStart: "시작",
|
||||||
|
historyEnd: "종료",
|
||||||
|
historyPoint: "기록 지점",
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
blog: "Blog",
|
blog: "Blog",
|
||||||
@@ -331,6 +347,22 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
|||||||
history: "History",
|
history: "History",
|
||||||
startTime: "Start",
|
startTime: "Start",
|
||||||
endTime: "End",
|
endTime: "End",
|
||||||
|
mapNormal: "Normal",
|
||||||
|
mapSatellite: "Satellite",
|
||||||
|
mapTraffic: "Traffic",
|
||||||
|
mapStreet: "Street",
|
||||||
|
mapFit: "Fit",
|
||||||
|
mapPlay: "Play",
|
||||||
|
mapPause: "Pause",
|
||||||
|
historySummary: "History Summary",
|
||||||
|
historyDistance: "Distance",
|
||||||
|
historyDuration: "Duration",
|
||||||
|
historyPoints: "Points",
|
||||||
|
historyAverageSpeed: "Avg Speed",
|
||||||
|
historyMaxGap: "Max Gap",
|
||||||
|
historyStart: "Start",
|
||||||
|
historyEnd: "End",
|
||||||
|
historyPoint: "History Point",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mapState = {
|
mapState = {
|
||||||
@@ -341,6 +373,13 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
|||||||
smallMarkers: [],
|
smallMarkers: [],
|
||||||
historyStartMarker: null,
|
historyStartMarker: null,
|
||||||
historyEndMarker: null,
|
historyEndMarker: null,
|
||||||
|
playbackMarker: null,
|
||||||
|
playbackTimer: null,
|
||||||
|
historyPoints: [],
|
||||||
|
historyStats: null,
|
||||||
|
layers: {},
|
||||||
|
layerActive: {},
|
||||||
|
activeMapType: 'normal',
|
||||||
openInfoWindow: null,
|
openInfoWindow: null,
|
||||||
mapClickHandlerAdded: false,
|
mapClickHandlerAdded: false,
|
||||||
initialInfoOpened: false,
|
initialInfoOpened: false,
|
||||||
@@ -813,6 +852,9 @@ function applyLanguage(langCode) {
|
|||||||
const label = row.querySelector('span');
|
const label = row.querySelector('span');
|
||||||
if (label && key) label.textContent = t[key] || key;
|
if (label && key) label.textContent = t[key] || key;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateMapToolLabels();
|
||||||
|
renderHistorySummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCookieHours(name, value, hours) {
|
function setCookieHours(name, value, hours) {
|
||||||
@@ -1008,11 +1050,281 @@ function updateActivitySection(data) {
|
|||||||
function initMap(latlng, accuracy = 50) {
|
function initMap(latlng, accuracy = 50) {
|
||||||
mapState.map = new naver.maps.Map('map', {
|
mapState.map = new naver.maps.Map('map', {
|
||||||
center: latlng,
|
center: latlng,
|
||||||
zoom: 12
|
zoom: 12,
|
||||||
|
zoomControl: true,
|
||||||
|
zoomControlOptions: {
|
||||||
|
position: naver.maps.Position.RIGHT_CENTER
|
||||||
|
},
|
||||||
|
scaleControl: true,
|
||||||
|
mapDataControl: false
|
||||||
});
|
});
|
||||||
|
|
||||||
createMainMarker(latlng);
|
createMainMarker(latlng);
|
||||||
createAccuracyCircle(latlng, accuracy);
|
createAccuracyCircle(latlng, accuracy);
|
||||||
|
initMapLayers();
|
||||||
|
ensureMapTools();
|
||||||
|
}
|
||||||
|
|
||||||
|
function initMapLayers() {
|
||||||
|
if (!mapState.map || mapState.layers.traffic) return;
|
||||||
|
mapState.layers.traffic = new naver.maps.TrafficLayer();
|
||||||
|
mapState.layers.street = new naver.maps.StreetLayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureMapTools() {
|
||||||
|
const container = document.getElementById('mapContainer') || document.getElementById('map')?.parentElement;
|
||||||
|
if (!container || document.getElementById('mapTools')) return;
|
||||||
|
container.classList.add('has-map-tools');
|
||||||
|
|
||||||
|
const tools = document.createElement('div');
|
||||||
|
tools.id = 'mapTools';
|
||||||
|
tools.className = 'map-tools';
|
||||||
|
tools.innerHTML = `
|
||||||
|
<button type="button" class="map-tool active" data-map-action="normal"></button>
|
||||||
|
<button type="button" class="map-tool" data-map-action="satellite"></button>
|
||||||
|
<button type="button" class="map-tool" data-map-action="traffic"></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="play"></button>
|
||||||
|
`;
|
||||||
|
container.appendChild(tools);
|
||||||
|
|
||||||
|
tools.addEventListener('click', event => {
|
||||||
|
const button = event.target.closest('[data-map-action]');
|
||||||
|
if (!button) return;
|
||||||
|
handleMapTool(button.dataset.mapAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
const summary = document.createElement('div');
|
||||||
|
summary.id = 'historySummaryPanel';
|
||||||
|
summary.className = 'history-summary-panel';
|
||||||
|
summary.hidden = true;
|
||||||
|
container.appendChild(summary);
|
||||||
|
updateMapToolLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateMapToolLabels() {
|
||||||
|
const t = translations[langCode] || translations.ko;
|
||||||
|
const labelMap = {
|
||||||
|
normal: t.mapNormal,
|
||||||
|
satellite: t.mapSatellite,
|
||||||
|
traffic: t.mapTraffic,
|
||||||
|
street: t.mapStreet,
|
||||||
|
fit: t.mapFit,
|
||||||
|
play: mapState.playbackTimer ? t.mapPause : t.mapPlay,
|
||||||
|
};
|
||||||
|
document.querySelectorAll('[data-map-action]').forEach(button => {
|
||||||
|
const action = button.dataset.mapAction;
|
||||||
|
button.textContent = labelMap[action] || action;
|
||||||
|
button.title = labelMap[action] || action;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMapTool(action) {
|
||||||
|
if (!mapState.map) return;
|
||||||
|
if (action === 'normal' || action === 'satellite') {
|
||||||
|
mapState.activeMapType = action;
|
||||||
|
const mapTypes = naver.maps.MapTypeId || {};
|
||||||
|
const typeId = action === 'satellite'
|
||||||
|
? (mapTypes.HYBRID || mapTypes.SATELLITE || 'satellite')
|
||||||
|
: (mapTypes.NORMAL || 'normal');
|
||||||
|
mapState.map.setMapTypeId(typeId);
|
||||||
|
document.querySelectorAll('[data-map-action="normal"], [data-map-action="satellite"]').forEach(button => {
|
||||||
|
button.classList.toggle('active', button.dataset.mapAction === action);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'traffic' || action === 'street') {
|
||||||
|
const layer = mapState.layers[action];
|
||||||
|
if (!layer) return;
|
||||||
|
const nextActive = !mapState.layerActive[action];
|
||||||
|
layer.setMap(nextActive ? mapState.map : null);
|
||||||
|
mapState.layerActive[action] = nextActive;
|
||||||
|
document.querySelector(`[data-map-action="${action}"]`)?.classList.toggle('active', nextActive);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'fit') {
|
||||||
|
fitHistoryOrCurrentBounds();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === 'play') {
|
||||||
|
toggleHistoryPlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toTimestamp(value) {
|
||||||
|
if (value instanceof Date) return value.getTime();
|
||||||
|
if (typeof value === 'number' && isFinite(value)) return value < 2e10 ? value * 1000 : value;
|
||||||
|
const parsed = Date.parse(value);
|
||||||
|
return Number.isNaN(parsed) ? 0 : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function distanceMeters(a, b) {
|
||||||
|
const earth = 6371000;
|
||||||
|
const lat1 = Number(a.lat);
|
||||||
|
const lat2 = Number(b.lat);
|
||||||
|
const lng1 = Number(a.lng);
|
||||||
|
const lng2 = Number(b.lng);
|
||||||
|
if (![lat1, lat2, lng1, lng2].every(Number.isFinite)) return 0;
|
||||||
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||||
|
const dLng = (lng2 - lng1) * Math.PI / 180;
|
||||||
|
const rLat1 = lat1 * Math.PI / 180;
|
||||||
|
const rLat2 = lat2 * Math.PI / 180;
|
||||||
|
const h = Math.sin(dLat / 2) ** 2 + Math.cos(rLat1) * Math.cos(rLat2) * Math.sin(dLng / 2) ** 2;
|
||||||
|
return 2 * earth * Math.asin(Math.min(1, Math.sqrt(h)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDistance(meters) {
|
||||||
|
if (!Number.isFinite(meters) || meters <= 0) return '-';
|
||||||
|
if (meters >= 1000) return `${(meters / 1000).toFixed(meters >= 10000 ? 1 : 2)}km`;
|
||||||
|
return `${Math.round(meters)}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(seconds) {
|
||||||
|
if (!Number.isFinite(seconds) || seconds <= 0) return '-';
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
seconds %= 86400;
|
||||||
|
const hours = Math.floor(seconds / 3600);
|
||||||
|
seconds %= 3600;
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const parts = [];
|
||||||
|
if (langCode === 'ko') {
|
||||||
|
if (days) parts.push(`${days}일`);
|
||||||
|
if (hours) parts.push(`${hours}시간`);
|
||||||
|
if (minutes) parts.push(`${minutes}분`);
|
||||||
|
} else {
|
||||||
|
if (days) parts.push(`${days}d`);
|
||||||
|
if (hours) parts.push(`${hours}h`);
|
||||||
|
if (minutes) parts.push(`${minutes}m`);
|
||||||
|
}
|
||||||
|
return parts.join(' ') || (langCode === 'ko' ? '1분 미만' : '<1m');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSpeed(kmh) {
|
||||||
|
if (!Number.isFinite(kmh) || kmh <= 0) return '-';
|
||||||
|
return `${kmh.toFixed(kmh >= 10 ? 1 : 2)}km/h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHistoryStats(points) {
|
||||||
|
if (!Array.isArray(points) || points.length < 2) return null;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
const startedAt = toTimestamp(points[0].time);
|
||||||
|
const endedAt = toTimestamp(points.at(-1).time);
|
||||||
|
const duration = Math.max(0, (endedAt - startedAt) / 1000);
|
||||||
|
return {
|
||||||
|
distance,
|
||||||
|
duration,
|
||||||
|
maxGap,
|
||||||
|
points: points.length,
|
||||||
|
averageSpeed: duration > 0 ? distance / duration * 3.6 : 0,
|
||||||
|
startedAt: points[0].time,
|
||||||
|
endedAt: points.at(-1).time,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHistorySummary() {
|
||||||
|
const panel = document.getElementById('historySummaryPanel');
|
||||||
|
if (!panel) return;
|
||||||
|
const stats = mapState.historyStats;
|
||||||
|
if (!stats) {
|
||||||
|
panel.hidden = true;
|
||||||
|
panel.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const t = translations[langCode] || translations.ko;
|
||||||
|
panel.hidden = false;
|
||||||
|
panel.innerHTML = `
|
||||||
|
<div class="history-summary-title">${t.historySummary}</div>
|
||||||
|
<div class="history-summary-grid">
|
||||||
|
<span>${t.historyDistance}</span><strong>${formatDistance(stats.distance)}</strong>
|
||||||
|
<span>${t.historyDuration}</span><strong>${formatDuration(Math.round(stats.duration))}</strong>
|
||||||
|
<span>${t.historyAverageSpeed}</span><strong>${formatSpeed(stats.averageSpeed)}</strong>
|
||||||
|
<span>${t.historyMaxGap}</span><strong>${formatDuration(Math.round(stats.maxGap))}</strong>
|
||||||
|
<span>${t.historyPoints}</span><strong>${formatNumber(stats.points)}</strong>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildHistoryInfoContent(point, type, label) {
|
||||||
|
const t = translations[langCode] || translations.ko;
|
||||||
|
const title = label || t.historyPoint;
|
||||||
|
const index = mapState.historyPoints.findIndex(item => item.time === point.time && item.lat === point.lat && item.lng === point.lng);
|
||||||
|
let fromStart = 0;
|
||||||
|
for (let i = 1; i <= index; i++) {
|
||||||
|
fromStart += distanceMeters(mapState.historyPoints[i - 1], mapState.historyPoints[i]);
|
||||||
|
}
|
||||||
|
const typeClass = type === 'start' ? 'start' : (type === 'end' ? 'end' : 'point');
|
||||||
|
return `
|
||||||
|
<div class="history-info-window history-info-${typeClass}">
|
||||||
|
<strong>${title}</strong>
|
||||||
|
<span>${getRelativeTime(point.time)}</span>
|
||||||
|
<span>${t.historyDistance}: ${index > 0 ? formatDistance(fromStart) : '-'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fitHistoryOrCurrentBounds() {
|
||||||
|
if (!mapState.map) return;
|
||||||
|
if (mapState.historyPoints?.length) {
|
||||||
|
const coords = mapState.historyPoints.map(point => point.latlng);
|
||||||
|
if (coords.length === 1) {
|
||||||
|
mapState.map.panTo(coords[0]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapState.map.fitBounds(coords, { top: 40, right: 28, bottom: 120, left: 28, maxZoom: 16 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hasValidCoords(lastLat, lastLng)) {
|
||||||
|
mapState.map.panTo(new naver.maps.LatLng(lastLat, lastLng));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopHistoryPlayback() {
|
||||||
|
if (mapState.playbackTimer) {
|
||||||
|
clearInterval(mapState.playbackTimer);
|
||||||
|
mapState.playbackTimer = null;
|
||||||
|
}
|
||||||
|
updateMapToolLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleHistoryPlayback() {
|
||||||
|
if (!mapState.map || !mapState.historyPoints?.length) return;
|
||||||
|
if (mapState.playbackTimer) {
|
||||||
|
stopHistoryPlayback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const points = mapState.historyPoints;
|
||||||
|
let index = 0;
|
||||||
|
if (!mapState.playbackMarker) {
|
||||||
|
mapState.playbackMarker = new naver.maps.Marker({
|
||||||
|
position: points[0].latlng,
|
||||||
|
map: mapState.map,
|
||||||
|
zIndex: 120,
|
||||||
|
icon: {
|
||||||
|
content: '<div class="history-playback-marker"></div>',
|
||||||
|
anchor: new naver.maps.Point(11, 11)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mapState.playbackMarker.setMap(mapState.map);
|
||||||
|
mapState.playbackMarker.setPosition(points[0].latlng);
|
||||||
|
}
|
||||||
|
|
||||||
|
mapState.playbackTimer = setInterval(() => {
|
||||||
|
const point = points[index];
|
||||||
|
mapState.playbackMarker.setPosition(point.latlng);
|
||||||
|
if (index % 8 === 0) mapState.map.panTo(point.latlng);
|
||||||
|
index += 1;
|
||||||
|
if (index >= points.length) stopHistoryPlayback();
|
||||||
|
}, Math.max(120, Math.min(450, 10000 / points.length)));
|
||||||
|
updateMapToolLabels();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainMarker(latlng) {
|
function createMainMarker(latlng) {
|
||||||
@@ -1093,6 +1405,7 @@ function bounceMarker(marker) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetHistoryLayer() {
|
function resetHistoryLayer() {
|
||||||
|
stopHistoryPlayback();
|
||||||
if (mapState.trackLine) {
|
if (mapState.trackLine) {
|
||||||
mapState.trackLine.setMap(null);
|
mapState.trackLine.setMap(null);
|
||||||
mapState.trackLine = null;
|
mapState.trackLine = null;
|
||||||
@@ -1113,6 +1426,12 @@ function resetHistoryLayer() {
|
|||||||
mapState.openInfoWindow.close();
|
mapState.openInfoWindow.close();
|
||||||
mapState.openInfoWindow = null;
|
mapState.openInfoWindow = null;
|
||||||
}
|
}
|
||||||
|
if (mapState.playbackMarker) {
|
||||||
|
mapState.playbackMarker.setMap(null);
|
||||||
|
}
|
||||||
|
mapState.historyPoints = [];
|
||||||
|
mapState.historyStats = null;
|
||||||
|
renderHistorySummary();
|
||||||
mapState.initialInfoOpened = false;
|
mapState.initialInfoOpened = false;
|
||||||
mapState.historyBoundsFitted = false;
|
mapState.historyBoundsFitted = false;
|
||||||
}
|
}
|
||||||
@@ -1136,6 +1455,7 @@ 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;
|
||||||
|
|
||||||
if (mapState.trackLine) mapState.trackLine.setMap(null);
|
if (mapState.trackLine) mapState.trackLine.setMap(null);
|
||||||
mapState.trackLine = new naver.maps.Polyline({
|
mapState.trackLine = new naver.maps.Polyline({
|
||||||
@@ -1143,7 +1463,25 @@ function updateHistoryMarkers(history) {
|
|||||||
path: points.map(pos => pos.latlng),
|
path: points.map(pos => pos.latlng),
|
||||||
strokeColor: '#007AFF',
|
strokeColor: '#007AFF',
|
||||||
strokeOpacity: 0.85,
|
strokeOpacity: 0.85,
|
||||||
strokeWeight: 4
|
strokeWeight: 4,
|
||||||
|
strokeLineCap: 'round',
|
||||||
|
strokeLineJoin: 'round',
|
||||||
|
endIcon: naver.maps.PointingIcon?.BLOCK_ARROW,
|
||||||
|
endIconSize: 16,
|
||||||
|
clickable: true
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
naver.maps.Event.addListener(mapState.trackLine, 'click', () => {
|
||||||
|
const panel = document.getElementById('historySummaryPanel');
|
||||||
|
if (panel) panel.hidden = !panel.hidden;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (mapState.smallMarkers?.length) {
|
if (mapState.smallMarkers?.length) {
|
||||||
@@ -1168,7 +1506,7 @@ function updateHistoryMarkers(history) {
|
|||||||
});
|
});
|
||||||
marker.userTime = pos.time;
|
marker.userTime = pos.time;
|
||||||
marker.infoWindow = new naver.maps.InfoWindow({
|
marker.infoWindow = new naver.maps.InfoWindow({
|
||||||
content: `<div style="font-size:12px;padding:6px 8px;line-height:1.4;">${label ? `${label}<br>` : ''}${getRelativeTime(pos.time)}</div>`
|
content: buildHistoryInfoContent(pos, type, label)
|
||||||
});
|
});
|
||||||
naver.maps.Event.addListener(marker, "click", () => {
|
naver.maps.Event.addListener(marker, "click", () => {
|
||||||
if (mapState.openInfoWindow && mapState.openInfoWindow !== marker.infoWindow) {
|
if (mapState.openInfoWindow && mapState.openInfoWindow !== marker.infoWindow) {
|
||||||
@@ -1198,9 +1536,7 @@ function updateHistoryMarkers(history) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!mapState.historyBoundsFitted) {
|
if (!mapState.historyBoundsFitted) {
|
||||||
const bounds = new naver.maps.LatLngBounds(first.latlng, first.latlng);
|
fitHistoryOrCurrentBounds();
|
||||||
points.forEach(pos => bounds.extend(pos.latlng));
|
|
||||||
mapState.map.fitBounds(bounds);
|
|
||||||
mapState.historyBoundsFitted = true;
|
mapState.historyBoundsFitted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user