2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Oracle AI Databaseでは地図情報を扱える。
聞いたことはあったものの、どうやってデータを取得して、どうやって保存して、どうやってアプリに実装するのかはよく分かっていませんでした。

そこで今回は、実際に地図アプリを作り、Oracle AI Databaseで地図情報を扱うとどんな感じなのか触ってみることにしました。

Oracle Autonomous AI Database(以降ADB)へカフェの場所を保存し、現在地の近くにあるカフェを表示するアプリです。
今回のデモでは、検索範囲を3kmに設定しています。

作り始める前は「緯度と経度をDBに保存して、画面にピンを置けば完成」くらいに考えていました。

実際に動かしてみると、地図データの構造も見えてきました。

この記事では、まず作ったアプリを紹介します。

後半は、地図データを初めて扱った自分向けのメモとして、今回初めて知った用語や仕組みを残しておきます。

作ったもの

外苑前駅、またはブラウザで取得した現在地を中心に、直線距離で3km以内にあるカフェを検索するアプリです。

検索結果は近い順に並びます。

せっかく地図アプリを作るので、画面は某RPGをイメージした探索地図風にしてみました。
カフェは宿屋風のアイコン、日本オラクル本社は城として表示しています。
カフェを選択すると、右側の「探索記録」で店名と距離を確認できます。

昔のRPGをイメージしたカフェ探索地図

今回使用したものは、役割で分けると次のようになります。

役割 使用したもの 今回やっていること
カフェ情報の取得元 OpenStreetMapの検索API(Overpass API) OpenStreetMapの中から、外苑前駅周辺のカフェだけを取得する
カフェ情報の保存と検索 Oracle Autonomous AI Database カフェ情報と場所をPointとして保存し、現在地から3km以内の店を検索する
ブラウザとDBの橋渡し Python / FastAPI ブラウザから現在地を受け取り、Oracle Autonomous AI Databaseの検索結果をブラウザへ返す
背景地図と検索結果の描画 MapLibre GL JS / OpenFreeMap OpenFreeMapが配信するOSM由来のベクター地図を描画し、GeoJSONで返ってきたカフェを地図へ重ねる
マーカーと詳細表示 app.js / DOM・SVG 城、宿屋、クリスタルのマーカーを重ね、右側の「探索記録」を更新する

oracle_ai_database_map_app_architecture

最初に、カフェ情報を準備

まず、OpenStreetMapに登録されている地図データの中から、外苑前駅周辺のカフェをOpenStreetMapの検索APIで取得します。

取得した経度・緯度はSDO_GEOMETRYのPointへ変換し、OSM ID、名前、カテゴリとともにADBへ保存しておきます。

地図を開いた後に、現在地から検索する

利用者が地図を開くと、ブラウザは初期地点、検索半径、カテゴリを取得し、外苑前駅周辺のカフェを自動検索します。

道路や建物などの背景には、OpenFreeMapが配信するOSM由来のベクター地図を使い、MapLibre GL JSがブラウザ上で描画します。
その上に、app.jsが検索地点やカフェなどのマーカーを重ねます。

ADBから返った検索結果は、FastAPIが地図表示用のGeoJSON形式をブラウザへ返し、
app.jsはそのデータをMapLibre GL JSへ渡し、宿屋風のマーカーと右側の「探索記録」へ反映します。

OpenStreetMapの検索APIは多くの人が共有して使う公開サービスなので、画面を開くたびに呼び出さず、必要なデータだけを事前に取得する構成にしました。

Oracle AI Databaseで地図情報を扱ってみる

今回の出発点は、「Oracle AI Databaseでも地図情報を扱えるなら、実際にアプリを作って触ってみたい」というくらいのものでした。

地図情報を扱うといっても、最初は緯度経度ぐらいの情報かなと思ってたんですが、
調べてみると、Oracle AI Databaseには、場所を点として保存したり、道路を線、地域を面として保存したりする仕組みが用意されていました。

Oracle AI Databaseには、点・線・面などの位置や形を保存するSDO_GEOMETRYというデータ型があります。

今回は、カフェの場所を「点」として保存してます。

SDO_GEOMETRY(
    2001,
    4326,
    SDO_POINT_TYPE(139.7178, 35.6708, NULL),
    NULL,
    NULL
)

この時点では20014326が何なのか、正直よく分かっていませんでした。

カフェの経度と緯度をADBへ保存し、次の関数へ検索地点と3kmという条件を渡すことで、指定範囲内のカフェを検索できます。

SDO_WITHIN_DISTANCE(
    location,
    検索地点,
    'distance=3 unit=KM'
) = 'TRUE'

検索結果を距離順に並べ、Pythonからブラウザへ返すと、地図上にカフェを表示できました。

「位置情報を保存するために、Oracle AI Databaseだけで距離検索までできる」というのは、
実際に作ってみて便利に感じたところです。

ここまでのまとめ

地図アプリというと、最初は地図サービス側ですべて処理するものだと思っていました。

今回作ったものでは、地図の見た目は外部の地図サービスを利用し、
「現在地の近くにあるカフェを探す」という処理はADBが担当しています。

Oracle AI Databaseには位置や形を保存する型と、距離を条件に検索する機能があるため、普段使っている表やSQLの延長で地図情報も扱えました。

複雑な地理情報システムを作ったわけではありませんが、「近くの店舗を検索する」程度のアプリなら、思っていたよりも短いコードで実装できました。

ここから先は、今回のデモを作りながら初めて知ったことのメモです。

以降は自分用メモ:地図アプリを作って初めて知ったこと

どこからがブラウザ処理で、どこからがサーバ処理なのか

最初は、MapLibre GL JSをPythonが動かしているのか、それともブラウザが動かしているのかも分かっていませんでした。

今回の構成では、ブラウザとPythonサーバの境界は次のようになります。

水色の範囲がユーザーのブラウザ内です。index.htmlを受け取ったブラウザが、app.jsとMapLibre GL JSを読み込んで実行します。

緑色の範囲がPythonサーバです。FastAPIは最初にHTMLやJavaScriptを配り、その後はブラウザから届いた検索依頼を受け取ります。Oracle Autonomous AI Databaseへ問い合わせる処理もサーバ側で実行します。

つまり、Pythonが地図を描いているわけではありません。
Pythonは画面のファイルと検索結果を返し、実際に背景地図や検索結果を描くのはブラウザ内のMapLibre GL JSです。
城、宿屋、クリスタルなどのマーカーや「探索記録」は、app.jsがDOM・SVGを使って表示します。

地図に見えているものと、検索する店のデータは別だった

最初は、画面に表示されている地図の中から、そのままカフェを検索できるものだと思っていました。

しかし今回のアプリでは、次の二つは別物です。

  • 道路、建物、地名などを表示する背景の地図
  • Oracle Autonomous AI Databaseで検索するカフェのデータ

背景の地図は、OpenFreeMapが配信するOSM由来のベクター地図です。

一方、検索するカフェの名前と場所は、OpenStreetMapの検索APIから事前に取得してOracle Autonomous AI Databaseへ保存しています。

POIも背景地図も元データはOSMですが、取得経路と用途が異なります。背景地図はOpenFreeMapからブラウザへ配信され、検索対象のPOIは事前にOracle Autonomous AI Databaseへ保存されます。

つまり、背景地図にカフェの名前が見えていても、Oracle Autonomous AI Databaseがその画面を見て検索しているわけではありません。

OpenStreetMap(OSM)とは

記事やドキュメントを調べると「OSM」という略称がよく出てきました。

OSMはOpenStreetMapの略です。道路や建物、店舗などの地図データを、世界中の利用者が共同で作成・更新しています。

今回のアプリでは、カフェの名前と緯度・経度を取得する元データとして利用しました。

なお、OpenStreetMapのデータを使うことと、背景地図を配信することは別です。
今回はPOIをOpenStreetMapの検索APIから取得し、背景にはOpenFreeMapが配信するOSM由来のベクター地図を使用しています。

POIとは

地図データを調べていると「POI」という言葉も頻繁に出てきます。

POIはPoint of Interestの略で、カフェ、駅、病院、公園など、地図上で目印や検索対象になる場所のことでした。

最初は単なる緯度・経度だと思っていましたが、実際には次のような情報がまとまっています。

  • 店の場所
  • 店の名前
  • カフェ、病院などの種類
  • 元データ側で付けられた番号

今回のカフェであれば、「緯度・経度に店名や種類を付けたデータ」と考えると、自分には分かりやすかったです。

SDO_GEOMETRYとは

Oracle Spatialで位置や形状を保存するためのオブジェクト型がMDSYS.SDO_GEOMETRYです。

カフェの位置を通常のNUMBER列へ緯度・経度として分けて保存することもできます。
しかし、SDO_WITHIN_DISTANCEなどの空間演算子や空間索引の対象として扱うには、形状をSDO_GEOMETRY型の列へ格納します。

今回のPLACESテーブルでは、カフェの位置を保存するLOCATION列を次のように定義しています。

location MDSYS.SDO_GEOMETRY NOT NULL

SDO_GEOMETRYはPoint専用ではありません。
Point、Line、Polygon、それらを複数まとめた形状などを、共通して格納できます。

オブジェクト型としては、次の5属性を持っています。

MDSYS.SDO_GEOMETRY(
    SDO_GTYPE,
    SDO_SRID,
    SDO_POINT,
    SDO_ELEM_INFO,
    SDO_ORDINATES
)
属性 データ型 役割
SDO_GTYPE NUMBER 次元数、LRSの測定次元、形状種別を表す
SDO_SRID NUMBER 座標参照系を識別するSRID
SDO_POINT SDO_POINT_TYPE 単純なPointをX・Y・Zで保持する
SDO_ELEM_INFO SDO_ELEM_INFO_ARRAY 座標配列をどのような要素・形状として解釈するかを定義する
SDO_ORDINATES SDO_ORDINATE_ARRAY 形状を構成する座標値を配列で保持する

今回格納するのは、WGS 84の経度・緯度で表した2次元のPointです。そのため、各属性には次の値を設定しています。

MDSYS.SDO_GEOMETRY(
    2001,  -- SDO_GTYPE:2次元のPoint
    4326,  -- SDO_SRID:WGS 84の地理座標系
    MDSYS.SDO_POINT_TYPE(
        139.7178,  -- X:経度
        35.6708,   -- Y:緯度
        NULL       -- Z:3次元目の座標
    ),
    NULL,  -- SDO_ELEM_INFO
    NULL   -- SDO_ORDINATES
)

SDO_GTYPE = 2001

SDO_GTYPEDLTTという4桁の形式です。

  • D:次元数
  • L:LRS(Linear Referencing System)で測定値を保持する次元。使用しない場合は0
  • TT:形状種別。01はPoint、02はLineまたはCurve、03はPolygon

したがって2001は、次のように分解できます。

2 0 01
│ │ └─ Point
│ └─── LRSの測定次元なし
└───── 2次元

カフェを地図上の2次元Pointとして格納するため、今回は2001です。線なら2002、面なら2003というように、格納する形状に応じて値が変わります。

SDO_SRID = 4326

SDO_SRIDは、その座標をどの座標参照系で解釈するかを示します。値はOracle AI DatabaseのMDSYS.CS_SRSに登録されているSRIDと対応します。

4326はWGS 84の地理座標系です。今回取得したカフェの経度・緯度をWGS 84として格納するため、SDO_SRID4326を設定しています。

なお、SRIDを指定しても座標値が自動変換されるわけではありません。異なる座標参照系のデータを格納する場合は、座標変換を行ってから同じSRIDへ揃える必要があります。

SDO_POINT

SDO_POINT属性の型はSDO_POINT_TYPEで、XYZの3属性を持ちます。

SRID 4326を使用する今回のPointでは、Xへ経度、Yへ緯度を設定します。SDO_GTYPEが2次元を表す2001なので、3次元目のZNULLです。

SDO_ELEM_INFOSDO_ORDINATES

SDO_ELEM_INFOは、座標配列の開始位置、要素種別、解釈方法を3つの値の組で定義します。SDO_ORDINATESは、その形状を構成する座標値を並べた配列です。

LineやPolygonなど、複数の座標から形状を構成する場合は、主にこの2属性を使用します。

今回のような単純なPointは、座標をSDO_POINTへ直接格納できます。そのため、座標配列とその構造定義は必要なく、SDO_ELEM_INFOSDO_ORDINATESNULLです。

NULLの場所 対象属性 今回NULLとなる理由
SDO_POINT_TYPEの3番目 Z SDO_GTYPEが2次元なので、3次元目の座標を持たない
SDO_GEOMETRYの4番目 SDO_ELEM_INFO Pointの座標をSDO_POINTへ直接格納するため、座標配列の構造定義が不要
SDO_GEOMETRYの5番目 SDO_ORDINATES Pointの座標をSDO_POINTへ直接格納するため、別の座標配列が不要

緯度と経度の順番

普段は「緯度・経度」と言うので、緯度を先に書きたくなります。

今回Oracle Autonomous AI Databaseへ点を保存するときは、次の順番でした。

経度, 緯度

東京周辺であれば、次のようになります。

139.7178, 35.6708

ブラウザへ位置情報を渡すときに使ったGeoJSONという形式でも、座標は「経度、緯度」の順です。

GeoJSONとは

Oracle Autonomous AI Databaseの検索結果を、そのままブラウザの地図へ表示できるわけではありません。

そこで、地図上の点や線などをJSONで表す「GeoJSON」という形式へ変換しました。

例えば、カフェの場所は次のようになります。

{
  "type": "Point",
  "coordinates": [139.7178, 35.6708]
}

Oracle AI Databaseには、保存した位置情報をGeoJSONへ変換するSDO_UTIL.TO_GEOJSONという関数もあります。

今回はDB側で変換してからPythonへ返したため、Python側でOracle独自の位置情報を一から変換せずに済みました。

FastAPIは、複数のカフェをGeoJSONのFeatureCollectionとしてまとめて返します。各カフェは一つのFeatureとなり、geometryにPoint、propertiesに店名・カテゴリ・距離などが入ります。

空間索引とは

普通のDB検索では、店の番号や名前などに索引を作ります。

位置情報の場合は、「この地点から3km以内」のような条件で探すための空間索引があります。

今回のデモはカフェが数百件なので、索引がなくても大きな差は感じにくいかもしれません。それでも、位置情報を検索する基本的な構成として空間索引を作成しました。

CREATE INDEX places_location_sidx
    ON places(location)
    INDEXTYPE IS MDSYS.SPATIAL_INDEX_V2;

索引内部の細かい仕組みまでは、今回の記事では扱いません。

さいごに

最初は「緯度と経度を保存すれば作れる」と考えていましたが、実際には背景地図、検索する店のデータ、座標のルールなどが別々に存在していました。

地図データの役割を整理すると、アプリ全体は次のシンプルな流れで動いています。

  1. OpenStreetMapからカフェ情報を取得する
  2. Oracle AI Databaseへ場所と店名を保存する
  3. Oracle AI Databaseで指定範囲内を検索する
  4. 検索結果をブラウザで表示できる形へ変換する
  5. 地図上に表示する

Oracle AI Databaseで位置情報まで扱えることを、実際に動くアプリとして確認できました。

同じように「Oracle AI Databaseで地図情報を扱うと何ができるのだろう」と思っている方の参考になれば幸いです。

最後までお付き合いいただき、ありがとうございます。

参考リンク

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?