Reference
Plugin API reference
Every Map Extender plugin is a plain JavaScript object named plugin, and
everything it can do goes through the methods on this page. This is the same contract an
AI assistant is given when you use the AI-assisted guide β
if you're writing code by hand instead, this page is the API you're writing against.
What a plugin can do
Map Extender hooks into Leaflet and Google Maps instances already embedded on a web page β it doesn't add its own map. A plugin talks to whichever one is on the page through the same API either way, so your code doesn't need to know which engine it's running against.
Plugins can:
- Detect when a map is ready, and read its bounds, center, zoom and type
- Move, pan, and fit the map view
- React to clicks, double-clicks, right-clicks, mouse movement, zoom, drag, and the map finishing a move
- Draw markers, polylines, polygons, circles, rectangles, GeoJSON shapes, image overlays, and custom controls
- Add raster tile overlays (XYZ tiles or a WMS service β see layers below for what those mean)
- Fetch remote data through the extension (so it isn't blocked by the host page's own security rules)
- Store plugin-specific data that survives a browser restart
- Read settings the user configured on the plugins page
- Log activity, warnings and errors to a panel the user can open
- React to page navigation and DOM elements appearing
Every one of these is enabled, disabled, edited, or deleted live β the extension applies the change to open tabs without a page reload.
Runtime rules
Plugin code runs as a plain script, not a module β no import,
export, TypeScript syntax, JSX, build tools, or npm packages. The
entry object every plugin receives is named plugin.
Ordinary browser APIs are available β setTimeout, clearTimeout,
Promise, document, window,
MutationObserver. Write it the way you'd write a userscript:
var state = {};
function run() {}
plugin.mapHook.onHook(function () {});
Avoid:
import x from '...';
export default {};
const enum X {}
How a plugin is stored
The plugins page stores each plugin as a record with this shape:
{
id: string,
name: string,
description: string,
usage?: string, // short how-to shown in the popup when enabled
enabled: boolean,
matchPatterns: string[], // which sites it runs on, see below
settingsSchema: PluginSettingSchema[],
settings: Record<string, string | number | boolean>,
code: string,
createdAt: number,
updatedAt: number
}
Match patterns are the URL patterns that decide which sites a plugin runs on β the same wildcard syntax Chrome's user-script system uses:
['*://*/*'] // every site
['https://www.openstreetmap.org/*'] // one site
['https://example.com/maps/*'] // one path on one site
Settings schema
A plugin can expose configurable options β a color, a toggle, a dropdown β on the plugins page. That's the settings schema: a JSON array describing each setting.
Paste it into the settings editor's JSON tab (or build it with the
form UI instead). Use double-quoted keys and strings, JSON true/false,
and JSON numbers β not JavaScript object syntax (no unquoted keys, trailing commas, or
undefined).
Supported types: text, number, boolean, select.
[
{ "key": "color", "label": "Marker color", "type": "text", "default": "red" },
{ "key": "opacity", "label": "Opacity", "type": "number", "default": 0.8 },
{ "key": "showLabels", "label": "Show labels", "type": "boolean", "default": true },
{
"key": "mode",
"label": "Mode",
"type": "select",
"default": "stations",
"options": ["stations", "stops", "both"]
}
]
Read them back in plugin code:
var color = String(plugin.settings.get('color') || 'red');
var opacity = Number(plugin.settings.get('opacity'));
var showLabels = plugin.settings.get('showLabels') !== false;
var allSettings = plugin.settings.getAll();
Core API
Basic info
plugin.pluginId
Logging
plugin.log('Loaded');
plugin.warn('Something looks unusual');
plugin.error('Something failed');
Logs appear in the log panel on the plugins page; errors also surface in the browser's DevTools console.
Page helpers
Run something after the page's DOM is ready:
plugin.onPageLoad(function () {
plugin.log('Page loaded');
});
Watch for URL changes (useful on single-page sites that never do a full navigation):
var stopUrlWatch = plugin.onUrlChange(function (url) {
plugin.log('URL changed:', url);
});
Watch for an element to appear anywhere on the page:
var stopElementWatch = plugin.onElementAppear('.some-selector', function (el) {
plugin.log('Element appeared:', el.textContent);
});
Or wait for exactly one:
plugin.waitForElement('.some-selector', 10000).then(function (el) {
plugin.log('Found element:', el.textContent);
});
Persistent store
Each plugin gets its own key-value storage space that survives a browser restart.
plugin.store.get('lastResult').then(function (value) {
plugin.log('Stored value:', value);
});
plugin.store.set('lastResult', { count: 10 });
plugin.store.remove('lastResult');
Fetch
Use plugin.fetch, never the browser's own fetch, for remote
requests. It runs through the extension's background process, which is what lets it use
the extension's network permissions instead of being bound by whatever the host page's
own security policy allows.
plugin
.fetch('https://overpass-api.de/api/interpreter', {
method: 'POST',
body: '[out:json];node["railway"="station"](45.0,7.6,45.1,7.7);out;'
})
.then(function (res) { return res.json(); })
.then(function (data) {
plugin.log('Received elements:', (data.elements || []).length);
})
.catch(function (err) {
plugin.error('Fetch failed:', err.message || String(err));
});
The extension's toolbar badge shows how many plugins are currently loading or fetching.
Map API
Everything about the map itself lives under plugin.mapHook.
Wait for the map
Always wrap map work in onHook β it fires once the map is detected and ready.
plugin.mapHook.onHook(function () {
plugin.log('Map is ready');
});
Read map state
var bounds = plugin.mapHook.getBounds();
var mapType = plugin.mapHook.getMapType();
var hooked = plugin.mapHook.isHooked();
var center = plugin.mapHook.getCenter();
var zoom = plugin.mapHook.getZoom();
The shape of bounds β the visible rectangle of the map, as its four edges:
{ south: number, west: number, north: number, east: number }
Move the view:
plugin.mapHook.setView({ lat: 45.0703, lng: 7.6869 }, 13);
plugin.mapHook.panTo({ lat: 45.0703, lng: 7.6869 });
plugin.mapHook.fitBounds({ south: 45.0, west: 7.6, north: 45.1, east: 7.7 });
React to map movement
onMoveEnd fires once whenever the map moves, zooms, pans, or finishes changing bounds β the natural place to reload viewport-based data.
plugin.mapHook.onHook(function () {
function reload(bounds) {
if (!bounds) return;
plugin.log('Map bounds:', bounds);
}
reload(plugin.mapHook.getBounds());
plugin.mapHook.onMoveEnd(reload);
});
React to clicks and other events
plugin.mapHook.onHook(function () {
plugin.mapHook.onClick(function (e) {
alert('Lat: ' + e.lat + '\nLng: ' + e.lng);
});
plugin.mapHook.onDoubleClick(function (e) { plugin.log('Double clicked:', e.lat, e.lng); });
plugin.mapHook.onRightClick(function (e) { plugin.log('Right clicked:', e.lat, e.lng); });
plugin.mapHook.onMouseMove(function (e) { /* fires often β use sparingly */ });
plugin.mapHook.onZoomEnd(function () { plugin.log('Zoom:', plugin.mapHook.getZoom()); });
plugin.mapHook.onDragEnd(function () { plugin.log('Center:', plugin.mapHook.getCenter()); });
});
Debounce (wait for movement to pause before acting) anything that triggers a network request:
var debounce;
function schedule(bounds) {
clearTimeout(debounce);
debounce = setTimeout(function () { loadForBounds(bounds); }, 500);
}
Layers
Layers are the things a plugin draws on the map. They're disposed of automatically whenever a plugin is disabled, edited, deleted, or re-run β you don't need to clean these up yourself (see Cleanup for what you do need to handle).
Marker layer
plugin.mapHook.onHook(function () {
var layer = plugin.mapHook.createMarkerLayer();
layer.addMarker({
id: 'torino-porta-nuova',
lat: 45.0703,
lng: 7.6869,
color: 'red',
popup: 'Torino Porta Nuova',
title: 'Station',
tooltip: 'Click for details'
});
layer.addMarker({
id: 'custom-image',
lat: 45.071,
lng: 7.687,
iconUrl: 'https://maps.google.com/mapfiles/ms/icons/blue-dot.png',
iconSize: { width: 32, height: 32 },
iconAnchor: { x: 16, y: 32 },
popup: 'Image marker'
});
layer.on('click', 'custom-image', function (e) {
plugin.log('Clicked image marker:', e.lat, e.lng);
layer.update('custom-image', { opacity: 0.6 });
});
// Optional:
// layer.remove('torino-porta-nuova');
// layer.clear();
// layer.dispose();
});
Feature event names supported by marker and shape layers: click, doubleClick, rightClick, mouseOver, mouseOut.
Polyline layer
plugin.mapHook.onHook(function () {
var layer = plugin.mapHook.createPolylineLayer();
layer.addPolyline({
id: 'sample-line',
path: [{ lat: 45.07, lng: 7.68 }, { lat: 45.08, lng: 7.69 }],
color: 'red',
weight: 2
});
});
Shape layers
plugin.mapHook.onHook(function () {
var polygons = plugin.mapHook.createPolygonLayer();
var circles = plugin.mapHook.createCircleLayer();
var rectangles = plugin.mapHook.createRectangleLayer();
polygons.addPolygon({
id: 'area',
path: [{ lat: 45.07, lng: 7.68 }, { lat: 45.08, lng: 7.69 }, { lat: 45.06, lng: 7.70 }],
strokeColor: 'orange',
fillColor: 'orange',
fillOpacity: 0.25
});
circles.addCircle({
id: 'radius',
center: { lat: 45.0703, lng: 7.6869 },
radius: 500,
strokeColor: 'green',
fillColor: 'green',
fillOpacity: 0.15
});
rectangles.addRectangle({
id: 'box',
bounds: { south: 45.06, west: 7.67, north: 45.08, east: 7.70 },
strokeColor: 'red',
fillOpacity: 0
});
});
XYZ tile layer
A raster overlay made of small square images ("tiles"), addressed by zoom (z) and grid position (x, y) β the standard way web maps serve imagery.
plugin.mapHook.onHook(function () {
var layer = plugin.mapHook.createTileLayer({
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: 'OpenStreetMap',
opacity: 0.5,
tileSize: 256,
minZoom: 2,
maxZoom: 19
});
layer.setOpacity(0.35);
});
Only {z}, {x} and {y} are substituted. Do not use an {s} subdomain placeholder β the Google Maps adapter leaves it literal and every tile request 404s.
GeoJSON layer
GeoJSON is a standard JSON format for map data β points, lines, and shapes with properties attached.
plugin.mapHook.onHook(function () {
var layer = plugin.mapHook.createGeoJsonLayer();
layer.addGeoJson({
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: { name: 'Point' },
geometry: { type: 'Point', coordinates: [7.6869, 45.0703] }
}]
});
});
Image overlay
plugin.mapHook.onHook(function () {
var overlay = plugin.mapHook.createImageOverlay({
url: 'https://example.com/overlay.png',
bounds: { south: 45.06, west: 7.67, north: 45.08, east: 7.70 },
opacity: 0.8
});
overlay.setOpacity(0.5);
});
Cleanup
Layers and map subscriptions are torn down for you. Anything else a plugin creates β an
injected <style> tag, a timer, a listener on the page β needs explicit
cleanup, or it survives until the page reloads.
var style = document.createElement('style');
style.textContent = '.leaflet-tile-pane{filter:invert(1)}';
document.head.appendChild(style);
plugin.onDispose(function () {
style.remove();
});
Custom control
plugin.mapHook.onHook(function () {
var control = plugin.mapHook.createControl({
id: 'my-control',
position: 'top-right',
html: '<button type="button">Run</button>'
});
});
Controls can be interactive. createControl inserts the markup into the page
and tags the wrapper with data-map-control-id, and plugin code shares that
DOM β so once the call returns (it's synchronous) you can find your own markup and wire
it up:
plugin.mapHook.onHook(function () {
plugin.mapHook.createControl({
id: 'my-control',
position: 'top-right',
html: '<button type="button" data-run>Run</button><span data-out></span>'
});
var root = document.querySelector('[data-map-control-id="my-control"]');
if (!root) {
plugin.error('control markup not found');
return;
}
var out = root.querySelector('[data-out]');
root.querySelector('[data-run]').addEventListener('click', function () {
var center = plugin.mapHook.getCenter();
out.textContent = center ? center.lat.toFixed(4) + ', ' + center.lng.toFixed(4) : 'no map';
});
});
Use inline style="β¦" on your markup β the host page's own CSS applies to it too. Put unique data-* attributes on the elements you need to find, and always null-check the lookup.
WMS layer
WMS (Web Map Service) is a standard protocol many government and scientific map services publish through β the same overlay type the built-in Italy Cadastral registry plugin uses.
plugin.mapHook.onHook(function () {
var layer = plugin.mapHook.createWmsLayer({
url: 'https://example.com/wms',
layers: 'layer_name',
format: 'image/png',
transparent: true,
opacity: 0.8,
version: '1.1.1',
srs: 'EPSG:4326',
tileSize: 512
});
layer.setOpacity(0.5);
});
Two useful patterns
Click to draw
var drawing = false;
var path = [];
plugin.mapHook.onHook(function () {
var points = plugin.mapHook.createMarkerLayer();
var polygons = plugin.mapHook.createPolygonLayer();
plugin.mapHook.onRightClick(function () {
drawing = !drawing;
path = [];
points.clear();
polygons.clear();
});
plugin.mapHook.onClick(function (e) {
if (!drawing) return;
path.push({ lat: e.lat, lng: e.lng });
points.addMarker({ id: 'point-' + path.length, lat: e.lat, lng: e.lng, color: 'black' });
if (path.length >= 3) {
polygons.clear();
polygons.addPolygon({ id: 'drawn', path: path, strokeColor: 'black', fillColor: 'black', fillOpacity: 0.15 });
}
});
});
Viewport-based loading
The recommended shape for any plugin that fetches data based on what's currently visible. It debounces, ignores stale responses with a sequence counter, and clears old results before drawing new ones β the three things reviewers and the AI output checklist look for.
var _seq = 0;
var _debounce;
plugin.mapHook.onHook(function () {
var layer = plugin.mapHook.createMarkerLayer();
function load(bounds) {
if (!bounds) { layer.clear(); return; }
var seq = ++_seq;
var bbox = [bounds.south, bounds.west, bounds.north, bounds.east].join(',');
var query = '[out:json][timeout:25];node["amenity"="cafe"](' + bbox + ');out;';
plugin
.fetch('https://overpass-api.de/api/interpreter', { method: 'POST', body: query })
.then(function (res) { return res.json(); })
.then(function (data) {
if (seq !== _seq) return; // a newer request already landed β drop this one
layer.clear();
var elements = data.elements || [];
for (var i = 0; i < elements.length; i++) {
var el = elements[i];
if (typeof el.lat !== 'number' || typeof el.lon !== 'number') continue;
var name = (el.tags && el.tags.name) || 'Cafe';
layer.addMarker({ lat: el.lat, lng: el.lon, color: 'orange', popup: name });
}
plugin.log('Loaded ' + elements.length + ' cafes');
})
.catch(function (err) {
plugin.error('Cafe load failed:', err.message || String(err));
});
}
function schedule(bounds) {
clearTimeout(_debounce);
_debounce = setTimeout(function () { load(bounds); }, 500);
}
schedule(plugin.mapHook.getBounds());
plugin.mapHook.onMoveEnd(schedule);
});
Built-in examples
The plugins that ship with the extension demonstrate most of the patterns above:
| Plugin | Demonstrates |
|---|---|
| Train Stations | Viewport-based marker loading (the pattern above) |
| Public Transport Finder | Click-to-search markers |
| OpenRailwayMap Overlay | An always-on XYZ tile layer |
| GeoJSON / GPX Viewer | Loading a local file onto the map |
| Dark Map | An injected stylesheet with cleanup on disable |
| Measure Distance | Map click handling with no network access |
The plugin registry adds a couple more: a WMS overlay (Italy Cadastral) and polyline rendering (Rail Tracks).