Newer
Older
naoki-shosou / shonai_tiiki_map_tansaku.js
document.addEventListener("DOMContentLoaded", () => {
  var mymap = L.map("locationmap").setView([38.855235,139.910744], 16);

  L.tileLayer('https://{s}.tile.osm.org/{z}/{x}/{y}.png', {
      attribution: '© OpenStreetMap contributors'
  }).addTo(mymap);

  var playerIcon = L.icon({
      iconUrl: 'images/yuusya.png',
      iconSize: [40, 40],
      iconAnchor: [20, 40],
      popupAnchor: [0, -40]
  });

  var locmarker = L.marker(mymap.getCenter(), { icon: playerIcon }).addTo(mymap);
  locmarker.bindPopup("STARTを押してね").openPopup();

  let circleRadius = 500;
  let goalReachRadius = 10;

  var radiusCircle = L.circle(mymap.getCenter(), {
      radius: circleRadius,
      color: 'blue',
      fillColor: 'blue',
      fillOpacity: 0.1
  }).addTo(mymap);

  var watchId = null;
  const urlParams = new URLSearchParams(window.location.search);
  const cheatMode = urlParams.get("cheat") === "true";

  let stores = [];
  let visitedStores = [];
  let storeMarkers = [];

  const canvas = document.getElementById("progressImage");
  const ctx = canvas.getContext("2d");
  const totalImages = 7;
  const images = [];

  for (let i = 0; i < totalImages; i++) {
      images[i] = new Image();
      images[i].src = `images/irasuto${i + 1}.png`;
  }

  function saveProgress() {
      localStorage.setItem("visitedStores", JSON.stringify(visitedStores));
  }

  function loadProgress() {
      const saved = localStorage.getItem("visitedStores");
      if (saved) {
          try {
              const parsed = JSON.parse(saved);
              if (Array.isArray(parsed)) visitedStores = parsed;
          } catch (e) { console.error("保存データの読み込みに失敗:", e); }
      }
  }

  function updateProgressImage() {
      if (!canvas) return;

      let storeTotal = stores.length > 0 ? stores.length : 1;
      let progress = visitedStores.length / storeTotal;

      let index = Math.floor(progress * totalImages);
      if (index >= totalImages) index = totalImages - 1;

      ctx.clearRect(0, 0, canvas.width, canvas.height);

      if (images[index].complete) {
          ctx.drawImage(images[index], 0, 0, canvas.width, canvas.height);
      } else {
          images[index].onload = () => {
              ctx.drawImage(images[index], 0, 0, canvas.width, canvas.height);
          };
      }
  }

  function renderBookmarkList() {
      const container = document.getElementById("bookmarkList");
      container.innerHTML = "";
      if (visitedStores.length === 0) {
          container.innerHTML = "<p>まだ訪問した店舗はありません。</p>";
          return;
      }
      visitedStores.forEach((store, idx) => {
          const div = document.createElement("div");
          div.className = "bookmark-item";

          div.innerHTML = `
              <strong>${store.name}</strong> (訪問日時: ${store.time})<br>
              <em>おすすめ: ${store.osusume || "〇〇"}</em><br>
              <label>メモ:</label><br>
              <textarea class="memo-textarea" data-index="${idx}">${store.memo || ""}</textarea><br>
              <label>画像アップロード:</label><br>
              <input type="file" accept="image/*" class="image-upload" data-index="${idx}"><br>
              <img class="preview-image" src="${store.image || ''}" style="max-width: 90%; max-height: 150px; display: ${store.image ? 'block' : 'none'};">
              <button class="image-delete" data-index="${idx}">画像を削除</button>
          `;
          container.appendChild(div);
      });

      const memos = container.querySelectorAll(".memo-textarea");
      memos.forEach(textarea => {
          textarea.addEventListener("input", (e) => {
              const index = e.target.dataset.index;
              visitedStores[index].memo = e.target.value;
              saveProgress();
          });
      });

      const uploads = container.querySelectorAll(".image-upload");
      uploads.forEach(input => {
          input.addEventListener("change", (e) => {
              const index = e.target.dataset.index;
              const file = e.target.files[0];
              if (!file) return;

              if (file.size > 5 * 1024 * 1024) {
                  alert("ファイルサイズは5MB以下にしてください。");
                  e.target.value = "";
                  return;
              }

              const reader = new FileReader();
              reader.onload = function(event) {
                  visitedStores[index].image = event.target.result;
                  saveProgress();

                  const img = e.target.parentElement.querySelector(".preview-image");
                  if (img) {
                      img.src = event.target.result;
                      img.style.display = "block";
                  }
              };
              reader.readAsDataURL(file);
          });
      });

      const deletes = container.querySelectorAll(".image-delete");
      deletes.forEach(button => {
          button.addEventListener("click", (e) => {
              const index = e.target.dataset.index;
              visitedStores[index].image = "";
              saveProgress();
              const img = e.target.parentElement.querySelector(".preview-image");
              if (img) {
                  img.src = "";
                  img.style.display = "none";
              }
              const inputFile = e.target.parentElement.querySelector(".image-upload");
              if (inputFile) {
                  inputFile.value = "";
              }
          });
      });
  }

  function loadCSV() {
      fetch('shonai_tiiki_map.csv')
          .then(res => res.ok ? res.text() : Promise.reject("CSV読み込み失敗"))
          .then(csvText => {
              const results = Papa.parse(csvText, { header: true, skipEmptyLines: true });
              stores = results.data.map(item => ({
                  name: item.name,
                  latitude: parseFloat(item.latitude),
                  longitude: parseFloat(item.longitude),
                  osusume: item.osusume || "",
                  osusumeImage: item.osusume_images || "",
                  iconUrl: item.icon || 'store.png',
                  marker: null,
                  redCircle: null
              })).filter(s => !isNaN(s.latitude) && !isNaN(s.longitude));

              visitedStores.forEach(obj => {
                  const s = stores.find(st => st.name === obj.name);
                  if (s) {
                      const customIcon = L.icon({
                          iconUrl: s.iconUrl,
                          iconSize: [40, 40],
                          iconAnchor: [20, 40],
                          popupAnchor: [0, -40]
                      });
                      let popupContent = `
                        訪問日時: ${obj.time}<br>
                        庄内総合高校の子のおすすめは${obj.osusume || "〇〇"}<br>
                        ${s.osusumeImage ? `<img src="${s.osusumeImage}" alt="おすすめ画像" style="max-width:90%; max-height:150px; margin-top:6px;">` : ""}
                      `;
                      let storeMarker = L.marker([s.latitude, s.longitude], { icon: customIcon })
                          .addTo(mymap)
                          .bindPopup(popupContent, {autoClose:false});
                      storeMarkers.push(storeMarker);
                  }
              });

              if (cheatMode) {
                  document.getElementById("title").textContent = "チートモード";
                  mymap.on("click", e => updateLocation(e.latlng));
              }
              updateProgressImage();
              renderBookmarkList();
          })
          .catch(err => alert("CSVの読み込みに失敗しました: " + err));
  }

  function getDistance(lat1, lng1, lat2, lng2) {
      return L.latLng(lat1, lng1).distanceTo(L.latLng(lat2, lng2));
  }

  function getNearestStore(latlng) {
      let nearest = null, minDist = Infinity;
      for (let s of stores) {
          if (visitedStores.some(c => c.name === s.name)) continue;
          let d = getDistance(latlng.lat, latlng.lng, s.latitude, s.longitude);
          if (d < minDist) { minDist = d; nearest = s; }
      }
      if (!nearest) return null;
      return { store: nearest, distance: minDist };
  }

  function updateLocation(latlng) {
      mymap.panTo(latlng);
      locmarker.setLatLng(latlng);
      radiusCircle.setLatLng(latlng);

      stores.forEach(s => {
          const d = getDistance(latlng.lat, latlng.lng, s.latitude, s.longitude);

          if (visitedStores.some(c => c.name === s.name)) {
              if (s.marker && mymap.hasLayer(s.marker)) mymap.removeLayer(s.marker);
              if (s.redCircle && mymap.hasLayer(s.redCircle)) mymap.removeLayer(s.redCircle);
              return;
          }

          if (d <= circleRadius) {
              if (!s.marker) {
                  s.marker = L.marker([s.latitude, s.longitude]).addTo(mymap).bindPopup(s.name);
              } else if (!mymap.hasLayer(s.marker)) {
                  s.marker.addTo(mymap);
              }

              if (!s.redCircle) {
                  s.redCircle = L.circle([s.latitude, s.longitude], {
                      radius: goalReachRadius,
                      color: 'red',
                      fillColor: 'red',
                      fillOpacity: 0.3
                  }).addTo(mymap);
              } else {
                  s.redCircle.setRadius(goalReachRadius);
                  if (!mymap.hasLayer(s.redCircle)) {
                      s.redCircle.addTo(mymap);
                  }
              }
          } else {
              if (s.marker && mymap.hasLayer(s.marker)) mymap.removeLayer(s.marker);
              if (s.redCircle && mymap.hasLayer(s.redCircle)) mymap.removeLayer(s.redCircle);
          }
      });

      let nearestData = getNearestStore(latlng);
      if (!nearestData) {
          locmarker.setPopupContent("すべての店舗を訪問しました!").openPopup();
          return;
      }

      const { store, distance } = nearestData;

      if (distance <= goalReachRadius) {
          const now = new Date();
          const options = {
              year: 'numeric', month: 'long', day: 'numeric',
              weekday: 'long', hour: '2-digit', minute: '2-digit'
          };
          const visitTime = now.toLocaleString('ja-JP', options);
          const recommendation = store.osusume.trim() !== "" ? store.osusume : "〇〇";

          locmarker.setPopupContent(
              `到着!(${store.name})<br>訪問日時: ${visitTime}<br>庄内総合高校の子のおすすめは${recommendation}`
          ).openPopup();

          visitedStores.push({ name: store.name, time: visitTime, osusume: recommendation });
          saveProgress();

          if (store.marker && mymap.hasLayer(store.marker)) mymap.removeLayer(store.marker);
          if (store.redCircle && mymap.hasLayer(store.redCircle)) mymap.removeLayer(store.redCircle);

          const customIcon = L.icon({
              iconUrl: store.iconUrl,
              iconSize: [40, 40],
              iconAnchor: [20, 40],
              popupAnchor: [0, -40]
          });

          let newMarker = L.marker([store.latitude, store.longitude], { icon: customIcon })
              .addTo(mymap)
              .bindPopup(`訪問日時: ${visitTime}<br>庄内総合高校の子のおすすめは${recommendation}`);
          storeMarkers.push(newMarker);

          updateProgressImage();
          renderBookmarkList();

      } else if (distance <= circleRadius) {
          locmarker.setPopupContent(`もう少しで${store.name}!`).openPopup();
      } else {
          locmarker.setPopupContent(`ここは lat=${latlng.lat.toFixed(5)}, lng=${latlng.lng.toFixed(5)} です.`).openPopup();
      }
  }

  function resetProgress() {
      if (confirm("進行状況をリセットして最初からやり直しますか?")) {
          localStorage.removeItem("visitedStores");
          visitedStores = [];
          storeMarkers.forEach(m => mymap.removeLayer(m));
          storeMarkers = [];
          alert("進行状況をリセットしました。");
          updateProgressImage();
          renderBookmarkList();
          location.reload();
      }
  }

  loadProgress();
  loadCSV();

  if (!cheatMode) {
      document.getElementById("start").addEventListener("click", () => {
          watchId = navigator.geolocation.watchPosition(
              pos => updateLocation(L.latLng(pos.coords.latitude, pos.coords.longitude)),
              err => console.log(err),
              { maximumAge: 0, timeout: 3000, enableHighAccuracy: true }
          );
      });
  }

  document.getElementById("stop").addEventListener("click", () => {
      if (watchId != null) navigator.geolocation.clearWatch(watchId);
      watchId = null;
  });
  document.getElementById("reset").addEventListener("click", resetProgress);

  const radiusSelect = document.getElementById("radiusSelect");
  const goalSelect = document.getElementById("goalSelect");
  const RADIUS_KEY = "selectedRadius";
  const GOAL_KEY = "selectedGoal";

  let savedRadius = localStorage.getItem(RADIUS_KEY);
  if (savedRadius) {
      circleRadius = parseInt(savedRadius, 10);
      radiusCircle.setRadius(circleRadius);
      radiusSelect.value = savedRadius;
  }

  let savedGoal = localStorage.getItem(GOAL_KEY);
  if (savedGoal) {
      goalReachRadius = parseInt(savedGoal, 10);
      goalSelect.value = savedGoal;
  }

  radiusSelect.addEventListener("change", (e) => {
      circleRadius = parseInt(e.target.value, 10);
      radiusCircle.setRadius(circleRadius);
      localStorage.setItem(RADIUS_KEY, circleRadius);
  });

  goalSelect.addEventListener("change", (e) => {
      goalReachRadius = parseInt(e.target.value, 10);
      localStorage.setItem(GOAL_KEY, goalReachRadius);

      stores.forEach(s => {
          if (s.redCircle) {
              s.redCircle.setRadius(goalReachRadius);
          }
      });
  });
});