Newer
Older
2024-Ikarashi / map.js
// 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;