How to Control Map Layers, Features, and Popups Using JavaScript in Oracle APEX

A tested guide to the Oracle APEX map region JavaScript interface, from layers and features to popups, MapLibre access, and map events.

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

TaskMethods
Find layers and featuresgetLayers, getLayerIdByName, getFeature
Change features in the browseraddFeature, updateFeature, removeFeature
Move and zoomsetCenter, setZoomLevel
Read the map's positiongetMapCenterAndZoomLevel, getMapBboxAndZoomLevel, getMapPitchAndBearing, getMapStatus
Show, hide, and reorder layershideLayer, showLayer, moveLayer
Open and close popupsdisplayPopup, closeInfoWindow, closeAllInfoWindows, closeTooltip
Circle search toolgetCircle, clearCircle
Everything elsegetMapObject, which returns the MapLibre map
Reload, restart, focusrefresh, reset, focus
Eventsspatialmapinitialized, 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:

PropertyTypeDescription
layersobject[]The layers, each with id, label, useSpatialIndex, minZoom, maxZoom, tooltip, and infoWindow. The last two each have template and cssClasses.
tileLayerobjectThe 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.
useVectorTileLayersbooleanVector tiles instead of raster tiles.
navigationBarobjecttype (none, no-compass, or full) and position (top-left, top-right, or bottom-left).
mapFeaturesobjectWhich tools are on: mousewheelZoom, scaleBar, circleTool, browserLocation, rectangleZoom, distanceTool, overviewMap, and infiniteMap.
mapUnitSystemstringmetric or imperial, for the scale bar and distance tool.
legendobjectposition (start, end, or selector), selector, title, and cssClasses.
layerMessagesobjectWhere No data found and More data found appear: position (top or bottom) and selector.
bboxCustomarrayNew in 26.1: an initial map window, as lower-left and upper-right coordinates.
resetMapPositionbooleanStart at the configured position rather than the one saved in session state.
mapStatusItemstringAn item that receives the map window every time it changes.
itemsToSubmitstringItems sent with the map's data requests.
lazyLoadingbooleanLoad the data in a separate request after the page.
customStylesobject[]Custom SVG shapes for point layers: type, name, width, height, viewBox, and elements.
copyrightNoticestringThe copyright notice shown on the map.
mapDataobjectThe 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, Stores

mapStatusItem 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: true

The 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])
ParameterTypeDescription
pTypestring"tooltip" or "infoWindow".
pLayerIdnumberThe layer.
pFeatureIdstringThe feature.
pFocusAfterOpenbooleanMove focus into the popup.
pLngLatobject{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
An Oracle APEX map zoomed to Denver with the info window of a store open
The info window of the Denver store, opened from JavaScript after centering and zooming the map.

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 dataTypeDescription
changeTypestringmap-drag, map-zoom, map-rotate, toggle-layer, circle-drawn, circle-removed, feature-added, feature-removed, or feature-updated.
layersobject[]The visible layers.
bboxarrayThe map window.
zoom, pitch, bearingnumberThe map's position.
circleobjectThe 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 dataTypeDescription
idnumberThe feature, when the layer has a primary key and no clustering.
lng, latnumberThe point, or for lines and polygons, where the user clicked.
tooltip, infoWindowstringThe feature's tooltip and info window, as JSON.
cluster_id, point_countnumberFor 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
An Oracle APEX map flown to Chicago with the info window of the River North store open
After showStore("7"), the map has flown to Chicago and opened the store's info window.

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.

Vinish Kapoor
Vinish Kapoor

An Oracle ACE and software veteran with 25+ years of experience, passionate about AI and IT innovation.

guest

0 Comments
Oldest
Newest Most Voted
00