ここ数年、自治体のオープンデータが増えています。その中でも、東京都のオープンデータは多岐にわたるデータが公開されており、アプリ開発に活用することができます。今回は、東京都オープンデータAPIカタログサイトの紹介と、実際にMonacaアプリの中で利用する方法を解説します。

東京都オープンデータAPIカタログサイトで扱っているデータ

東京都オープンデータカタログでは、執筆時点(2024年3月)で53,487件のAPIが公開されています。

たとえば、以下のようなデータが含まれています。

これらのデータは東京都全体で出ているものもあれば、市区町村ごとに分かれているものもあります。

データの使い方

API検索 - 東京都オープンデータAPIカタログサイト以下で公開されているものは、JSONまたはXMLによるAPIが公開されています。データセット - 東京都オープンデータカタログサイトは、ExcelやPDFファイルがアップロードされているので、そのままではアプリの中では使いづらいでしょう。

各データはライセンスが明記されています。ライセンスの範囲内で利用するようにしましょう。

Monacaアプリでの利用方法

今回は【位置情報】みどころマップ情報 三鷹市みどころマップ - 東京都オープンデータAPIカタログサイトを使って、取得したデータを地図に表示してみます。

サンプルプロジェクトのソースコードは、GitHubに配置しています。
https://github.com/monaca-samples/tokyo-open-data-sample-map

なお、検索はできるようですが、位置情報による絞り込みや並び替えはできないようなので、その点は注意が必要です。

データは以下のようなJSONで取得できます。データのキーが日本語なので注意してください。

{
  "total": 76,
  "subtotal": 1,
  "limit": 1,
  "offset": null,
  "metadata": {
    "apiId": "t132047d0000000004-44947822b3c13ba51b59e3278e2d018c-0",
    "title": "【位置情報】みどころマップ情報 三鷹市みどころマップ",
    "datasetId": "t132047d0000000004",
    "datasetTitle": "【位置情報】みどころマップ情報",
    "datasetDesc": "【三鷹市】三鷹市わがまちマップに掲載している観光・文化・自然のデータです。",
    "dataTitle": "三鷹市みどころマップ",
    "dataDesc": "既に「三鷹市わがまちマップ」の中で、美術館、歴史的建造物、公園などの観光・文化・自然の情報について「三鷹市みどころマップ」として公開しており、それらのデータファイル形式版(CSV)を掲載します。",
    "sheetname": "",
    "version": "1.0.2",
    "created": "",
    "updated": ""
  },
  "hits": [
    {
      "row": 1,
      "名称": "三鷹市大沢の里古民家",
      "ふりがな": "みたかしおおさわのさとこみんか",
      "所在地": "三鷹市大沢二丁目17-3",
      "電話番号": "0422-29-9862",
      "FAX": "0422-29-9040",
      "URL": "",
      "解説": "明治35(1902)年に創建された「四ツ間取り」の典型的な農家で、かつて、わさび栽培や養蚕などの生業を営んでいました。平成21(2009)年に三鷹市指定有形文化財に指定されました。",
      "連絡先": "三鷹市スポーツと文化部  生涯学習課",
      "開場時間": "10:00~17:00(11月~3月は16:00まで)",
      "定休日": "火曜日(火曜日が祝日にあたる場合は、翌日以降の最初の平日)。年末年始(12月28日~1月4日)",
      "料金": "200円(大沢の里水車経営農家との共通券。中学生以下は無料)10名以上の団体は予約が必要です。",
      "アクセス": "JR中央線三鷹駅南口より小田急バス「榊原記念病院」「朝日町三丁目」「車返団地」行きバス、「竜源寺」下車徒歩約5分",
      "駐車場": "なし",
      "大分類__": "歴史的建造物",
      "中分類__": "歴史的建造物",
      "小分類__": "歴史的建造物",
      "経度": 139.5313134,
      "緯度": 35.67902814
    }
  ]
}

今回はFramework7とMapboxを使っています。Mapboxの利用時にはユーザー登録と、アクセストークンの取得が必要です。

HTML

www/index.html では、MapboxのCSSとJavaScriptを読み込んでいます。また、ライブラリのturfも読込みます。

次に、turfライブラリについて説明します。これは、地図上の複数の位置情報から中心点を計算するのに便利なJavaScriptライブラリです。このライブラリを使うことで、地図上に複数の点がある場合にその中心を簡単に見つけ出すことができます。

<link href="https://api.mapbox.com/mapbox-gl-js/v3.1.2/mapbox-gl.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.1.2/mapbox-gl.js"></script>
<script src="https://unpkg.com/@turf/turf@6/turf.min.js"></script>

さらに、MapboxではWebワーカーを使用しているため、Content Security Policy(CSP)を設定して、ワーカーがblobにアクセスできるようにする必要があります。以下のメタタグは、そのCSP設定です。

<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: content: https://ssl.gstatic.com; style-src * "unsafe-inline"; script-src * "unsafe-inline" "unsafe-eval"; worker-src blob:;">

画面は別途 www/pages/map.html に記述しています。 index.html ではFramework7のルーティング設定のみです。

<div id="app">
  <div class="view view-init safe-areas" data-url="/map/">
  </div>
</div>

JavaScriptの初期設定

アプリの初期化の際に、Mapboxのアクセストークンを設定します。

const accessToken = "YOUR_ACCESS_TOKEN";
mapboxgl.accessToken = accessToken;

続いて www/js/route.js にてルーティング設定を行っています。

const routes = [
  {
    path: "/",
    url: "./index.html",
  },
  {
    path: "/map/",
    componentUrl: "./pages/map.html",
  }
];

ここからは www/pages/map.html に記述している内容を紹介します。

テンプレート

地図表示とリスト表示を切り替えるためのボタンを設置します。地図上のマーカーをタップした際に表示するシートもここに記述しています。

<template>
  <div class="page">
    <!-- Top Navbar -->
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner">
        <div class="title sliding">地図コンポーネント</div>
      </div>
    </div>
    <div class="page-content">
      <!-- 地図表示とリスト表示を切り替えます -->
      <p class="segmented segmented-raised">
        <button class="button button-map button-active" @click=${() => changeView("map")}>地図</button>
        <button class="button button-list" @click=${() => changeView("list")}>リスト</button>
      </p>
      <!-- 地図用 -->
      <span class="sub-page page-map">
        <div id='map'></div>
      </span>
      <!-- リスト用 -->
      <span class="sub-page page-list">
        <div class="list media-list">
          <ul>
            ${landmarks.map(landmark => $h`
              <li>
                <a href="#" class="item-content">
                  <div class="item-inner">
                    <div class="item-title-row">
                      <div class="item-title">${landmark['名称']}店</div>
                      <div class="item-after">${distance(landmark['緯度'], landmark['経度'])}km</div>
                    </div>
                    <div class="item-subtitle">${landmark['所在地']}</div>
                    <div class="item-text">${landmark['電話番号']}</div>
                  </div>
                </a>
              </li>
            `)}
          </ul>
        </div>
      </span>
    </div>
    <!-- マーカーをタップした際のシート表示用 -->
    <div class="sheet-modal landmark-sheet">
      <div class="toolbar">
        <div class="toolbar-inner">
          <div class="left"></div>
          <div class="right"><a class="link sheet-close" href="#">閉じる</a></div>
        </div>
      </div>
      <div class="sheet-modal-inner">
        <div class="block">
          <h4>${landmark['名称']}店</h4>
          <p>
            <ul>
              <li>電話番号: ${landmark['電話番号']}</li>
              <li>住所: ${landmark['所在地']}</li>
            </ul>
          </p>
        </div>
      </div>
    </div>
  </div>
</template>
<style>
  #map {
    width: 100%;
    height: 100%;
    position: relative;
  }
  canvas, .mapboxgl-canvas {
    height: 100%;
  }
  .page-list {
    display: none;
  }
</style>
<script>
  // JavaScriptをこの中に記述します
</script>

JavaScriptの実装

JavaScriptの実装を解説します。

まず、東京都オープンデータカタログのAPIのエンドポイントを設定します。

// オープンデータのURL
const url = 'https://service.api.metro.tokyo.lg.jp/api/t132047d0000000004-44947822b3c13ba51b59e3278e2d018c-0/json';
// 取得件数
const limit = 20;
// Framework7のコンポーネントを使うための記述
export default function (props, {$f7, $on, $onMounted, $update}) {
  // ここからの説明は、この中に記述していきます
  return $render;
}

必要な変数を定義します

  // Mapboxが初期化されているかチェックします
  if (typeof mapboxgl.accessToken === 'undefined') throw 'Mapboxが初期化されていません';
  // 地図コンポーネントで利用している変数
  let map;
  let landmarks = [];
  let landmark = {};
  let markers = [];
  let position = {};

Framework7の画面がマウントされた際には、 $onMounted が呼ばれます。

この中で地図の初期化や、データの取得と表示を行います。

// マウントされた際に実行します
$onMounted(async () => {
  // ローディングアイコンを表示
  app.preloader.show();
  try {
    // ポジションとして緯度経度を記録します(緯度・経度は三鷹市中央あたり)
    position = {
      latitude: 35.6835424,
      longitude: 139.5592677,
    };
    // 地図を表示します
    initMap();
    // 現在位置の付近にあるランドマーク情報を取得します
    landmarks = await getData();
    // 取得したランドマークを地図に表示します
    addLandmark();
    // 地図の表示を調整します
    fitBounds();
    // ローディングアイコンを消します
    app.preloader.hide();
  } catch (e) {
    console.log(e);
    $f7.dialog.alert("初期表示中にエラーが発生しました");
  }
});

initMap では、Mapboxの初期化を行います。

// 地図を初期化します
const initMap = () => {
  const { latitude, longitude } = position;
  map = new mapboxgl.Map({
    container: "map",
    style: "mapbox://styles/mapbox/streets-v11",
    center: [longitude, latitude],
    zoom: 11,
  });
}

getData では、APIからランドマーク情報を取得します。

// オープンデータからランドマーク情報を取得します
const getData = async () => {
  const res = await fetch(${url}?limit=${limit}, {
    body: JSON.stringify({}),
    headers: {
      Accept: "application/json",
      "Content-Type": "application/json"
    },
    method: "POST"
  });
  const json = await res.json();
  return json.hits;
}

addLandmark では、取得したランドマーク情報を地図に表示します。
JSONのキー名が日本語なので注意してください。

// 地図にマーカーを表示します
const addLandmark = () => {
  landmarks.forEach(landmark => {
    markers.push(new clickableMarker()
      .setLngLat([landmark["経度"], landmark["緯度"]])
      .setLandmark(landmark)
      .addTo(map));
  });
};

ランドマークを表示したら、各ランドマークがすべて表示されるように地図の表示を調整します。

// ランドマーク情報がすべて表示される形で地図の中心点を変更します
const fitBounds = () => {
  // turf.jsを利用します
  const line = turf.lineString(landmarks.map(landmark => [landmark["経度"], landmark["緯度"]]));
  const bbox = turf.bbox(line);
  map.fitBounds(bbox, {
    padding: {top: 10, bottom: 10, left: 10, right: 10}
  });
}

地図上のマーカーをクリックした際に、シートを表示するためのクラス clickableMarker を以下のように実装します。

// マーカーをタップした際にシートを表示するため、Markerクラスの処理を書き換えています
class clickableMarker extends mapboxgl.Marker{
  // ランドマーク情報をクラス内に入れておきます
  setLandmark(landmark) {
    this.landmark = landmark;
    return this;
  }
  // マーカーをタップした際のイベントです
  _onMapClick(e) {
    const targetElement = e.originalEvent.target;
    const element = this._element;
    if (targetElement === element || element.contains((targetElement))) {
      // ランドマーク情報をセットします
      landmark = this.landmark;
      // 表示を更新します
      $update();
      // シートを表示します
      app.sheet.open(".landmark-sheet");
    }
  }
}

地図とリスト表示の切り替え

アプリケーションでは、地図とリストの表示を切り替えることができます。

この切り替えを行うための関数 changeView を以下のように実装しています。

// 地図表示とマーカー表示を切り替えます
const changeView = (value) => {

  // セグメントボタンのアクティブを切り替えます
  $('.segmented button').removeClass('button-active');
  $(.button-${value}).addClass('button-active');

  // 地図とリスト表示を切り替えます
  $('.sub-page').hide();

  // 表示を更新します
  $update();
  $(.page-${value}).show();
};

この関数では、以下の処理を行っています。

  1. セグメントボタンのアクティブ状態を切り替えます。
  2. 地図とリストの表示を切り替えます。
  3. 表示を更新します。

ランドマークまでの距離計算

リスト表示では、現在地からランドマークまでの距離を計算して表示します。この距離計算を行うための関数 distance を以下のように実装しています。

// 一覧表示用に、ランドマークまでの距離を計算します
const distance = (latitude, longitude) => {
  const R = Math.PI / 180;
  latitude2 = position.latitude;
  longitude2 = position.longitude;
  latitude *= R;
  longitude *= R;
  latitude2 *= R;
  longitude2 *= R;
  return (6371
    * Math.acos(Math.cos(latitude)
    * Math.cos(latitude2)
    * Math.cos(longitude2 - longitude) + Math.sin(latitude)
    * Math.sin(latitude2))).toFixed(1); 
}

この関数では、以下の処理を行っています。

  1. 緯度経度をラジアンに変換します。
  2. 現在地の緯度経度を取得します。
  3. 球面三角法を用いて、現在地とランドマークの距離を計算します。
  4. 計算結果を小数点以下1桁に丸めて返します。

これで、オープンデータを取得して地図に表示するアプリができあがります。

まとめ

今回は東京都オープンデータAPIカタログサイトからデータを取得し、Monacaアプリに表示する方法を解説しました。オープンデータを活用することで、アプリの魅力を高められるでしょう。ぜひ、自治体のオープンデータを活用してみてください。

JSONデータはCORS上の制限もなさそうで、Monacaアプリからも利用できます。ぜひ皆さんのアプリでも利用してください。

東京都オープンデータカタログサイトホームページ