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.
- 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.
- Create ORDS REST endpoints.
- 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;
- 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;
- Define a template for an endpoint with pattern as
‘alps/:z/:x/:y’
on moduleservice2
.In the preceding pattern,
alps
is the differentiator for this specific endpoint. The parametersx
,y
, andz
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;
- Define a handler for the template defined in the preceding
step.
The handler must have the same
p_module_name
andp_pattern
as in the template definition. The PL/SQL block specified in the parameterp_source
invokes SDO_GEOR_UTL.get_rasterTile, passing parametersx
,y
, andz
, and specifying‘ALPS’
as the value for parameterTABLE_NAME
and'RASTER'
as the value for parameterGEOR_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;
- Create another template for the endpoint identified by
pattern
‘terrainrgb/:z/:x/:y’
on moduleservice2
to retrieve raster tiles from theelevation
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;
- Define a handler for the preceding template by using the
same
p_module_name
andp_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 thedem
column of theelevation
table. Use‘terrainrgb’
as theimage_processing
parameter passed to thesdo_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}
- 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
andhttp://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.
- Use the following PL/SQL block to REST-enable schema
- Consume the endpoints using MapLibre.To render the virtual mosaic for the GeoRaster objects in the
alps
and theelevation
tables, MapLibre can be configured to consume the endpoints created at step 2. The raster tiles obtained from theelevation
table are displayed in a terrain layer and the raster tiles obtained from thealps
table are displayed in a raster layer which will cover the terrain layer.- 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; /
- 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 attributetype
with value‘raster-dem’
, and attributeurl
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 attributetype
with value‘raster’
. It requires a source object named'alpsSource'
defined using attributetype
with value‘raster’
and attributeurl
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 attributetype
with value‘hillshade’
. It requires a source object named'hillshadeSource'
defined using attributetype
with value‘raster-dem’
and attributeurl
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 ofmlcontour.DemSource
. A layer object'contour'
is defined using attributetype
with value‘line’
, and another layer object'contour-text'
is defined using attributetype
with value‘symbol’
. Both layer objects require the source object names'contourSourceFeet'
defined using attributetype
with value‘vector’
and attributetiles
as an array with one elementdemSource.contourProtocolUrl
. - Elevation chart: An example of an interactive chart with the
elevation in 20 different points along a selected line. The libraries
zingchart
andturf
are needed to display the chart which uses thequeryTerrainElevation
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>
- Terrain tile layer: This terrain layer is configured to the
map by invoking
- 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.