The Map region in Oracle APEX shows your spatial data on a background of map tiles with almost no code. Its JavaScript interface, mapRegion, takes over when you need more: flying the map to a record the user picked, opening a feature's info window from a list, adding a temporary marker, hiding a layer, or reacting when the user pans and clicks.
This guide covers the whole interface: the region's properties, every method for layers, features, position, and popups, the events it fires, and a recipe that flies the map to a store and opens its info window. Each method has a tested example with the real output.
Quick Reference
| Task | Methods |
|---|---|
| Find layers and features | getLayers, getLayerIdByName, getFeature |
| Change features in the browser | addFeature, updateFeature, removeFeature |
| Move and zoom | setCenter, setZoomLevel |
| Read the map's position | getMapCenterAndZoomLevel, getMapBboxAndZoomLevel, getMapPitchAndBearing, getMapStatus |
| Show, hide, and reorder layers | hideLayer, showLayer, moveLayer |
| Open and close popups | displayPopup, closeInfoWindow, closeAllInfoWindows, closeTooltip |
| Circle search tool | getCircle, clearCircle |
| Everything else | getMapObject, which returns the MapLibre map |
| Reload, restart, focus | refresh, reset, focus |
| Events | spatialmapinitialized, spatialmapchanged, spatialmapclick, spatialmapobjectclick |
How to Run These Examples
The examples ran in Oracle APEX 26.1 on a store map page. The Map region has the DOM ID store_map and two layers: Customers, a heat map, and Stores, a point layer with a tooltip and an info window. The output under each example is exactly what the browser console printed.
A map needs a moment after the page loads before it is usable: it loads its data and tiles, then zooms to fit its features. Until it fires its initialized event, the getters return empty objects and the setters do nothing. So when you try these in the console, wait until the map has finished drawing, and in page code, run map logic from a handler of that event, as shown near the end. A few examples pause with await at the top level, which the console allows; inside a dynamic action, wrap such code in an async function.
For building the region in Page Designer, see the guide to maps, calendars, and trees in Oracle APEX.
Map Region Properties
The map is drawn with MapLibre GL JS. Its type is SpatialMap and its widget is spatialMap. Its configuration is available as properties of the region interface, alongside the members every region shares, which are covered in the guide to controlling regions with apex.region:
| Property | Type | Description |
|---|---|---|
| layers | object[] | The layers, each with id, label, useSpatialIndex, minZoom, maxZoom, tooltip, and infoWindow. The last two each have template and cssClasses. |
| tileLayer | object | The background map. type is oracle-default, oracle-custom, or shared-component. name and darkmodeName pick a style such as osm-bright, osm-positron, osm-dark-matter, bi-world-map, or world-map. Custom backgrounds add url, layer_type, api_key, attribution, http_headers, zoom_min, and zoom_max, each with a _dark variant. |
| useVectorTileLayers | boolean | Vector tiles instead of raster tiles. |
| navigationBar | object | type (none, no-compass, or full) and position (top-left, top-right, or bottom-left). |
| mapFeatures | object | Which tools are on: mousewheelZoom, scaleBar, circleTool, browserLocation, rectangleZoom, distanceTool, overviewMap, and infiniteMap. |
| mapUnitSystem | string | metric or imperial, for the scale bar and distance tool. |
| legend | object | position (start, end, or selector), selector, title, and cssClasses. |
| layerMessages | object | Where No data found and More data found appear: position (top or bottom) and selector. |
| bboxCustom | array | New in 26.1: an initial map window, as lower-left and upper-right coordinates. |
| resetMapPosition | boolean | Start at the configured position rather than the one saved in session state. |
| mapStatusItem | string | An item that receives the map window every time it changes. |
| itemsToSubmit | string | Items sent with the map's data requests. |
| lazyLoading | boolean | Load the data in a separate request after the page. |
| customStyles | object[] | Custom SVG shapes for point layers: type, name, width, height, viewBox, and elements. |
| copyrightNotice | string | The copyright notice shown on the map. |
| mapData | object | The data from the last request: map.bbox, and map.series with one series of features per layer. |
const map = apex.region("store_map");
console.log("tileLayer:", map.tileLayer.type, map.tileLayer.name, "- vector tiles:", map.useVectorTileLayers);
console.log("navigationBar:", map.navigationBar);
console.log("mapFeatures:", map.mapFeatures);
console.log("layers:", map.layers.map((layer) => layer.label).join(", "));Output:
tileLayer: oracle-default osm-positron - vector tiles: true
navigationBar: {"type":"full", "position":"top-right"}
mapFeatures: {
"mousewheelZoom": false,
"scaleBar": true,
"circleTool": false,
"browserLocation": false,
"rectangleZoom": true,
"distanceTool": false,
"overviewMap": false,
"infiniteMap": true
}
layers: Customers, StoresmapStatusItem deserves a mention: point it at a hidden item and the item always holds the current map window, which a report beside the map can use to show only what is visible.
Finding Layers and Features
getLayers and getLayerIdByName
getLayers returns the layers with their configuration. getLayerIdByName returns a layer's ID, which most other methods need.
region.getLayers() → object[] region.getLayerIdByName(pName) → number
const map = apex.region("store_map");
for (const layer of map.getLayers()) {
console.log(layer.name.padEnd(10), String(layer.id).padEnd(18), layer.style?.layerType || "point");
}
console.log("Stores:", map.getLayerIdByName("Stores"));Output:
Customers 77734828109363710 heatMap Stores 77726813589332850 svg Stores: 77726813589332850
Those long numbers are the layers' internal IDs in the application, so they differ from one copy of an application to another. Always look them up by name with getLayerIdByName rather than hard-coding them.
getFeature
Returns one feature of a layer. The result has id, which is the value of the layer's primary key column, geometry as GeoJSON, tooltip, and columns, the values the tooltip and info window templates can use. For an unknown ID it returns an empty object and logs Invalid feature ID to the console.
region.getFeature(pLayerId, pFeatureId) → object
const map = apex.region("store_map");
const feature = map.getFeature(map.getLayerIdByName("Stores"), "1");
console.log(feature.id, feature.geometry);
console.log(feature.columns);Output:
1 {"type":"Point", "coordinates":[-105.0003, 39.7527]}
{
"STORE_NAME": "Orbit Denver Union Station",
"CITY": "Denver",
"STATE": "Colorado",
"OPENED_ON": "4/15/2016",
"FLOOR_AREA_SQFT": "12500"
}GeoJSON puts longitude first, so the Denver store sits at -105 longitude and 39.75 latitude. Mixing up the order is the most common reason a feature lands in the wrong hemisphere.
Changing Features in the Browser
addFeature, updateFeature, and removeFeature
Add a feature to a layer, replace one, or remove one. These changes happen in the browser only; the next refresh reloads the layer from the server and they are gone. A feature is an object shaped like the one getFeature returns, and id and geometry are required.
region.addFeature(pLayerId, pFeature) region.updateFeature(pLayerId, pFeature) region.removeFeature(pLayerId, pFeatureId)
const map = apex.region("store_map");
const stores = map.getLayerIdByName("Stores");
map.on("spatialmapchanged", (event, data) => console.log("changed:", data.changeType));
map.addFeature(stores, {
id: "901",
geometry: { type: "Point", coordinates: [-149.9003, 61.2181] },
tooltip: "Orbit Anchorage (planned)"
});
console.log("added:", map.getFeature(stores, "901").tooltip);
map.updateFeature(stores, { id: "901", geometry: { type: "Point", coordinates: [-149.9003, 61.2181] },
tooltip: "Orbit Anchorage Midtown" });
console.log("updated:", map.getFeature(stores, "901").tooltip);
map.removeFeature(stores, "901");
console.log("removed:", map.getFeature(stores, "901"));Output:
changed: feature-added
added: Orbit Anchorage (planned)
changed: feature-updated
updated: Orbit Anchorage Midtown
changed: feature-removed
Error: Invalid feature ID
removed: {}Each method fires the changed event, and its changeType says which change happened. The Error line after removal is getFeature's console message for an ID that no longer exists. These methods suit previews, such as showing where a store would go before the user saves it; to store the feature, insert it in the database and refresh the map.
Moving the Map and Reading Its Position
setCenter and setZoomLevel
Move the map to a position, given as a [longitude, latitude] array with longitude first, and set the zoom level, from 0 for the whole world to 24.
region.setCenter(pCenter) region.setZoomLevel(pZoomLevel)
const map = apex.region("store_map");
map.on("spatialmapchanged", (event, data) => console.log("changed:", data.changeType, "- zoom", data.zoom.toFixed(2)));
map.setCenter([-104.99, 39.74]); // longitude, latitude: Denver
map.setZoomLevel(10);
await new Promise((resolve) => setTimeout(resolve, 2500));
const { center, zoom } = map.getMapCenterAndZoomLevel();
console.log("center:", center.lng.toFixed(2), center.lat.toFixed(2), "- zoom:", zoom);Output:
changed: map-drag - zoom 3.34 changed: map-zoom - zoom 10.00 center: -104.99 39.74 - zoom: 10
The two calls fire two changed events, a map-drag for the move and a map-zoom for the zoom. For an animated move in one step, use the MapLibre flyTo method, as the recipe at the end does.
getMapCenterAndZoomLevel, getMapBboxAndZoomLevel, getMapPitchAndBearing, and getMapStatus
Report where the map is: the center, the bounding box as [west, south, east, north], the zoom level, the pitch (tilt in degrees), and the bearing (direction clockwise from north). getMapStatus returns everything except the center.
region.getMapCenterAndZoomLevel() → {center: {lng, lat}, zoom}
region.getMapBboxAndZoomLevel() → {bbox, zoom}
region.getMapPitchAndBearing() → {pitch, bearing}
region.getMapStatus() → {bbox, zoom, pitch, bearing}const map = apex.region("store_map");
const round = (n) => Math.round(n * 100) / 100;
const status = map.getMapStatus();
console.log("bbox:", status.bbox.map(round), "- zoom:", round(status.zoom));
console.log("pitch and bearing:", map.getMapPitchAndBearing());
console.log("same bbox:", JSON.stringify(map.getMapBboxAndZoomLevel().bbox) === JSON.stringify(status.bbox));Output:
bbox: [-139.27, 24.42, -54.88, 51.99] - zoom: 3.34
pitch and bearing: {"pitch":0, "bearing":0}
same bbox: trueThe bounding box and zoom of 3.34 are where the map settled on its own to fit all the stores and customers across North America.
Showing, Hiding, and Ordering Layers
hideLayer and showLayer
New in 26.1. Hide and show a layer by name or ID, which is what the legend's checkboxes do.
region.hideLayer(pNameOrId) region.showLayer(pNameOrId)
const map = apex.region("store_map");
const mapLibre = map.getMapObject();
const visible = (id) => mapLibre.getLayoutProperty(String(id), "visibility") ?? "visible";
const customers = map.getLayerIdByName("Customers");
map.hideLayer("Customers");
console.log("Customers:", visible(customers));
map.showLayer(customers);
console.log("Customers:", visible(customers));Output:
Customers: none Customers: visible
The example uses the MapLibre map only to check the result. Note that hideLayer and showLayer do not fire the changed event, while clicking the legend's checkboxes fires it with toggle-layer, so if you track layer visibility in a changed handler, update it yourself after these calls.
moveLayer
New in 26.1. Changes the drawing order: the layer moves just below the layer you name, or to the top when you name none.
region.moveLayer(pLayerId, [pBeforeLayerId])
const map = apex.region("store_map");
const order = () => map.getMapObject().getStyle().layers.slice(-2)
.map((layer) => map.getLayers().find((l) => String(l.id) === layer.id).name).join(" below ");
console.log(order());
map.moveLayer(map.getLayerIdByName("Stores"), map.getLayerIdByName("Customers"));
console.log(order());Output:
Customers below Stores Stores below Customers
Here, moving Stores below Customers lets the heat map cover the store pins. Usually you want the opposite, points above areas, so that markers stay clickable.
Opening and Closing Popups
displayPopup
Opens a feature's tooltip or info window, as if the user had pointed at it or clicked it.
region.displayPopup(pType, pLayerId, pFeatureId, pFocusAfterOpen, [pLngLat])
| Parameter | Type | Description |
|---|---|---|
| pType | string | "tooltip" or "infoWindow". |
| pLayerId | number | The layer. |
| pFeatureId | string | The feature. |
| pFocusAfterOpen | boolean | Move focus into the popup. |
| pLngLat | object | {lng, lat}: where to open the popup, for a feature that is a line or polygon rather than a point. |
const map = apex.region("store_map");
const stores = map.getLayerIdByName("Stores");
map.setCenter([-105.0003, 39.7527]);
map.setZoomLevel(11);
await new Promise((resolve) => setTimeout(resolve, 2000));
map.displayPopup("infoWindow", stores, "1", false);
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log("open:", map.element.find(".maplibregl-popup").length, "popup");Output:
open: 1 popup

The info window's content comes from the layer's info window template, filled with the feature's columns. The code only chooses which feature to open.
closeInfoWindow, closeAllInfoWindows, and closeTooltip
Close one feature's info window, every info window (of one layer or of all layers), or the tooltip.
region.closeInfoWindow(pLayerId, pFeatureId) region.closeAllInfoWindows([pLayerId]) region.closeTooltip()
const map = apex.region("store_map");
const stores = map.getLayerIdByName("Stores");
const popups = () => map.element.find(".maplibregl-popup").length;
map.displayPopup("infoWindow", stores, "1", false);
map.displayPopup("infoWindow", stores, "2", false);
console.log("open:", popups());
map.closeInfoWindow(stores, "1");
console.log("after closeInfoWindow:", popups());
map.closeAllInfoWindows();
console.log("after closeAllInfoWindows:", popups());
map.displayPopup("tooltip", stores, "3", false);
console.log("tooltip:", map.element.find(".maplibregl-popup").text().trim());
map.closeTooltip();
console.log("after closeTooltip:", popups());Output:
open: 2 after closeInfoWindow: 1 after closeAllInfoWindows: 0 tooltip: Layer: StoresOrbit Seattle Capitol Hill after closeTooltip: 0
Several info windows can be open at once, as the first line shows, which gets cluttered quickly. Calling closeAllInfoWindows before opening a new one keeps the map readable.
The Circle Tool
getCircle and clearCircle
When the region's circle tool is enabled, the user can draw a circle on the map to find features within a distance. getCircle returns that circle as a GeoJSON object, and clearCircle removes it. This map has no circle tool, so there is nothing to return:
region.getCircle() → object | null region.clearCircle()
const map = apex.region("store_map");
console.log("circle tool:", map.mapFeatures.circleTool);
console.log("getCircle:", map.getCircle());
map.clearCircle(); // nothing to clear: no error
console.log("cleared");Output:
circle tool: false getCircle: null cleared
On a map with the tool, the changed event reports circle-drawn and circle-removed, and circle-drawn includes the circle in its data, ready to send to the server for a distance query.
Going Beyond the Region Interface
getMapObject
Returns the underlying MapLibre Map object, for anything the region interface does not offer, such as pitch, animations, and your own sources and layers. Code that uses it depends on the MapLibre version APEX ships, so it can break when an APEX upgrade moves to a newer version.
region.getMapObject() → maplibregl.Map | null
const mapLibre = apex.region("store_map").getMapObject();
console.log("MapLibre", maplibregl.getVersion ? maplibregl.getVersion() : maplibregl.version);
console.log("style layers:", mapLibre.getStyle().layers.length, "- zoom:", mapLibre.getZoom().toFixed(2));
mapLibre.setPitch(45); // tilt: no region method for this
console.log("pitch:", apex.region("store_map").getMapPitchAndBearing().pitch);Output:
MapLibre 5.6.1 style layers: 52 - zoom: 3.34 pitch: 45
APEX 26.1 ships MapLibre 5.6.1, so the MapLibre documentation for that version is the reference for everything you do with this object. The region still tracks what you change: after setPitch, getMapPitchAndBearing reports the new pitch.
Reloading, Resetting, and Focusing
refresh
Reloads every layer's data from the server and returns a promise. Features you added in the browser are gone afterwards.
region.refresh() → Promise
const map = apex.region("store_map");
const stores = map.getLayerIdByName("Stores");
map.addFeature(stores, { id: "901", geometry: { type: "Point", coordinates: [-149.90, 61.22] } });
console.log("before refresh:", Object.keys(map.getFeature(stores, "901")).length > 0);
await map.refresh(); // fetches the layers again
console.log("after refresh: ", Object.keys(map.getFeature(stores, "901")).length > 0);Output:
before refresh: true Error: Invalid feature ID after refresh: false
Refresh the map after a page item that its layers use in their queries changes, making sure that item is listed in the region's Items to Submit so the new value reaches the server.
reset
Initializes the map again: it returns to its starting position and reloads its data.
region.reset()
const map = apex.region("store_map");
const zoom = () => map.getMapCenterAndZoomLevel().zoom.toFixed(2);
console.log("zoom:", zoom());
map.setZoomLevel(8);
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log("zoom:", zoom());
map.reset();
await new Promise((resolve) => setTimeout(resolve, 3000));
console.log("after reset:", map.getMapObject() ? zoom() : "(map not ready)");Output:
zoom: 3.34 zoom: 8.00 after reset: 3.34
focus
Moves keyboard focus to the map, where the arrow keys pan and the plus and minus keys zoom.
region.focus()
apex.region("store_map").focus();
console.log("focus:", document.activeElement.className);Output:
focus: maplibregl-canvas
Map Events
initialized
Fires when the map is ready, with its scripts loaded and the map drawn. The full name is spatialmapinitialized, and the dynamic action event is Map Initialized [Map]. Any code that works with the map as the page loads belongs in a handler of this event, either in JavaScript or in a dynamic action on that event, as described in the complete guide to dynamic actions.
apex.jQuery(apex.gPageContext$).on("spatialmapinitialized", (event) => {
const map = apex.region("store_map");
console.log("spatialmapinitialized on", event.target.id);
console.log("layers:", map.getLayers().map((layer) => layer.name).join(", "),
"- zoom:", map.getMapCenterAndZoomLevel().zoom.toFixed(2));
});Output:
spatialmapinitialized on store_map_map_region layers: Customers, Stores - zoom: 1.00
The handler must be attached before the map becomes ready, so this code goes in the page's Function and Global Variable Declaration attribute; pasting it into the console after the page has loaded does nothing. The event fires on the widget element, whose ID is the region's ID plus _map_region. Notice the zoom of 1.00: the map is ready but has not yet zoomed to fit its features, so do not read the map's final position in this handler.
changed
Fires when the map changes. The full name is spatialmapchanged, and the dynamic action event is Map Changed [Map]. The addFeature and setCenter examples above listen to it.
| Property of data | Type | Description |
|---|---|---|
| changeType | string | map-drag, map-zoom, map-rotate, toggle-layer, circle-drawn, circle-removed, feature-added, feature-removed, or feature-updated. |
| layers | object[] | The visible layers. |
| bbox | array | The map window. |
| zoom, pitch, bearing | number | The map's position. |
| circle | object | The circle, for circle-drawn. |
click and objectclick
click, full name spatialmapclick, fires when the user clicks an empty spot on the map, with the lng and lat of the click. objectclick, full name spatialmapobjectclick, fires when the user clicks a feature.
| Property of data | Type | Description |
|---|---|---|
| id | number | The feature, when the layer has a primary key and no clustering. |
| lng, lat | number | The point, or for lines and polygons, where the user clicked. |
| tooltip, infoWindow | string | The feature's tooltip and info window, as JSON. |
| cluster_id, point_count | number | For a cluster, its ID and how many points it holds. |
const map = apex.region("store_map");
const mapLibre = map.getMapObject();
map.on("spatialmapclick", (event, data) => console.log("spatialmapclick:", data.lng.toFixed(2), data.lat.toFixed(2)));
map.on("spatialmapobjectclick", (event, data) => console.log("spatialmapobjectclick:", data.id, data.tooltip));
// what a click on the map does, done with MapLibre (for the example only)
const lngLat = { lng: -90, lat: 10 };
mapLibre.fire("click", { lngLat, point: mapLibre.project(lngLat), originalEvent: {} });Output:
spatialmapclick: -90.00 10.00
The example simulates a click through MapLibre, which is what a real click does internally; in a real application the user's clicks fire the events. A common use of objectclick is setting a page item to data.id and refreshing a detail region, which turns the map into a master that drives the rest of the page.
Recipe: Fly the Map to a Feature
A list of stores beside the map, a search result, or a link in an email should bring one store into view. This function looks up the store's feature by ID with getFeature, flies the MapLibre map there with flyTo, an animated combination of centering and zooming, and opens the store's info window once the movement ends.
Put the function in the page's Function and Global Variable Declaration attribute. To call it from links in a report, give each link the class js-show-store and a data-id attribute holding the store ID, then create a dynamic action on the Click event of the jQuery selector .js-show-store whose Execute JavaScript Code action calls showStore with this.triggeringElement.dataset.id. A javascript: URL in the link also works, but not under a strict Content Security Policy.
// === Page › Function and Global Variable Declaration ===
// Flies the store map to a store and opens its info window.
function showStore(storeId) {
const map = apex.region("store_map");
const layerId = map.getLayerIdByName("Stores");
const feature = map.getFeature(layerId, storeId);
if (!feature) return;
const [lng, lat] = feature.geometry.coordinates;
map.getMapObject().flyTo({ center: [lng, lat], zoom: 12 }); // the MapLibre map
map.getMapObject().once("moveend", () => map.displayPopup("infoWindow", layerId, storeId));
}
// === Try it: a link or a button calls showStore with the ID of the Chicago store ===
showStore("7");
await new Promise((resolve) => setTimeout(resolve, 4000));
const { center, zoom } = apex.region("store_map").getMapCenterAndZoomLevel();
console.log("center:", center.lng.toFixed(3), center.lat.toFixed(3), "- zoom:", zoom);
const popup = $("#store_map .maplibregl-popup-content")[0];
console.log("info window:", popup.innerText.trim().split("\n").filter(Boolean).join(" / "));Output:
center: -87.634 41.892 - zoom: 12 info window: Orbit Chicago River North / Chicago, Illinois / Opened 11/3/2018 / 11200 sq ft

The feature ID is the value of the layer's Primary Key column. A feature can be missing from the map even when it exists in the table, for example in a layer with Maximum Rows set or one that is filtered. In the earlier tests, getFeature returned an empty object rather than null for an unknown ID, which the recipe's if (!feature) check lets through; if the store might not be loaded, also check that the feature has an id before flying to it.
Conclusion
The mapRegion interface gives you full control of an Oracle APEX map from JavaScript once it has fired spatialmapinitialized. getLayerIdByName and getFeature find layers and features without hard-coded IDs. addFeature, updateFeature, and removeFeature change the map in the browser until the next refresh. setCenter and setZoomLevel move it, with longitude always first, and the four getters report where it is. hideLayer, showLayer, and moveLayer, new in 26.1, manage layers; displayPopup and the close methods manage popups. getMapObject hands you MapLibre for anything else, at the cost of depending on its version. The changed, click, and objectclick events let the rest of the page react to the map, and flyTo plus displayPopup make a smooth "show me this store" feature in a dozen lines.
