diff --git a/map.js b/map.js new file mode 100644 index 0000000..c00b830 --- /dev/null +++ b/map.js @@ -0,0 +1,249 @@ +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('
'); + + 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 = + ; + + 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(`${name}さんが利用中です`) + .openOn(map); + } + }); + }; + + ws.onclose = () => { + console.log("WebSocket closed"); + }; +}