Kepler.glはReactのコンポーネントとして開発されていますが、GitHubのリポジトリのexamplesにumd-clientの実装が含まれています。
上記のumd-clientのindex.htmlを参考にして、Kepler.glのOracle APEXへの組み込みを行います。
Kepler.glに表示させるデータセットは、データベースへ保存できるようにします。また、データベースに保存したデータセットを表示できるようにします。これらの実装はAPI ReferenceのAdvanced usagesのSaving and Loading Maps with Schema Managerのセクションを参考に実装します。
作成したAPEXアプリケーションは以下のように動作します。データベースにロードしているサンプル・データは以下より取得しています。
https://github.com/uber-web/kepler.gl-data
https://github.com/uber-web/kepler.gl-data
create table kepler_datasets (
id number generated by default on null as identity
constraint kepler_datasets_id_pk primary key,
name varchar2(80 char) not null,
config clob check (config is json),
dataset clob check (dataset is json)
);
空白のページをベースにKepler.glを組み込みます。以下よりページ番号を2として説明を進めます。ページ番号が異なる場合は、P2といった接頭辞を置き換えてください。
ボタンは3つ作成します。ボタンLOADにより、ページ・アイテムP2_NAMEで指定したデータセットをKepler.glに読み込みます。ボタンSAVEはKepler.glで扱っているデータセットをデータベースに保存します。同名のデータセットがある場合は上書きします。ボタンDELETEで保存されているデータセットを削除します。ページ遷移が発生するとKepler.glは初期化されるため、これらはすべてページ遷移が発生しない動的アクションとして実装します(DELETEは例外で、Kepler.glを初期化するためJavaScript中でページ遷移を呼び出しています)。
https://unpkg.com/react@18.3.1/umd/react.production.min.js
https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js
https://unpkg.com/redux@4.2.1/dist/redux.js
https://unpkg.com/react-redux@8.1.3/dist/react-redux.min.js
https://unpkg.com/styled-components@6.1.13/dist/styled-components.min.js
https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.js
https://unpkg.com/kepler.gl@3.1.0-alpha.1/umd/keplergl.min.js
Kepler.glの3.0以降はMapBoxの代わりにMapLibreを使います。そのため、MapLibreのライブラリをロード対象に含めています。
ファンクションおよびグローバル変数の宣言に以下を記述します。概ねindex.htmlのscript要素に記述されているコードと同じですが、MapBox関連のコードを削除しています。また、Kepler.glを表示する幅と高さは、APEXのリージョンに収まるように調整しています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* LOADとSAVE時に画面操作をブロックする。 */ | |
var spinner; | |
/* | |
* Original: https://github.com/keplergl/kepler.gl/blob/master/examples/umd-client/index.html | |
* KeplerGl 3.0以降はMapLibreがデフォルトなので、MapBox関連のコードは削除した。 | |
*/ | |
/** STORE **/ | |
const reducers = (function createReducers(redux, keplerGl) { | |
return redux.combineReducers({ | |
// mount keplerGl reducer | |
keplerGl: keplerGl.keplerGlReducer | |
}); | |
})(Redux, KeplerGl); | |
const middleWares = (function createMiddlewares(keplerGl) { | |
return keplerGl.enhanceReduxMiddleware([ | |
// Add other middlewares here | |
]); | |
})(KeplerGl); | |
const enhancers = (function craeteEnhancers(redux, middles) { | |
return redux.applyMiddleware(...middles); | |
})(Redux, middleWares); | |
const store = (function createStore(redux, enhancers) { | |
const initialState = {}; | |
return redux.createStore(reducers, initialState, redux.compose(enhancers)); | |
})(Redux, enhancers); | |
/** END STORE **/ | |
/** COMPONENTS **/ | |
const KeplerElement = (function (react, keplerGl) { | |
return function () { | |
const element = document.getElementById('app'); | |
/* | |
* KeplerGlの高さは、現時点ではコンテンツがないためheightが0になる。 | |
* そのため、ヘッダー、フッター、アイテム・コンテナの高さを | |
* 180pxとして、innerHeightから減らした値を高さとしている。 | |
*/ | |
const props = element.getBoundingClientRect(); | |
if ( props.height === 0 ) { | |
props.height = window.innerHeight - 180; | |
} | |
return react.createElement( | |
'div', | |
props, | |
react.createElement(keplerGl.KeplerGl, { | |
id: 'map', | |
width: props.width, | |
height: props.height | |
}) | |
); | |
}; | |
})(React, KeplerGl); | |
const app = (function createReactReduxProvider(react, reactRedux, KeplerElement) { | |
return react.createElement( | |
reactRedux.Provider, | |
{ store }, | |
react.createElement(KeplerElement, null) | |
); | |
})(React, ReactRedux, KeplerElement); | |
/** END COMPONENTS **/ |
ページ・ロード時に実行に以下を記述します。Kepler.glであるReactコンポーネントを、IDがappのDIV要素に描画します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Original: https://github.com/keplergl/kepler.gl/blob/master/examples/umd-client/index.html | |
*/ | |
/** Render **/ | |
(function render(react, reactDOM, app) { | |
const container = document.getElementById('app'); | |
const root = reactDOM.createRoot(container); | |
root.render(app); | |
})(React, ReactDOM, app); | |
/* | |
* 以下のカスタマイズ用のコードは未使用。 | |
*/ | |
/** | |
* Customize map. | |
* Interact with map store to customize data and behavior | |
*/ | |
(function customize(keplerGl, store) { | |
// store.dispatch(keplerGl.toggleSplitMap()); | |
})(KeplerGl, store); |
https://d1a3f4spazzrp4.cloudfront.net/kepler.gl/uber-fonts/4.0.0/superfine.css
https://unpkg.com/maplibre-gl@4.7.1/dist/maplibre-gl.css
https://unpkg.com/kepler.gl@3.1.0-alpha.1/umd/keplergl.min.css
Kepler.glを描画する静的コンテンツのリージョンを作成します。ソースのHTMLコードに以下を記述します。
<div id="app"></div>
余計な装飾を省くために、外観のテンプレートとしてBlank with Attributes(No Grid)を選択します。
以上でKepler.glがページに描画され、単体で使えるようになります。
これから、Kepler.glとデータベースを連携させる実装を追加します。
Kepler.glが扱う構成データとデータセットを、それぞれ保持するページ・アイテムを作成します。構成データはP2_CONFIG、データセットはP2_DATASETに保持します。ブラウザのJavaScriptからデータベースにこれらの値を送信する時、逆にデータベース・サーバーから取り出した値をブラウザに送信する時に、これらのページ・アイテムに値を保存します。
これらのページ・アイテムのタイプは非表示とします。動的アクションで値を設定するため、設定の保護された値はオフにします。セッション・ステートのデータ型はCLOB、ストレージはリクエストごと(メモリーのみ)を選択します。
ボタンLOADの動的アクションとして、以下の処理が行われます。
最初のTRUEアクションで画面にスピナーを表示させ、画面操作のブロックを開始します。以下のJavaScriptを実行します。
/* スピナーを開始し、画面操作をブロックする */
spinner = apex.widget.waitPopup();
続いてボタンLOADを無効化します。
select config, dataset into :P2_CONFIG, :P2_DATASET
from kepler_datasets where name = :P2_NAME;
送信するアイテムとしてP2_NAME、戻すアイテムとしてP2_CONFIG、P2_DATASETを指定します。
ページ・アイテムP2_CONFIG、P2_DATASETに読み込んだデータを、Kepler.glに渡します。以下のJavaScriptコードを実行します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* データベースから取り出したconfigとdatasetsをKeplerGlに適用する。 | |
*/ | |
const KeplerGlSchema = KeplerGl.KeplerGlSchema; | |
const savedDatasets = JSON.parse(apex.item("P2_DATASET").getValue()); | |
const savedConfig = JSON.parse(apex.item("P2_CONFIG").getValue()); | |
const mapToLoad = KeplerGlSchema.load(savedDatasets, savedConfig); | |
const data = KeplerGl.addDataToMap(mapToLoad); | |
store.dispatch(data); | |
/* | |
* ボタンLOADを有効化する。 | |
* 動的アクションで無効化しているので、CSSクラスを削除する必要がある。 | |
*/ | |
const loadButton = document.getElementById("B_LOAD"); | |
loadButton.classList.remove("is-disabled","apex_disabled"); | |
loadButton.disabled = false; | |
/* スピナーを削除 */ | |
spinner.remove(); |
データベースからKepler.glへの、データセットのロード処理の実装は以上です。
ボタンSAVEで実行されるTRUEアクションは、JavaScriptコードの実行のみです。以下のコードを実行します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* スピナーを開始し、画面操作をブロックする。 | |
*/ | |
spinner = apex.widget.waitPopup(); | |
/* | |
* ボタンSAVEを無効化する。 | |
*/ | |
const saveButton = document.getElementById("B_SAVE"); | |
saveButton.disabled = true; | |
/* | |
* KeplerGlからマップのconfigとdatasetsを取り出す。 | |
*/ | |
const KeplerGlSchema = KeplerGl.KeplerGlSchema; | |
const state = store.getState().keplerGl.map; | |
const dataToSave = JSON.stringify(KeplerGlSchema.getDatasetToSave(state)); | |
const configToSave = JSON.stringify(KeplerGlSchema.getConfigToSave(state)); | |
/* | |
* サーバーに送信するため、ページ・アイテムに保存する。 | |
*/ | |
apex.item("P2_DATASET").setValue(dataToSave); | |
apex.item("P2_CONFIG").setValue(configToSave); | |
/* | |
* name, config, datasetsをサーバーに送信し、データベースに保存する。 | |
*/ | |
apex.server.process( "UPSERT_DATASET", { | |
pageItems: ["P2_NAME","P2_CONFIG","P2_DATASET"] | |
}, { | |
success: function(data) { | |
if ( data.success ) { | |
apex.message.showPageSuccess("マップが保存されました。"); | |
apex.item("P2_NAME").refresh(); | |
} else { | |
apex.message.clearErrors(); | |
apex.message.showErrors([ | |
{ | |
type: "error", | |
location: "page", | |
message: "マップの保存に失敗しました。", | |
unsafe: false | |
} | |
]); | |
}; | |
saveButton.disabled = false; | |
/* スピナーの削除 */ | |
spinner.remove(); | |
}, | |
error: function( jqXHR, textStatus, errorThrown ) { | |
apex.message.clearErrors(); | |
apex.message.showErrors([ | |
{ | |
type: "error", | |
location: "page", | |
message: "マップの保存に失敗しました。", | |
unsafe: false | |
} | |
]); | |
saveButton.disabled = false; | |
/* スピナーの削除 */ | |
spinner.remove(); | |
} | |
}); |
構成データとデータセットをデータベースに書き込むために、AjaxコールバックUPSERT_DATASETを呼び出しています。
AjaxコールバックのPL/SQLコードとして以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
begin | |
merge into kepler_datasets t | |
using | |
( | |
select :P2_NAME as name, :P2_CONFIG as config, :P2_DATASET as dataset from dual | |
) s | |
on ( t.name = s.name ) | |
when matched then | |
update set | |
t.config = s.config | |
,t.dataset = s.dataset | |
when not matched then | |
insert (name, config, dataset) | |
values (s.name, s.config, s.dataset); | |
htp.p('{ "success": true }'); | |
exception | |
when others then | |
htp.p(apex_string.format('{ "success": false, "error": "%s" }', SQLERRM )); | |
end; |
Kepler.glからデータベースへの、データセットの保存処理の実装は以上です。
ボタンDELETEの処理では、最初にPL/SQLコードとして以下を実行し、ページ・アイテムP2_NAMEのデータを削除します。
delete from kepler_datasets where name = :P2_NAME;
データを削除したのち、ページを再描画して初期化します。以下のJavaScriptコードを実行します。
const thisPage = 'f?p=' + apex.env.APP_ID + ':' + apex.env.APP_PAGE_ID + ':' + apex.env.APP_SESSION + ':::::';
apex.navigation.redirect(thisPage, true);
ボタンDELETEの実装は以上で完了です。
以上でAPEXアプリケーションへのKepler.glの組み込みは完了です。
今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/sample-kepler-gl-on-apex.zip
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完