Newer
Older
WhoMatch / system / public / script.js
// ===== URL パラメータでチートモード制御 =====
const params = new URLSearchParams(location.search);
let cheatMode = params.get("t") === "true" || params.get("cheat") === "true";

// ===== トークン取得 =====
let token = localStorage.getItem("whomatch_token");
if (!token) {
  token = prompt("WhoMatch認証トークンを入力してください");
  if (token) {
    localStorage.setItem("whomatch_token", token);
  } else {
    alert("認証トークンが必要です");
    throw new Error("トークン未設定");
  }
}

// ===== 地図の初期化 =====
const map = L.map('map').setView([38.88, 139.83], 15);

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

let userMarker = null;

const userIcon = L.icon({ iconUrl: '/icons/user.png', iconSize: [40, 40], iconAnchor: [20, 40] });

// ✅ 雪かきカテゴリを追加
const requestIcons = {
  "shopping": L.icon({ iconUrl: '/icons/shopping.png', iconSize: [40, 40], iconAnchor: [20, 40] }),
  "dog-walk": L.icon({ iconUrl: '/icons/dog-walk.png', iconSize: [40, 40], iconAnchor: [20, 40] }),
  "chat": L.icon({ iconUrl: '/icons/chat.png', iconSize: [40, 40], iconAnchor: [20, 40] }),
  "snow-shovel": L.icon({ iconUrl: '/icons/snow-shovel.png', iconSize: [40, 40], iconAnchor: [20, 40] })
};

const categoryNames = {
  "dog-walk": "犬の散歩",
  "shopping": "買い物",
  "chat": "おしゃべり",
  "snow-shovel": "雪かき"
};

const requestLayer = L.layerGroup().addTo(map);
let requests = [];

// ===== 信頼係数 =====
const TRUST_COEFFICIENT = 0.9;

// ===== 開示スコア =====
function disclosureScore(distanceKm, Iu, Is, K = 10000) {
  return (K * Iu * Is) / (distanceKm * distanceKm);
}

// ===== ✅ 正しい方位角計算 =====
function getBearing(lat1, lon1, lat2, lon2) {
  const toRad = x => x * Math.PI / 180;
  const toDeg = x => x * 180 / Math.PI;

  const φ1 = toRad(lat1);
  const φ2 = toRad(lat2);
  const Δλ = toRad(lon2 - lon1);

  const y = Math.sin(Δλ) * Math.cos(φ2);
  const x = Math.cos(φ1) * Math.sin(φ2) -
            Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);

  const θ = Math.atan2(y, x);
  return (toDeg(θ) + 360) % 360;
}

// ===== ✅ コンパス針を回転 =====
function rotateCompass(bearing) {
  const needle = document.getElementById("compass-needle");
  if (needle) {
    needle.style.transform = `translate(-50%, -100%) rotate(${bearing}deg)`;
  }
}

// ===== ✅ 開示スコア最大の依頼を取得(km 変換済み) =====
function getBestRequest(userLatLng) {
  let best = null;
  let bestScore = -1;

  requests.forEach(req => {
    const lat = parseFloat(req.latitude);
    const lng = parseFloat(req.longitude);
    if (isNaN(lat) || isNaN(lng)) return;

    const reqLatLng = L.latLng(lat, lng);

    // ✅ 距離を km に変換
    const distanceKm = map.distance(userLatLng, reqLatLng) / 1000;

    const Is = parseFloat(req.info_level) || 0.5;
    const score = disclosureScore(distanceKm, TRUST_COEFFICIENT, Is);

    if (score > bestScore) {
      bestScore = score;
      best = req;
    }
  });

  return best;
}

// ===== 依頼データをロード =====
function loadRequests() {
  fetch(`http://127.0.0.1:4567/api/requests?token=${token}`)
    .then(res => res.json())
    .then(data => {
      requests = data;
      updateMarkers();
    })
    .catch(err => {
      alert("依頼データの取得に失敗しました");
    });
}

// ===== マーカー更新(km 変換済み) =====
function updateMarkers() {
  if (!userMarker) return;
  const userLatLng = userMarker.getLatLng();
  requestLayer.clearLayers();

  requests.forEach(req => {
    const lat = parseFloat(req.latitude);
    const lng = parseFloat(req.longitude);
    if (isNaN(lat) || isNaN(lng)) return;

    const type = req.category?.trim();
    const icon = requestIcons[type] || requestIcons["dog-walk"];
    const label = categoryNames[type] || "その他";
    const reqLatLng = L.latLng(lat, lng);

    // ✅ km に変換
    const distanceKm = map.distance(userLatLng, reqLatLng) / 1000;

    const Is = parseFloat(req.info_level) || 0.5;
    const A = disclosureScore(distanceKm, TRUST_COEFFICIENT, Is);

    if (A >= 0.1) {
      L.marker(reqLatLng, { icon: icon })
        .addTo(requestLayer)
        .bindPopup(
          `<b>${label}</b><br>${req.description || ""}<br>距離: ${Math.round(map.distance(userLatLng, reqLatLng))}m`
        );
    } else if (A >= 0.01) {
      const offsetLat = (Math.random() - 0.5) * 0.002;
      const offsetLng = (Math.random() - 0.5) * 0.002;
      const blurredLatLng = L.latLng(lat + offsetLat, lng + offsetLng);

      L.circle(blurredLatLng, { radius: 500, color: 'gray' }).addTo(requestLayer);
    }
  });
}

// ===== ✅ チートモード:クリックで現在地ワープ =====
map.on("click", function(e) {
  if (!cheatMode) return;

  const latlng = e.latlng;

  if (!userMarker) {
    userMarker = L.marker(latlng, { icon: userIcon })
      .addTo(map)
      .bindPopup("あなたの現在地(チートモード)");
  } else {
    userMarker.setLatLng(latlng);
  }

  map.setView(latlng, 15);
  updateMarkers();

  const best = getBestRequest(latlng);
  if (best) {
    const bearing = getBearing(
      latlng.lat, latlng.lng,
      parseFloat(best.latitude), parseFloat(best.longitude)
    );
    rotateCompass(bearing);

    const directionName = getDirectionName(bearing);
    document.getElementById("direction-text").textContent =
      `依頼は ${directionName} 方向にあります。(チートモード)`;
  }
});

// ===== 現在地を監視 =====
let firstLocationUpdate = true;

if (navigator.geolocation) {
  navigator.geolocation.watchPosition(
    pos => {
      const userLatLng = L.latLng(pos.coords.latitude, pos.coords.longitude);

      if (!cheatMode) {
        if (!userMarker) {
          userMarker = L.marker(userLatLng, { icon: userIcon })
            .addTo(map)
            .bindPopup("あなたの現在地");
          map.setView(userLatLng, 15);
        } else {
          userMarker.setLatLng(userLatLng);
        }
      }

      if (firstLocationUpdate) {
        loadRequests();
        setInterval(loadRequests, 10000);
        firstLocationUpdate = false;
      }

      updateMarkers();

      const best = getBestRequest(userLatLng);
      if (best) {
        const bearing = getBearing(
          userLatLng.lat, userLatLng.lng,
          parseFloat(best.latitude), parseFloat(best.longitude)
        );

        rotateCompass(bearing);
        const directionName = getDirectionName(bearing);
        document.getElementById("direction-text").textContent =
          `依頼は ${directionName} 方向にあります。`;
      }
    },
    err => {
      alert("位置情報を取得できません: " + err.message);
    },
    { enableHighAccuracy: true }
  );
}

function getDirectionName(bearing) {
  if (bearing >= 337.5 || bearing < 22.5) return "北";
  if (bearing >= 22.5 && bearing < 67.5) return "北東";
  if (bearing >= 67.5 && bearing < 112.5) return "東";
  if (bearing >= 112.5 && bearing < 157.5) return "南東";
  if (bearing >= 157.5 && bearing < 202.5) return "南";
  if (bearing >= 202.5 && bearing < 247.5) return "南西";
  if (bearing >= 247.5 && bearing < 292.5) return "西";
  if (bearing >= 292.5 && bearing < 337.5) return "北西";
}