Reactベースのルート検索アプリを作成してみる

**※ 記事内のコードを最新のHARP engine対応に変更しました。**
# はじめに
HEREでエンジニアをしておりますkekishidaと申します。
以前、拙記事においてHERE Maps API for Javascripを使用してReactベースで簡単な地図アプリ作成方法の紹介をしました。

https://qiita.com/kekishida/items/e3cfb97f2e7458fb0610

今回はこちらの記事で紹介したサンプルをベースにルート検索アプリを作成してみたいと思います。
完成イメージは以下の通りです。
それぞれのテキストボックス(origin/destination)に出発地及び目的地の名前を入力し、Calculate ROUTEボタンをクリックすることで、ルート検索されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/c4275493-a663-7d44-e4fc-affed5d779be.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/b579f64b-0843-8882-76a3-9a7c1a6383ea.png)
Zoom levelを上げてみると、初台南ランプ（東京）より高速に入り、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/ac7e72df-b100-234f-a513-149272367e76.png)
森ノ宮ランプ（大阪）より高速から降りている経路となっていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/ef254d7a-8bfb-f5f9-fd4d-b8ed5c435be5.png)

今回使用するプロダクトおよびサービスは以下の通りです。

- HERE Maps API for Javascript

https://www.here.com/docs/bundle/maps-api-for-javascript-developer-guide/page/README.html

- HERE Geocoding & Search

https://www.here.com/docs/bundle/geocoding-and-search-api-developer-guide/page/README.html

- HERE Routing v8

https://www.here.com/docs/bundle/routing-api-developer-guide-v8/page/README.html

それでは始めましょう。

# 必要なもの
ご紹介するサンプルコードは、HEREアカウントの取得が必要になります。
## HEREアカウントの取得
以下の記事が大変参考になります。

https://qiita.com/kekomat/items/d2e29bac84a27c1ec521

# 作成手順 (step by step)
まずは、以下拙記事のサンプルコードを実装してください。

https://qiita.com/kekishida/items/e3cfb97f2e7458fb0610

## Routing.jsの作成
新規コンポーネントRouting.jsを作成します。こちらのコンポーネントでは出発地、目的地をそれぞれ入力して、Geocoding&Search APIとRouting  APIを組み合わせてルート検索結果を求めます。
```javascript:Routing.js
import * as React from 'react';
import { useState } from 'react';


const Routing = (props) => {
  const [text1,setText1] = useState("");
  const [text2,setText2] = useState("");

  const apikey = props.apikey;

  const H = window.H;
  const platform = new H.service.Platform({
      apikey: apikey
  });
  const searchService = platform.getSearchService();
  const routingService = platform.getRoutingService(null, 8);
  const routingFunction =(origin, destination) => {
      let onError = (error) => {
          alert(error.message);
      }
      let onResult = function(result) {
        if (result.routes.length) {
          result.routes[0].sections.forEach((section) => {
              props.onRoutingResult(section.polyline);
          });
        }
      }
      let routingParameters = {
        'transportMode': 'car',
        'return': 'polyline'
      };
      let calculateRoute = () => {
        if (!origin || !destination) return;
        routingParameters.origin = origin.lat+","+origin.lng;
        routingParameters.destination = destination.lat+","+destination.lng;
        routingService.calculateRoute(routingParameters, onResult, onError);
      }
      calculateRoute();
  }
  const onClickRouting = () => {
      searchService.geocode({
        q: text1,
        limit: 1
      }, (result) => {
        result.items.forEach((item) => {
          console.log(item.position);
          let origin = item.position;
          props.onOriginResult(origin);
          searchService.geocode({
            q: text2,
            limit: 1
          }, (result) => {
            result.items.forEach((item) => {
              console.log(item.position);
              let destination = item.position;
              props.onDestinationResult(destination);
              routingFunction(origin,destination);
            });
          }, alert);
            });
      }, alert);
      setText1("");
      setText2("");
  }


  return (
    <div >
        <div >
          <label>
            Origin : 
          </label>
          &nbsp;
          <input type="text" name="origin" id="query1" value={text1}
              onChange={(event)=>setText1(event.target.value)}
              placeholder="Origin location" />
          &emsp;
          <label>
            Destination :
          </label>
          &nbsp;
          <input type="text" name="destination" id="query2" value={text2}
              onChange={(event)=>setText2(event.target.value)}
              placeholder="Destination location" />
        </div>
        <div>
          <button onClick={onClickRouting}>
              Calculate ROUTE!
          </button>
        </div>
    </div>
  );
};
export default Routing;
```
Routing APIにより取得したい情報をコントロールする方法は、以下のコード部分になります。
(こちらについては、次回投稿予定の記事についてより詳細を解説したいと多います。）
```javascript
      let onResult = function(result) {
        if (result.routes.length) {
          result.routes[0].sections.forEach((section) => {
              props.onRoutingResult(section.polyline);
          });
        }
      }
      let routingParameters = {
        'transportMode': 'car',
        'return': 'polyline'
      };
```
## Map.jsの修正
Routing APIが算出した経路情報を地図上に表示するための処理を追加します。

```diff_javascript:Map.js
import * as React from 'react';

const Map = (props) => {
  const apikey = props.apikey;
-  const gps = props.gps;
+  const gps = props.origin;
+  const destination = props.destination;
+  const polyline = props.polyline;
  // Create a reference to the HTML element we want to put the map on
  const mapRef = React.useRef(null);
  /**
   * Create the map instance
   * While `useEffect` could also be used here, `useLayoutEffect` will render
   * the map sooner
   */
   React.useLayoutEffect(() => {
    // `mapRef.current` will be `undefined` when this hook first runs; edge case that
    if (!mapRef.current) return;
    const H = window.H;
    const platform = new H.service.Platform({
        apikey: apikey
    });
    const defaultLayers = platform.createDefaultLayers({
      engineType: H.Map.EngineType.HARP
    });
    const engineType = H.Map.EngineType["HARP"];
    // configure an OMV service to use the `core` endpoint
    const omvService = platform.getOMVService({ 
      path: "v2/vectortiles/core/mc", 
      // Request the transit vector layer
      queryParams: {
        content: "default,transit"
      },
    });
    const baseUrl = `https://js.api.here.com/v3/3.1/styles/harp/oslo`;

    // create a Japan specific style
    const style = new H.map.render.harp.Style(`${baseUrl}/tko.normal.day.json`);
    // instantiate provider and layer for the base map
    const omvProvider = new H.service.omv.Provider(omvService, style, {
      engineType,
      lg: "ja",
    });
    var omvlayer = new H.map.layer.TileLayer(omvProvider, { max: 22 ,dark:true});

    // instantiate (and display) a map:
    var map = new H.Map(mapRef.current, omvlayer, {
        zoom: 16,
        center: { lat: gps.lat, lng: gps.lng },
        engineType,
    });

    // MapEvents enables the event system
    // Behavior implements default interactions for pan/zoom (also on mobile touch environments)
    var behavior = new H.mapevents.Behavior(new H.mapevents.MapEvents(map));
    // Create the default UI components
    var ui = H.ui.UI.createDefault(map, defaultLayers); 

    // Marker code goes here
    var LocationOfMarker = { lat: gps.lat, lng: gps.lng };
    var pngIcon = new H.map.Icon('<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2c2.131 0 4 1.73 4 3.702 0 2.05-1.714 4.941-4 8.561-2.286-3.62-4-6.511-4-8.561 0-1.972 1.869-3.702 4-3.702zm0-2c-3.148 0-6 2.553-6 5.702 0 3.148 2.602 6.907 6 12.298 3.398-5.391 6-9.15 6-12.298 0-3.149-2.851-5.702-6-5.702zm0 8c-1.105 0-2-.895-2-2s.895-2 2-2 2 .895 2 2-.895 2-2 2zm10.881-2.501c0-1.492-.739-2.83-1.902-3.748l.741-.752c1.395 1.101 2.28 2.706 2.28 4.5s-.885 3.4-2.28 4.501l-.741-.753c1.163-.917 1.902-2.256 1.902-3.748zm-3.381 2.249l.74.751c.931-.733 1.521-1.804 1.521-3 0-1.195-.59-2.267-1.521-3l-.74.751c.697.551 1.141 1.354 1.141 2.249s-.444 1.699-1.141 2.249zm-16.479 1.499l-.741.753c-1.395-1.101-2.28-2.707-2.28-4.501s.885-3.399 2.28-4.5l.741.752c-1.163.918-1.902 2.256-1.902 3.748s.739 2.831 1.902 3.748zm.338-3.748c0-.896.443-1.698 1.141-2.249l-.74-.751c-.931.733-1.521 1.805-1.521 3 0 1.196.59 2.267 1.521 3l.74-.751c-.697-.55-1.141-1.353-1.141-2.249zm16.641 14.501c0 2.209-3.581 4-8 4s-8-1.791-8-4c0-1.602 1.888-2.98 4.608-3.619l1.154 1.824c-.401.068-.806.135-1.178.242-3.312.949-3.453 2.109-.021 3.102 2.088.603 4.777.605 6.874-.001 3.619-1.047 3.164-2.275-.268-3.167-.296-.077-.621-.118-.936-.171l1.156-1.828c2.723.638 4.611 2.016 4.611 3.618z"/></svg>', { size: { w: 56, h: 56 } });

    // Create a marker using the previously instantiated icon:
    var marker = new H.map.Marker(LocationOfMarker, { icon: pngIcon });

    // Add the marker to the map:
    map.addObject(marker);
    //Zooming so that the marker can be clearly visible
    map.setZoom(16)   
+    if(polyline) {
+      let linestring = H.geo.LineString.fromFlexiblePolyline(polyline);
+      let routeLine = new H.map.Polyline(linestring, {
+        style: { strokeColor: 'blue', lineWidth: 8 }
+      });
+      let startMarker = new H.map.Marker(gps);
+      let endMarker = new H.map.Marker(destination);
+      map.addObjects([routeLine, startMarker,endMarker]);
+      map.getViewModel().setLookAtData({bounds: routeLine.getBoundingBox()});
+    }
    // This will act as a cleanup to run once this hook runs again.
    // This includes when the component un-mounts
    return () => {
      map.dispose();
    };
-  }, [props.gps]); // This will run this hook every time this ref is updated
+  }, [props.polyline]); // This will run this hook every time this ref is updated
  return <div style={ { width: "100%", height: "500px" } }  ref={mapRef}  />;
};
export default Map;
```
## App.jsの編集
最後にApp.jsを修正します。
```diff_javascript:App.js
import Map from './Map';
import { useState } from 'react';
-import Search from './Search';
+import Routing from './Routing';
let apikey = <APIKEY>;

function App() {
+    const [ origin, setOrigin ] = useState({lat: "35.6814568602531", lng: "139.76799772026422"});
+    const [ destination, setDestination ] = useState({lat: "35.6814568602531", lng: "139.76799772026422"});
+    const [polyline, setPolyline] = useState(null);
    return (
    <div>
-      <Search apikey={apikey} onGeocodeResult={query=> setGps(query)}/>
-      <Map apikey={apikey} gps={gps}/>
+      <Routing apikey={apikey} onOriginResult={query=> setOrigin(query)} onDestinationResult={query=> setDestination(query)} onRoutingResult={query=> setPolyline(query)}/>
+      <Map apikey={apikey} origin={origin} destination={destination} polyline={polyline}/>
    </div>
  );
};

export default App;
```

# プロジェクトの実行
```shell
npm start
```
![routing.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3104985/1b50b31b-c23a-6c4e-1b1c-7854fa87bf56.gif)
# おわりに
いかがでしたでしょうか？今回はReactベースの地図アプリにルート検索機能を追加してみました。本記事では最も基本的なRouting API v8の使用方法紹介のみとなっておりますが、本APIは単純なルート検索以外にも、非常に多くの機能を備えております。**次回投稿予定の記事**では、近々リリースを予定しております**日本向けの高速道路料金計算**について、今回紹介しましたコードベースに高速道路料金計算機能を追加したうえで、その解説をしたいと考えております。ここまで読んでいただいてありがとうございました。