Newer
Older
jikken / map.js
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");
  };
}