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