ixg_platform/apps/compliance/static/js/ixg_zone_map.js
ixgadmin d5bc06802e Serve map tiles from our own origin
The origin-zone map drew its background from a public tile server, which made it
the only part of the admin page that depended on a third-party domain — and the
part that breaks. Where a viewer's network, proxy or browser filter refuses
tile.openstreetmap.org (the refusal arrives as a 403) the map never draws, while
the page, the Leaflet bundle and the polygon data are all same-origin and fine.

Fetch the tiles server-side and serve them from /tiles/{z}/{x}/{y}.png: cached
on disk, so a page opened repeatedly does not re-fetch, and gated on being
signed in, so the route is not an open tile relay. The upstream stays
configurable via LEAFLET_TILE_URL, and LEAFLET_TILE_PROXY_ENABLED=False puts the
browser back on direct fetches.

When a tile still cannot be fetched, answer an empty tile rather than an error
page and say so under the map, instead of leaving a viewer with a blank grid and
no explanation.
2026-09-17 13:00:59 +00:00

85 lines
2.6 KiB
JavaScript

/* Draw the stored origin-zone polygon on a Leaflet map.
*
* Display only: this renders what is already stored on the record. It does not
* validate the geometry, and it does not approve the zone. Validation and the
* two-step verification are separate pieces of work.
*
* The geometry is passed in a data-* attribute so the browser decodes it (no
* JSON injected into a script block).
*/
(function () {
"use strict";
var OSM_ATTRIBUTION =
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
function init() {
var el = document.getElementById("zone-map");
if (!el || typeof L === "undefined") {
return;
}
var geometry;
try {
geometry = JSON.parse(el.dataset.geometry || "null");
} catch (err) {
el.innerHTML =
'<p style="padding:1rem;color:#b91c1c">Stored geometry is not valid JSON, so it cannot be drawn.</p>';
return;
}
if (!geometry || !geometry.coordinates) {
return;
}
var map = L.map(el, { scrollWheelZoom: false });
if (el.dataset.tileUrl) {
var tiles = L.tileLayer(el.dataset.tileUrl, {
maxZoom: 19,
attribution: OSM_ATTRIBUTION,
}).addTo(map);
// If the tile source cannot be reached, the polygon still draws but the
// background stays blank. Say so, rather than leaving the viewer to wonder
// whether the record itself is broken.
var note = null;
tiles.on("tileerror", function () {
if (note) {
return;
}
note = document.createElement("p");
note.style.cssText =
"max-width:900px;margin-top:.5rem;font-size:.8rem;color:#b45309";
note.textContent =
"Map imagery could not be loaded. The area below is still drawn from the " +
"stored coordinates, so the shape can still be checked.";
el.insertAdjacentElement("afterend", note);
});
}
var layer = L.geoJSON(geometry, {
style: { color: "#0f766e", weight: 2, fillColor: "#14b8a6", fillOpacity: 0.25 },
}).addTo(map);
var bounds = layer.getBounds();
if (bounds && bounds.isValid()) {
map.fitBounds(bounds.pad(0.25));
} else {
map.setView([0, 0], 2);
}
// Grappelli lays the form out after load; a stale size leaves grey tiles.
setTimeout(function () {
map.invalidateSize();
if (bounds && bounds.isValid()) {
map.fitBounds(bounds.pad(0.25));
}
}, 250);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();