// Firebase関連のimport文
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-app.js";
import { getFirestore, collection, addDoc, onSnapshot, query, orderBy, deleteDoc, doc, updateDoc } from "https://www.gstatic.com/firebasejs/9.6.0/firebase-firestore.js";
// あなたのウェブアプリのFirebase設定
const firebaseConfig = {
apiKey: "AIzaSyB9dSyM4ThvkJb0LadQHH3UeAzEdf5ZTcw",
authDomain: "iki-ikimap.firebaseapp.com",
projectId: "iki-ikimap",
storageBucket: "iki-ikimap.firebasestorage.app",
messagingSenderId: "513869697456",
appId: "1:513869697456:web:22696630d0174e80bcc675",
measurementId: "G-N2VWBTHNS2"
};
// Firebaseの初期化
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const commentsCollection = collection(db, "comments"); // "comments"コレクションへの参照
// 地図の初期化 (山形県酒田市の中心に近い座標を設定)
const map = L.map('map').setView([38.9122, 139.8360], 13);
// 国土地理院の地図タイルレイヤーを追加
L.tileLayer('https://cyberjapandata.gsi.go.jp/xyz/std/{z}/{x}/{y}.png', {
attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html">国土地理院</a>',
maxZoom: 18,
minZoom: 5
}).addTo(map);
// ページ読み込み時の処理
window.addEventListener('load', () => {
// Firebaseからコメントをロード (onSnapshotリスナーを設定)
loadCommentsFromFirestore();
// マップのリサイズを強制 (表示崩れ対策)
setTimeout(() => {
map.invalidateSize();
}, 100);
});
// マーカーを管理する配列。この配列の要素(マーカーオブジェクト)にFirestoreのドキュメントIDを紐付けます。
let markers = [];
// 地図をクリックしてマーカーと情報ウィンドウを追加
map.on('click', (e) => {
const { lat, lng } = e.latlng;
addMarker(lat, lng);
});
// マーカーとコメント入力ウィンドウを追加する関数
function addMarker(lat, lng) {
const marker = L.marker([lat, lng]).addTo(map);
// マーカーのLeaflet内部IDを取得 (これは一時的なIDで、Firestoreに保存されるまで使用)
const leafletId = marker._leaflet_id;
// ポップアップ内容を定義。保存ボタンのonclickに緯度、経度、Leaflet IDを渡す
marker.bindPopup(`
<div class="info-window">
<p>活動日記を入力してください:</p>
<textarea id="comment" rows="3" style="width:100%;"></textarea>
<button onclick="saveComment('${lat}', '${lng}', '${leafletId}')">保存</button>
</div>
`).openPopup();
// マーカーリストに、まだFirestore IDが紐付いていない状態のマーカーを仮で追加
// Firestoreに保存された後、onSnapshotの"added"でFirestore IDが紐付けられます。
markers.push(marker);
}
// コメントを保存し、Firestoreへ追加する非同期関数
async function saveComment(lat, lng, leafletId) {
const comment = document.getElementById("comment").value;
const timestamp = new Date().toLocaleString(); // 現在の日時を取得
if (!comment.trim()) {
alert("コメントを入力してください。");
return;
}
const entry = {
lat: parseFloat(lat), // 緯度を数値型で保存
lng: parseFloat(lng), // 経度を数値型で保存
comment,
timestamp,
leafletId: parseInt(leafletId) // Leaflet IDもFirestoreに保存(オプション)
};
try {
const docRef = await addDoc(commentsCollection, entry); // Firestoreに新しいドキュメントを追加
console.log("Document written with ID: ", docRef.id);
// Firestoreに保存された後、markers配列内の対応するマーカーにFirestoreのdocIdを紐付け
// leafletIdを使って該当マーカーを特定し、docIdプロパティを追加
const marker = markers.find(m => m._leaflet_id === parseInt(leafletId));
if (marker) {
marker.docId = docRef.id;
}
// コメント保存後、ポップアップが開いている場合は閉じる
map.closePopup();
} catch (e) {
console.error("Error adding document: ", e);
alert("コメントの保存に失敗しました。");
}
// FirebaseのonSnapshotリスナーが変更を検知してUIを自動的に更新するため、
// ここで明示的にreloadCommentsのような関数を呼び出す必要はありません。
}
// 履歴にコメント要素を動的に追加する関数 (FirestoreのdocIdを使用)
// onSnapshotから呼び出されるため、データオブジェクト(docIdを含む)を受け取ります。
function addCommentToHistory(data) { // dataにはlat, lng, comment, timestamp, docIdなどが含まれる
const commentsList = document.getElementById("comments-list");
const commentEntryDiv = document.createElement('div');
commentEntryDiv.className = "history-entry";
// FirestoreのドキュメントIDをdata-doc-id属性に設定し、UI要素とデータを紐付けます。
commentEntryDiv.setAttribute('data-doc-id', data.docId);
// コメントエントリにランダムな回転を適用(デザイン用)
const rotation = (Math.random() * 2 - 1).toFixed(1); /* -1.0から1.0までのランダムな角度 */
commentEntryDiv.style.setProperty('--rotation', rotation);
commentEntryDiv.innerHTML = `
<p><strong>日時:</strong> ${data.timestamp}</p>
<p><strong>場所:</strong> 緯度 ${data.lat}, 経度 ${data.lng}</p>
<p><strong>活動日記:</strong>
<span class="comment-text" contenteditable="true" onblur="updateComment('${data.docId}', this)">
${data.comment}
</span>
</p>
<button onclick="deleteComment('${data.docId}')">削除</button>
`;
commentsList.appendChild(commentEntryDiv);
}
// コメントを削除する非同期関数 (Firestoreから削除)
async function deleteComment(docId) {
if (!confirm("本当にこの活動日記を削除しますか?")) {
return; // ユーザーがキャンセルしたら何もしない
}
try {
await deleteDoc(doc(commentsCollection, docId)); // Firestoreから指定のドキュメントを削除
console.log("Document successfully deleted with ID:", docId);
// FirebaseのonSnapshotリスナーが変更を検知し、UIから要素を自動的に削除します。
} catch (e) {
console.error("Error removing document: ", e);
alert("コメントの削除に失敗しました。");
}
}
// コメントを直接編集し、Firestoreを更新する非同期関数
async function updateComment(docId, element) {
const newComment = element.textContent.trim();
// 元のコメントを取得し、変更がなかった場合やエラー時に戻すために使用
// 最も確実なのは、要素のdata-original-comment属性などに初期値を保存しておく方法です。
// ここでは、マーカーのポップアップ内容から元のコメントを取得することを試みます。
const marker = markers.find(m => m.docId === docId);
let oldComment = '';
if (marker && marker.getPopup()) {
const popupContent = marker.getPopup().getContent();
const match = popupContent.match(/<strong>コメント:<\/strong>\s*(.*)<\/p>/);
if (match && match[1]) {
oldComment = match[1].trim();
}
}
// もしそれでも取得できなければ、編集前のテキストを一時的に保持することも検討
if (!oldComment && element.dataset.currentComment) {
oldComment = element.dataset.currentComment;
}
// 編集開始時に現在のコメントを保持
element.dataset.currentComment = element.textContent.trim();
if (!newComment) {
alert("コメントは空白にできません。元の内容に戻します。");
element.textContent = oldComment; // 元のコメントに戻す
return;
}
if (newComment === oldComment) { // コメント内容に変更がなければ、Firestoreへの更新をスキップ
console.log("No change in comment, skipping update.");
return;
}
try {
await updateDoc(doc(commentsCollection, docId), { comment: newComment }); // Firestoreのドキュメントを更新
console.log("Document successfully updated with ID:", docId);
// FirebaseのonSnapshotリスナーが変更を検知し、UIを自動的に更新します。
} catch (e) {
console.error("Error updating document: ", e);
alert("コメントの更新に失敗しました。");
element.textContent = oldComment; // エラー時は元のコメントに戻す
}
}
// Firebase Firestoreからコメントをリアルタイムでロードし、表示する関数
function loadCommentsFromFirestore() {
// リスナー設定前に、既存のUI要素とマーカーを一度クリアします。
// onSnapshotは差分更新を行いますが、初回ロード時の状態をクリーンにするためです。
document.getElementById("comments-list").innerHTML = "";
markers.forEach(marker => map.removeLayer(marker));
markers = [];
// "timestamp"フィールドで昇順にソートするクエリを作成
const q = query(commentsCollection, orderBy("timestamp", "asc"));
// リアルタイムリスナーを設定
// このonSnapshotコールバック関数は、Firestoreのコレクションに変更があるたびに実行されます。
onSnapshot(q, (snapshot) => {
// docChanges() は、前回のスナップショットからの変更点(追加、変更、削除)を提供します。
snapshot.docChanges().forEach((change) => {
const data = change.doc.data(); // 変更されたドキュメントのデータ
const docId = change.doc.id; // 変更されたドキュメントのID
if (change.type === "added") {
// 新しいドキュメントがFirestoreに追加された場合
addCommentToHistory({ ...data, docId }); // データとFirestoreのdocIdを渡して履歴に追加
// 地図に新しいマーカーを追加
const marker = L.marker([data.lat, data.lng]).addTo(map);
marker.docId = docId; // 作成したマーカーにFirestoreのdocIdを紐付け
markers.push(marker); // markers配列にも追加して管理
// マーカーのポップアップ内容を設定
marker.bindPopup(`
<div class="info-window">
<p><strong>日時:</strong> ${data.timestamp}</p>
<p><strong>コメント:</strong> ${data.comment}</p>
</div>
`);
} else if (change.type === "modified") {
// 既存のドキュメントがFirestoreで変更された場合
const historyEntry = document.querySelector(`.history-entry[data-doc-id="${docId}"]`);
if (historyEntry) {
// サイドバーのコメントテキストを更新
historyEntry.querySelector(".comment-text").textContent = data.comment;
}
// 地図上の対応するマーカーのポップアップ内容も更新
const marker = markers.find(m => m.docId === docId);
if (marker) {
marker.setPopupContent(`
<div class="info-window">
<p><strong>日時:</strong> ${data.timestamp}</p>
<p><strong>コメント:</strong> ${data.comment}</p>
</div>
`);
// もしポップアップが開いているなら、内容更新後に再度開く(閉じないでおく)
if (marker.getPopup().isOpen()) {
marker.openPopup();
}
}
} else if (change.type === "removed") {
// ドキュメントがFirestoreから削除された場合
const historyEntry = document.querySelector(`.history-entry[data-doc-id="${docId}"]`);
if (historyEntry) {
historyEntry.remove(); // サイドバーから該当要素を削除
}
// markers配列から該当マーカーを見つけて地図から削除
const markerIndex = markers.findIndex(m => m.docId === docId);
if (markerIndex !== -1) {
map.removeLayer(markers[markerIndex]); // 地図からマーカーを削除
markers.splice(markerIndex, 1); // markers配列からも削除
}
}
});
// onSnapshotが走るたびに、DOMが変更される可能性があるため、
// 全ての履歴エントリにランダムな回転を再適用(既に設定済みのものは上書きしないように条件を追加)
document.querySelectorAll('.history-entry').forEach((el) => {
if (!el.style.getPropertyValue('--rotation')) { // '--rotation'がまだ設定されていなければ設定
const rotation = (Math.random() * 2 - 1).toFixed(1);
el.style.setProperty('--rotation', rotation);
}
});
});
}
// HTMLのonclick属性から関数を呼び出せるように、グローバルスコープに公開する
window.saveComment = saveComment;
window.deleteComment = deleteComment;
window.updateComment = updateComment;