document.addEventListener('DOMContentLoaded', () => { const map = L.map('map').setView([38.9175, 139.8353], 16); const markers = []; L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); // 現在地表示 if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(position => { const lat = position.coords.latitude; const lng = position.coords.longitude; L.marker([lat, lng], { icon: L.icon({ iconUrl: 'images/current-location-icon.png', iconSize: [24, 24], iconAnchor: [12, 12] }) }).addTo(map).bindPopup('現在地'); }); } const dayMap = ['日', '月', '火', '水', '木', '金', '土']; const today = new Date(); const todayStr = dayMap[today.getDay()]; function isClosedToday(desc1) { if (!desc1) return false; const closed = desc1.replace('定休日:', '').replace(/:/g, '').split(/[、,/・\/\s]/).filter(Boolean); for (let entry of closed) { entry = entry.trim(); if (entry.includes('不定休')) return true; if (entry.includes(todayStr) && !entry.match(/第\d/)) return true; const nthMatch = entry.match(/第(\d)(.)曜日/); if (nthMatch) { const nth = parseInt(nthMatch[1], 10); const day = nthMatch[2]; if (day !== todayStr) continue; let count = 0; for (let d = 1; d <= 31; d++) { const date = new Date(today.getFullYear(), today.getMonth(), d); if (date.getMonth() !== today.getMonth()) break; if (date.getDay() === dayMap.indexOf(day)) count++; if (date.getDate() === today.getDate() && count === nth) return true; } } } return false; } function isOpenNow(desc2) { if (!desc2) return false; const now = new Date(); const day = now.getDay(); const time = now.getHours() * 60 + now.getMinutes(); const segments = desc2.split(/\/|\\n|\\r\\n/); for (let segment of segments) { const match = segment.match(/(月|火|水|木|金|土|日)(?:~(月|火|水|木|金|土|日))?[::]?\s*(\d{1,2})[::](\d{2})\s*〜\s*(\d{1,2})[::](\d{2})/); if (match) { const [_, startDay, endDay, sh, sm, eh, em] = match; const startMin = parseInt(sh) * 60 + parseInt(sm); let endMin = parseInt(eh) * 60 + parseInt(em); const dayIndex = { '日': 0, '月': 1, '火': 2, '水': 3, '木': 4, '金': 5, '土': 6 }; let validDay = false; if (!endDay) { validDay = day === dayIndex[startDay]; } else { const startIdx = dayIndex[startDay]; const endIdx = dayIndex[endDay]; validDay = startIdx <= endIdx ? day >= startIdx && day <= endIdx : day >= startIdx || day <= endIdx; } if (!validDay) continue; if (endMin <= startMin) endMin += 1440; const currentTime = time < startMin ? time + 1440 : time; if (currentTime >= startMin && currentTime <= endMin) return true; } const simple = segment.match(/(\d{1,2})[::](\d{2})\s*〜\s*(\d{1,2})[::](\d{2})/); if (simple) { const startMin = parseInt(simple[1]) * 60 + parseInt(simple[2]); let endMin = parseInt(simple[3]) * 60 + parseInt(simple[4]); if (endMin <= startMin) endMin += 1440; const currentTime = time < startMin ? time + 1440 : time; if (currentTime >= startMin && currentTime <= endMin) return true; } } return false; } fetch('snack.csv') .then(response => response.text()) .then(text => { const rows = text.split('\n').filter(row => row.trim()); const headers = rows[0].split(',').map(h => h.trim()); for (let i = 1; i < rows.length; i++) { const values = rows[i].split(',').map(v => v.replace(/^"|"$/g, '').trim()); const data = {}; headers.forEach((key, index) => data[key] = values[index] || ''); const lat = parseFloat(data.latitude); const lng = parseFloat(data.longitude); if (isNaN(lat) || isNaN(lng)) continue; const isOpen = !isClosedToday(data.description1) && isOpenNow(data.description2); const iconPath = isOpen ? (data.icon || 'images/snack-icon.png') : 'images/favicon-door-32x32.png'; const customIcon = L.icon({ iconUrl: iconPath, iconSize: [32, 32], iconAnchor: [16, 32], popupAnchor: [0, -32] }); const description = [data.description1, data.description2, data.description3, data.mama] .filter(Boolean) .map(d => d.trim()) .join('<br>'); const image1 = data.img1 && data.img1.trim() !== '' ? data.img1 : 'images/snacktitle.png'; const image2 = data.img2 && data.img2.trim() !== '' ? data.img2 : 'images/snacktitle.png'; const popupContent = <div class="popup-content"> <h3>${data.name.replace(/"$/, '')}</h3> <p>${description}</p> <img src="${image1}" width="106" height="73" style="cursor: pointer;" onclick="openModal('${image1}')" /> <img src="${image2}" width="106" height="73" style="cursor: pointer;" onclick="openModal('${image2}')" /> <button onclick="addToFavorites('${data.name}')">行きたいお店リストに追加</button> </div>; const marker = L.marker([lat, lng], { icon: customIcon }).addTo(map).bindPopup(popupContent); marker.isOpen = isOpen; markers.push(marker); } }) .catch(error => { console.error('CSV読み込みエラー:', error); }); const toggleBtn = document.createElement('button'); toggleBtn.textContent = '空いてる店だけ表示'; toggleBtn.style.position = 'absolute'; toggleBtn.style.top = '10px'; toggleBtn.style.right = '10px'; toggleBtn.style.zIndex = 1000; toggleBtn.style.padding = '6px 10px'; toggleBtn.style.background = 'white'; toggleBtn.style.border = '1px solid #ccc'; toggleBtn.style.cursor = 'pointer'; document.body.appendChild(toggleBtn); let showingOnlyOpen = false; toggleBtn.addEventListener('click', () => { showingOnlyOpen = !showingOnlyOpen; toggleBtn.textContent = showingOnlyOpen ? '全ての店を表示' : '空いてる店だけ表示'; markers.forEach(marker => { if (showingOnlyOpen) { if (marker.isOpen) marker.addTo(map); else map.removeLayer(marker); } else { marker.addTo(map); } }); }); }); window.addToFavorites = function(name) { const favorites = JSON.parse(localStorage.getItem('favorites') || '[]'); favorites.push({ name, addedAt: new Date().toISOString() }); localStorage.setItem('favorites', JSON.stringify(favorites)); alert(${name}を行きたいお店リストに追加しました!); }; window.openModal = function(src) { const modal = document.getElementById("modal"); const modalImg = document.getElementById("modal-img"); modal.style.display = "block"; modalImg.src = src; }; window.closeModal = function() { document.getElementById("modal").style.display = "none"; }; let ws; let userName = prompt("ニックネームを入力してください") || "ゲスト"; function getDistanceMeters(lat1, lng1, lat2, lng2) { const R = 6371000; // 地球の半径 (m) const toRad = x => x * Math.PI / 180; const dLat = toRad(lat2 - lat1); const dLng = toRad(lng2 - lng1); const a = Math.sin(dLat/2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLng/2) ** 2; const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } const storeData = []; // スナックの位置情報 // ★ スナック情報を保存(fetchの中の処理に追記) headers.forEach((key, index) => data[key] = values[index] || ''); storeData.push({ name: data.name, lat: parseFloat(data.latitude), lng: parseFloat(data.longitude) }); ... // ★ WebSocket 開始(DOM loaded の後半) if ("WebSocket" in window) { ws = new WebSocket("ws://localhost:8080/ws"); ws.onopen = () => { console.log("WebSocket connected"); // 位置を定期送信 setInterval(() => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition(pos => { const lat = pos.coords.latitude; const lng = pos.coords.longitude; ws.send(JSON.stringify({ name: userName, lat, lng })); }); } }, 5000); // 5秒ごとに送信 }; ws.onmessage = (event) => { const data = JSON.parse(event.data); const { name, lat, lng } = data; // 各店舗に対して5m以内かチェック storeData.forEach(store => { const distance = getDistanceMeters(lat, lng, store.lat, store.lng); if (distance <= 5) { L.popup() .setLatLng([store.lat, store.lng]) .setContent(`<b>${name}さんが利用中です</b>`) .openOn(map); } }); }; ws.onclose = () => { console.log("WebSocket closed"); }; }