2022年4月26日火曜日

Oracle Spatialのsdo_obj_tracing.get_all_durationsの結果をAPEXで扱う

 Oracle Spatialを使った接触追跡という記事を参照して、この結果をOracle APEXのマップ・リージョンを使って可視化しようとすると、sdo_obj_tracing.get_all_durationsが行を返してくれない、という現象がありました。(2022年5月9日追記 - sdo_obj_tracing.get_all_durationsの引数track_table_nameにスキーマ名を付けることにより、正常に動作します。そのため、RESTサービスよりは表関数で実装する方が効果的でしょう。)

Oracle APEXは少し特殊なSQLの実行方法なので(以前に説明記事を書いています)、おそらくそれが理由だと思われるのですが、Oracle Spatialの接触追跡を使える環境は限られているため、ワークアラウンドを実装してみました。

APEXから呼ぶと行が返されないのですが、RESTサービスとして呼ぶとsdo_obj_tracing.get_all_durationsは行を返します。なので、RESTサービスとして接触追跡を実装します。

Always FreeのAutonomous DatabaseでOracle Database 21cが使えるのは北米のリージョンだけなので、Ashburnでインスタンスを作成しました。APEXのワークスペースはAPEXDEV、Oracle Spatialの記事の作業はSPATIALUSERではなく、APEXのワークスペース・ユーザーとなるAPEXDEVで作業を行なっています。

以下、実装になります。

RESTサービスは、APEXのRESTfulサービスのUIを使って実装しました。

モジュールcom.oracle.contacttracingベース・パス/contacts/として作成しています。認証による保護は省略しています。

URIテンプレートtraceとしています。ハンドラGETだけを作成しています。

GETハンドラを作成します。ソース・タイプPL/SQLとし、レスポンスはPL/SQLのコードで生成します。

ソースは以下になります。

declare
l_user_id number;
l_start_time date;
l_end_time date;
l_distance number;
l_time_tolerance_in_sec number;
l_chaining_tolerance_in_sec number;
begin
/* NVLで値がないときに結果が返るようにすることで、APEXのRESTの自動検出が使える */
l_user_id := nvl(:user_id,7);
l_start_time := nvl(:start_time,to_date('2020-05-31 00:00:00','YYYY-MM-DD HH24:MI:SS'));
l_end_time := nvl(:end_time,to_date('2020-05-31 23:59:59','YYYY-MM-DD HH24:MI:SS'));
l_distance := nvl(:distance,15);
l_time_tolerance_in_sec := nvl(:time_tolerance_in_sec,5);
l_chaining_tolerance_in_sec := nvl(:chaining_tolerance_in_sec,20);
/* JSONオブジェクトの配列で結果を返す。 */
apex_json.open_object;
apex_json.open_array('items');
for c in (
SELECT
ROWNUM AS contact_id,
t.in_user_id,
t.out_user_id,
ROUND(t.duration / 60, 2) AS duration_in_minutes,
t.start_time,
t.end_time,
t.num_contact_times,
t.geom
FROM
TABLE(
sdo_obj_tracing.get_all_durations(
user_id => l_user_id,
start_time => l_start_time,
end_time => l_end_time,
distance => l_distance,
time_tolerance_in_sec => l_time_tolerance_in_sec,
chaining_tolerance_in_sec => l_chaining_tolerance_in_sec,
track_table_name => 'track_geom',
geom_column_name => 'geom',
user_id_column_name => 'user_id',
time_column_name => 'capture_time',
date_as_number_column_name => 'capture_time_as_number'
)
) t
WHERE
t.segment_or_all = 'ALL'
ORDER BY
t.in_user_id,
t.out_user_id,
t.start_time
)
loop
for p in (
select p.x, p.y
from table(sdo_util.getvertices(c.geom)) p
)
loop
apex_json.open_object;
apex_json.write('contact_id', c.contact_id);
apex_json.write('in_user_id', c.in_user_id);
apex_json.write('out_user_id', c.out_user_id);
apex_json.write('duration_in_minutes', c.duration_in_minutes);
apex_json.write('start_time', c.start_time);
apex_json.write('end_time', c.end_time);
apex_json.write('num_contact_times', c.num_contact_times);
apex_json.write('x', p.x);
apex_json.write('y', p.y);
apex_json.close_object;
end loop;
end loop;
apex_json.close_array;
apex_json.close_object;
end;

APEXのアプリケーションからRESTサービスを検出する際に、パラメータを指定できないケースがあります。実行結果が返されないと、想定されるレスポンスを手作業で登録する必要が出てきます。それは避けたいため、パラメータの指定が無い場合に、必ず実行結果が返される値に置き換えています。

URLの問合せ文字列が、ソースで使用しているバインド変数に渡されるよう、パラメータを設定します。user_id、start_time、end_time、distance、time_tolerance_in_sec、chaining_tolerance_in_secをURLの問合せ文字列として受け付けます。


RESTサービスの作成は以上です。

続いて、このRESTサービスを使ったAPEXアプリケーションを作成します。

アプリケーション作成ウィザードを起動し、空のアプリケーションを作成します。名前Contact Tracingとします。


アプリケーションが作成されます。最初にRESTデータ・ソースを作成します。

共有コンポーネントを開きます。


RESTデータ・ソースを開きます。


作成済みのRESTデータ・ソースが一覧されます。作成をクリックします。


RESTデータ・ソースの作成最初からのままで、に進みます。


RESTデータ・ソース・タイプ簡易HTTPを選択します。RESTサービス自体はORDSを使って実装していますが、RESTサービスはORDSの機能を使わず、PL/SQLでコーディングしているためです。名前Contact TracingURLエンドポイントとして、先ほど作成したRESTサービスのURLを入力します。

へ進みます。


作成される内容の確認です。リモート・サーバーベースURLサービスURLパスの変更が必要な場合はほぼ無いので、変更せずにへ進みます。


ページ区切り(ページング)は実装していないので、ページ区切りタイプページ区切りなしのままで、へ進みます。


認証は不要なので、認証が必要ですOFFのままにしておきます。

以上の条件で、検出をクリックします。


RESTサービスのレスポンスが表示されます。APEXのアプリケーションで扱いやすいようにPL/SQLのコードを記述しているので、変更が必要な作業はありません。

RESTデータ・ソースの作成をクリックします。


RESTソースContact Tracingが作成されました。これから、問合せ文字列の設定を追加します。編集のためにContact Tracingを開きます。


RESTデータ・ソースの編集画面から、パラメータの追加を実行します。


パラメータのタイプ問合せ文字列変数です。名前はuser_idstart_timeend_timedistancetime_tolerance_in_secchaining_tolerance_in_secの6つです。はPL/SQLのソースとして設定したデフォルト値と同じにしておきます。目的入力静的OFF必須ONにします。

追加後、さらに追加をクリックし、パラメータをすべて作成します。


パラメータを作成したら、RESTデータ・ソースの作成は完了です。


変更の適用をクリックします。


ホーム・ページに接触追跡の結果を表示するマップ・リージョンを作成します。

その前に、接触追跡の実行での引数になるページ・アイテムを作成します。

リージョンを作成し、識別名前パラメータとします。タイプ静的コンテンツです。作成したリージョンに、6つのページ・アイテム、P1_USER_ID、P1_START_TIME、P1_END_TIME、P1_DISTANCE、P1_TIME_TOLERANCE_IN_SEC、P1_CHAINING_TOLERANCE_IN_SECを作成します。


マップ・リージョンを作成します。識別タイトルContact Tracingタイプマップとします。


レイヤーを選択します。識別名前接触レイヤー・タイプポイントとします。ソース位置として、RESTソースを選択し、RESTソースContact Tracingを選択します。列のマッピングジオメトリ列のデータ型として、経度/緯度を選択し、経度列X緯度列Yを設定します。


パラメータを選択し、タイプアイテムに変更します。アイテムにRESTサービスのパラメータとなるページ・アイテムを選択します。


最後に送信ボタンを作成します。ボタン名B_SUBMITラベル送信とします。


以上で、アプリケーションは完成です。


APEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/rest-contact-tracing.sql

ORDSのRESTサービスのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/ORDS_REST_APEXDEV_com.oracle.contacttracing_2022_04_26.sql

Oracle APEXのアプリケーション作成の参考になれば幸いです。

追記

RESTサービスが呼び出せることの確認だったので、APEXアプリの見栄えは気にしていなかったのですが、せめてCONTACT_IDで色分けしてみます。

レイヤーを選択して、ローカル後処理タイプSQL問合せに変更します。SQL問合せに以下を記述します。
select
    X,
    Y,
    to_char(END_TIME, 'YYYY-MM-DD HH24:MI') end_time,
    CONTACT_ID,
    IN_USER_ID,
    to_char(START_TIME, 'YYYY-MM-DD HH24:MI') start_time,
    OUT_USER_ID,
    NUM_CONTACT_TIMES,
    DURATION_IN_MINUTES,
    case
    when contact_id = 1 then
        'red'
    when contact_id = 2 then
        'green'
    when contact_id = 3 then
        'yellow'
    when contact_id = 4 then
        'blue'
    else
        'black'
    end col
from #APEX$SOURCE_DATA#

色を指定する列COLを追加したので、それを外観塗りつぶしの色&COL.として指定します。


結果として、以下のように色付けされます。


色だけでなく、例えばアイコンなども同様の手法で変更することができます。