Oracle APEX - Native Maps with Draggable Markers

As you might know, the map region feature was introduced in APEX 21.1 and further enhanced in 23.2, when custom map backgrounds were added to the supported feature list.

While the MapLibre APEX implementation offers a robust set of declarative features, there were still some missing functionalities that were crucial for my latest project.

As usual, when a client has a problem, there are two ways to approach it: either I have the solution, or I find one.

In this case, it was the latter.

The problem

In this particular case, the goal was to display multiple markers, representing spatial points, on a map. The key functionality required was to allow users to interact with the map by manually adjusting the position of each marker. This would provide a way for users to fine-tune the location of these spatial points directly on the map interface.

As the markers are moved, the updated coordinates would need to be captured and reflected in real time, with the new positions being automatically saved and updated in a corresponding database table for each record. This feature was essential to ensure data accuracy and flexibility for the user.

Considering a solution

It’s no secret that I’m a fan of solving problems in Oracle APEX in the simplest and most native way possible. While exploring my options for this particular challenge, I quickly realized a few limitations:

  • There was no declarative option to create draggable markers within the Maps Region.

  • There were no dynamic actions available that could handle or simulate this behavior.

  • The component lacked an 'Initialization JavaScript Function' that would suggest room for further customization or flexibility.

  • To make matters more challenging, there was little to no information available online about how to address a similar issue.

This left me with no choice but to dive deeper and come up with a custom solution.

Exploring MapLibre documentation

The first thing I needed to do was understand the technology behind the scenes and determine whether the JavaScript library supported this functionality at all.

After diving into the MapLibre documentation, I quickly discovered that draggable markers were indeed a built-in feature. It was as simple as setting the draggable flag to true when creating the markers.

    new maplibregl.Marker({ draggable: true});

You can find two working examples in the official documentation:

Exploring the Oracle APEX JavaScript API

The next logical step was to check the Oracle APEX JavaScript API documentation to find a way to influence how APEX creates the points on the map.

As you might have already noticed, there is a mapRegion interface in the API, which, as stated in the documentation, is used to access the properties and methods of the spatialMap API. More information can be found here.

Unfortunately, there is no method or property in the API that would allow me to solve my problem in an obvious way. However, after some time spent reading, I eventually found what I needed.

The getMapObject method

As stated in the documentation, the getMapObject method returns the MapLibre GL JS Map object after initialization.

To me, this reads as, 'If we cannot solve your problems, here is the map; do as you please'—in other words, exactly what I needed.

You might wonder, 'What can you do with this?' Well, in this case, you can take control of the map instance using a Map Initialized Dynamic Action and operate with it using all the features offered by the library itself that are not wrapped in the APEX JavaScript API implementation.

For example, you can fly to a certain location on the map with just the following code:

let map = apex.region("airportMap").getMapObject();
map.flyTo({
    center: [0, 0], 
    zoom: 11, 
    speed: 0.2,
    curve: 1
});

The official documentation can be found here.

💡
Developer code that uses the MapLibre API may not be forward compatible if the MapLibre API changes

The actual solution

Given that there is no native way to influence the creation of markers in APEX, the solution was clear to me: I would need to create my own markers.

Here’s the ingredient list:

  • A collection containing at least the ID, latitude, and longitude of the points

  • A hidden item computed with a JSON string from the collection to read the points’ data for adding the markers to the map object.

  • A native Map Region

  • An AJAX Callback Process to update the collection when the drag ends

  • A 'Map Initialized' Dynamic Action to load my draggable markers with custom JavaScript

  • Optionally, you can include a report on top of the collection.

The collection

Just a collection—nothing too complicated here. In this example, I am using the Sample Airport Data from the Sample Maps APEX application and limit the result set to 5 records for simplicity:

DECLARE
    l_query varchar2(4000);
BEGIN
    l_query := q'!
        select
            ID,
            nvl(
                to_number (
                    JSON_VALUE (geojson_result, '$.coordinates[0]')
                ),0) lat,
            nvl(
                to_number (
                    JSON_VALUE (geojson_result, '$.coordinates[1]')
                ),0) lng, 
            IATA_CODE,
            AIRPORT_TYPE,
            AIRPORT_NAME,
            CITY
        from (
            select a.*, 
                SDO_UTIL.TO_GEOJSON (geometry) AS geojson_result
            FROM EBA_SAMPLE_MAP_AIRPORTS a
            where city = 'ANCHORAGE'
            and rownum < 6
        )!';

    APEX_COLLECTION.CREATE_COLLECTION_FROM_QUERY_B (
        p_collection_name => 'MAP_MARKERS',
        p_truncate_if_exists => 'YES',
        p_query => l_query
     );
END;

The Computation

In order to create markers, you will need information to populate the markers’ attributes. This information will be read in the JavaScript section from a JSON file. In my case, I placed this data in a hidden item and computed it after the collection, using the collection sequence as an ID as it is required for the UPDATE_MEMBER_ATTRIBUTE procedure.

Here’s my SQL query (returning a single value) computation that generates the JSON in a format adapted from the official example and stores it in the hidden item P5_JSON:

SELECT
    JSON_OBJECT (
        'features' VALUE JSON_ARRAYAGG (
            JSON_OBJECT (
                'id' VALUE seq_id,
                'cssClasses' VALUE 'fa fa-2x fa-map-marker u-color-1-text',
                'geometry' VALUE JSON_OBJECT (
                    'type' VALUE 'Point',
                    'coordinates' VALUE JSON_ARRAY (
                       c002, c003
                    )
                )
            )
        )
    ) AS jstr
from apex_collections
where collection_name = 'MAP_MARKERS'

The map region

The map region is, in my case, simply a map region—not too complicated. However, be sure to set a static ID for the map region, in my case, airportMap.

Since at least one layer is mandatory for the region and this layer wasn’t needed for my purposes, I opted to pass a query that retrieves no data from the database to conserve computing resources.

Oracle APEX - Fake query in map region to return no rows

This will render and empty map of the world, but feel free to customize it to fit your needs.

The AJAX callback process

Once again, there’s no magic—just an AJAX Callback process that executes some PL/SQL code to update the collection member with the new coordinates.

APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE (
    p_collection_name   => 'MAP_MARKERS',
    p_seq               => apex_application.g_x01,
    p_attr_number              => 2,
    p_attr_value              => apex_application.g_x02
);


APEX_COLLECTION.UPDATE_MEMBER_ATTRIBUTE (
    p_collection_name   => 'MAP_MARKERS',
    p_seq               => apex_application.g_x01,
    p_attr_number              => 3,
    p_attr_value              => apex_application.g_x03
);

Creating Markers: The Actual Magic

The final step involves a block of JavaScript that will be triggered by the Map Initialized component event. I created a Dynamic Action and configured the When section attributes as shown below, ensuring that the correct map region is selected:

Oracle APEX - Dynamic Action Configuration

Next, create an Execute JavaScript Code True Action with the following code:

// Retrieve the MapLibre map object from the APEX region
let map = apex.region("airportMap").getMapObject();

// Parse the JSON string stored in the hidden item P5_JSON
let geojson = JSON.parse($v("P5_JSON"));

// Function to handle the drag end event of markers
function onDragEnd() {
    // Get the longitude and latitude of the marker after dragging
    const lngLat = this.getLngLat();
    const element = this.getElement(); // Get the marker element

    // Send an AJAX request to update the location in the database
    apex.server.process('UPDATE_LOCATION', {                             
        x01: element.dataset.id, // Marker ID
        x02: lngLat.lng,        // Updated longitude
        x03: lngLat.lat         // Updated latitude
    }, {
        success: function () {             
            // Refresh the collection report on success (Optional)
            apex.region('markerReport').refresh();
            console.log("Location updated in collection");
        },
        dataType: "text" // Specify the response type (plain text)
    });
}

// Loop through each feature in the GeoJSON data to create markers
geojson.features.forEach(marker => {
    // Create a new div element for the marker
    const el = document.createElement("div");
    el.className = "maplibregl-marker maplibregl-marker-anchor-center"; // Set marker classes
    el.dataset.id = marker.id; // Set the marker ID as a data attribute

    // Create a span element for additional marker styling
    const chd = document.createElement("span");
    chd.className = marker.cssClasses; // Set CSS classes from the marker data
    el.appendChild(chd); // Append the span to the marker element

    // Create a new draggable marker and add it to the map
    new maplibregl.Marker({ draggable: true, element: el })
        .setLngLat(marker.geometry.coordinates) // Set the initial position of the marker
        .addTo(map) // Add the marker to the map
        .on("dragend", onDragEnd); // Attach the drag end event handler
});

// Center the map on the last marker's coordinates
if (geojson.features.length > 0) {
    const lastCoordinates = geojson.features[geojson.features.length - 1].geometry.coordinates; // Get last marker coordinates
    map.flyTo({ center: lastCoordinates, zoom: 11 }); // Fly to the last marker's position
}

The result

As you can see, even if APEX doesn't support certain features declaratively, it provides the tools necessary to solve your problems creatively. With a bit of ingenuity, the sky is the limit!

Enjoy Life!