8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

HERE Trafficを使って、事故情報を地図上にマッピング(React+FastAPI)してみた。

Last updated at Posted at 2024-08-03

とある件で地図関連の内容を扱う必要があったので、ちょっとしたアプリを作成してみました。内容はHERE TrafficAPIを利用してニューヨークの事故情報を地図上にマッピングさせるというものになっています。地図は情報も豊富なReact Leafletを使っています。

前提

OS:Linux amznlinux2023 6.1.94-99.176.amzn2023.x86_64
FastAPI:0.111.1
node:v18.20.4
npm:10.7.0
react-leaflet:4.2.1
axios:1.7.3

事前準備

まずはHereプラットフォームサイトに登録し、APIキーをゲットしましょう。

また、地図上には2地点間の線を引くわけですが、そのままだと地図上の道なりに表示がされず直線表示されてしまうため、Mapbox Directions APIに登録してこちらのAPIキーも利用て道なりに線が表示されるようにします。以下のサイトから登録できます。

1. エンドポイント作成

まずはFastAPIでReactからリクエストを受け付けるエンドポイントを作成します。

main.py
import json
import requests
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/incident")
def search():
    base_url = "https://data.traffic.hereapi.com/v7/incidents"
    api_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # Here trafficのAPIキー
    bbox_v7 = "-74.2591,40.4774,-73.7002,40.9176"  # ニューヨークの西経、南緯、東経、北緯を入れます。

    params = {
        "locationReferencing": "shape",
        "in": f"bbox:{bbox_v7}",
        "apiKey": api_key
    }

    response = requests.get(base_url, params=params)
    if response.status_code != 200:
        raise HTTPException(status_code=response.status_code, detail="Failed to fetch incidents")

    data = response.json()
    incidents = data.get('results', [])
    resultdata = []
    for incident in incidents:
        location = incident.get('location', {}).get('shape', {}).get('links', [])[0].get('points', [])
        description = incident.get('incidentDetails', {}).get('description', {}).get('value', '')
        if location:
            start_point = location[0]
            end_point = location[-1]
            resultdata.append({
                "Start Point": {"Lat": start_point['lat'], "Lng": start_point['lng']},
                "End Point": {"Lat": end_point['lat'], "Lng": end_point['lng']},
                "Description": description
            })

    return {"data": resultdata}

Here TrafficのAPI関する情報は以下ページより確認できます。

作成できたら、以下コマンドで起動しておきます。

uvicorn main:app --host 0.0.0.0 --reload

2. Reactコード実装

src/App.js
import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, Popup, Polyline } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L from 'leaflet';
import axios from 'axios';

delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
  iconRetinaUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon-2x.png',
  iconUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-icon.png',
  shadowUrl: 'https://unpkg.com/leaflet@1.7.1/dist/images/marker-shadow.png',
});

const App = () => {
  const [routes, setRoutes] = useState([]);
  const mapboxToken = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; // Mapbox APIキー

  useEffect(() => {
    const fetchData = async () => {
      try {
        const apiResponse = await axios.get('http://[FastAPI_IP:8000]/incident');
        const data = apiResponse.data.data;

        const routePromises = data.map(async (route) => {
          const startPoint = [route['Start Point'].Lat, route['Start Point'].Lng];
          const endPoint = [route['End Point'].Lat, route['End Point'].Lng];
          const url = `https://api.mapbox.com/directions/v5/mapbox/driving/${startPoint[1]},${startPoint[0]};${endPoint[1]},${endPoint[0]}?geometries=geojson&access_token=${mapboxToken}`;
          const response = await axios.get(url);
          const routeData = response.data.routes[0].geometry.coordinates;
          return {
            start: startPoint,
            end: endPoint,
            description: route.Description,
            path: routeData.map(coord => [coord[1], coord[0]])
          };
        });

        const allRoutes = await Promise.all(routePromises);
        setRoutes(allRoutes);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, [mapboxToken]);

  return (
    <MapContainer center={[40.81092, -73.93109]} zoom={15} style={{ height: '100vh', width: '100%' }}>
      <TileLayer
        url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
      />
      {routes.map((route, index) => (
        <React.Fragment key={index}>
          <Marker position={route.start}>
            <Popup>
              Start Point: {route.description}
            </Popup>
          </Marker>
          <Marker position={route.end}>
            <Popup>
              End Point: {route.description}
            </Popup>
          </Marker>
          <Polyline positions={route.path} color="blue" />
        </React.Fragment>
      ))}
    </MapContainer>
  );
}

export default App;

3. ブラウザからアクセス確認

それでは、ブラウザからアクセス確認してみます。

http://[ip]:3000/

image.png

2地点間の線が直線ではなく、道なりに表示されていることがわかります。クローズアップしてみます。

image.png

また、Pinマークをクリックすると詳細がわかります。
image.png

その他

残念ながらHere Trafficの事故情報ですが、日本は非対応でした。(ただ別サービスは+&を加えることで色々なことに利用できそうだなと思いました。)

image.png
image.png

8
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?