前回までの記事 アプリ開発を楽しむ【#1:アプリの概要】 アプリ開発を楽しむ【#2:環境構築1(React+TypeScript)】 アプリ開発を楽しむ【#3:環境構築2 (ESLint+Prettier)】 アプリ開発を楽しむ【#4:ヘッダー】 アプリ開発を楽しむ【#5:MyTogoList】 アプリ開発を楽しむ【#6:Reduxで状態管理1】 アプリ開発を楽しむ【#7:Reduxで状態管理2】 アプリ開発を楽しむ【#8:Google Map API】 アプリ開発を楽しむ【#9:新規追加モーダルUI(togo)】

今回のブログでは、次の2つのことをやっていきます。 ①Google Mapから座標を取得する ②検索した住所の地図を表示する

①は、新規のTogoを登録する際に座標が必要。 ②は、登録したい位置を住所などから検索し、その位置をGoogle Map上に表示させることで、ユーザーが登録したい場所を探しやすくする。

1.クリックした位置の座標を取得する

次のメソッドで座標を取得することができます。

const createMarker = (e: google.maps.MapMouseEvent) => {
    const lat = e.latLng?.lat();
    const lng = e.latLng?.lng();
    if (lat && lng) {
      console.log(lat, lng);
    }
};

これをfrontend/app/src/components/togo/AddTogoModal.tsxに記述します。

import React from 'react';
import { Marker } from '@react-google-maps/api';
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogTitle,
  IconButton,
  InputBase,
  Paper,
  TextField,
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import GMap from '../common/GMap';

type AddTogoModalProps = {
  isOpenAddTogoModal: boolean;
  handleCloseAddTogoModal: () => void;
};

const AddTogoModal: React.FC<AddTogoModalProps> = ({
  isOpenAddTogoModal,
  handleCloseAddTogoModal,
}) => {
  const mapStyles = {
    height: '500px',
    width: '100%',
  };

  const mapOptions = {
    gestureHandling: 'cooperative',
    zoomControl: false,
    scaleControl: false,
    streetViewControl: false,
    panControl: false,
    mapTypeControl: false,
    fullscreenControl: false,
  };

  const mapCenterPosition = { lat: 35.6808610662155, lng: 139.76856460990368 };
  const mapMarkerPosition = { lat: 35.6808610662155, lng: 139.76856460990368 };

  // ここから追加
  const createMarker = (e: google.maps.MapMouseEvent) => {
    const lat = e.latLng?.lat();
    const lng = e.latLng?.lng();
    if (lat && lng) {
      console.log(lat, lng);
    }
  };
  // ここまで追加

  return (
    <Dialog open={isOpenAddTogoModal} onClose={handleCloseAddTogoModal} fullWidth maxWidth="md">
      <DialogTitle>あなたが行きたいところを登録しましょう!</DialogTitle>
      <DialogContent>
        <TextField
          autoFocus
          margin="dense"
          id="name"
          label="場所"
          type="text"
          fullWidth
          variant="standard"
        />
        <TextField margin="dense" id="name" label="タグ" type="text" fullWidth variant="standard" />
        <Paper
          component="form"
          elevation={2}
          sx={{ p: '2px 4px', m: '10px 0px', display: 'flex', alignItems: 'center', width: 400 }}
        >
          <InputBase
            sx={{ ml: 1, flex: 1 }}
            placeholder="Search Google Maps"
            inputProps={{ 'aria-label': 'search google maps' }}
          />
          <IconButton sx={{ p: '10px' }} aria-label="search">
            <SearchIcon />
          </IconButton>
        </Paper>
        <Paper elevation={0} sx={{ mt: 2 }}>
          <GMap
            mapStyles={mapStyles}
            zoom={10}
            mapCenterPosition={mapCenterPosition}
            mapOptions={mapOptions}
                        
            // ここから追加
            // クリック時に座標を取得する関数をGMapコンポーネントにpropsとして渡しています。
            handleClick={(e) => createMarker(e)}
            // ここまで追加

          >
            {mapMarkerPosition && <Marker position={mapMarkerPosition} />}
          </GMap>
        </Paper>
      </DialogContent>
      <DialogActions sx={{ mb: 1, ml: 2, display: 'flex', justifyContent: 'flex-start' }}>
        <Button variant="contained">登録</Button>
        <Button variant="outlined" onClick={handleCloseAddTogoModal}>
          キャンセル
        </Button>
      </DialogActions>
    </Dialog>
  );
};

export default AddTogoModal;

GMapコンポーネントも修正します。 frontend/app/src/components/common/GMap.tsx

import React from 'react';
import { GoogleMap, LoadScript } from '@react-google-maps/api';

type GMapProps = {
  children?: React.ReactNode;
  mapStyles: { height: string; width: string };
  zoom: number;
  mapCenterPosition: { lat: number; lng: number };
  mapOptions: {
    gestureHandling: string;
    zoomControl: boolean;
    scaleControl: boolean;
    streetViewControl: boolean;
    panControl: boolean;
    mapTypeControl: boolean;
    fullscreenControl: boolean;
  };

  // ここから追加
  handleClick?: (e: google.maps.MapMouseEvent) => void;
  // ここまで追加

};

const GMap: React.FC<GMapProps> = ({
  children,
  mapStyles,
  zoom,
  mapCenterPosition,
  mapOptions,

  // ここから追加
  handleClick,
  // ここまで追加

}) => (
  <LoadScript googleMapsApiKey={import.meta.env.VITE_GOOGLE_MAP_API_KEY}>
    <GoogleMap
      mapContainerStyle={mapStyles}
      zoom={zoom}
      center={mapCenterPosition}
      options={mapOptions}

      // ここから追加
      onClick={handleClick}
      // ここまで追加

    >
      {children}
    </GoogleMap>
  </LoadScript>
);

export default GMap;

これで、Google Map上をクリックすると座標が取得できます。(consoleに表示)。ただし、マーカーは移動しませんので、マーカーをクリックした位置に移動するようにしていきます。

frontend/app/src/components/togo/AddTogoModal.tsx

// ここから修正
import React, { useState } from 'react';
// ここまで修正

import { Marker } from '@react-google-maps/api';
import {
  Button,
  Dialog,
  DialogActions,
  DialogContent,
  DialogTitle,
  IconButton,
  InputBase,
  Paper,
  TextField,
} from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import GMap from '../common/GMap';

type AddTogoModalProps = {
  isOpenAddTogoModal: boolean;
  handleCloseAddTogoModal: () => void;
};

const AddTogoModal: React.FC<AddTogoModalProps> = ({
  isOpenAddTogoModal,
  handleCloseAddTogoModal,
}) => {
  const mapStyles = {
    height: '500px',
    width: '100%',
  };

  const mapOptions = {
    gestureHandling: 'cooperative',
    zoomControl: false,
    scaleControl: false,
    streetViewControl: false,
    panControl: false,
    mapTypeControl: false,
    fullscreenControl: false,
  };

  const mapCenterPosition = { lat: 35.6808610662155, lng: 139.76856460990368 };

  // ここから削除
  const mapMarkerPosition = { lat: 35.6808610662155, lng: 139.76856460990368 };
  // ここまで削除

  // ここから追加
  // マーカーの位置情報を保持するステートを定義
  const [mapMarkerPosition, setMapMarkerPosition] = useState({
    lat: 35.6808610662155,
    lng: 139.76856460990368,
  });
  // ここまで追加

  const createMarker = (e: google.maps.MapMouseEvent) => {
    const lat = e.latLng?.lat();
    const lng = e.latLng?.lng();
    if (lat && lng) {

    // ここから削除
    console.log(lat, lng)
    // ここまで削除
            
    // ここから追加
    // mapMarkerPositionのステートを変更
    setMapMarkerPosition({ lat, lng });
    // ここから追加
            
    }
  };

マーカーの位置情報をuseStateを使って保持し、地図上をクリックしたときにその座標を変更できるようにしました。 これでGoogle Map上をクリックしたときに、マーカーがクリックした位置に表示されるようになりました。

2.住所検索をもとに地図を表示する

住所から座標を取得するには以下のメソッドでできます。

const geocode = () => {
    const geocoder = new window.google.maps.Geocoder();
    geocoder
      .geocode({ address: 住所などの地名 }, (results, status) => {
        if (results && status === google.maps.GeocoderStatus.OK) {
          const lat = results[0].geometry?.location.lat();
          const lng = results[0].geometry?.location.lng();
          console.log(lat, lng)
        }
      })
      .catch((err) => console.log(err));
 };

frontend/app/src/components/todo/AddTogoModal.tsxを修正していきます。

  // ここから削除
  const mapCenterPosition = { lat: 35.6808610662155, lng: 139.76856460990368 };
  // ここまで削除

  // ここから追加
  // 検索ワードを保持するステートを定義
  const [searchLocationKeyword, setSearchLocationKeyword] = useState<string>('');

  // 取得した座標を地図の中央にするためのステートを定義
  const [mapCenterPosition, setMapCenterPosition] = useState({
    lat: 35.6808610662155,
    lng: 139.76856460990368,
  });
  // ここまで追加

  const [mapMarkerPosition, setMapMarkerPosition] = useState({
    lat: 35.6808610662155,
    lng: 139.76856460990368,
  });

  const createMarker = (e: google.maps.MapMouseEvent) => {
    const lat = e.latLng?.lat();
    const lng = e.latLng?.lng();
    if (lat && lng) {
      setMapMarkerPosition({ lat, lng });
    }
  };

  // ここから追加
  const geocode = () => {
    const geocoder = new window.google.maps.Geocoder();
    geocoder
      .geocode({ address: searchLocationKeyword }, (results, status) => {
        if (results && status === google.maps.GeocoderStatus.OK) {
          const lat = results[0].geometry?.location.lat();
          const lng = results[0].geometry?.location.lng();
          setMapCenterPosition({ lat, lng });
        }
      })
      .catch((err) => console.log(err));
  };

  const searchLocation = () => {
    if (!searchLocationKeyword) {
      return;
    }
    geocode();
  };

  // Enter Keyを押したときに実行するメソッド
  const handleKeyPressSearchLocation = (e: React.KeyboardEvent) => {
    if (e.key === 'Enter') {
      e.preventDefault();
      searchLocation();
    }
  };

  const handleSearchLocation = () => {
    searchLocation();
  };
  // ここまで追加


return (
    <Dialog open={isOpenAddTogoModal} onClose={handleCloseAddTogoModal} fullWidth maxWidth="md">
      <DialogTitle>あなたが行きたいところを登録しましょう!</DialogTitle>
      <DialogContent>
        <TextField
          autoFocus
          margin="dense"
          id="name"
          label="場所"
          type="text"
          fullWidth
          variant="standard"
        />
        <TextField margin="dense" id="name" label="タグ" type="text" fullWidth variant="standard" />
        <Paper
          component="form"
          elevation={2}
          sx={{ p: '2px 4px', m: '10px 0px', display: 'flex', alignItems: 'center', width: 400 }}
        >
          <InputBase
            sx={{ ml: 1, flex: 1 }}
            placeholder="Search Google Maps"
            inputProps={{ 'aria-label': 'search google maps' }}

            // ここから追加
            onChange={(e) => setSearchLocationKeyword(e.target.value)}
            onKeyDown={handleKeyPressSearchLocation}
            // ここまで追加

          />
                    
          // ここから修正
          <IconButton sx={{ p: '10px' }} aria-label="search" onClick={handleSearchLocation}>
          // ここまで修正

            <SearchIcon />
          </IconButton>
        </Paper>
        <Paper elevation={0} sx={{ mt: 2 }}>
          <GMap
            mapStyles={mapStyles}
            zoom={10}
            mapCenterPosition={mapCenterPosition}
            mapOptions={mapOptions}
            handleClick={(e) => createMarker(e)}
          >
            {mapMarkerPosition && <Marker position={mapMarkerPosition} />}
          </GMap>
        </Paper>
      </DialogContent>
      <DialogActions sx={{ mb: 1, ml: 2, display: 'flex', justifyContent: 'flex-start' }}>
        <Button variant="contained">登録</Button>
        <Button variant="outlined" onClick={handleCloseAddTogoModal}>
          キャンセル
        </Button>
      </DialogActions>
    </Dialog>
  );
};

これで、住所検索をもとに地図を表示することができるようになりました。

最後になりますが、mapCenterPositionとmapMarkerPositionの型定義ができていないので、定義しておきたいと思います。

const [mapCenterPosition, setMapCenterPosition] = useState({
    lat: 35.6808610662155,
    lng: 139.76856460990368,
  });
  const [mapMarkerPosition, setMapMarkerPosition] = useState({
    lat: 35.6808610662155,
    lng: 139.76856460990368,
  });

frontend/app/src/types/map.tsを次のようにつくります。

export type MapPosition = {
  lat: number;
  lng: number;
};

frontend/app/src/components/togo/AddTogo.tsxを修正します。

import SearchIcon from '@mui/icons-material/Search';
import GMap from '../common/GMap';

// ここから追加
import type { MapPosition } from '../../types/map';
// ここまで追加

type AddTogoModalProps = {
  isOpenAddTogoModal: boolean;
  handleCloseAddTogoModal: () => void;
};

const AddTogoModal: React.FC<AddTogoModalProps> = ({
  isOpenAddTogoModal,
  handleCloseAddTogoModal,
}) => {
  const mapStyles = {
    height: '500px',
    width: '100%',
  };

  const mapOptions = {
    gestureHandling: 'cooperative',
    zoomControl: false,
    scaleControl: false,
    streetViewControl: false,
    panControl: false,
    mapTypeControl: false,
    fullscreenControl: false,
  };

  const [searchLocationKeyword, setSearchLocationKeyword] = useState<string>('');

  // ここから修正
  const [mapCenterPosition, setMapCenterPosition] = useState<MapPosition>({
    lat: 35.6808610662155,
    lng: 139.76856460990368,
  });
  const [mapMarkerPosition, setMapMarkerPosition] = useState<MapPosition>({
    lat: 35.6808610662155,
    lng: 139.76856460990368,
  });
  // ここまで修正

  const createMarker = (e: google.maps.MapMouseEvent) => {
    const lat = e.latLng?.lat();
    const lng = e.latLng?.lng();
    if (lat && lng) {
      setMapMarkerPosition({ lat, lng });
    }
  };

今回はここまでにしておきます。 次回は、Togoを登録できるようにして、MyTogoListに表示できるようにまでしたいと思います。

コードはGitHubに置いてありますのでよければ参考にしてください。 mainブランチは常に最新のものになります。 今回の内容はblog_10のブランチを参照してください。 https://github.com/KINE-M/togo_app