Beyond Pins: Live Clustered Maps in Oracle APEX
I started this because the normal APEX Map Region was giving me a plain page with dots on it and nothing else. No clustering, no live updates, no custom colors for different asset status. Just a map with pins sitting there. I needed something that actually looked alive, showing trucks, stores and sensors moving and updating on their own without me refreshing the page every few seconds.
So I decided to skip the built in Map Region completely and build the whole thing with Leaflet.js on top of an ORDS REST endpoint instead. This post is basically my notes from that process, the parts that worked, and a good number of parts that did not work on the first try.
Setting up the base data
First thing was a plain table to hold assets, trucks, stores, sensors, with a lat and lng column and a status column. Nothing fancy at this stage.
CREATE TABLE map_assets (
asset_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
asset_name VARCHAR2(100) NOT NULL,
asset_type VARCHAR2(30) NOT NULL,
status VARCHAR2(20) DEFAULT 'ACTIVE',
lat NUMBER(9,6) NOT NULL,
lng NUMBER(9,6) NOT NULL,
last_updated TIMESTAMP DEFAULT SYSTIMESTAMP
);
Getting the data out through ORDS
Instead of writing a page level SQL query, I exposed the table as a REST module in ORDS. This way the same JSON feed could be reused later for a mobile app too, not locked inside one APEX page.
SELECT asset_id,
asset_name,
asset_type,
status,
lat,
lng,
TO_CHAR(last_updated, 'YYYY-MM-DD"T"HH24:MI:SS') AS last_updated
FROM map_assets
ORDER BY asset_id
One issue I faced here was hitting the endpoint from the browser and getting nothing back, just an empty items array. Turned out I forgot to commit my insert statements in SQL Workshop. Small mistake, wasted almost half an hour thinking the REST module itself was broken.
The clustering part
This is where the real work started. I tried using the native APEX Map Region with a Point layer first, since it is the obvious built in option. It showed the markers fine but there was no clustering at all, so with fifty plus points close together the map just turned into a mess of overlapping dots. There was also no clean way to auto refresh it without doing a full region reload, which made the page flicker every time.
That flicker on every refresh was actually the main reason I dropped the native region completely.
So I brought in Leaflet.js along with the markercluster plugin. Loaded both as File URLs on the page and built a plain div for the map instead of using a region type at all.
<div class="map-dashboard-wrapper">
<div class="map-header">
<h2>Live Asset Map</h2>
<span id="asset-count">Loading</span>
</div>
<div id="asset-map"></div>
</div>
var map = L.map('asset-map').setView([13.06, 80.24], 11);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: 'OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
var markerClusterGroup = L.markerClusterGroup();
map.addLayer(markerClusterGroup);
fetch('https://oracleapex.com/ords/jefith_shalin/map/assets')
.then(function (r) { return r.json(); })
.then(function (data) {
data.items.forEach(function (item) {
var marker = L.marker([item.lat, item.lng]);
markerClusterGroup.addLayer(marker);
});
});
First time I ran this the map just showed as a grey box, nothing on it at all. I checked the console and saw the Leaflet CSS file was never loaded, I had only added the JS file URL and forgot the CSS one. Added the stylesheet link and the map appeared straight away.
Custom pins and status colors
Default Leaflet markers are the blue teardrop shape, fine for a demo but not useful when you need to tell an active truck apart from one in alert status at a glance. I switched to divIcon so I could style the pin with plain CSS, a small rotated square that turns into a teardrop shape, colored based on the status field coming back from the REST call.
For the alert status pins I wanted something that grabs attention without being too much, so I added a pulsing ring animation using box shadow keyframes. Tried a plain color change first, just switching red to a brighter red, but that did not stand out enough on a map full of other colored pins. The pulsing ring worked much better.
Auto refresh without breaking the zoom level
Getting new data every fifteen seconds was easy with setInterval, the hard part was doing it without resetting the user's zoom and pan position each time, and without duplicating markers on the map.
I tried clearing the whole cluster group and adding all markers fresh on every refresh call. It technically worked but the map would flash and briefly reset the zoom each time, which looked broken even though the data itself was correct. Finally this approach worked, I kept a small object in memory mapping asset id to its marker, and on every refresh I just moved existing markers to their new position instead of removing and recreating them, only adding or removing markers for assets that actually appeared or disappeared from the list.
var markerIndex = {};
function updateMarkers(items) {
items.forEach(function (item) {
if (markerIndex[item.asset_id]) {
markerIndex[item.asset_id].setLatLng([item.lat, item.lng]);
} else {
var marker = L.marker([item.lat, item.lng]);
markerClusterGroup.addLayer(marker);
markerIndex[item.asset_id] = marker;
}
});
}
The dark mode toggle bug that had me stuck for a while
I added a small button to switch the map tiles into a dark theme using a CSS filter. Wrote the click handler inside a DOMContentLoaded listener, since that felt like the safe standard way to do it. Except clicking the button did nothing at all, not even an error in the console, just silence.
I tried moving the CSS filter rule around, tried renaming classes, tried checking if the button id was correct, spent a good hour on this. Finally realised that the APEX page load JavaScript already runs after the DOM is ready, so wrapping it in another DOMContentLoaded listener meant that event had already fired before my listener even got attached. Removed the wrapper, called the function directly at the top level, and the toggle started working on the very first click.
What I would tell myself before starting
- Test the REST endpoint directly in a browser tab before wiring it into any JavaScript.
- Load both the Leaflet CSS and JS file, missing the CSS gives a blank grey box with zero errors.
- Never wrap APEX page load JavaScript in a DOMContentLoaded listener.
- Keep a marker index by id so refreshes update markers in place instead of recreating them.
That is basically the full journey from a plain table to a live clustered map with custom pins and a working dark mode toggle. The demo link and the full source are both above if you want to see it running or go through the code yourself.
Comments
Post a Comment