Skip to main content

Beyond Pins: Live Clustered Maps in Oracle APEX

Rise With Apex · Oracle APEX 26.1

Beyond Pins: Live Clustered Maps in Oracle APEX

A build log, written while actually building it

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.

01

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.

map_assets.sql
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
);
note I tried putting the coordinates as VARCHAR2 first, just to move fast, but then Leaflet kept throwing errors because it expects numbers and I was passing strings straight from JSON. Switched the column type to NUMBER and that small change fixed a lot of confusing bugs later on.
02

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.

map_assets_get.sql
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.

03

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.

map-region.html
<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>
map-init.js
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.

04

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.

tip Keep iconAnchor at half the pin width and the full pin height, otherwise the pin points at the wrong spot on the map once you resize it.

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.

05

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.

marker-index.js
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;
    }
  });
}
06

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.

fix If a button listener silently does nothing in APEX page load JavaScript, check whether you wrapped it in DOMContentLoaded. That event usually already fired by the time this script runs.
07

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.

Thanks for reading, feel free to open an issue on the repo if something does not work for you.

Comments

Popular posts from this blog

Face Detection in Oracle Apex

Face Detection Blog - Embedded JS Jefith Shalin Oracle APEX Developer · May 2025 Live Demo GitHub Oracle APEX · AI Integration A Developer's Real Story Building a Face Detection Attendance System in Oracle APEX A real story of trial, error, and finally making face-api.js actually work inside an APEX app. No fluff — just what happened and what finally worked. Try Live Demo View on GitHub 0.22.2 face-api.js Version 2 APEX Pages Built 4 AJAX Processes face-api.js Oracle APEX JavaScript AI / ML Face Recognition CDN Attendance System PL/SQL The Beginning So here is how it all started I was tasked with building an attendance system for our off...

Screen Recorder in Oracle APEX (Single Page)

Oracle APEX Tutorial Screen Recorder in Oracle APEX : Single Page Build a fully functional browser-based screen recorder inside Oracle APEX using just one Static Content region and native JavaScript. No plugins, no external libraries, no server uploads required. ✍️ Why I Built This I was working on a client project where the support team needed to record screen issues and share them directly from the APEX application, without switching to any external tool. Installing third-party software was not an option on their machines, and every screen recorder extension required IT approval. That is when I thought: the browser already has everything we need. Why not build it right inside APEX? That idea turned into this. &#127916; Start / Stop Recording &#128065; Instant Preview ⬇️ One-click Download &#128266; Audio + Video ...

Sticky Notes Widget Inside Oracle APEX

Oracle APEX Project Building a Sticky Notes Widget in Oracle APEX How I built a fully draggable, color-coded, per-user sticky notes board using jQuery UI, APEX Ajax callbacks, and a bit of patience. Live Demo GitHub Repo Oracle APEX jQuery UI PL/SQL Ajax Callbacks JavaScript CSS Introduction Why I Built This I have been building internal tools on Oracle APEX for a while now, and one thing I always felt was missing was a place where users could quickly jot down thoughts without leaving the page. Think of it like a personal scratchpad that lives right inside the app. I had seen sticky note UIs in some Google products and I thought, how hard can this be in APEX? It turned out to be more interesting than I expected. There were a few wrong tu...