diff --git a/map4.js b/map4.js index 07339e9..d637f74 100644 --- a/map4.js +++ b/map4.js @@ -25,7 +25,6 @@ let selfMarker = null; let otherMarkers = []; let latestByDevice = {}; -let allMembers = []; let lastLat = null; let lastLng = null; @@ -36,11 +35,11 @@ let stationMarkers = []; -// 自分の状態 +// =================================== +// 自分の状態(移動中 / 遅れます / 寄り道中 / 到着) +// =================================== let myStatus = localStorage.getItem("myStatus") || "移動中"; -// =================================== -// ステータス(移動中 / 遅れます / 寄り道中 / 到着) -// =================================== + async function updateStatus(newStatus) { myStatus = newStatus; localStorage.setItem("myStatus", newStatus); @@ -51,57 +50,45 @@ .eq("group_name", currentGroup) .eq("device_id", deviceId); - showOtherUsers(); // メンバー更新 + showOtherUsers(); } function setupStatusButtons() { document.querySelectorAll(".stBtn").forEach((btn) => { - btn.addEventListener("click", () => updateStatus(btn.dataset.status)); + btn.addEventListener("click", () => + updateStatus(btn.dataset.status) + ); }); } // =================================== -// エレベーター情報(CSV読み込み) +// エレベーター情報(CSV 読み込み) // =================================== - let elevatorInfo = new Map(); -fetch("./elevators.csv") // ← 同じフォルダなのでこれ - .then(res => res.text()) - .then(text => { - const lines = text.split("\n").slice(1); // 1行目ヘッダ +// elevators.csv は map4.js と同じフォルダ +fetch("./elevators.csv") + .then((res) => res.text()) + .then((text) => { + const lines = text.split("\n").slice(1); // 1行目ヘッダ除外 - lines.forEach(line => { + lines.forEach((line) => { if (!line.trim()) return; const [station, hasEV] = line.split(","); elevatorInfo.set(station.trim(), hasEV.trim() === "1"); }); - console.log("エレベーターデータ読込完了:", elevatorInfo); + console.log("EVデータ読み込み成功:", elevatorInfo); }); +// エレベーターアイコン const elevatorIcon = L.icon({ - iconUrl: "./elevator.png", // ← 同じ場所にある画像を使う - iconSize: [22, 22] + iconUrl: "./elevator.png", + iconSize: [22, 22], }); - -// 駅のピン表示(既存) -const mk = L.marker([st.lat, st.lon]).addTo(map); -mk.bindPopup(name + "駅"); -stationMarkers.push(mk); - -// ★ エレベーターがある駅は EVアイコンを重ねて表示 -if (elevatorInfo.get(name)) { - const ev = L.marker([st.lat, st.lon], { icon: elevatorIcon }) - .addTo(map) - .bindPopup(`${name}:エレベーターあり`); - - stationMarkers.push(ev); -} - // =================================== -// URL パラメータ +// URL パラメータ読み込み // =================================== function loadParams() { const p = new URLSearchParams(location.search); @@ -111,8 +98,9 @@ document.getElementById("groupName").textContent = currentGroup || ""; document.getElementById("userName").textContent = currentUser || ""; } + // =================================== -// 現在位置 +// 現在位置取得 // =================================== function getPosition() { return new Promise((resolve, reject) => { @@ -125,7 +113,7 @@ } // =================================== -// ピンのテンプレ +// 名前ラベル付きピン作成 // =================================== function createLabeledMarker(lat, lng, name, isSelf) { const pinUrl = isSelf @@ -167,13 +155,7 @@ if (selfMarker) map.removeLayer(selfMarker); - selfMarker = createLabeledMarker( - lastLat, - lastLng, - currentUser, - true, - myStatus - ); + selfMarker = createLabeledMarker(lastLat, lastLng, currentUser, true); selfMarker.addTo(map); } @@ -196,6 +178,7 @@ status: myStatus, }); } + // =================================== // 待ち合わせピンアイコン // =================================== @@ -208,7 +191,6 @@ iconAnchor: [12, 41], }); - // =================================== // 地図初期化 // =================================== @@ -219,7 +201,7 @@ maxZoom: 19, }).addTo(map); - // ★ ホストだけが待ち合わせ場所を設定できる + // ホストだけが待ち合わせピンを設置できる map.on("click", async (e) => { if (!isHost) return; @@ -233,13 +215,10 @@ targetLat = newLat; targetLng = newLng; - // 既存の待ち合わせピンを消す if (targetMarker) map.removeLayer(targetMarker); - // 新しい待ち合わせピン表示 targetMarker = L.marker([targetLat, targetLng], { icon: meetIcon }).addTo(map); - // 共有データとして Supabase に保存 await supa.from("shared_target").upsert({ group_name: currentGroup, lat: targetLat, @@ -250,6 +229,7 @@ "待ち合わせ場所を設定しました!"; }); } + // =================================== // 共有待ち合わせ読み込み // =================================== @@ -264,6 +244,7 @@ console.error("shared_target 読み込みエラー:", error); return; } + if (!data) return; targetLat = data.lat; @@ -283,7 +264,7 @@ // 距離計算 // =================================== function distanceMeters(lat1, lng1, lat2, lng2) { - const R = 6371000; // 地球の半径(m) + const R = 6371000; const toRad = (deg) => (deg * Math.PI) / 180; const dLat = toRad(lat2 - lat1); @@ -297,6 +278,7 @@ return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } + // =================================== // 待ち合わせ距離表示 // =================================== @@ -309,6 +291,10 @@ document.getElementById("targetInfo").innerHTML = `待ち合わせ場所まで 約 ${km} km`; } + +// =================================== +// メンバー一覧 +// =================================== function getStatusColor(s) { return { "移動中": "#4caf50", @@ -317,22 +303,12 @@ "到着": "#9c27b0", }[s] || "#ccc"; } -// =================================== -// メンバー一覧 -// =================================== + function showMemberList(latestByDevice) { const list = document.getElementById("memberList"); list.innerHTML = ""; - // ★最新データでメンバー配列を作成 - const members = Object.values(latestByDevice).map(row => ({ - device_id: row.device_id, - user_name: row.user_name, - status: row.status, - updated_at: row.updated_at, - lat: row.lat, - lng: row.lng - })); + const members = Object.values(latestByDevice); members.forEach((member) => { const online = @@ -366,7 +342,7 @@ `; - // メンバーをタップするとそのメンバーの位置へ移動 + // メンバータップでその場所へ移動 li.addEventListener("click", () => { const r = latestByDevice[member.device_id]; if (!r) return; @@ -380,8 +356,6 @@ // 他ユーザーのピン表示 // =================================== async function showOtherUsers() { - - // ---- Supabase から最新位置取得 ---- const { data, error } = await supa .from("locations") .select("*") @@ -393,41 +367,35 @@ return; } - // ---- デバイスごとの最新位置だけ残す ---- + // デバイスごとに最新のみ残す latestByDevice = {}; (data || []).forEach((r) => { if (!latestByDevice[r.device_id]) latestByDevice[r.device_id] = r; }); - // ---- ホストの自動交代(位置情報の後に実行)---- + // ホスト自動交代(位置情報が揃ったあと) await checkHostAuto(); - // ---- メンバー一覧を更新 ---- + // メンバー一覧更新 showMemberList(latestByDevice); - // ---- 既存の他ユーザーピンを消す ---- + // 既存の他ユーザーピン削除 otherMarkers.forEach((m) => map.removeLayer(m)); otherMarkers = []; - // ---- 他ユーザーのピンを描画し直す ---- + // 他ユーザーのピンを再描画 Object.values(latestByDevice).forEach((r) => { if (r.device_id === deviceId) return; // 自分は除外 - const mk = createLabeledMarker( - r.lat, - r.lng, - r.user_name, - false, - r.status - ); - + const mk = createLabeledMarker(r.lat, r.lng, r.user_name, false); mk.addTo(map); otherMarkers.push(mk); }); - // ---- 全員到着判定 ---- + // 全員到着判定 checkAllArrived(); } + // =================================== // 待ち合わせ到着判定 // =================================== @@ -439,7 +407,7 @@ const ok = users.every((u) => { const d = distanceMeters(u.lat, u.lng, targetLat, targetLng); - return d < 120; // 指定距離以内 + return d < 120; // 120m以内を到着とみなす }); if (!ok) return; @@ -459,12 +427,17 @@ await supa.from("shared_target").delete().eq("group_name", currentGroup); } + +// =================================== +// 時刻フォーマット(チャット用) +// =================================== function formatTime(t) { const d = new Date(t); return `${String(d.getHours()).padStart(2, "0")}:${String( d.getMinutes() ).padStart(2, "0")}`; } + // =================================== // チャット読み込み // =================================== @@ -482,7 +455,6 @@ return; } - // ---- 表示 HTML を構築 ---- chatList.innerHTML = (data || []) .map((m) => { const isMe = m.user_name === currentUser; @@ -500,9 +472,9 @@ }) .join(""); - // ---- 下までスクロール ---- chatList.scrollTop = chatList.scrollHeight; } + // =================================== // チャット送信 // =================================== @@ -520,14 +492,7 @@ input.value = ""; loadMessages(); } -const chatSend = document.getElementById("chatSend"); -const chatInput = document.getElementById("chatInput"); -if (chatSend && chatInput) { - chatSend.onclick = sendMessage; - chatInput.addEventListener("keydown", (e) => { - if (e.key === "Enter") sendMessage(); - }); -} + // =================================== // ホスト自動交代 // =================================== @@ -542,14 +507,16 @@ currentHostName = group.host_name; - // 6秒以内に更新があればオンライン扱い + // 6秒以内に更新があればオンライン const onlineUsers = Object.values(latestByDevice).filter( (u) => (Date.now() - new Date(u.updated_at)) / 1000 < 6 ); - const hostOnline = onlineUsers.some((u) => u.user_name === currentHostName); + const hostOnline = onlineUsers.some( + (u) => u.user_name === currentHostName + ); - // ホストがオフライン → 最新の人がホストに昇格 + // ホストがオフライン → 最新更新者を新ホストに if (!hostOnline && onlineUsers.length > 0) { const newHost = onlineUsers.sort( (a, b) => new Date(b.updated_at) - new Date(a.updated_at) @@ -563,11 +530,9 @@ currentHostName = newHost.user_name; isHost = currentUser === currentHostName; } else { - // ホストは通常通り isHost = currentUser === currentHostName; } - // UI 更新 const hostLabel = document.getElementById("hostStatus"); if (hostLabel) { hostLabel.textContent = isHost ? "あなたはホストです" : "一般メンバーです"; @@ -578,6 +543,7 @@ disbandBtn.style.display = isHost ? "inline-block" : "none"; } } + // =================================== // グループ解散 // =================================== @@ -594,6 +560,7 @@ location.href = `archive.html?group=${encodeURIComponent(currentGroup)}`; } + // =================================== // グループ状態監視 // =================================== @@ -631,18 +598,19 @@ return (await res.json()).elements || []; } + // =================================== -// 最寄り駅のピン +// 最寄り駅のピン(EV対応) // =================================== async function loadStations() { if (!map || lastLat === null) return; const box = document.getElementById("nearestBox"); - // ---- 駅データ取得 ---- + // 駅データ取得 const stations = await fetchStationsAround(lastLat, lastLng); - // 既存の駅ピンを消す + // 既存ピン削除 stationMarkers.forEach((m) => map.removeLayer(m)); stationMarkers = []; @@ -654,73 +622,79 @@ let nearest = null; let nearestDist = Infinity; - // ---- 各駅を地図へ追加 ---- stations.forEach((st) => { const name = st.tags?.name || "駅名不明"; const d = distanceMeters(lastLat, lastLng, st.lat, st.lon); - // 最寄駅判定 + // 最寄駅更新 if (d < nearestDist) { nearest = { name, lat: st.lat, lng: st.lon }; nearestDist = d; } - // 駅のピン表示 + // 通常の駅ピン const mk = L.marker([st.lat, st.lon]).addTo(map); mk.bindPopup(name + "駅"); stationMarkers.push(mk); + + // 🔵 EVピン(CSV と名前一致する駅のみ) + if (elevatorInfo.get(name)) { + const ev = L.marker([st.lat, st.lon], { icon: elevatorIcon }) + .addTo(map) + .bindPopup(`${name}:エレベーターあり`); + stationMarkers.push(ev); + } }); - // ---- 最寄り駅の表示 ---- + // 最寄駅表示 box.textContent = `最寄り駅:${nearest.name}(約 ${(nearestDist / 1000).toFixed(2)} km)`; } + // =================================== // メイン処理 // =================================== async function main() { - loadParams(); // URLパラメータ (group,user) - setupStatusButtons(); // ステータスボタン設定 + loadParams(); + setupStatusButtons(); - // ---- 初期値は皇居 ---- + // デフォルト位置は皇居 let lat = 35.681236, lng = 139.767125; - // ---- 現在位置を取得 ---- + // 現在位置 try { const pos = await getPosition(); lat = pos.latitude; lng = pos.longitude; } catch { - // 位置取得失敗 → 初期値のまま + // 位置取得失敗 → 皇居のまま } lastLat = lat; lastLng = lng; - // ---- 地図初期化 ---- + // 地図初期化 initMap(lat, lng); - // ---- 自分のピン描画 & Supabase へ保存 ---- + // 自分のピン・位置保存 renderSelfMarker(); await saveMyLocation(lat, lng); - // ---- 他のユーザーの位置表示 ---- + // 他ユーザー表示 await showOtherUsers(); - // ---- 共有された待ち合わせ場所の取得 ---- + // 共有待ち合わせ読み込み loadSharedTarget(); - // ---- チャット読み込み ---- + // チャット読み込み loadMessages(); - // ---- 駅表示 ---- + // 駅表示 loadStations(); - // --------------------------------------- - // ▼ ▼ ▼ 定期的な更新処理 ▼ ▼ ▼ - // --------------------------------------- - - // GPS更新 & 自分の位置保存 + // =========================== + // 定期更新タイマー + // =========================== setInterval(async () => { try { const pos = await getPosition(); @@ -733,36 +707,19 @@ } catch {} }, 2500); - // 他ユーザーの位置更新 setInterval(showOtherUsers, 1500); - - // 待ち合わせアイコンの再読込 setInterval(loadSharedTarget, 9000); - - // チャット更新 setInterval(loadMessages, 2500); - - // ホスト自動交代 setInterval(checkHostAuto, 4000); - - // グループ解散監視 setInterval(checkGroupActive, 4000); - - // 駅の再検索 setInterval(loadStations, 15000); - // --------------------------------------- - // ▼ ▼ ▼ ボタンの設定 ▼ ▼ ▼ - // --------------------------------------- + // ボタン const exitBtn = document.getElementById("exitBtn"); - if (exitBtn) { - exitBtn.onclick = () => (location.href = "index.html"); - } + if (exitBtn) exitBtn.onclick = () => (location.href = "index.html"); const disbandBtn = document.getElementById("disbandBtn"); - if (disbandBtn) { - disbandBtn.onclick = disbandGroup; - } + if (disbandBtn) disbandBtn.onclick = disbandGroup; const chatSend = document.getElementById("chatSend"); const chatInput = document.getElementById("chatInput"); @@ -773,9 +730,10 @@ }); } - // ---- 地図のサイズを調整(スマホ対応) ---- + // マップサイズ調整 setTimeout(() => map.invalidateSize(), 500); } + // =================================== // DOMContentLoaded // =================================== @@ -784,7 +742,7 @@ const g = p.get("group"); const u = p.get("user"); - // ---- グループ情報を確認 ---- + // グループ確認 const { data, error } = await supa .from("groups") .select("*") @@ -809,23 +767,20 @@ return; } - // ---- グループ情報の反映 ---- + // 反映 currentGroup = g; currentUser = u; currentHostName = data.host_name; - isHost = currentHostName === currentUser; + isHost = currentUser === currentHostName; - // ---- ホスト表示の更新 ---- const hostLabel = document.getElementById("hostStatus"); if (hostLabel) { hostLabel.textContent = isHost ? "あなたはホストです" : "一般メンバーです"; } const disbandBtn = document.getElementById("disbandBtn"); - if (disbandBtn) { - disbandBtn.style.display = isHost ? "inline-block" : "none"; - } + if (disbandBtn) disbandBtn.style.display = isHost ? "inline-block" : "none"; - // ---- すべて準備完了 → アプリ起動 ---- + // アプリ起動 main(); });