STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

ジオコーディングとGeolocation APIを用いて店舗と現在地との距離を計算する

はじめに

初めまして、STORES でエンジニアをしているhiromu617です。この度、STORES では、STORES モバイルオーダーというサービスをリリースしました。

www.st.inc

今回は、STORES モバイルオーダー内に現在地から店舗までの距離を表示してみようと思います。

なお、モバイルオーダーを開発する上での取り組みについて、wanijiさんが紹介されているのでこちらも合わせて閲覧していただけると幸いです。

product.st.inc

やりたいこと

STORES モバイルオーダーには、注文する店舗を選択するための店舗一覧画面が存在します。

モバイルオーダーの店舗一覧画面

現在地から店舗までの距離を表示し、近くの店舗で注文できると便利そうなので、それを本記事のゴールとします。

方針

店舗は予め位置情報を持っており、DBに永続化されています。その位置情報からジオコーディングにより、地理座標を取得します。

ジオコーディングとは地名や住所を地理座標に変換する技術のことです。

ジオコーディングを行うためのAPIは様々な企業から提供されています。Googleが提供する Geocoding API が有名でしょう。

今回は、AWSが提供するAmazon Location Serviceを使用します。

また、現在地の地理座標はWeb APIであるGeolocation APIにより取得します。

そのようにして得られた2地点の地理座標から距離を計算します。

本記事では、実装に以下のフレームワーク、ライブラリを使用します。主旨とずれるためこれらについて詳細な説明はしません。

バックエンド(以降BE)

フロントエンド(以降FE)

  • Next.js

【STEP1】住所から地理座標を取得する

事前準備として、プレースインデックスのリソースをコンソール上から作っておく必要があります。

BEでAmazon Location ServiceのAPIを叩いてクライアントに返します。以下のようなスキーマでクエリを実装します。

type Query {
  """
  店舗一覧取得
  """
  shops: [Shop!]!
}

type Shop {
  """
  住所
  """
  address: String!

  """
  識別子
  """
  id: ID!
  
  """
  店舗名
  """
  name: String!

  """
  位置情報
  """
  position: Position!

  """
  郵便番号
  """
  zip: String!
}

"""
位置情報
"""
type Position {
  """
  緯度
  """
  latitude: Float!

  """
  経度
  """
  longitude: Float!
}

実装は以下のようになります。

実装にはSDKを使用しています。index_name: test-indexの箇所は作成したプレースインデックスの名前を入れています。

module Types
  class ShopType < Types::BaseObject
    description "店舗"

    field :id, ID, null: false, description: "識別子"
    field :name, String, null: false, description: "店舗名"
    field :zip, String, null: false, description: "郵便番号"
    field :address, String, null: false, description: "住所"
    field :position, Types::PositionType, null: false, description: "位置情報"

    Position = Data.define(:latitude, :longitude)

    def position
      client = Aws::LocationService::Client.new(region: 'ap-northeast-1')
      resp = client.search_place_index_for_text(index_name: 'test-index', text: object.address)
      longitude, latitude = resp.results.first.place.geometry.point
      Position.new(latitude, longitude)
    end
  end
end
module Types
  class PositionType < Types::BaseObject
    description "位置情報"

    field :latitude, Float, null: false, description: "緯度"
    field :longitude, Float, null: false, description: "経度"
  end
end

今回は実装していませんが、APIを叩いた回数に応じて料金が加算されていくので、APIを叩いて得た地理座標はDBに永続化しておき、住所が変更された時にAPIを叩くというような実装にするとお財布に優しくて良いでしょう。

 店舗の地理座標を返すことができました。とても簡単ですね。

{
  "data": {
    "shops": [
      {
        "id": "4bd61fcc-5475-47d6-b65c-e3f16ea65656",
        "name": "恵比寿店",
        "address": "東京都渋谷区東2-20-18",
        "position": {
          "latitude": 35.653650297622,
          "longitude": "139.709110259874"
        }
      },
      {
        "id": "87f92eb0-4b36-489f-a6fa-d548d4fd4617",
        "name": "渋谷店",
        "address": "東京都渋谷区渋谷道玄坂1-1-1",
        "position": {
          "latitude": 35.6590546,
          "longitude": 139.70502249
        }
      },
      {
        "id": "28d31f3b-0c65-4385-b9e3-28cbec7c3fb7",
        "name": "代官山店",
        "address": "東京都渋谷区代官山町19-4",
        "position": {
          "latitude": 35.648110530175,
          "longitude": "139.703194480251"
        }
      }
    ]
  }
}

【STEP2】現在地の地理座標を取得する

次にGeolocation APIを使用して、現在地の地理情報を取得します。現在地点はgetCurrentPosition()により取得することができます。

FEのコードは以下のようになります。

'use client'

import Link from 'next/link'
import { useEffect } from 'react'
import { useState } from 'react'
import { ShopList } from '../../../_components/ShopList'
import { ShopListItem } from '../../../_components/ShopListItem'

export function ShopListContainer({ shops }: { shops: Shop[] }) {
  const [currentPosition, setCurrentPosition] = useState<{
    latitude: number
    longitude: number
  } | null>(null)

  useEffect(() => {
    navigator.geolocation.getCurrentPosition((position) => {
      setCurrentPosition({
        latitude: position.coords.latitude,
        longitude: position.coords.longitude,
      })
    })
  }, [])

  return (
    <ShopList>
      {shops.map((shop) => (
        <ShopListItem
          key={shop.identifier}
          {...shop}
          currentPosition={currentPosition}
        />
      ))}
    </ShopList>
  )
}

【STEP3】地理座標から2地点間の距離を計算する

2地点の地理座標間の距離を計算するには、地球表面は曲面であることと緯度と経度は直交座標ではないことにより複雑な計算が必要になります。

具体的にはHaversineの公式により計算することができるようです。

自前で計算するのは面倒なのでここではgeolibというnpm ライブラリを使用します。

距離を計算して表示している箇所のコードは以下のようになります。

import { getDistance } from 'geolib'
import { Text } from '../../../_components/Text'
import type {
  Position,
} from '../../../_lib/gql/graphql'
import styles from './styles.module.css'

type ShopListItemProps = {
  name: string
  address?: string | null
  position?: Position | null
  currentPosition: {
    latitude: number
    longitude: number
  } | null
}

export function ShopListItem({
  name,
  address,
  currentPosition,
  position,
}: ShopListItemProps) {
  const distanceKm = () => {
    if (currentPosition === null || position == null) return null
    return getDistance(currentPosition, position) / 1000
  }

  return (
    <span className={styles.listItem}>
      <div className={styles.shopInfo}>
        <Text as="span" size="lg" bold>
          {name}
        </Text>

        {address && (
          <Text as="span" size="md">
            {distanceKm()}km・{address}
          </Text>
        )}
      </div>
    </span>
  )
}

無事、現在地から店舗までの距離を表示することができました!

店舗一覧画面

おわりに

本記事では、ジオコーディングとGeolocation APIの活用例を紹介しました。

参考にしていただければ幸いです。