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,
alpsis the differentiator for this specific endpoint. The parametersx,y, andzwill 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_nameandp_patternas in the template definition. The PL/SQL block specified in the parameterp_sourceinvokes SDO_GEOR_UTL.get_rasterTile, passing parametersx,y, andz, and specifying‘ALPS’as the value for parameterTABLE_NAMEand'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 moduleservice2to retrieve raster tiles from theelevationtable.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_nameandp_pattern valueas used in the template definition.Specify the PL/SQL block for the
p_sourceparameter. 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 thedemcolumn of theelevationtable. Use‘terrainrgb’as theimage_processingparameter passed to thesdo_geor_utl.get_rastertilefunction 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/0andhttp://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
alpsand theelevationtables, MapLibre can be configured to consume the endpoints created at step 2. The raster tiles obtained from theelevationtable are displayed in a terrain layer and the raster tiles obtained from thealpstable 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
elevationtable 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 attributetypewith value‘raster-dem’, and attributeurlpointing 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 attributetypewith value‘raster’. It requires a source object named'alpsSource'defined using attributetypewith value‘raster’and attributeurlpointing 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 attributetypewith value‘hillshade’. It requires a source object named'hillshadeSource'defined using attributetypewith value‘raster-dem’and attributeurlpointing to a TileJSON document. - Contours layer: Maplibre displays lines by connecting points
at the same elevation. This requires adding
maplibre-contourlibrary to the map by creating an instance ofmlcontour.DemSource. A layer object'contour'is defined using attributetypewith value‘line’, and another layer object'contour-text'is defined using attributetypewith value‘symbol’. Both layer objects require the source object names'contourSourceFeet'defined using attributetypewith value‘vector’and attributetilesas 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
zingchartandturfare needed to display the chart which uses thequeryTerrainElevationfunction 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.