Require signed PWA auth for findmydevice
This commit is contained in:
1 parent
1ba8c257a3
commit
76f9c7c549
3 files changed
+346
-56
No files matched your search
+251
-24
@@ -30,6 +30,18 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
batteryLevel: "배터리 잔량",
|
||||
watchbatteryState: "워치 배터리 상태",
|
||||
watchbatteryLevel: "워치 배터리 잔량",
|
||||
watch: "워치",
|
||||
watchBatteryState: "워치 배터리 상태",
|
||||
watchBatteryLevel: "워치 배터리 잔량",
|
||||
watchChargerType: "워치 충전 방식",
|
||||
watchDeviceState: "워치 위치 상태",
|
||||
watchInteractive: "워치 화면 활성",
|
||||
watchPowerSave: "워치 절전 모드",
|
||||
watchDozeMode: "워치 Doze 모드",
|
||||
watchBedtimeMode: "워치 취침 모드",
|
||||
watchTheaterMode: "워치 극장 모드",
|
||||
watchWetMode: "워치 방수 모드",
|
||||
watchOnBodySensor: "워치 착용 감지",
|
||||
isCharging: "충전 여부",
|
||||
chargerType: "충전 방식",
|
||||
batteryHealth: "배터리 건강",
|
||||
@@ -39,6 +51,7 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
batteryCycleCount: "배터리 사이클",
|
||||
activity: "활동",
|
||||
currentActivity: "현재 활동",
|
||||
activityState: "활동 상태",
|
||||
activityAccuracy: "활동 정확도",
|
||||
distance: "도보로 움직인 거리",
|
||||
steps: "걸음 수",
|
||||
@@ -55,6 +68,10 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
phoneState: "전화 상태",
|
||||
interactive: "화면 활성",
|
||||
dozeMode: "Doze 모드",
|
||||
bedtimeMode: "취침 모드",
|
||||
theaterMode: "극장 모드",
|
||||
wetMode: "방수 모드",
|
||||
onBodySensor: "착용 감지",
|
||||
powerSave: "절전 모드",
|
||||
deviceLocked: "잠금 상태",
|
||||
network: "네트워크",
|
||||
@@ -138,6 +155,10 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
height: "키",
|
||||
dailyHydration: "일일 수분",
|
||||
dailyElevationGained: "일일 고도 상승",
|
||||
dailyCalories: "일일 칼로리",
|
||||
dailyDistance: "일일 이동 거리",
|
||||
dailyFloors: "일일 오른 층",
|
||||
dailySteps: "일일 걸음 수",
|
||||
stepsSensor: "걸음 센서",
|
||||
androidAuto: "Android Auto",
|
||||
carName: "차량 이름",
|
||||
@@ -194,6 +215,10 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
historyStay: "체류",
|
||||
historyStops: "체류 지점",
|
||||
historyFiltered: "GPS 튐 보정",
|
||||
historyTrackGap: "추적 끊김",
|
||||
historyTrackGapCount: "끊김",
|
||||
historyTrackGapUnit: "개",
|
||||
historyTrackGapNotice: "이 구간에서는 고정밀 위치 추적이 이어지지 않았습니다.",
|
||||
historyTimeline: "시간 위치",
|
||||
speedLegend: "속도",
|
||||
speedStill: "정지",
|
||||
@@ -241,6 +266,18 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
batteryLevel: "Battery Level",
|
||||
watchbatteryState: "Watch Battery State",
|
||||
watchbatteryLevel: "Watch Battery Level",
|
||||
watch: "Watch",
|
||||
watchBatteryState: "Watch Battery State",
|
||||
watchBatteryLevel: "Watch Battery Level",
|
||||
watchChargerType: "Watch Charger Type",
|
||||
watchDeviceState: "Watch Location State",
|
||||
watchInteractive: "Watch Interactive",
|
||||
watchPowerSave: "Watch Power Save",
|
||||
watchDozeMode: "Watch Doze Mode",
|
||||
watchBedtimeMode: "Watch Bedtime Mode",
|
||||
watchTheaterMode: "Watch Theater Mode",
|
||||
watchWetMode: "Watch Water Lock",
|
||||
watchOnBodySensor: "Watch On Body",
|
||||
isCharging: "Charging",
|
||||
chargerType: "Charger Type",
|
||||
batteryHealth: "Battery Health",
|
||||
@@ -250,6 +287,7 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
batteryCycleCount: "Battery Cycles",
|
||||
activity: "Activity",
|
||||
currentActivity: "Current Activity",
|
||||
activityState: "Activity State",
|
||||
activityAccuracy: "Activity Confidence",
|
||||
distance: "Distance walked",
|
||||
steps: "Steps",
|
||||
@@ -266,6 +304,10 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
phoneState: "Phone State",
|
||||
interactive: "Interactive",
|
||||
dozeMode: "Doze Mode",
|
||||
bedtimeMode: "Bedtime Mode",
|
||||
theaterMode: "Theater Mode",
|
||||
wetMode: "Water Lock",
|
||||
onBodySensor: "On Body",
|
||||
powerSave: "Power Save",
|
||||
deviceLocked: "Device Locked",
|
||||
network: "Network",
|
||||
@@ -349,6 +391,10 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
height: "Height",
|
||||
dailyHydration: "Daily Hydration",
|
||||
dailyElevationGained: "Daily Elevation Gained",
|
||||
dailyCalories: "Daily Calories",
|
||||
dailyDistance: "Daily Distance",
|
||||
dailyFloors: "Daily Floors",
|
||||
dailySteps: "Daily Steps",
|
||||
stepsSensor: "Step Sensor",
|
||||
androidAuto: "Android Auto",
|
||||
carName: "Car Name",
|
||||
@@ -405,6 +451,10 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
historyStay: "Stay",
|
||||
historyStops: "Stops",
|
||||
historyFiltered: "GPS Filter",
|
||||
historyTrackGap: "Tracking Gap",
|
||||
historyTrackGapCount: "Gaps",
|
||||
historyTrackGapUnit: "",
|
||||
historyTrackGapNotice: "High accuracy location tracking was not continuous in this segment.",
|
||||
historyTimeline: "Timeline",
|
||||
speedLegend: "Speed",
|
||||
speedStill: "Still",
|
||||
@@ -466,6 +516,7 @@ const deviceLang = (navigator.language || navigator.userLanguage || "en").starts
|
||||
homeCenters: [],
|
||||
rawHistoryPoints: [],
|
||||
historyPoints: [],
|
||||
trackGaps: [],
|
||||
historyRawCount: null,
|
||||
historyRenderCount: null,
|
||||
historyStats: null,
|
||||
@@ -514,10 +565,11 @@ const BACK_LINK_PATH_TITLE = {
|
||||
'/blog': '블로그',
|
||||
};
|
||||
const FINDMYDEVICE_AUTH_REQUIRED = location.pathname.startsWith('/findmydevice');
|
||||
const FINDMYDEVICE_PWA_SERVICE = 'findmydevice';
|
||||
function isStandalonePwa() {
|
||||
return window.navigator.standalone || window.matchMedia('(display-mode: standalone)').matches;
|
||||
}
|
||||
let findmydeviceAuthenticated = !FINDMYDEVICE_AUTH_REQUIRED || isStandalonePwa();
|
||||
let findmydeviceAuthenticated = !FINDMYDEVICE_AUTH_REQUIRED;
|
||||
|
||||
function pad2(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
@@ -583,6 +635,7 @@ function renderAuthErrorPage() {
|
||||
placeholder: '비밀번호',
|
||||
backLabel: '채건닷컴',
|
||||
backHref: 'https://chaegeon.com/',
|
||||
pwaService: isStandalonePwa() ? FINDMYDEVICE_PWA_SERVICE : '',
|
||||
onSuccess: () => location.reload(),
|
||||
});
|
||||
if (window.CustomAuthPin) {
|
||||
@@ -596,11 +649,14 @@ function renderAuthErrorPage() {
|
||||
}
|
||||
|
||||
const findmydeviceAuthPromise = FINDMYDEVICE_AUTH_REQUIRED
|
||||
&& !isStandalonePwa()
|
||||
? fetch('/custom/launcher/api.php?session=1', { credentials: 'same-origin' })
|
||||
? fetch(`/custom/launcher/api.php?session=1&pwaService=${encodeURIComponent(FINDMYDEVICE_PWA_SERVICE)}`, { credentials: 'same-origin' })
|
||||
.then(res => res.ok ? res.json() : Promise.reject())
|
||||
.then(session => {
|
||||
findmydeviceAuthenticated = !!session.authenticated;
|
||||
if (session.loginBlocked) {
|
||||
location.replace('https://chaegeon.com/');
|
||||
return false;
|
||||
}
|
||||
findmydeviceAuthenticated = !!session.authenticated || (isStandalonePwa() && !!session.pwaAuthenticated);
|
||||
return findmydeviceAuthenticated;
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -770,8 +826,8 @@ function ensureFindMyDeviceShell() {
|
||||
<div class="settingTitle" data-live-status-section="1"><span id="titleBattery"></span></div>
|
||||
<div class="settingSection" id="batterySection" data-live-status-section="1">
|
||||
<div class="settingRow"><span id="labelBatteryState"></span><span id="batteryStatus"></span></div>
|
||||
<div class="settingRow"><span id="labelBatteryLevel"></span><span id="Battery"></span></div>
|
||||
<div class="settingRow"><span id="labelWatchBatteryState"></span><span id="WbatteryStatus"></span></div>
|
||||
<div class="settingRow"><span id="labelBatteryLevel"></span><span id="Battery"></span></div>
|
||||
<div class="settingRow"><span id="labelWatchBatteryLevel"></span><span id="WBattery"></span></div>
|
||||
</div>
|
||||
<div class="settingTitle" data-live-status-section="1"><span id="titleActivity"></span></div>
|
||||
@@ -1338,6 +1394,16 @@ function cleanupLegacyConnectionRow() {
|
||||
}
|
||||
}
|
||||
|
||||
function sensorLabel(t, key) {
|
||||
if (t[key]) return t[key];
|
||||
if (key.startsWith('watch') && key.length > 5) {
|
||||
const baseKey = key.charAt(5).toLowerCase() + key.slice(6);
|
||||
const baseLabel = t[baseKey] || baseKey;
|
||||
return (t.watch || 'Watch') + ' ' + baseLabel;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
function updateSensorSections(sections) {
|
||||
const t = translations[langCode] || translations.ko;
|
||||
const activeSectionIds = new Set();
|
||||
@@ -1359,7 +1425,7 @@ function updateSensorSections(sections) {
|
||||
|
||||
entries.forEach(([key, value]) => {
|
||||
activeRowIds.add(`sensor_${sectionKey}_${key}`);
|
||||
upsertOptionalRow(section, `sensor_${sectionKey}_${key}`, t[key] || key, value);
|
||||
upsertOptionalRow(section, `sensor_${sectionKey}_${key}`, sensorLabel(t, key), value);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1515,6 +1581,7 @@ function initMap(latlng, accuracy = 50) {
|
||||
ensureMapLayers();
|
||||
naver.maps.Event.addListener(mapState.map, 'zoom_changed', () => {
|
||||
renderStayMarkers();
|
||||
renderTrackGapMarkers(mapState.trackGaps);
|
||||
if (mapState.map.getZoom() < CURRENT_STAY_MIN_ZOOM) {
|
||||
stopCurrentStayBadge();
|
||||
setStayMarkersVisibleForCurrentPoint(
|
||||
@@ -1798,6 +1865,9 @@ const HOME_RADIUS_M = 50;
|
||||
const HOME_JITTER_ABSORB_RADIUS_M = 1200;
|
||||
const HOME_JITTER_SAMPLE_SECONDS = 30 * 60;
|
||||
const TRACK_MAX_GAP_SECONDS = 30 * 60;
|
||||
const TRACK_MAX_GAP_DISTANCE_M = 300;
|
||||
const TRACK_GAP_MARKER_MIN_ZOOM = 12;
|
||||
const TRACK_GAP_SINGLE_MIN_ZOOM = 15;
|
||||
const AWAY_MAX_GAP_SECONDS = 2 * 60 * 60;
|
||||
|
||||
function normalizeHomeConfig(home) {
|
||||
@@ -1839,16 +1909,33 @@ function pointWithLatLng(point, lat, lng, extra = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHomeJitterPoint(point) {
|
||||
function isSameHome(homeA, homeB) {
|
||||
return !!homeA && !!homeB && homeA.lat === homeB.lat && homeA.lng === homeB.lng;
|
||||
}
|
||||
|
||||
function isTemporaryHomeJitter(prev, cur, next, nearest) {
|
||||
if (!prev || !cur || !next || !nearest?.home) return false;
|
||||
if (nearest.distance <= nearest.home.radius) return true;
|
||||
|
||||
const prevHome = nearestHome(prev);
|
||||
const nextHome = nearestHome(next);
|
||||
if (!isSameHome(prevHome.home, nearest.home) || !isSameHome(nextHome.home, nearest.home)) return false;
|
||||
if (prevHome.distance > prevHome.home.radius || nextHome.distance > nextHome.home.radius) return false;
|
||||
|
||||
const spanSeconds = Math.max(0, (toTimestamp(next.time) - toTimestamp(prev.time)) / 1000);
|
||||
return spanSeconds <= HOME_JITTER_SAMPLE_SECONDS;
|
||||
}
|
||||
|
||||
function normalizeHomeJitterPoint(point, prevRaw = null, nextRaw = null) {
|
||||
if (!mapState.filterOutliers || !mapState.homeCenters?.length || !point) return point;
|
||||
const nearest = nearestHome(point);
|
||||
const distance = nearest.distance;
|
||||
if (distance > HOME_JITTER_ABSORB_RADIUS_M) return point;
|
||||
if (nearest.distance > HOME_JITTER_ABSORB_RADIUS_M) return point;
|
||||
if (nearest.distance > nearest.home.radius && !isTemporaryHomeJitter(prevRaw, point, nextRaw, nearest)) return point;
|
||||
return pointWithLatLng(point, nearest.home.lat, nearest.home.lng, {
|
||||
homeJitter: true,
|
||||
originalLat: point.lat,
|
||||
originalLng: point.lng,
|
||||
originalDistanceFromHome: distance,
|
||||
originalDistanceFromHome: nearest.distance,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1870,14 +1957,14 @@ function isSinglePointSpike(prev, cur, next) {
|
||||
|
||||
function filterHistoryOutliers(points) {
|
||||
if (!mapState.filterOutliers || points.length < 3) return points;
|
||||
const filtered = [normalizeHomeJitterPoint(points[0])];
|
||||
const filtered = [normalizeHomeJitterPoint(points[0], null, points[1])];
|
||||
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 = normalizeHomeJitterPoint(points[i]);
|
||||
const cur = normalizeHomeJitterPoint(points[i], points[i - 1], points[i + 1] || null);
|
||||
const seconds = Math.max(1, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
|
||||
const distance = distanceMeters(prev, cur);
|
||||
const kmh = distance / seconds * 3.6;
|
||||
@@ -2020,7 +2107,6 @@ function classifyHomePoint(point) {
|
||||
const nearest = nearestHome(point);
|
||||
if (!nearest.home) return 'away';
|
||||
if (nearest.distance <= nearest.home.radius) return 'home';
|
||||
if (mapState.filterOutliers && nearest.distance <= HOME_JITTER_ABSORB_RADIUS_M) return 'home';
|
||||
return 'away';
|
||||
}
|
||||
|
||||
@@ -2112,8 +2198,9 @@ function buildHistoryStats(points) {
|
||||
let maxGap = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const gap = Math.max(0, (toTimestamp(points[i].time) - toTimestamp(points[i - 1].time)) / 1000);
|
||||
const trackGap = buildTrackGap(points[i - 1], points[i]);
|
||||
maxGap = Math.max(maxGap, gap);
|
||||
if (gap <= TRACK_MAX_GAP_SECONDS) {
|
||||
if (!trackGap) {
|
||||
distance += distanceMeters(points[i - 1], points[i]);
|
||||
}
|
||||
}
|
||||
@@ -2182,7 +2269,9 @@ function buildHistoryInfoContent(point, type, label) {
|
||||
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]);
|
||||
if (!buildTrackGap(mapState.historyPoints[i - 1], mapState.historyPoints[i])) {
|
||||
fromStart += distanceMeters(mapState.historyPoints[i - 1], mapState.historyPoints[i]);
|
||||
}
|
||||
}
|
||||
const typeClass = type === 'start' ? 'start' : (type === 'end' ? 'end' : 'point');
|
||||
return `
|
||||
@@ -2212,6 +2301,49 @@ function buildStayInfoContent(stay) {
|
||||
`;
|
||||
}
|
||||
|
||||
function buildTrackGap(prev, cur) {
|
||||
if (!prev || !cur) return null;
|
||||
const seconds = Math.max(0, (toTimestamp(cur.time) - toTimestamp(prev.time)) / 1000);
|
||||
const distance = distanceMeters(prev, cur);
|
||||
if (seconds <= TRACK_MAX_GAP_SECONDS && distance <= TRACK_MAX_GAP_DISTANCE_M) return null;
|
||||
const lat = (Number(prev.lat) + Number(cur.lat)) / 2;
|
||||
const lng = (Number(prev.lng) + Number(cur.lng)) / 2;
|
||||
return { from: prev, to: cur, seconds, distance, lat, lng };
|
||||
}
|
||||
|
||||
function shouldShowTrackGapMarker(gap) {
|
||||
if (!gap || !hasValidCoords(gap.lat, gap.lng)) return false;
|
||||
if (isHomePoint(gap) || isHomePoint(gap.from) || isHomePoint(gap.to)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildTrackGapInfoContent(gapGroup) {
|
||||
const t = translations[langCode] || translations.ko;
|
||||
const gaps = Array.isArray(gapGroup?.gaps) && gapGroup.gaps.length ? gapGroup.gaps : [gapGroup];
|
||||
const first = gaps[0];
|
||||
const last = gaps[gaps.length - 1];
|
||||
const maxDistance = gaps.reduce((max, gap) => Math.max(max, Number(gap.distance) || 0), 0);
|
||||
const maxSeconds = gaps.reduce((max, gap) => Math.max(max, Number(gap.seconds) || 0), 0);
|
||||
const samples = gaps.slice(0, 4).map((gap, index) => `
|
||||
<span>${index + 1}. ${formatDateTime(gap.from.time)} - ${formatDateTime(gap.to.time)} · ${formatDistance(gap.distance)} · ${formatDuration(Math.round(gap.seconds))}</span>
|
||||
`).join('');
|
||||
const more = gaps.length > 4 ? `<span>+${formatNumber(gaps.length - 4)}</span>` : '';
|
||||
const countText = langCode === 'ko'
|
||||
? `${t.historyTrackGapCount} ${formatNumber(gaps.length)}${t.historyTrackGapUnit}`
|
||||
: `${formatNumber(gaps.length)} ${t.historyTrackGapCount}`;
|
||||
|
||||
return `
|
||||
<div class="history-info-window history-info-gap">
|
||||
<strong>${gaps.length > 1 ? countText : t.historyTrackGap}</strong>
|
||||
<span>${t.historyTrackGapNotice}</span>
|
||||
<span>${formatDateTime(first.from.time)} - ${formatDateTime(last.to.time)}</span>
|
||||
<span>${formatDistance(maxDistance)} · ${formatDuration(Math.round(maxSeconds))}</span>
|
||||
${samples}
|
||||
${more}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function fitHistoryOrCurrentBounds() {
|
||||
if (!mapState.map) return;
|
||||
if (mapState.historyPoints?.length) {
|
||||
@@ -2228,6 +2360,23 @@ function fitHistoryOrCurrentBounds() {
|
||||
}
|
||||
}
|
||||
|
||||
function fitPointBounds(points, fallbackPoint = null, maxZoom = 15) {
|
||||
if (!mapState.map) return;
|
||||
const coords = (Array.isArray(points) ? points : [])
|
||||
.filter(point => hasValidCoords(point.lat, point.lng))
|
||||
.map(point => new naver.maps.LatLng(point.lat, point.lng));
|
||||
if (coords.length >= 2) {
|
||||
mapState.map.fitBounds(coords, { top: 60, right: 48, bottom: 140, left: 48, maxZoom });
|
||||
return;
|
||||
}
|
||||
const point = coords[0] || (fallbackPoint && hasValidCoords(fallbackPoint.lat, fallbackPoint.lng)
|
||||
? new naver.maps.LatLng(fallbackPoint.lat, fallbackPoint.lng)
|
||||
: null);
|
||||
if (!point) return;
|
||||
mapState.map.panTo(point);
|
||||
mapState.map.setZoom(Math.max(mapState.map.getZoom(), Math.min(mapState.map.getZoom() + 2, maxZoom)));
|
||||
}
|
||||
|
||||
function stopHistoryPlayback(restoreCurrent = true) {
|
||||
if (mapState.playbackTimer) {
|
||||
clearInterval(mapState.playbackTimer);
|
||||
@@ -2338,16 +2487,25 @@ function clearTrackLines() {
|
||||
|
||||
function drawSpeedTrack(points) {
|
||||
clearTrackLines();
|
||||
if (!points?.length || points.length < 2) return;
|
||||
if (mapState.smallMarkers?.length) {
|
||||
mapState.smallMarkers.forEach(marker => marker.setMap(null));
|
||||
mapState.smallMarkers = [];
|
||||
}
|
||||
if (!points?.length || points.length < 2) {
|
||||
mapState.trackGaps = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const segments = [];
|
||||
const trackGaps = [];
|
||||
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) {
|
||||
const gap = buildTrackGap(prev, cur);
|
||||
if (gap) {
|
||||
if (segment.length >= 2) segments.push(segment);
|
||||
if (shouldShowTrackGapMarker(gap)) trackGaps.push(gap);
|
||||
segment = [cur];
|
||||
continue;
|
||||
}
|
||||
@@ -2369,6 +2527,79 @@ function drawSpeedTrack(points) {
|
||||
mapState.trackLines.push(line);
|
||||
if (!mapState.trackLine) mapState.trackLine = line;
|
||||
});
|
||||
|
||||
mapState.trackGaps = trackGaps;
|
||||
renderTrackGapMarkers(mapState.trackGaps);
|
||||
}
|
||||
|
||||
function trackGapClusterRadiusForZoom(zoom) {
|
||||
if (zoom >= 16) return 35;
|
||||
if (zoom >= 14) return 90;
|
||||
if (zoom >= 12) return 250;
|
||||
if (zoom >= 10) return 900;
|
||||
return 2500;
|
||||
}
|
||||
|
||||
function buildTrackGapGroups(gaps) {
|
||||
const radius = trackGapClusterRadiusForZoom(mapState.map?.getZoom?.() || 12);
|
||||
const groups = [];
|
||||
gaps.forEach(gap => {
|
||||
if (!hasValidCoords(gap.lat, gap.lng)) return;
|
||||
const group = groups.find(item => distanceMeters(item, gap) <= radius);
|
||||
if (group) {
|
||||
const oldCount = group.gaps.length;
|
||||
const nextCount = oldCount + 1;
|
||||
group.lat = ((group.lat * oldCount) + Number(gap.lat)) / nextCount;
|
||||
group.lng = ((group.lng * oldCount) + Number(gap.lng)) / nextCount;
|
||||
group.gaps.push(gap);
|
||||
} else {
|
||||
groups.push({ lat: Number(gap.lat), lng: Number(gap.lng), gaps: [gap] });
|
||||
}
|
||||
});
|
||||
return groups;
|
||||
}
|
||||
|
||||
function renderTrackGapMarkers(gaps) {
|
||||
if (mapState.smallMarkers?.length) {
|
||||
mapState.smallMarkers.forEach(marker => marker.setMap(null));
|
||||
mapState.smallMarkers = [];
|
||||
}
|
||||
if (!mapState.map || !Array.isArray(gaps) || !gaps.length) return;
|
||||
const zoom = mapState.map.getZoom?.() || 12;
|
||||
if (zoom < TRACK_GAP_MARKER_MIN_ZOOM) return;
|
||||
const t = translations[langCode] || translations.ko;
|
||||
|
||||
buildTrackGapGroups(gaps).forEach(group => {
|
||||
const count = group.gaps.length;
|
||||
if (count === 1 && zoom < TRACK_GAP_SINGLE_MIN_ZOOM) return;
|
||||
const countLabel = langCode === 'ko'
|
||||
? `${formatNumber(count)}${t.historyTrackGapUnit}`
|
||||
: formatNumber(count);
|
||||
const marker = new naver.maps.Marker({
|
||||
position: new naver.maps.LatLng(group.lat, group.lng),
|
||||
map: mapState.mapLayerState.history ? mapState.map : null,
|
||||
zIndex: count > 1 ? 72 : 70,
|
||||
icon: {
|
||||
content: `<div class="history-marker history-marker-stay">${count > 1 ? countLabel : t.historyTrackGap}</div>`,
|
||||
anchor: new naver.maps.Point(count > 1 ? 28 : 34, 14)
|
||||
}
|
||||
});
|
||||
marker.infoWindow = new naver.maps.InfoWindow({
|
||||
content: buildTrackGapInfoContent(group)
|
||||
});
|
||||
naver.maps.Event.addListener(marker, "click", () => {
|
||||
if (count > 1) {
|
||||
fitPointBounds(group.gaps.flatMap(gap => [gap.from, gap.to]), group, 15);
|
||||
return;
|
||||
}
|
||||
if (mapState.openInfoWindow && mapState.openInfoWindow !== marker.infoWindow) {
|
||||
mapState.openInfoWindow.close();
|
||||
}
|
||||
marker.infoWindow.open(mapState.map, marker);
|
||||
mapState.openInfoWindow = marker.infoWindow;
|
||||
});
|
||||
mapState.smallMarkers.push(marker);
|
||||
});
|
||||
}
|
||||
|
||||
function updateTimelinePanel() {
|
||||
@@ -2651,6 +2882,7 @@ function resetHistoryLayer() {
|
||||
}
|
||||
mapState.rawHistoryPoints = [];
|
||||
mapState.historyPoints = [];
|
||||
mapState.trackGaps = [];
|
||||
mapState.historyRawCount = null;
|
||||
mapState.historyRenderCount = null;
|
||||
mapState.historyStats = null;
|
||||
@@ -2743,8 +2975,7 @@ function renderStayMarkers() {
|
||||
marker.stayPoints = group.stays;
|
||||
if (isCluster) {
|
||||
naver.maps.Event.addListener(marker, "click", () => {
|
||||
mapState.map.panTo(marker.getPosition());
|
||||
mapState.map.setZoom(Math.max(mapState.map.getZoom() + 2, STAY_CLUSTER_MIN_ZOOM));
|
||||
fitPointBounds(group.stays, group, STAY_CLUSTER_MIN_ZOOM);
|
||||
});
|
||||
} else {
|
||||
const stay = group.stays[0];
|
||||
@@ -2783,10 +3014,6 @@ function renderHistoryPoints(rawPoints) {
|
||||
renderHistorySummary();
|
||||
updateTimelinePanel();
|
||||
|
||||
if (mapState.smallMarkers?.length) {
|
||||
mapState.smallMarkers.forEach(marker => marker.setMap(null));
|
||||
mapState.smallMarkers = [];
|
||||
}
|
||||
if (mapState.historyStartMarker) {
|
||||
mapState.historyStartMarker.setMap(null);
|
||||
mapState.historyStartMarker = null;
|
||||
|
||||
Reference in new issue
Block a user