C.2 Example 2: A Raster Tile Service Using ORDS With a Terrain Layer and an Image Layer for 3D Visualization

The example described in this section assumes that the raster data is stored using two tables called alps and elevation under schema scott.

The table alps is defined as shown:

CREATE TABLE alps (
  id	 NUMBER PRIMARY KEY,
  raster SDO_GEORASTER
);

The table alps contains four rows and each GeoRaster object in the raster column is a georeferenced 3 band image with SRID 4258, with a cell depth of 8BIT_U, and the spatial extent of the virtual mosaic is WGS84 (7, 45, 9, 47).

The table elevation contains Digital Elevation Model (DEM) rasters. It is defined a shown:

CREATE TABLE elevation (
  id  NUMBER PRIMARY KEY,
  dem SDO_GEORASTER
);

In addition, the table elevation contains four rows and each GeoRaster object in the dem column is a georeferenced one band DEM raster with SRID 4258, with a cell depth of 32BIT_REAL and the spatial extent of the virtual mosaic is WGS84 (7, 45, 9, 47).

Perform the following steps to create two endpoints (one for each table) to call the SDO_GEOR_UTL.get_rasterTile function and consume these endpoints using MapLibre. Ensure to replace any default values as needed for your environment.

  1. Configure the ORDS connection if not previously set.

    Use the command ords install (see Installing and Configuring Oracle REST Data Services) to create a new connection to ORDS by specifying the database connection parameters.

  2. Create ORDS REST endpoints.
    1. Use the following PL/SQL block to REST-enable schema scott:
      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. Use the following PL/SQL block to define a module named 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. Define a template for an endpoint with pattern as ‘alps/:z/:x/:y’ on module service2.

      In the preceding pattern, alps is the differentiator for this specific endpoint. The parameters x, y, and z will be passed to the SDO_GEOR_UTL.get_rasterTile function in the next step.

      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. Define a handler for the template defined in the preceding step.

      The handler must have the same p_module_name and p_pattern as in the template definition. The PL/SQL block specified in the parameter p_source invokes SDO_GEOR_UTL.get_rasterTile, passing parameters x, y, and z, and specifying ‘ALPS’ as the value for parameter TABLE_NAME and 'RASTER' as the value for parameter GEOR_COL_NAME.

      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. Create another template for the endpoint identified by pattern ‘terrainrgb/:z/:x/:y’ on module service2 to retrieve raster tiles from the elevation table.
      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. Define a handler for the preceding template by using the same p_module_name and p_pattern value as used in the template definition.

      Specify the PL/SQL block for the p_source parameter. The PL/SQL block invokes the SDO_GEOR_UTL.get_rasterTile function to retrieve a tile from the virtual mosaic of the GeoRaster objects in the dem column of the elevation table. Use ‘terrainrgb’ as the image_processing parameter passed to the sdo_geor_utl.get_rastertile function to encode the elevation data stored in the GeoRaster object. The returned encoded tile is a 3 bands PNG Image in Mapbox Terrain-RGB format that is supported by MapLibre for rendering.

      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;

      The raster tile service is now set up with the following two endpoints:

      • http://localhost:8080/ords/scott/service2/alps/{z}/{x}/{y}
      • http://localhost:8080/ords/scott/service2/terrainrgb/{z}/{x}/{y}
    7. Verify the endpoint configurations by sending a GET request using curl or a web browser to the URLs: http://localhost:8080/ords/scott/service2/alps/0/0/0 and http://localhost:8080/ords/scott/service2/terrainrgb/0/0/0.

      {z}, {x}, and {y} in the URL pattern (shown in the previous step) are replaced by zeros to request the unique tile at zoom level zero.

  3. Consume the endpoints using MapLibre.
    To render the virtual mosaic for the GeoRaster objects in the alps and the elevation tables, MapLibre can be configured to consume the endpoints created at step 2. The raster tiles obtained from the elevation table are displayed in a terrain layer and the raster tiles obtained from the alps table are displayed in a raster layer which will cover the terrain layer.
    1. Create the following endpoint that returns a TileJSON document (see MapLibre documentation) that specifies the information related to the raster tiles to be served, such as the URL to the raster tiles, the boundary of the raster tiles, and so on.

      In this example, the bounds is defined as the spatial extent for the virtual mosaic of the GeoRaster objects at the elevation table and tiles as the endpoint with the pattern ‘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. Run the following HTML and JavaScript code.

      The following HTML is an example of how to configure a terrain tile layer and a raster tile layer in MapLibre GL (Graphics Library) JS (Java Script). The example code includes the following elements:

      • Terrain tile layer: This terrain layer is configured to the map by invoking map.setTerrain. Only one terrain layer can be configured for a map. This layer requires a source object named 'terrainSource' defined using attribute type with value ‘raster-dem’, and attribute url pointing to a TileJSON document that includes attribute bounds with WGS84 bounds of the layer data.
      • Raster tile layer: A layer object 'georaster' is defined using attribute type with value ‘raster’. It requires a source object named 'alpsSource' defined using attribute type with value ‘raster’ and attribute url pointing to a TileJSON document.
      • Hill shade layer: Maplibre uses Mapbox Terrain-RGB tiles to generate shaded relief tiles. It simulates a light source to produce shadows according to the terrain. A layer object 'hills' is defined using attribute type with value ‘hillshade’. It requires a source object named 'hillshadeSource' defined using attribute type with value ‘raster-dem’ and attribute url pointing to a TileJSON document.
      • Contours layer: Maplibre displays lines by connecting points at the same elevation. This requires adding maplibre-contour library to the map by creating an instance of mlcontour.DemSource. A layer object 'contour' is defined using attribute type with value ‘line’, and another layer object 'contour-text' is defined using attribute type with value ‘symbol’. Both layer objects require the source object names 'contourSourceFeet' defined using attribute type with value ‘vector’ and attribute tiles as an array with one element demSource.contourProtocolUrl.
      • Elevation chart: An example of an interactive chart with the elevation in 20 different points along a selected line. The libraries zingchart and turf are needed to display the chart which uses the queryTerrainElevation function to obtain the elevation at different points.

      Note that each layer described in the following code is independent and can be deleted if not needed.

      <!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>