Newer
Older
2025-shino / map4.js
// ===================================
// Supabase 設定
// ===================================
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) {
  deviceId = crypto.randomUUID();
  localStorage.setItem("deviceId", deviceId);
}

// ===================================
let allMembers = [];
let currentGroup = "";
let currentUser = "";
let map;
let selfMarker = null;
let otherMarkers = [];
let latestByDevice = {};
let lastLat = null;
let lastLng = null;

let isHost = false;

// 駅関連
let stationMarkers = [];

// 目的地関連
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],
});

// ===================================
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;
}

// ===================================
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 ? "あなたはホストです" : "一般メンバーです";
}

// ===================================
function createLabeledMarker(lat, lng, name, isSelf) {
  const pinUrl = isSelf
    ? "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png"
    : "https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-gold.png";

  const html = `
    <div class="pin-wrapper">
      <img src="${pinUrl}" class="pin-img">
      <div class="pin-label">${name}</div>
    </div>
  `;

  return L.marker([lat, lng], {
    icon: L.divIcon({
      className: "custom-pin",
      html: html,
      iconSize: [32, 32],
      iconAnchor: [16, 32],
    }),
  });
}

// ===================================
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 (selfMarker) map.removeLayer(selfMarker);
  selfMarker = createLabeledMarker(lastLat, lastLng, currentUser, true);
  selfMarker.addTo(map);
}

// ===================================
function showMemberList(latest) {
  const list = document.getElementById("memberList");

  Object.values(latest).forEach((row) => {
    if (!allMembers.some((m) => m.device_id === row.device_id)) {
      allMembers.push({
        device_id: row.device_id,
        user_name: row.user_name,
      });
    }
  });

  allMembers.forEach((m) => {
    let li = list.querySelector(`li[data-id="${m.device_id}"]`);
    if (!li) {
      li = document.createElement("li");
      li.dataset.id = m.device_id;
      li.innerHTML = `
        <div class="member-icon"></div>
        <span class="member-name"></span>
        <span class="member-status"></span>`;
      list.appendChild(li);
    }

    li.querySelector(".member-name").textContent = m.user_name;

    const row = latest[m.device_id];
    const online =
      row &&
      (Date.now() - new Date(row.updated_at)) / 1000 < 6;

    li.querySelector(".member-icon").style.background =
      m.device_id === deviceId ? "#58c16b" : "#FFD700";

    li.querySelector(".member-status").textContent =
      online ? "🟢" : "🔴";
  });
}

// ===================================
async function showOtherUsers() {
  const { data } = 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;
  });

  showMemberList(latestByDevice);

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

// ===================================
function distanceMeters(a, b, c, d) {
  const R = 6371000;
  const toRad = (x) => (x * Math.PI) / 180;
  const dLat = toRad(c - a);
  const dLng = toRad(d - b);
  const y =
    Math.sin(dLat / 2) ** 2 +
    Math.cos(toRad(a)) *
      Math.cos(toRad(c)) *
      Math.sin(dLng / 2) ** 2;
  return R * 2 * Math.atan2(Math.sqrt(y), Math.sqrt(1 - y));
}

// ===================================
async function fetchStationsAround(lat, lng) {
  const query = `
    [out:json];
    node["railway"="station"](around:5000,${lat},${lng});
    out;
  `;
  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");

  try {
    const stations = await fetchStationsAround(lastLat, lastLng);

    stationMarkers.forEach((m) => map.removeLayer(m));
    stationMarkers = [];

    if (stations.length === 0) {
      box.textContent = "駅なし";
      return;
    }

    let nearest = null;
    let nearestDist = Infinity;

    stations.forEach((st) => {
      const d = distanceMeters(lastLat, lastLng, st.lat, st.lon);
      if (d < nearestDist) {
        nearestDist = d;
        nearest = st;
      }
      const mk = L.marker([st.lat, st.lon], {
        icon: stationIcon,
      }).addTo(map);
      stationMarkers.push(mk);
    });

    box.textContent = `最寄り駅:${nearest.tags.name}(約 ${(nearestDist /
      1000).toFixed(2)} km)`;
  } catch {
    box.textContent = "駅情報エラー";
  }
}

// ===================================
async function loadSharedTarget() {
  const { data } = await supa
    .from("shared_target")
    .select("*")
    .eq("group_name", currentGroup)
    .maybeSingle();

  if (!data) return;

  if (targetLat === data.lat && targetLng === data.lng) return;

  targetLat = data.lat;
  targetLng = data.lng;
  arrived = false;

  if (targetMarker) map.removeLayer(targetMarker);

  targetMarker = L.marker([targetLat, targetLng], {
    icon: targetIcon,
  }).addTo(map);

  document.getElementById("targetInfo").textContent =
    "目的地が共有されました!";
}

// ===================================
async function main() {
  loadParams();
  await decideHost();

  let lat = DEFAULT_LAT;
  let lng = DEFAULT_LNG;

  try {
    const pos = await getPosition();
    lat = pos.latitude;
    lng = pos.longitude;
  } catch {}

  lastLat = lat;
  lastLng = lng;

  initMap(lat, lng);
  renderSelfMarker();

  await saveMyLocation(lat, lng);
  await showOtherUsers();

  loadStations();
  loadSharedTarget();

  setInterval(async () => {
    try {
      const pos = await getPosition();
      lastLat = pos.latitude;
      lastLng = pos.longitude;
      renderSelfMarker();
      await saveMyLocation(lastLat, lastLng);
    } catch {}
  }, 2000);

  setInterval(showOtherUsers, 1200);
  setInterval(loadSharedTarget, 10000);

  document.getElementById("exitBtn").onclick = () => {
    location.href = "index.html";
  };

  setTimeout(() => map.invalidateSize(), 300);
}

window.addEventListener("DOMContentLoaded", main);

// ===================================
// ★ チャット機能(ここから下だけ追加)
// ===================================

// ユーザー名
let chatUser = localStorage.getItem("userName");
if (!chatUser) {
  chatUser = prompt("名前を入力してください");
  localStorage.setItem("userName", chatUser);
}

// グループ
const chatGroup = localStorage.getItem("group");

// DOM
const chatList = document.getElementById("chatList");
const chatInput = document.getElementById("chatInput");
const chatSend = document.getElementById("chatSend");

// メッセージ送信
chatSend.addEventListener("click", async () => {
  const text = chatInput.value.trim();
  if (text === "") return;

  await supa.from("messages").insert({
    group_name: chatGroup,
    user_name: chatUser,
    message: text,
  });

  chatInput.value = "";
});

// メッセージ取得
async function loadMessages() {
  const { data } = await supa
    .from("messages")
    .select("*")
    .eq("group_name", chatGroup)
    .order("created_at", { ascending: true });

  chatList.innerHTML = data
    .map(
      (m) =>
        `<div><strong>${m.user_name}:</strong> ${m.message}</div>`
    )
    .join("");

  chatList.scrollTop = chatList.scrollHeight;
}

setInterval(loadMessages, 2000);
loadMessages();