この記事では,Next.js + deck.gl + HERE Geocoding & Search APIで,地図上における「場所の検索」機能を実装してみます.
何を実装するのか
検索欄から「駅」や「コンビニ」といった単語を検索すると,周辺の関連プレイスを表示するWebアプリです.機能自体はシンプルなものです.
今回,検索のAPIは HERE Geocoding & Search API を使用します.
(ちなみにデモ画像は,私の地元である愛知県豊橋市周辺です.)
実装環境
- Next.js 12.3.0
- deck.gl 8.8.11
- react-map-gl 7.0.19
- MapLibre GL JS 2.4.0
- HERE Geocoding & Search API v7
今回は,Typescriptで実装します.また,地図表示の基となるベクタースタイルは MapTiler から取得します.
Next.jsやその他諸々の初期設定は割愛します.
ソースコードは基本的に src
ディレクトリに格納します.
ディレクトリ構成
ここでは Next.js の src
ディレクトリ構成を示します.
.
├── components
│ ├── Map.tsx
│ └── Search.tsx
├── models
│ └── index.ts
├── pages
│ ├── _app.tsx
│ └── index.tsx
└── styles
└── globals.css
-
components
は,ページ内の部分的なコンポーネントファイル- 今回は,地図を表示するコンポーネントと検索フォームのコンポーネントを用意
-
model
は,データの型ファイル -
pages
は,Webアプリのページを構成するファイル -
styles
は,スタイル関連のファイル- 今回は
globals.css
のみ用意して tailwind の設定を記載
- 今回は
実装
まずは,コンポーネント部分を実装していきます.今回は,地図を表示するコンポーネントと,検索フォームのコンポーネントを用意します.
地図を表示するコンポーネント
DeckGL + MapLibreGLで,ベースとなる地図表示と,地図上に載せるアイコンなどを表示するコンポーネントです.
import ReactMapGL from "react-map-gl";
import maplibreGl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import DeckGL from "@deck.gl/react";
import { IconLayer, TextLayer } from "@deck.gl/layers";
import { NextPage } from "next";
import { useState } from "react";
import { PlaceData } from "~/models";
import Search from "./Search";
const Map: NextPage = () => {
// 地図上に表示する場所のデータ
const [places, setPlaces] = useState<[PlaceData]>();
// 地図の初期位置設定(豊橋駅)
const [viewState, setViewState] = useState({
longitude: 137.381651,
latitude: 34.762811,
zoom: 12,
});
// 場所名を表示するテキストレイヤー
const textLayer = new TextLayer({
id: "text-layer",
data: places,
background: true,
backgroundPadding: [12, 4, 12, 4],
getPixelOffset: [0, -60],
getColor: [255, 255, 255, 255],
getBackgroundColor: [64, 64, 64, 255],
getText: d => d.title,
getSize: 14,
fontWeight: 700,
getTextAnchor: 'middle',
getAlignmentBaseline: 'bottom',
getPosition: d => d.coordinates
})
// アイコンの情報と表示位置などの設定
const icon_mapping = {
url: './img/pin.png',
x: 0,
y: 0,
width: 256,
height: 256,
anchorY: 256,
}
// アイコンレイヤーの設定
const iconLayer = new IconLayer({
id: 'icon-layer',
data: places,
sizeScale: 10,
getIcon: d => icon_mapping,
getSize: d => 5,
getPosition: d => d.coordinates,
})
// レンダリング
return (
<DeckGL
initialViewState={viewState}
layers={[iconLayer, textLayer]}
style={{ width: "100vw", height: "100vh" }}
controller={true}
onViewStateChange={(event) => setViewState(event.viewState)}
>
<ReactMapGL mapStyle={process.env.MAP_URL} mapLib={maplibreGl} />
<Search
centerLon={viewState.longitude}
centerLat={viewState.latitude}
setPlaces={setPlaces}
/>
</DeckGL>
);
};
export default Map;
useStateで格納するデータ
useState
の型は,独自で PlaceData
型を定義しています.これは緯度経度の情報と,場所のタイトル(Toyohashi Stationといった名前)を定義しています.この型は /src/models/index.ts
で定義しています.
export type PlaceData = {
coordinates: [number],
title: string;
}
deck.glで使用するレイヤー定義
アイコンや場所を表示するテキストについては deck.gl で実装するため,TextLayer
と IconLayer
を定義しています.IconLayer
で表示するピンの画像は /public/img/pin.png
に格納されています.
レンダリング
ReactMapGL
にあるmapStyle
は,MapTiler のスタイルURLを .env
などの環境変数に格納します.今回は Basic マップを利用しています.
DeckGL
コンポーネントに内包する形で,検索フォームの Search
コンポーネントを置いています.propsとして,表示している地図の中心座標( viewState
の情報)と,表示場所の情報を格納するために必要な setPlaces
を設定しています.
検索フォームコンポーネント
入力フォームと検索ボタンを表示するコンポーネントです.ユーザーが「Search」を押すと,HERE Geocoding & Search API を使って場所の検索をします.
import { NextPage } from "next";
import { Dispatch, SetStateAction } from "react";
import { PlaceData } from "~/models";
type Props = {
centerLon: number;
centerLat: number;
setPlaces: Dispatch<SetStateAction<[PlaceData]>>;
};
const Search: NextPage<Props> = (props: Props) => {
// 送信ボタンを押したときの処理
const submit = async (event) => {
event.preventDefault();
// データを取得
const data = {
value: event.target.search.value,
};
// URLのParamを定義
// at には,いま表示している地図の中心点を格納
// 今回は最大100件の場所を検索します
const param = {
at: `${props.centerLat},${props.centerLon}`,
q: data.value,
limit: "100",
lang: 'en',
apiKey: process.env.HERE_API_KEY
};
// URLのParamをエンコード
const qs = new URLSearchParams(param);
try {
// ジオコーディング検索
const response = await fetch(
`https://discover.search.hereapi.com/v1/discover?${qs.toString()}`
)
.then((response) => {
return response.json();
})
.then((data) => {
// タイトル・場所をPlaceDataに合わせる形で抽出
const placeDatas: [PlaceData] = data.items.map((item) => {
return {
title: item.title,
coordinates: [item.position.lng, item.position.lat],
};
});
// 検索結果を格納
props.setPlaces(placeDatas);
});
} catch (e) {
console.log(e);
}
};
// レンダリング
return (
<form onSubmit={submit}>
<input
className="w-80 border-2 border-slate-400 rounded px-2 py-2 ml-8 mr-2 my-2"
type="text"
id="search"
name="search"
placeholder="Let's Search"
autoComplete="off"
required
/>
<button
className="bg-blue-700 hover:bg-blue-600 text-white rounded shadow-md px-4 py-2 ml-2 mr-2 my-2"
type="submit"
>
Search
</button>
</form>
);
};
export default Search;
propsのデータ型
setPlaces
は,親コンポーネントの更新関数であるため,Dispatch<SetStateAction<[PlaceData]>>
という型を定義しています.
submit関数
HEREのAPIを使用するためにはAPI Keyが必要です.予め取得してください.
ユーザーが「Search」ボタンを押したら実行する関数です.
まず,ユーザーが入力した値を取得して,HEREのAPIを呼び出すためのURLのParamsを定義します.今回は,APIの中の discover request
を使用するため,下記のURLに沿うようにParamsを定義します(公式ドキュメントから参照).
https://discover.search.hereapi.com/v1/
discover
?at=52.5228,13.4124
&q=petrol+station
&apiKey={YOUR_API_KEY}
今回は最大100件の場所を表示したいので limit: "100"
も定義します.また,今回の取得言語は英語に設定しています(日本語を TextLayer
で表示する場合は characterSet
の設定が必要です).
定義したParamsをURLエンコードします.その後 fetch
を使ってURLを生成してAPIの呼び出しをします.
HEREのAPIのレスポンスデータは下記になります(公式ドキュメントから参照).
{
"items": [
{
"title": "TOTAL",
"id": "here:pds:place:276u33dc-2e01d17cb4a24c14bcad179ed8946016",
"ontologyId": "here:cm:ontology:petrol_gasoline_station",
"resultType": "place",
"address": {
"label": "TOTAL, Prenzlauer Allee 1-4, 10405 Berlin, Deutschland",
"countryCode": "DEU",
"countryName": "Deutschland",
"stateCode": "BE",
"state": "Berlin",
"countyCode": "B",
"county": "Berlin",
"city": "Berlin",
"district": "Prenzlauer Berg",
"street": "Prenzlauer Allee",
"postalCode": "10405",
"houseNumber": "1-4"
},
"position": { "lat": 52.52896, "lng": 13.41802 },
"access": [{ "lat": 52.52906, "lng": 13.41775 }],
"distance": 783,
"categories": [
{ "id": "700-7600-0116", "name": "Tankstelle", "primary": true },
{ "id": "100-1000-0009", "name": "Fastfood" },
{ "id": "700-7850-0121", "name": "Autowäsche/-reinigung" }
],
"chains": [{ "id": "35" }],
"references": [
{ "supplier": { "id": "core" }, "id": "50664440" },
{ "supplier": { "id": "yelp" }, "id": "OQn9q-QzNNu8v3eQQQIFNg" },
{ "supplier": { "id": "yelp" }, "id": "j926GEeY9jUex9ESkzJLJg" }
],
"contacts": [
{
"phone": [
{ "value": "+49304425643" },
{ "value": "+4949304425643", "categories": [{ "id": "700-7600-0116" }] },
{ "value": "304-425643" }
],
"fax": [
{ "value": "030 4417600", "categories": [{ "id": "700-7600-0116" }] },
{ "value": "304417600", "categories": [{ "id": "700-7600-0116" }] }
],
"www": [
{
"value": "http://store.total.de/de/germany/store-total-de/berlin/berlin-prenzlauer-allee-1-4/ND020511",
"categories": [{ "id": "700-7600-0116" }]
},
{ "value": "http://store.total.de/de_DE/ND020511" },
{ "value": "http://www.total.de", "categories": [{ "id": "700-7600-0116" }] }
]
}
],
"openingHours": [
{
"text": ["Mon-Sun: 00:00 - 24:00"],
"isOpen": true,
"structured": [
{ "start": "T000000", "duration": "PT24H00M", "recurrence": "FREQ:DAILY;BYDAY:MO,TU,WE,TH,FR,SA,SU" }
]
}
]
},
(...)
]
このレスポンデータから PlaceData
型に合わせる形で title
と position
を取り出し, setPlaces
で親コンポーネントの places
にデータを格納します.
レンダリング
このコンポーネントでは,検索フォームと「Search」ボタンをレンダリングとして定義しています.ユーザーがボタンを押したときは submit
関数を呼ぶように設定します.それぞれのスタイルについては,tailwindでいい感じに設定しています.
pages の設定
コンポーネントを実装した後,pages
でコンポーネントを呼び出します. index.tsx
では Map
コンポーネントを定義します.
import type { AppProps } from "next/app";
import "~/styles/globals.css";
export default function MyApp({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}
import { NextPage } from "next";
import Map from "~/components/Map";
const Index: NextPage = () => {
return (
<div className="h-screen">
<Map />
</div>
);
};
export default Index;
完成例
Next.jsのサーバーを立ち上げてトップページにアクセスすると,地図画面と検索フォームが表示されます.ユーザーが単語を入力して検索ボタンを押すと,地図の中心点から100件の関連した場所が表示されます.
(デモ画像は,最初に貼った画像と同じです)
最後に
このように HERE Geocoding & Search API を使うことで,APIを呼び出すだけで手軽に場所の検索ができます.
他にもHEREではルート案内のAPI(HERE Routing API) などが用意されています.ナビゲーションアプリの実装もやってみたいところです.