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);
}
});
});
});