【外部公開もOK】ReactとLeafletを使ったお手軽地図アプリの作成方法

MaterialUI
 

はじめに

  • Reactが少しずつ分かってきた!何かお手軽なアプリを作ってみたい!
  • 就活で勉強をしていることをアピールしたいのだが、口だけでは説得力ないなぁ〜(笑)
  • 就活で成果物を披露したい!けどPC持っていくのもなぁ〜

こんな悩みの一助となれるような無料で作成できる地図サービスを使ったアプリを今回は作成していきます。

第1弾ではTodoアプリを作成してみたので良かったら作ってみてください!

今回で第2弾となります。

地図サービスをアプリに組み込んでちょっとイケたアプリを作っていこうと思います!

地図アプリといえば、GoogleMapを思い浮かべることと思いますが、なんせAPIを使うのにお金がかかります(笑)

ですが、今回使用するOpenStreetMapという地図サーバーは無料で使えます!!!!

オープン系の作成者様にはリスペクトしかないですね…

ということで、OpenStreetMap(以下OSM)を使ってお手軽地図アプリを作っていきましょう!

どんなアプリを作るのか

外部に公開!?!

まず最初にタイトルにもあった「外部に公開できる!」というのを説明していきます。

Reactでお手軽地図アプリ - CodeSandbox
Reactでお手軽地図アプリ by atsushiIMG using @material-ui/core, leaflet, react, react-dom, react-leaflet, react-scripts

今回、codesandboxというオンラインでWeb開発ができてしまうエディタツールを使ってTodoアプリを作成していきます。(似たようなものでcodepenやsocket.io)

このツールですが、開いていただくとわかるように、ソースと並行してブラウザが存在していることがわかります。

ですので、そのURLにアクセスしてあげることでどの端末からもそのアプリにアクセスでき、誰でも確認することができるのです。

地図サービスアプリの概要

今回作成する地図アプリ

このアプリのテーマは「G7の場所とその首都くらい覚えときたい!」です。

最近の世界情勢を見ていた時に「脱エネルギー」だの「NATOの加入」だのニュースを見たりするのですが、各国の場所を知らずしてニュースは見れないだろう、ということで私の好きなアプリを作ることとかけ合わせてついでに覚えてしまおう!ということでこのアプリを作成するに至りました。笑

機能要件としては大まかに以下の3機能となります。

  • 世界地図を表示する
  • 各国のボタンをクリックすると、その国の首都にピンたてされ、地図が遷移する
  • ピンをクリックするとその国の首都名が表示される

技術要件

  • React(主にHookを使用)
  • Material UI
  • React-Leaflet

Material-UIとはスタイルがコンポーネントとして用意されている便利なフレームワークです。

最新バージョンはV5系ではありますが、codesandboxにV4系しか無かったので今回は申し訳ないのですが、V4系を使用していきます。

下の記事を確認いただくと分かるように、V4とV5ではもはやモジュール名が異なります。

React-LeafletとはOSMの地図データをReactにてうまく扱えるモジュールです。

マップを表示したり、ピンを立てたりできるコンポーネントが用意されています。

LeafletとOSMは別物です!

Leafletの初回起動はこちらから

実装方法

環境構築

今回の環境構築はほんとに秒で終わります。(codesandboxのいいとこ!)

まず、以下のURLにアクセスします。

その後「Fork」ボタンを押下して、何かしらのアカウントでサインアップしてあげれば、あなただけのWebアプリがもう完成です。

以下の3ステップで地図アプリを作成していこうと思います。

  • React-Leafletで地図を描画する
  • ボタン押下時の処理を追加する
  • 全体的なスタイルを整える

React-Leafletで地図を描画する

公式からソースをパクってきて地図を描画してみる

Reactはコンポーネント指向ですので、機能ごとにファイルを分けてあげると優秀みたいです。

私も拙くはありますが、それに乗っ取ってやっていきたいと思います。

と言うことで、まずはそのコンポーネントを集めた「componentsフォルダ」を作成してあげます。

次にその中に「MapArea.js」というファイルを作成します。

そこにはReact-leafletにて取得した地図を描画するコードを記述し、App.jsにてMapArea.jsの内容を描画してあげます。

地図を描画するコードは以下のReact-Leafletの公式サイトに飛び、地図を描画するコードをパクってきます!

App.js

import MapArea from "./components/MapArea";
import "./styles.css";

export default function App() {

  return (
    <div className="App">
      <MapArea />
    </div>
  );
}

MapArea.js

import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";

const MapArea = () => {
  const position = [51.505, -0.09];
  const MapContainerStyle = {
    width: "400px",
    height: "300px",
    display: "inline-block",
    padding: "10px"
  };
  return (
    <MapContainer center={position} zoom={13} scrollWheelZoom={false} style={MapContainerStyle}>
    <TileLayer
      attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
      url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
    />
    <Marker position={position}>
      <Popup>
        A pretty CSS3 popup. <br /> Easily customizable.
      </Popup>
    </Marker>
  </MapContainer>
  );
};

export default MapArea;

ここでなぜかひっかけなのですが、公式サイトのソースをコペピしただけではレイアウトが崩れた地図が表示されるようになります。

公式サイトの別ページに記載されているのですが、「MapContainer」にてスタイルを定義してあげる必要があります。

それが、上記ソースのconstにて定義されている「MapContainerStyle」です。

それでも地図が正しく表示されない時はHTMLファイルにてCDNをロードしてあげましょう。

以下にもまとめているので、どうしても表示されなければ確認してみて下さい。

上記ソースにより出力される地図の様子

試しに東京にピン刺ししてみる

上記のソースによってロンドンにピン刺しされた地図が出力されると思います。

このピン刺しは「Marker」タグのposition属性の値によって管理されており、初期表示にロンドンが中央に来ているのは「MapContainer」のcenter属性、マップのズーム強度はzoom属性によって管理されています。

今回はボタンを押下することで各国にピンが立ち、そのピンが中央に来るようにしたいです。

ですので、中央の座標を何かしらの形で動的に保持しておく必要があります。

そこでReactのuseStateにてその座標を保持させることで後で動的に座標を変更することができます。

前段階として東京にピン刺しをやってみます。

App.js

import { useState } from "react";
import MapArea from "./components/MapArea";
import "./styles.css";

export default function App() {
  const [coordinate, setCoordinate] = useState({
    latitude: 35.6803997,
    longitude: 139.7690174,
    capital: "Tokyo"
  });
  return (
    <div className="App">
      <MapArea coordinate={coordinate} />
    </div>
  );
}

MapArea.js

import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
import ChangeCenter from "./ChangeCenter";

const MapArea = (props) => {
  // const position = [51.505, -0.09];
  const MapContainerStyle = {
    width: "400px",
    height: "300px",
    display: "inline-block",
    padding: "10px"
  };
  return (
    <MapContainer
      center={[props.coordinate.latitude, props.coordinate.longitude]}
      zoom={5}
      scrollWheelZoom={false}
      style={MapContainerStyle}
    >
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker
        position={[props.coordinate.latitude, props.coordinate.longitude]}
      >
        <Popup>{props.coordinate.capital}</Popup>
      </Marker>
    </MapContainer>
  );
};

export default MapArea;

今回は「App.js」にて描画されるピン刺しの位置をuseStateにて管理しています。

「App.js」のMapAreaコンポーネントにuseStateにて管理された値を渡します。

それを「MapArea.js」にてpropsとして受け取ります。

useStateはオブジェクトにて管理されているので、positionやcenterに適宜値を参照してあげると無事に表示されます。

東京駅から少しずれてしまいましたね…

上記ソースにより出力されるイメージ

ボタン押下時の処理を追加する

ボタンを画面に追加してフランスにピン刺しする

段階としては、まずボタンを追加して画面表示させてあげ、そこからボタンクリックのイベントメソッドを記述していく流れです。

新規で作成するボタンものちにG7の数だけ(7個)作りたいので、コンポーネントで一つ作っておきます。

やっとコンポーネントに分けることのメリットに出会いましたね。笑

一つ作成しておくだけで、残りの6個は再利用できるというわけです。

App.js

import { useState } from "react";
import CountryButton from "./components/CountryButton";
import CountryButtons from "./components/CountryButtons";
import MapArea from "./components/MapArea";
import "./styles.css";

export default function App() {
  const [coordinate, setCoordinate] = useState({
    latitude: 35.6803997,
    longitude: 139.7690174,
    capital: "Tokyo"
  });
  return (
    <div className="App">
      <MapArea  />
      <CountryButton
        countryInfo={{
          capital: "パリ",
          country: "フランス",
          latitude: 48.856614,
          longitude: 2.3522219
        }}
        setCoordinate={setCoordinate}
      />
    </div>
  );
}

CountryButton.js

import { Button, Grid, makeStyles } from "@material-ui/core";

const useStyles = makeStyles((theme) => ({
  styledButton: {
    width: "200px"
  }
}));
const CountryButton = (props) => {
  const classes = useStyles();
  const buttonClick = () => {
    props.setCoordinate({
      latitude: props.countryInfo.latitude,
      longitude: props.countryInfo.longitude,
      capital: props.countryInfo.capital
    });
  };
  return (
      <Button
        variant="contained"
        className={classes.styledButton}
        value={props.countryInfo.capital}
      >
        {props.countryInfo.country}
      </Button>
  );
};
export default CountryButton;

今回ButtonはMaterialUIを使ってスタイルしています。(MaterialUI相当助かってます。。)

「CountryButton」コンポーネントは後々に再利用もするため、Buttonに表示される文字(今回はフランスですが)も引数で取得するようにしています。

何度もしつこいようですが、これにより引数に他の国名を渡しあげることで再利用できるという訳です。

ボタンクリック時のイベントメソッドは「CountryButton.js」のbuttonClickメソッドにて定義されています。

その中では引数で渡されたフランスの座標を「App.js」の座標セッターメソッドに渡して座標を更新してあげています。

ボタンの名称は少し下の画像とは違いますが、パリにピン刺しすることができたと思います。

上記コードを実装した様子

ピン刺しされた箇所に地図遷移するように調整する

さて、先ほどのコードにてボタン押下時に「App.js」の座標定義を更新してあげるロジックを構築してあげたと思います。

が!何かおかしくないですか?

ピン刺しの位置は変わったのに、地図の中央に来る座標は変わっていないと思います。

これはMapContainerの仕様みたいで、一度定義された地図中央の座標は不変みたいです。

ですが、それでは困るので別で地図中央の座標を変えてあげるメソッドが用意されています。

MapArea.jsの一部

  return (
    <MapContainer
      center={[props.coordinate.latitude, props.coordinate.longitude]}
      zoom={5}
      scrollWheelZoom={false}
      style={MapContainerStyle}
    >
      <ChangeCenter
        center={[props.coordinate.latitude, props.coordinate.longitude]}
      />
      <TileLayer
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
      />
      <Marker
        position={[props.coordinate.latitude, props.coordinate.longitude]}
      >
        <Popup>{props.coordinate.capital}</Popup>
      </Marker>
    </MapContainer>
  );
};

ChangeCenter.js

import { useMap } from "react-leaflet";

const ChangeCenter = (props) => {
  const map = useMap();
  map.setView(props.center);
  return null;
};

export default ChangeCenter;

新しく作成したChangeCenter.jsコンポーネントをMapContainerコンポーネントでラップしてあげるような形で定義してあげます。

ChangeCenterコンポーネントのsetViewメソッドに地図中央の座標を渡してあげることで、描画されている地図の中央が更新される仕組みになっています。

これにより、「フランスボタン」を押下した時にピン刺しとそこに地図遷移されると思います。

(*´∀`*)

全体的なスタイルを整える

最後はこのアプリの主題であったG7の位置と名称を覚えると言うことで、ボタンを数を増やして全体的なスタイルを整えていきます。

作業イメージとしては、

  • CountryButtonsという新たなコンポーネントを作成
  • その中に7個のCountryButtonを配置
  • App.jsにCountryButtonsを定義する

App.js

import { useState } from "react";
import CountryButton from "./components/CountryButton";
import CountryButtons from "./components/CountryButtons";
import MapArea from "./components/MapArea";
import "./styles.css";

export default function App() {
  const [coordinate, setCoordinate] = useState({
    latitude: 35.6803997,
    longitude: 139.7690174,
    capital: "Tokyo"
  });
  return (
    <div className="App">
      <MapArea coordinate={coordinate} />
      <CountryButtons setCoordinate={setCoordinate} />
    </div>
  );
}

CountryButtons.js

import { Grid } from "@material-ui/core";
import CountryButton from "./CountryButton";

const CountryButtons = (props) => {
  return (
    <Grid container spacing={3} alignItems="center" justifyContent="center">
      <CountryButton
        countryInfo={{
          capital: "パリ",
          country: "フランス",
          latitude: 48.856614,
          longitude: 2.3522219
        }}
        setCoordinate={props.setCoordinate}
      />
      <CountryButton
        countryInfo={{
          capital: "ワシントンD.C.",
          country: "アメリカ合衆国",
          latitude: 38.9071923,
          longitude: -77.0368707
        }}
        setCoordinate={props.setCoordinate}
      />
      <CountryButton
        countryInfo={{
          capital: "ロンドン",
          country: "イギリス",
          latitude: 51.5072178,
          longitude: -0.1275862
        }}
        setCoordinate={props.setCoordinate}
      />
      <CountryButton
        countryInfo={{
          capital: "ベルリン",
          country: "ドイツ",
          latitude: 52.5200066,
          longitude: 13.404954
        }}
        setCoordinate={props.setCoordinate}
      />
      <CountryButton
        countryInfo={{
          capital: "ローマ",
          country: "イタリア",
          latitude: 41.9027835,
          longitude: 12.4963655
        }}
        setCoordinate={props.setCoordinate}
      />
      <CountryButton
        countryInfo={{
          capital: "オタワ",
          country: "カナダ",
          latitude: 45.4215296,
          longitude: -75.6971931
        }}
        setCoordinate={props.setCoordinate}
      />
      <CountryButton
        countryInfo={{
          capital: "東京",
          country: "日本",
          latitude: 35.6803997,
          longitude: 139.7690174
        }}
        setCoordinate={props.setCoordinate}
      />
    </Grid>
  );
};

export default CountryButtons;

G7それぞれの座標を調べるのは面倒だと思うので、適宜ここからコピペしてってください。

部品を等間隔に並べるのにかなり最適かつ簡単なMaterialUIのGridを使っています。

親GridにContainer、子GridにItemを指定してあげることで簡単にそれっぽいボタンを作成することができます。

できた ∩^ω^∩

さいごに

ここまでお疲れ様でした。

作成したアプリは添付画像の赤枠のURLにアクセスすることで外部からアプリにアクセスすることができます。

Youtubeにも作成風景の動画を載せますので合わせてご確認いただけると幸いです。

G7覚えます!

おわり

コメント

タイトルとURLをコピーしました