diff --git a/map4.js b/map4.js index 3dd96c7..14f9ca9 100644 --- a/map4.js +++ b/map4.js @@ -1,15 +1,14 @@ // =================================== // Supabase 設定 // =================================== -const SUPABASE_URL = - "https://ogtlmtnjkpsxsqzqlacj.supabase.co"; +const SUPABASE_URL = "https://ogtlmtnjkpsxsqzqlacj.supabase.co"; const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Im9ndGxtdG5qa3BzeHNxenFsYWNqIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjMyOTU3NjUsImV4cCI6MjA3ODg3MTc2NX0.JnCE7oUQwrSgGqiu-QRbwnaLBZrO8JX1_RUb37VIMFI"; const supa = window.supabase.createClient(SUPABASE_URL, SUPABASE_KEY); // =================================== -// デバイスID +// グローバル変数 // =================================== let deviceId = localStorage.getItem("deviceId"); if (!deviceId) { @@ -17,19 +16,19 @@ localStorage.setItem("deviceId", deviceId); } -// =================================== -let allMembers = []; let currentGroup = ""; let currentUser = ""; +let isHost = false; + let map; let selfMarker = null; let otherMarkers = []; let latestByDevice = {}; +let allMembers = []; + let lastLat = null; let lastLng = null; -let isHost = false; - // 駅関連 let stationMarkers = []; @@ -37,59 +36,124 @@ let targetMarker = null; let targetLat = null; let targetLng = null; -let arrived = false; -let lastShownDistance = null; - -// 皇居 -const DEFAULT_LAT = 35.681236; -const DEFAULT_LNG = 139.767125; // =================================== -// アイコン -// =================================== -const stationIcon = L.icon({ - iconUrl: - "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-blue.png", - shadowUrl: - "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png", - iconSize: [25, 41], - iconAnchor: [12, 41], -}); - -const targetIcon = L.icon({ - iconUrl: - "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-red.png", - shadowUrl: - "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png", - iconSize: [25, 41], - iconAnchor: [12, 41], -}); - +// URL パラメータ読み込み // =================================== function loadParams() { const p = new URLSearchParams(location.search); - currentGroup = p.get("group"); - currentUser = p.get("user"); - document.getElementById("groupName").textContent = currentGroup; - document.getElementById("userName").textContent = currentUser; + currentGroup = p.get("group") || ""; + currentUser = p.get("user") || ""; + + // host=1 or true ならホスト + const hostParam = p.get("host"); + isHost = hostParam === "1" || hostParam === "true"; + + // 画面表示 + const gSpan = document.getElementById("groupName"); + const uSpan = document.getElementById("userName"); + const hostLabel = document.getElementById("hostStatus"); + + if (gSpan) gSpan.textContent = currentGroup; + if (uSpan) uSpan.textContent = currentUser; + if (hostLabel) { + hostLabel.textContent = isHost ? "あなたはホストです" : "一般メンバーです"; + } + + // チャット用に保存(任意) + localStorage.setItem("group", currentGroup); + localStorage.setItem("userName", currentUser); } // =================================== -async function decideHost() { - const { data } = await supa - .from("groups") - .select("host_name") - .eq("group_name", currentGroup) - .maybeSingle(); - - isHost = data?.host_name === currentUser; - - const hostLabel = document.getElementById("hostStatus"); - hostLabel.textContent = - isHost ? "あなたはホストです" : "一般メンバーです"; +// 位置情報取得(Promise 化) +// =================================== +function getPosition() { + return new Promise((resolve, reject) => { + if (!navigator.geolocation) { + reject(new Error("Geolocation not supported")); + return; + } + navigator.geolocation.getCurrentPosition( + (pos) => resolve(pos.coords), + (err) => reject(err), + { enableHighAccuracy: true } + ); + }); } // =================================== +// 地図初期化 +// =================================== +function initMap(lat, lng) { + map = L.map("map").setView([lat, lng], 16); + + L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19, + }).addTo(map); + + // ホストだけ目的地設定 + map.on("click", async (e) => { + if (!isHost) return; + + const newLat = e.latlng.lat; + const newLng = e.latlng.lng; + + targetLat = newLat; + targetLng = newLng; + + if (targetMarker) { + map.removeLayer(targetMarker); + } + + targetMarker = L.marker([targetLat, targetLng]).addTo(map); + + // Supabase に共有 + try { + await supa.from("shared_target").upsert({ + group_name: currentGroup, + lat: targetLat, + lng: targetLng, + }); + + const box = document.getElementById("targetInfo"); + if (box) { + box.textContent = "目的地を設定しました!"; + } + } catch (e) { + console.error("shared_target への保存失敗", e); + } + }); +} + +// =================================== +// 自分の位置を保存 +// =================================== +async function saveMyLocation(lat, lng) { + try { + // 自分の古いレコード削除 + await supa + .from("locations") + .delete() + .eq("group_name", currentGroup) + .eq("device_id", deviceId); + + // 挿入 + await supa.from("locations").insert({ + group_name: currentGroup, + user_name: currentUser, + device_id: deviceId, + lat, + lng, + }); + } catch (e) { + console.error("位置情報保存エラー", e); + } +} + +// =================================== +// 自分のピン表示 +// =================================== function createLabeledMarker(lat, lng, name, isSelf) { const pinUrl = isSelf ? "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png" @@ -112,79 +176,22 @@ }); } -// =================================== -function getPosition() { - return new Promise((res, rej) => { - navigator.geolocation.getCurrentPosition( - (pos) => res(pos.coords), - rej, - { enableHighAccuracy: true } - ); - }); -} - -// =================================== -function initMap(lat, lng) { - map = L.map("map").setView([lat, lng], 16); - - L.tileLayer( - "https://tile.openstreetmap.org/{z}/{x}/{y}.png", - { maxZoom: 19 } - ).addTo(map); - - // ホストだけ目的地設定 - map.on("click", async (e) => { - if (!isHost) return; - - targetLat = e.latlng.lat; - targetLng = e.latlng.lng; - - if (targetMarker) map.removeLayer(targetMarker); - - targetMarker = L.marker([targetLat, targetLng], { - icon: targetIcon, - }).addTo(map); - - await supa.from("shared_target").upsert({ - group_name: currentGroup, - lat: targetLat, - lng: targetLng, - }); - - document.getElementById("targetInfo").textContent = - "目的地を設定しました!"; - }); -} - -// =================================== -async function saveMyLocation(lat, lng) { - await supa - .from("locations") - .delete() - .eq("group_name", currentGroup) - .eq("device_id", deviceId); - - await supa.from("locations").insert({ - group_name: currentGroup, - user_name: currentUser, - device_id: deviceId, - lat, - lng, - }); -} - -// =================================== function renderSelfMarker() { + if (!map || lastLat === null || lastLng === null) return; if (selfMarker) map.removeLayer(selfMarker); selfMarker = createLabeledMarker(lastLat, lastLng, currentUser, true); selfMarker.addTo(map); } // =================================== -function showMemberList(latest) { +// 他メンバー表示 +// =================================== +function showMemberList(latestByDevice) { const list = document.getElementById("memberList"); + if (!list) return; - Object.values(latest).forEach((row) => { + // 新しいメンバーを allMembers に追加 + Object.values(latestByDevice).forEach((row) => { if (!allMembers.some((m) => m.device_id === row.device_id)) { allMembers.push({ device_id: row.device_id, @@ -193,79 +200,102 @@ } }); - allMembers.forEach((m) => { - let li = list.querySelector(`li[data-id="${m.device_id}"]`); + // 表示更新 + allMembers.forEach((member) => { + let li = list.querySelector(`li[data-id="${member.device_id}"]`); + if (!li) { li = document.createElement("li"); - li.dataset.id = m.device_id; + li.dataset.id = member.device_id; li.innerHTML = `
- - `; + + `; list.appendChild(li); } - li.querySelector(".member-name").textContent = m.user_name; + const row = latestByDevice[member.device_id]; - const row = latest[m.device_id]; - const online = - row && - (Date.now() - new Date(row.updated_at)) / 1000 < 6; + // 名前 + const nameElem = li.querySelector(".member-name"); + if (nameElem) nameElem.textContent = member.user_name; - li.querySelector(".member-icon").style.background = - m.device_id === deviceId ? "#58c16b" : "#FFD700"; + // オンライン判定 + let online = false; + if (row && row.updated_at) { + const diffSec = (Date.now() - new Date(row.updated_at)) / 1000; + online = diffSec < 6; + } - li.querySelector(".member-status").textContent = - online ? "🟢" : "🔴"; + if (online) { + li.classList.add("online"); + li.classList.remove("offline"); + } else { + li.classList.add("offline"); + li.classList.remove("online"); + } }); } -// =================================== async function showOtherUsers() { - const { data } = await supa - .from("locations") - .select("*") - .eq("group_name", currentGroup) - .order("updated_at", { ascending: false }); + try { + const { data, error } = await supa + .from("locations") + .select("*") + .eq("group_name", currentGroup) + .order("updated_at", { ascending: false }); - latestByDevice = {}; - data?.forEach((r) => { - if (!latestByDevice[r.device_id]) latestByDevice[r.device_id] = r; - }); + if (error) { + console.error("locations 取得エラー", error); + return; + } - showMemberList(latestByDevice); + latestByDevice = {}; + (data || []).forEach((r) => { + if (!latestByDevice[r.device_id]) { + latestByDevice[r.device_id] = r; + } + }); - otherMarkers.forEach((m) => map.removeLayer(m)); - otherMarkers = []; + showMemberList(latestByDevice); - Object.values(latestByDevice).forEach((row) => { - if (row.device_id === deviceId) return; - const mk = createLabeledMarker( - row.lat, - row.lng, - row.user_name, - false - ); - mk.addTo(map); - otherMarkers.push(mk); - }); + // マーカー更新 + otherMarkers.forEach((m) => map.removeLayer(m)); + otherMarkers = []; + + Object.values(latestByDevice).forEach((row) => { + if (row.device_id === deviceId) return; + const mk = createLabeledMarker(row.lat, row.lng, row.user_name, false); + mk.addTo(map); + otherMarkers.push(mk); + }); + } catch (e) { + console.error("他ユーザー表示エラー", e); + } } // =================================== -function distanceMeters(a, b, c, d) { +// 距離計算 +// =================================== +function distanceMeters(lat1, lng1, lat2, lng2) { const R = 6371000; - const toRad = (x) => (x * Math.PI) / 180; - const dLat = toRad(c - a); - const dLng = toRad(d - b); - const y = + const toRad = (deg) => (deg * Math.PI) / 180; + + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + + const a = Math.sin(dLat / 2) ** 2 + - Math.cos(toRad(a)) * - Math.cos(toRad(c)) * + Math.cos(toRad(lat1)) * + Math.cos(toRad(lat2)) * Math.sin(dLng / 2) ** 2; - return R * 2 * Math.atan2(Math.sqrt(y), Math.sqrt(1 - y)); + + return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } // =================================== +// 駅関連 +// =================================== async function fetchStationsAround(lat, lng) { const query = ` [out:json]; @@ -275,14 +305,15 @@ const url = "https://overpass-api.de/api/interpreter?data=" + encodeURIComponent(query); + const res = await fetch(url); const json = await res.json(); return json.elements || []; } -// =================================== async function loadStations() { const box = document.getElementById("nearestBox"); + if (!box || lastLat === null || lastLng === null) return; try { const stations = await fetchStationsAround(lastLat, lastLng); @@ -291,7 +322,7 @@ stationMarkers = []; if (stations.length === 0) { - box.textContent = "駅なし"; + box.textContent = "駅が見つかりませんでした。"; return; } @@ -299,148 +330,202 @@ let nearestDist = Infinity; stations.forEach((st) => { - const d = distanceMeters(lastLat, lastLng, st.lat, st.lon); + const sLat = st.lat; + const sLng = st.lon; + const name = (st.tags && st.tags.name) || "駅名不明"; + + const d = distanceMeters(lastLat, lastLng, sLat, sLng); if (d < nearestDist) { nearestDist = d; - nearest = st; + nearest = { name, lat: sLat, lng: sLng }; } - const mk = L.marker([st.lat, st.lon], { - icon: stationIcon, - }).addTo(map); + + const mk = L.marker([sLat, sLng]).addTo(map); + mk.bindPopup(name + "駅"); stationMarkers.push(mk); }); - box.textContent = `最寄り駅:${nearest.tags.name}(約 ${(nearestDist / - 1000).toFixed(2)} km)`; - } catch { - box.textContent = "駅情報エラー"; + box.textContent = + "最寄り駅:" + + nearest.name + + "(約 " + + (nearestDist / 1000).toFixed(2) + + " km)"; + } catch (e) { + console.error("駅情報取得エラー", e); + box.textContent = "駅情報を取得できませんでした。"; } } // =================================== +// 共有目的地の読込 +// =================================== async function loadSharedTarget() { - const { data } = await supa - .from("shared_target") - .select("*") - .eq("group_name", currentGroup) - .maybeSingle(); + const box = document.getElementById("targetInfo"); + if (!box) return; - if (!data) return; + try { + const { data, error } = await supa + .from("shared_target") + .select("*") + .eq("group_name", currentGroup) + .maybeSingle(); - if (targetLat === data.lat && targetLng === data.lng) return; + if (error) { + console.error("shared_target 読み込みエラー", error); + return; + } + if (!data) return; - targetLat = data.lat; - targetLng = data.lng; - arrived = false; + if (targetLat === data.lat && targetLng === data.lng) return; - if (targetMarker) map.removeLayer(targetMarker); + targetLat = data.lat; + targetLng = data.lng; - targetMarker = L.marker([targetLat, targetLng], { - icon: targetIcon, - }).addTo(map); + if (targetMarker) { + map.removeLayer(targetMarker); + } + targetMarker = L.marker([targetLat, targetLng]).addTo(map); - document.getElementById("targetInfo").textContent = - "目的地が共有されました!"; + box.textContent = "目的地が共有されました!"; + } catch (e) { + console.error("共有目的地読込エラー", e); + } } // =================================== +// チャット機能 +// =================================== +async function loadMessages() { + const chatList = document.getElementById("chatList"); + if (!chatList) return; + + try { + const { data, error } = await supa + .from("messages") + .select("*") + .eq("group_name", currentGroup) + .order("created_at", { ascending: true }); + + if (error) { + console.error("messages 読み込みエラー", error); + return; + } + + chatList.innerHTML = (data || []) + .map( + (m) => + `