はじめに (対象読者・この記事でわかること)

この記事は、JavaとWeb開発の基本的な知識があり、Google Maps APIを使用したことがある方を対象としています。特に、地図上のマーカーをクリックした際に表示されるInfoWindowをユーザーが直接編集できるようにしたいと考えている開発者に向けています。

本記事を読むことで、Google MapsのInfoWindowを編集可能にするための具体的な実装方法、イベントハンドリングの追加方法、そしてJavaを使用したサーバーサイドでのデータ処理方法が理解できます。これにより、インタラクティブなマップアプリケーションを開発する際の重要なスキルが身につきます。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - HTML/CSSの基本的な知識 - JavaScriptの基本的な知識 - Google Maps APIの基本的な使用経験 - Javaの基本的な知識(サーブレットやJSP)

InfoWindowの基本とカスタマイズの必要性

Google Maps APIで利用されるInfoWindowは、マーカーをクリックした際に表示される情報ウィンドウです。デフォルトのInfoWindowは単なる表示機能に特化しており、ユーザーが直接情報を編集することはできません。しかし、実際のアプリケーションでは、ユーザーが場所情報を直接編集したり、コメントを追加したりといったインタラクティブな機能が求められることが多いです。

InfoWindowをカスタマイズする主な目的は以下の通りです: 1. ユーザー体験の向上 - 直感的な操作で情報を更新可能に 2. データ収集効率化 - 現場で即座に情報を反映 3. アプリケーションの機能拡張 - マップを活用した業務システムの構築 4. コラボレーション機能の実現 - 複数ユーザーによる情報共有

基本的な考え方として、InfoWindowのHTMLコンテンツを動的に生成し、フォーム要素を含むようにカスタマイズします。その上で、イベントハンドリングを追加してユーザーの操作を検知し、Javaのバックエンドと連携してデータを保存・更新する流れとなります。

InfoWindowの編集可能化実装手順

ステップ1: 基本的なInfoWindowの表示

まずは、マーカーとInfoWindowを表示する基本的な実装から始めます。以下はHTMLとJavaScriptの基本的なコード例です。

Html
<!DOCTYPE html> <html> <head> <title>編集可能なInfoWindow</title> <style> #map { height: 500px; width: 100%; } .edit-form { display: none; padding: 10px; background: white; border: 1px solid #ccc; border-radius: 3px; } </style> </head> <body> <div id="map"></div> <script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"></script> <script> let map; let marker; let infoWindow; function initMap() { // マップの初期化 map = new google.maps.Map(document.getElementById('map'), { zoom: 15, center: {lat: 35.6762, lng: 139.6503} // 東京駅を中心に }); // マーカーの作成 marker = new google.maps.Marker({ position: {lat: 35.6762, lng: 139.6503}, map: map, title: "東京駅" }); // InfoWindowの初期化 infoWindow = new google.maps.InfoWindow({ content: '<div id="info-content">東京駅</div>' }); // マーカークリックイベント marker.addListener('click', function() { infoWindow.open(map, marker); }); } </script> </body> </html>

このコードでは、東京駅にマーカーを配置し、クリックすると「東京駅」というテキストが表示されるInfoWindowを表示しています。次のステップで、このInfoWindowを編集可能なフォームに変換していきます。

ステップ2: 編集可能なInfoWindowの作成

InfoWindowのコンテンツを編集可能なフォームに変更します。以下に編集可能なInfoWindowの実装例を示します。

Javascript
// InfoWindowのコンテンツを編集可能なフォームに変更 function createEditableInfoWindow(markerData) { const content = ` <div id="info-content"> <div class="info-display"> <h3>${markerData.title}</h3> <p>${markerData.description}</p> <button id="edit-btn">編集</button> </div> <div class="edit-form"> <form id="edit-form"> <input type="text" id="title-input" value="${markerData.title}" required> <textarea id="desc-input">${markerData.description}</textarea> <button type="submit">保存</button> <button type="button" id="cancel-btn">キャンセル</button> </form> </div> </div> `; return content; } // マーカーデータの初期化 const markerData = { id: 1, title: "東京駅", description: "日本の交通の拠点となる駅", lat: 35.6762, lng: 139.6503 }; // InfoWindowの更新 infoWindow.setContent(createEditableInfoWindow(markerData));

次に、CSSでスタイリングを追加します。

Css
.info-display, .edit-form { padding: 10px; background: white; border: 1px solid #ccc; border-radius: 3px; } .info-display h3 { margin-top: 0; } .info-display p { margin-bottom: 10px; } button { padding: 5px 10px; margin-right: 5px; background: #4285f4; color: white; border: none; border-radius: 3px; cursor: pointer; } button:hover { background: #3367d6; } #edit-form input, #edit-form textarea { width: 100%; padding: 5px; margin-bottom: 10px; border: 1px solid #ddd; border-radius: 3px; } #edit-form textarea { height: 80px; } .edit-form { display: none; }

ステップ3: イベントハンドリングの追加

編集ボタンとフォームのイベントハンドリングを追加します。

Javascript
// イベントハンドリングの追加 document.addEventListener('DOMContentLoaded', function() { // 編集ボタンクリックイベント document.addEventListener('click', function(e) { if (e.target.id === 'edit-btn') { // 表示モードを編集モードに切り替え document.querySelector('.info-display').style.display = 'none'; document.querySelector('.edit-form').style.display = 'block'; } // キャンセルボタンクリックイベント if (e.target.id === 'cancel-btn') { // 編集モードを表示モードに切り替え document.querySelector('.info-display').style.display = 'block'; document.querySelector('.edit-form').style.display = 'none'; } // フォーム送信イベント if (e.target.id === 'edit-form') { e.preventDefault(); // フォームデータの取得 const title = document.getElementById('title-input').value; const description = document.getElementById('desc-input').value; // データの更新 markerData.title = title; markerData.description = description; // InfoWindowの更新 infoWindow.setContent(createEditableInfoWindow(markerData)); // 表示モードに戻す document.querySelector('.info-display').style.display = 'block'; document.querySelector('.edit-form').style.display = 'none'; // ここでサーバーへの送信処理を実行 saveMarkerData(markerData); } }); }); // サーバーへのデータ保存関数 function saveMarkerData(data) { // Fetch APIを使用してJavaサーブレットにデータを送信 fetch('/api/markers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(result => { console.log('保存成功:', result); // 成功時のフィードバック showNotification('情報が更新されました'); }) .catch(error => { console.error('保存エラー:', error); // エラー時のフィードバック showNotification('保存に失敗しました', true); }); } // 通知表示関数 function showNotification(message, isError = false) { const notification = document.createElement('div'); notification.textContent = message; notification.style.position = 'fixed'; notification.style.bottom = '20px'; notification.style.right = '20px'; notification.style.padding = '10px'; notification.style.backgroundColor = isError ? '#f44336' : '#4caf50'; notification.style.color = 'white'; notification.style.borderRadius = '4px'; notification.style.zIndex = '1000'; document.body.appendChild(notification); // 3秒後に通知を削除 setTimeout(() => { notification.remove(); }, 3000); }

ステップ4: Javaサーバーサイドでのデータ処理

クライアントサイドで編集されたデータを保存するためのJavaサーバーサイドの実装例を以下に示します。

Java
// Marker.java (データモデル) public class Marker { private int id; private String title; private String description; private double lat; private double lng; // コンストラクタ、ゲッター、セッター public Marker() {} public Marker(int id, String title, String description, double lat, double lng) { this.id = id; this.title = title; this.description = description; this.lat = lat; this.lng = lng; } // ゲッターとセッター public int getId() { return id; } public void setId(int id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public double getLat() { return lat; } public void setLat(double lat) { this.lat = lat; } public double getLng() { return lng; } public void setLng(double lng) { this.lng = lng; } } // MarkerDAO.java (データアクセスオブジェクト) import java.sql.*; import java.util.ArrayList; import java.util.List; public class MarkerDAO { private Connection connection; public MarkerDAO(Connection connection) { this.connection = connection; } public void saveMarker(Marker marker) throws SQLException { String sql = "INSERT INTO markers (id, title, description, lat, lng) VALUES (?, ?, ?, ?, ?) " + "ON DUPLICATE KEY UPDATE title = ?, description = ?"; try (PreparedStatement stmt = connection.prepareStatement(sql)) { stmt.setInt(1, marker.getId()); stmt.setString(2, marker.getTitle()); stmt.setString(3, marker.getDescription()); stmt.setDouble(4, marker.getLat()); stmt.setDouble(5, marker.getLng()); stmt.setString(6, marker.getTitle()); stmt.setString(7, marker.getDescription()); stmt.executeUpdate(); } } public Marker getMarker(int id) throws SQLException { String sql = "SELECT id, title, description, lat, lng FROM markers WHERE id = ?"; try (PreparedStatement stmt = connection.prepareStatement(sql)) { stmt.setInt(1, id); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { return new Marker( rs.getInt("id"), rs.getString("title"), rs.getString("description"), rs.getDouble("lat"), rs.getDouble("lng") ); } } } return null; } } // MarkerServlet.java (サーブレット) import com.fasterxml.jackson.databind.ObjectMapper; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.Connection; import java.sql.DriverManager; import java.util.stream.Collectors; @WebServlet("/api/markers") public class MarkerServlet extends HttpServlet { private ObjectMapper objectMapper = new ObjectMapper(); private MarkerDAO markerDAO; @Override public void init() throws ServletException { try { // データベース接続の初期化 String url = "jdbc:mysql://localhost:3306/mapdb"; String user = "username"; String password = "password"; Connection connection = DriverManager.getConnection(url, user, password); markerDAO = new MarkerDAO(connection); } catch (Exception e) { throw new ServletException("データベース接続に失敗しました", e); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // リクエストボディのJSONデータを取得 String json = request.getReader().lines().collect(Collectors.joining()); Marker marker = objectMapper.readValue(json, Marker.class); // データベースに保存 markerDAO.saveMarker(marker); // レスポンスを返す response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString( new Response("success", "マーカー情報が保存されました") )); } catch (Exception e) { // エラーレスポンスを返す response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.getWriter().write(objectMapper.writeValueAsString( new Response("error", "保存に失敗しました: " + e.getMessage()) )); } } // レスポンス用の内部クラス private static class Response { private String status; private String message; public Response(String status, String message) { this.status = status; this.message = message; } // ゲッター public String getStatus() { return status; } public String getMessage() { return message; } } }

ハマった点やエラー解決

この実装を行う際に、多くの開発者が遭遇する問題があります。

問題1: InfoWindowのクローズ時のイベントハンドリング

InfoWindowはデフォルトでは、マップ外をクリックすると自動的に閉じます。この際にフォームデータが未保存の場合、ユーザーは意図せず編集内容を失ってしまう可能性があります。

解決策: InfoWindowが閉じる前に確認ダイアログを表示するようにします。

Javascript
// InfoWindowのcloseイベントをオーバーライド const originalClose = infoWindow.close; infoWindow.close = function() { // フォームが編集モードの場合 if (document.querySelector('.edit-form').style.display === 'block') { if (confirm('編集中の内容があります。保存しますか?')) { // フォームを送信 document.getElementById('edit-form').dispatchEvent(new Event('submit')); } else { // 変更を破棄して閉じる document.getElementById('cancel-btn').click(); } } originalClose.apply(this, arguments); };

問題2: 非同期処理時の競合状態

データ保存中にユーザーが複数の操作を行うと、競合状態が発生することがあります。特に、保存処理が完了する前に別のマーカーを編集しようとした場合です。

解決策: 保存中であることを示すUIフィードバックと、保存が完了するまで操作を制限する仕組みを実装します。

Javascript
let isSaving = false; function saveMarkerData(data) { if (isSaving) { showNotification('保存中です、少々お待ちください', true); return; } isSaving = true; const saveBtn = document.querySelector('#edit-form button[type="submit"]'); const originalText = saveBtn.textContent; saveBtn.textContent = '保存中...'; saveBtn.disabled = true; fetch('/api/markers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) .then(response => response.json()) .then(result => { console.log('保存成功:', result); showNotification('情報が更新されました'); }) .catch(error => { console.error('保存エラー:', error); showNotification('保存に失敗しました', true); }) .finally(() => { isSaving = false; saveBtn.textContent = originalText; saveBtn.disabled = false; }); }

問題3: CORSの問題

ローカル環境で開発を行う場合、異なるオリジン(ポート番号が異なるなど)からのリクエストがブラウザによってブロックされることがあります。

解決策: JavaサーブレットでCORSを許可するフィルタを実装します。

Java
// CorsFilter.java import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebFilter("/*") public class CorsFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; HttpServletRequest httpRequest = (HttpServletRequest) request; // CORSヘッダーを設定 httpResponse.setHeader("Access-Control-Allow-Origin", "*"); httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); // プリフライトリクエストへの対応 if ("OPTIONS".equalsIgnoreCase(httpRequest.getMethod())) { httpResponse.setStatus(HttpServletResponse.SC_OK); return; } chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void destroy() {} }

まとめ

本記事では、Google Maps APIを使用してInfoWindowを編集可能にする方法について解説しました。

  • 要点1: InfoWindowのコンテンツを動的に生成し、フォーム要素を含むようにカスタマイズする
  • 要点2: イベントハンドリングを追加してユーザーの操作を検知し、UIを適切に切り替える
  • 要点3: Javaサーバーサイドと連携してデータを保存・更新し、適切なフィードバックを提供する

この記事を通して、Google Mapsをよりインタラクティブでユーザーフレンドリーなアプリケーションにするための実装スキルが身についたことと思います。今後は、複数マーカーの一括編集機能やリアルタイムでの同期機能など、さらに高度な機能についても記事にする予定です。

参考資料

参考にした記事、ドキュメント、書籍などがあれば、必ず記載しましょう。