Map Extender
Docs Gallery

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.

Must be valid JSON

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);
});
Pick one hostname

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:

PluginDemonstrates
Train StationsViewport-based marker loading (the pattern above)
Public Transport FinderClick-to-search markers
OpenRailwayMap OverlayAn always-on XYZ tile layer
GeoJSON / GPX ViewerLoading a local file onto the map
Dark MapAn injected stylesheet with cleanup on disable
Measure DistanceMap click handling with no network access

The plugin registry adds a couple more: a WMS overlay (Italy Cadastral) and polyline rendering (Rail Tracks).