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を使用してこれらのエンドポイントを使用します。必要に応じて、使用する環境にあわせてデフォルト値を置き換えてください。

  1. まだ設定されていない場合は、ORDS接続を構成します。

    コマンドords install (「Oracle REST Data Servicesのインストールおよび構成」を参照)を使用して、データベース接続パラメータを指定してORDSへの新しい接続を作成します。

  2. ORDS RESTエンドポイントを作成します。
    1. 次の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;
    2. 次の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;
    3. モジュールservice2で、パターンが'alps/:z/:x/:y'のエンドポイントのテンプレートを定義します。

      前述のパターンでは、alpsがこの特定のエンドポイントの識別要素です。パラメータxyおよび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;
    4. 前のステップで定義したテンプレートのハンドラを定義します。

      ハンドラのp_module_nameおよびp_patternは、テンプレート定義と同じにする必要があります。パラメータp_sourceで指定されたPL/SQLブロックは、SDO_GEOR_UTL.get_rasterTileを起動し、パラメータxyおよび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;
    5. モジュール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;
    6. テンプレート定義で使用されているのと同じ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}
    7. 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 (ゼロ)で一意のタイルをリクエストするために、ゼロに置き換えられます。

  3. MapLibreを使用してエンドポイントを使用します。
    alps表およびelevation表内のGeoRasterオブジェクトの仮想モザイクをレンダリングするために、ステップ2で作成したエンドポイントを使用するようにMapLibreを構成できます。elevation表から取得されたラスター・タイルは地形レイヤーに表示され、alps表から取得されたラスター・タイルは地形レイヤーをカバーするラスター・レイヤーに表示されます。
    1. ラスター・タイルへの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; 
      /
    2. 次の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>