C.2 例2: 地形レイヤーおよびイメージ・レイヤーとともにORDSを使用して3Dビジュアライゼーションを実現するラスター・タイル・サービス
この項で説明する例では、ラスター・データがスキーマscott下のalpsおよびelevationという2つの表を使用して格納されていることを前提としています。
表alpsは、次のように定義されています:
CREATE TABLE alps (
id NUMBER PRIMARY KEY,
raster SDO_GEORASTER
);
表alpsには4つの行が含まれており、raster列内の各GeoRasterオブジェクトは、SRIDが4258、セル深度が8BIT_U、仮想モザイクの空間エクステントがWGS84 (7, 45, 9, 47)の地理参照3バンド・イメージです。
表elevationには、数値標高モデル(DEM)ラスターが含まれています。次のように定義されています:
CREATE TABLE elevation (
id NUMBER PRIMARY KEY,
dem SDO_GEORASTER
);
さらに、表elevationには4つの行が含まれており、dem列内の各GeoRasterオブジェクトは、SRIDが4258、セル深度が32BIT_REAL、仮想モザイクの空間エクステントがWGS84 (7, 45, 9, 47)の地理参照1バンドDEMラスターです。
次のステップを実行して、SDO_GEOR_UTL.get_rasterTileファンクションをコールするエンドポイントを2つ(表ごとに1つずつ)作成し、MapLibreを使用してこれらのエンドポイントを使用します。必要に応じて、使用する環境にあわせてデフォルト値を置き換えてください。
- まだ設定されていない場合は、ORDS接続を構成します。
コマンドords install (「Oracle REST Data Servicesのインストールおよび構成」を参照)を使用して、データベース接続パラメータを指定してORDSへの新しい接続を作成します。
- ORDS RESTエンドポイントを作成します。
- 次のPL/SQLブロックを使用して、スキーマ
scottをREST対応にします:BEGIN ORDS.ENABLE_SCHEMA( p_enabled => TRUE, p_schema => 'SCOTT', p_url_mapping_type => 'BASE_PATH', p_url_mapping_pattern => 'scott', p_auto_rest_auth => FALSE); COMMIT; END; - 次のPL/SQLブロックを使用して、
service2というモジュールを定義します:BEGIN ORDS.DEFINE_MODULE( p_module_name => 'service2', p_base_path => '/service2/', p_items_per_page => 25, p_status => 'PUBLISHED', p_comments => NULL); COMMIT; END; - モジュール
service2で、パターンが'alps/:z/:x/:y'のエンドポイントのテンプレートを定義します。前述のパターンでは、
alpsがこの特定のエンドポイントの識別要素です。パラメータx、yおよびzは、次のステップでSDO_GEOR_UTL.get_rasterTileファンクションに渡されます。BEGIN ORDS.DEFINE_TEMPLATE( p_module_name => 'service2', p_pattern => 'alps/:z/:x/:y', p_priority => 0, p_etag_type => 'HASH', p_etag_query => NULL, p_comments => NULL); COMMIT; END; - 前のステップで定義したテンプレートのハンドラを定義します。
ハンドラの
p_module_nameおよびp_patternは、テンプレート定義と同じにする必要があります。パラメータp_sourceで指定されたPL/SQLブロックは、SDO_GEOR_UTL.get_rasterTileを起動し、パラメータx、yおよびzを渡し、パラメータTABLE_NAMEの値として'ALPS'を、パラメータGEOR_COL_NAMEの値として'RASTER'を指定します。BEGIN ORDS.DEFINE_HANDLER( p_module_name => 'service2', p_pattern => 'alps/:z/:x/:y', p_method => 'GET', p_source_type => ords.source_type_media, p_items_per_page => 0, p_mimes_allowed => '', p_comments => NULL, p_mle_env_name => NULL, p_source => 'SELECT ''image/png'' as mediatype, SDO_GEOR_UTL.GET_RASTERTILE( TABLE_NAME=>''ALPS'', GEOR_COL_NAME=> ''RASTER'', TILE_X=>:x, TILE_Y=>:y, TILE_ZOOM=>:z, MOSAIC_PARAM=>''resFilter=false'') from dual' ); COMMIT; END; - モジュール
service2で、パターン'terrainrgb/:z/:x/:y'で識別されるエンドポイント用に別のテンプレートを作成し、elevation表からラスター・タイルを取得します。BEGIN ORDS.DEFINE_TEMPLATE( p_module_name => 'service2', p_pattern => ' terrainrgb/:z/:x/:y', p_priority => 0, p_etag_type => 'HASH', p_etag_query => NULL, p_comments => NULL); COMMIT; END; - テンプレート定義で使用されているのと同じ
p_module_nameおよびp_pattern valueを使用して、前述のテンプレートのハンドラを定義します。p_sourceパラメータのPL/SQLブロックを指定します。このPL/SQLブロックは、SDO_GEOR_UTL.get_rasterTileファンクションを起動して、elevation表のdem列にあるGeoRasterオブジェクトの仮想モザイクからタイルを取得します。sdo_geor_utl.get_rastertileファンクションに渡されるimage_processingパラメータとして'terrainrgb'を使用し、GeoRasterオブジェクトに格納されている標高データをエンコードします。返されるエンコードされたタイルは、レンダリング用にMapLibreでサポートされているMapbox Terrain-RGB形式の3バンドPNGイメージです。BEGIN ORDS.DEFINE_HANDLER( p_module_name => 'service2', p_pattern => ' terrainrgb/:z/:x/:y', p_method => 'GET', p_source_type => ords.source_type_media, p_items_per_page => 0, p_mimes_allowed => '', p_comments => NULL, p_mle_env_name => NULL, p_source => 'SELECT ''image/png'' as mediatype, SDO_GEOR_UTL.GET_RASTERTILE( TABLE_NAME=>''ELEVATION'', GEOR_COL_NAME=> ''DEM'', TILE_X=>:x, TILE_Y=>:y, TILE_ZOOM=>:z, IMAGE_PROCESSING=>''TERRAINRGB'', MOSAIC_PARAM=>''resFilter=false, nodata=true'') from dual' ); COMMIT; END;ラスター・タイル・サービスは、次の2つのエンドポイントで設定されるようになりました:
http://localhost:8080/ords/scott/service2/alps/{z}/{x}/{y}http://localhost:8080/ords/scott/service2/terrainrgb/{z}/{x}/{y}
- curlまたはWebブラウザを使用して、URL (
http://localhost:8080/ords/scott/service2/alps/0/0/0およびhttp://localhost:8080/ords/scott/service2/terrainrgb/0/0/0)にGETリクエストを送信して、エンドポイント構成を確認します。URLパターン(前のステップを参照)の
{z}、{x}および{y}は、ズーム・レベル0 (ゼロ)で一意のタイルをリクエストするために、ゼロに置き換えられます。
- 次のPL/SQLブロックを使用して、スキーマ
- MapLibreを使用してエンドポイントを使用します。
alps表およびelevation表内のGeoRasterオブジェクトの仮想モザイクをレンダリングするために、ステップ2で作成したエンドポイントを使用するようにMapLibreを構成できます。elevation表から取得されたラスター・タイルは地形レイヤーに表示され、alps表から取得されたラスター・タイルは地形レイヤーをカバーするラスター・レイヤーに表示されます。- ラスター・タイルへのURL、ラスター・タイルの境界など、提供されるラスター・タイルに関連する情報を指定するTileJSONドキュメント(MapLibreドキュメントを参照)を返す次のエンドポイントを作成します。
この例では、範囲は
elevation表にあるGeoRasterオブジェクトの仮想モザイクの空間エクステントとして定義され、タイルはパターンが'terrainrgb/:z/:x/:y'のエンドポイントとして定義されます。BEGIN ORDS.DEFINE_TEMPLATE( p_module_name => 'service2', p_pattern => 'terrainrgb/tile.json', p_priority => 0, p_etag_type => 'HASH', p_etag_query => NULL, p_comments => NULL); COMMIT; END; BEGIN ORDS.DEFINE_HANDLER( p_module_name => 'service2', p_pattern => 'terrainrgb/tile.json', p_method => 'GET', p_items_per_page => 0, p_mimes_allowed => '', p_comments => NULL, p_mle_env_name => NULL, p_source_type => ORDS.source_type_plsql, p_source => q'< DECLARE geom sdo_geometry; BEGIN SELECT sdo_geor_aggr.getMosaicExtent('ELEVATION','DEM',4326) INTO geom from dual; owa_util.mime_header ('application/json', true); htp.p('{ "tilejson": "2.0.0", "name": "oracle_terrainrgb", "description": "oracle_terrainrgb", "attribution":"<a href=\"http://localhost://test\">example</a>", "legend":"", "minzoom": 0, "maxzoom": 22, "version": "1.0.0", "bounds": [' || geom.sdo_ordinates(1) ||',' || geom.sdo_ordinates(2) ||',' || geom.sdo_ordinates(3) ||',' || geom.sdo_ordinates(4) ||' ], "tiles": ["' || owa_util.get_cgi_env('REQUEST_SCHEME') || '://' || owa_util.get_cgi_env('HTTP_HOST') || owa_util.get_cgi_env('SCRIPT_NAME') || '/{z}/{x}/{y}"] }'); END; >' ); COMMIT; END; / - 次のHTMLおよびJavaScriptコードを実行します。
次のHTMLは、MapLibre GL (グラフィック・ライブラリ) JS (Javaスクリプト)で地形タイル・レイヤーおよびラスター・タイル・レイヤーを構成する方法の例です。このサンプル・コードには、次の要素が含まれています:
- 地形タイル・レイヤー: この地形レイヤーは、
map.setTerrainを起動してマップに構成されます。マップに対して構成できる地形レイヤーは1つのみです。このレイヤーには、値が'raster-dem'の属性typeと、レイヤー・データのWGS84範囲が指定された属性範囲を含むTileJSONドキュメントを指す属性urlを使用して定義された'terrainSource'というソース・オブジェクトが必要です。 - ラスター・タイル・レイヤー: レイヤー・オブジェクト
'georaster'は、値が'raster'の属性typeを使用して定義されます。値が'raster'の属性typeとTileJSONドキュメントを指す属性urlを使用して定義された'alpsSource'というソース・オブジェクトが必要です。 - ヒル・シェード・レイヤー: Maplibreでは、Mapbox Terrain-RGBタイルを使用して影付きレリーフ・タイルを生成します。光源をシミュレートして、地形に従って影を生成します。レイヤー・オブジェクト
'hills'は、値が'hillshade'の属性typeを使用して定義されます。値が'raster-dem'の属性typeとTileJSONドキュメントを指す属性urlを使用して定義された'hillshadeSource'というソース・オブジェクトが必要です。 - 輪郭レイヤー: Maplibreでは、同じ標高にある点をつないで線を表示します。これには、
mlcontour.DemSourceのインスタンスを作成して、maplibre-contourライブラリをマップに追加する必要があります。レイヤー・オブジェクト'contour'は、値が'line'の属性typeを使用して定義され、もう1つのレイヤー・オブジェクト'contour-text'は、値が'symbol'の属性typeを使用して定義されます。どちらのレイヤー・オブジェクトにも、値が'vector'の属性typeと、1つの要素demSource.contourProtocolUrlが含まれる配列として属性tilesを使用して定義された'contourSourceFeet'というソース・オブジェクトが必要です。 - 標高チャート: 選択された線に沿って20の異なる点に標高が含まれる対話型チャートの例。ライブラリ
zingchartおよびturfは、queryTerrainElevationファンクションを使用して様々なポイントで標高を取得するチャートを表示するのに必要です。
次のコードで記述された各レイヤーは独立しており、必要に応じて削除できます。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>3D Terrain</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta property="og:description" content="Go beyond hillshade and show elevation in actual 3D."> <link rel="stylesheet" href="https://unpkg.com/maplibre-gl@5.6.2/dist/maplibre-gl.css"> <script src="https://unpkg.com/maplibre-gl@5.6.2/dist/maplibre-gl.js"></script> <script src="https://cdn.zingchart.com/zingchart.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script> <style> body { margin: 0; padding: 0; } html, body, #map { height: 100%; } #myChart { position: absolute; bottom: 20px; right: 0; width: 400px; height: 100px; } #clear { position: absolute; bottom: 10px; margin: 6px 6px; right: 300px; z-index:100; } </style> </head> <body> <div id="map"></div> <div id="clear"> <button onClick="clearRoute()">CLEAR</button> </div> <div id="myChart"></div> <script src="https://unpkg.com/maplibre-contour@0.0.5/dist/index.min.js"></script> <script> let chartData = []; drawChart(); var demSource = new mlcontour.DemSource({ url: 'http://localhost:8080/ords/scott/service2/terrainrgb/{z}/{x}/{y}', encoding: 'mapbox', // 'mapbox' or 'terrarium' default='terrarium' maxzoom: 13, worker: true, // offload isoline computation to a web worker cacheSize: 100, // number of most-recent tiles to cache timeoutMs: 10_000, // timeout on fetch requests }); demSource.setupMaplibre(maplibregl); const map = (window.map = new maplibregl.Map({ container: 'map', style: 'https://maps.oracle.com/mapviewer/pvt/res/style/osm-positron/style.json', zoom: 0, pitch: 70, hash: true, transformRequest: (url, resourceType) => { if (resourceType === 'Tile' && ( url.startsWith('https://maps.oracle.com/mapviewer/pvt') || url.startsWith('https://elocation.oracle.com/mapviewer/pvt')) ){ return { url: url, headers: {'x-oracle-pvtile': 'OracleSpatial'}, credentials: 'include' }; } }, maxZoom: 18, maxPitch: 85 })); map.on('load', function () { map.addSource('alpsSource', { 'type': 'raster', 'tiles' : ['http://localhost:8080/ords/scott/service2/alps/{z}/{x}/{y}'], 'minzoom': 0, 'maxzoom': 22, 'tileSize' : 256 }); map.addSource('terrainSource', { 'type': 'raster-dem', 'url' : 'http://localhost:8080/ords/scott/service2/terrainrgb/tile.json', 'minzoom': 0, 'maxzoom': 22, 'tileSize' : 256 }); map.addSource('hillshadeSource', { 'type': 'raster-dem', 'url' : 'http://localhost:8080/ords/scott/service2/terrainrgb/tile.json', 'minzoom': 0, 'maxzoom': 22, 'tileSize' : 256 }); map.addSource('contourSourceFeet', { 'type': 'vector', 'tiles' : [ demSource.contourProtocolUrl({ // meters to feet 'multiplier': 3.28084, 'overzoom': 1, 'thresholds': { // zoom: [minor, major] 11: [200, 1000], 12: [100, 500], 13: [100, 500], 14: [50, 200], 15: [20, 100] }, 'elevationKey': 'ele', 'levelKey': 'level', 'contourLayer': 'contours' }) ], 'maxzoom': 15 }); map.addLayer({ 'id': 'georaster', 'type': 'raster', 'source': 'alpsSource', 'paint': { 'raster-opacity': 1, 'raster-hue-rotate': 0, 'raster-brightness-min': 0, 'raster-brightness-max': 1, 'raster-saturation': 0, 'raster-contrast': 0, 'raster-fade-duration': 300 } }); map.addLayer({ 'id': 'hills', 'type': 'hillshade', 'source': 'hillshadeSource', 'layout': {'visibility': 'visible'}, 'paint': {'hillshade-shadow-color': '#473B24'} }); map.addLayer({ 'id': 'contours', 'type': 'line', 'source': 'contourSourceFeet', 'source-layer': 'contours', 'paint': { 'line-opacity': 0.5, // "major" contours have level=1, "minor" have level=0 'line-width': ['match', ['get', 'level'], 1, 1, 0.5] } }); map.addLayer({ 'id': 'contour-text', 'type': 'symbol', 'source': 'contourSourceFeet', 'source-layer': 'contours', 'filter': ['>', ['get', 'level'], 0], 'paint': { 'text-halo-color': 'white', 'text-halo-width': 1 }, 'layout': { 'symbol-placement': 'line', 'text-size': 10, 'text-field': [ 'concat', ['number-format', ['get', 'ele'], {}], '\'' ], 'text-font': ['Noto Sans Bold'] } }); map.setTerrain({ 'source': 'terrainSource', 'exaggeration': 1 }); }); map.addControl( new maplibregl.NavigationControl({ visualizePitch: true, showZoom: true, showCompass: true }) ); map.addControl( new maplibregl.TerrainControl({ source: 'terrainSource', exaggeration: 1 }) ); function drawChart() { var myConfig = { type: "line", "scale-x": { "line-color": "none", item: { visible: false }, tick: { "line-color": "none" } }, "scale-y": { "line-color": "none", item: { visible: false }, tick: { "line-color": "none" } }, plotarea: { margin: "20 20" }, series: [{ values: chartData }], plot: { aspect: "spline" } }; zingchart.render({ id: "myChart", data: myConfig, height: "100%", width: "100%" }); } function clearRoute() { geojson.features = [] map.getSource("geojson").setData(geojson) chartData = [] drawChart() } // GeoJSON object to hold our measurement features var geojson = { type: "FeatureCollection", features: [] }; // Used to draw a line between points var linestring = { type: "Feature", geometry: { type: "LineString", coordinates: [] } }; map.on("load", function () { map.addSource("geojson", { type: "geojson", data: geojson }); // Add styles to the map map.addLayer({ id: "measure-points", type: "circle", source: "geojson", paint: { "circle-radius": 5, "circle-color": "#000" }, filter: ["in", "$type", "Point"] }); map.addLayer({ id: "measure-lines", type: "line", source: "geojson", layout: { "line-cap": "round", "line-join": "round" }, paint: { "line-color": "#000", "line-width": 2.5 }, filter: ["in", "$type", "LineString"] }); map.on("click", function (e) { var features = map.queryRenderedFeatures(e.point, { layers: ["measure-points"] }); // Remove the linestring from the group // So we can redraw it based on the points collection if (geojson.features.length > 1) geojson.features.pop(); // If a feature was clicked, remove it from the map if (features.length) { var id = features[0].properties.id; geojson.features = geojson.features.filter(function (point) { return point.properties.id !== id; }); } else { var point = { type: "Feature", geometry: { type: "Point", coordinates: [e.lngLat.lng, e.lngLat.lat] }, properties: { id: String(new Date().getTime()) } }; geojson.features.push(point); } if (geojson.features.length > 1) { linestring.geometry.coordinates = geojson.features.map(function (point) { return point.geometry.coordinates; }); geojson.features.push(linestring); // get length of line let lineLength = turf.length(linestring, {units: 'meters'}) // however many subdivisions we want let divisionLength = lineLength / 20 let newLine = turf.lineChunk(linestring, divisionLength, {units: 'meters'}) chartData = newLine.features.map(el => map.queryTerrainElevation(el.geometry.coordinates[0])) drawChart(); } map.getSource("geojson").setData(geojson); }); }); </script> </body> </html> - 地形タイル・レイヤー: この地形レイヤーは、
- ラスター・タイルへのURL、ラスター・タイルの境界など、提供されるラスター・タイルに関連する情報を指定するTileJSONドキュメント(MapLibreドキュメントを参照)を返す次のエンドポイントを作成します。