# Map Extender AI Skills

Paste this whole file into an AI chat (ChatGPT, Claude, Cursor, etc.) as context, describe the
plugin you want, and ask it to return the plugin code plus an optional settings schema (a form
that lets the plugin have configurable options). See mapextender.com/docs/ai-assisted.html for a
full walkthrough, and mapextender.com/docs/api.html for this same reference as a readable page.

Use this file as context when asking an AI to write a Map Extender plugin. Paste the relevant sections, describe what you want the plugin to do, and ask the AI to return the plugin code plus optional settings schema.

## What The System Can Do

Map Extender is a Chrome extension that hooks into web maps and runs user plugins on matching pages.

Supported map engines:

- Leaflet maps
- Google Maps

Supported plugin capabilities:

- Detect when a map is hooked and ready
- Read current map bounds
- Read current map center and zoom
- Read current map type
- Move, pan, and fit the map view
- React when the user clicks the map
- React to double-click, right-click, mouse move, zoom end, and drag end
- React when the map moves, zooms, pans, or finishes loading new bounds
- Add marker layers
- Add polyline layers
- Add polygon, circle, and rectangle layers
- Add GeoJSON layers
- Add XYZ tile overlay layers
- Add image overlays
- Add custom controls
- Add WMS tile overlay layers
- Clear or dispose plugin-created layers
- Fetch remote data through the extension background service
- Store plugin-specific local data
- Read plugin settings
- Log plugin activity, warnings, and errors
- React to page load, URL changes, and DOM element appearance
- Apply enable, disable, edit, and delete changes live without refreshing the map page

## Runtime Rules

Plugins run as plain JavaScript. Do not use imports, exports, TypeScript syntax, JSX, build tools, or external npm packages.

The plugin entry object is named `plugin`.

Use browser APIs that are available in a userscript-like environment, such as `setTimeout`, `clearTimeout`, `Promise`, `document`, `window`, and `MutationObserver`.

Prefer old-style JavaScript that works as direct script source:

```js
var state = {};
function run() {}
plugin.mapHook.onHook(function () {});
```

Avoid:

```js
import x from '...';
export default {};
const enum X {}
```

## Plugin Record Shape

The plugins page stores plugins as records:

```ts
{
  id: string,
  name: string,
  description: string,
  usage?: string, // short how-to shown in the popup when enabled
  enabled: boolean,
  matchPatterns: string[],
  settingsSchema: PluginSettingSchema[],
  settings: Record<string, string | number | boolean>,
  code: string,
  createdAt: number,
  updatedAt: number
}
```

Match patterns support common wildcard URL patterns, for example:

```js
['*://*/*']
['https://www.openstreetmap.org/*']
['https://example.com/maps/*']
```

## Settings Schema

Plugins can expose settings on the plugins page.

**The settings schema must be valid JSON** — a JSON array of setting objects. On the plugins page, paste it into the **JSON** tab of the settings editor (or use the form UI). Use double-quoted keys and strings, JSON booleans (`true` / `false`), and JSON numbers. Do not use JavaScript object syntax (unquoted keys, trailing commas, or `undefined`).

Supported setting types:

- `text`
- `number`
- `boolean`
- `select`

Example settings schema (JSON):

```json
[
  { "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 settings in plugin code:

```js
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();
```

## Plugin API

### Basic Info

```js
plugin.pluginId
```

### Logging

```js
plugin.log('Loaded');
plugin.warn('Something looks unusual');
plugin.error('Something failed');
```

Logs appear on the plugins page and important errors appear in DevTools.

### Page Helpers

Run after DOM is ready:

```js
plugin.onPageLoad(function () {
  plugin.log('Page loaded');
});
```

Watch URL changes:

```js
var stopUrlWatch = plugin.onUrlChange(function (url) {
  plugin.log('URL changed:', url);
});
```

Watch for DOM elements:

```js
var stopElementWatch = plugin.onElementAppear('.some-selector', function (el) {
  plugin.log('Element appeared:', el.textContent);
});
```

Wait for one element:

```js
plugin.waitForElement('.some-selector', 10000).then(function (el) {
  plugin.log('Found element:', el.textContent);
});
```

### Persistent Store

Each plugin gets its own key-value storage namespace.

```js
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`, not direct `fetch`, for remote API requests. It runs through the extension background and supports extension host permissions.

```js
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 badge shows how many plugins are currently loading/fetching.

## Map API

All map operations are under `plugin.mapHook`.

### Wait For Map Hook

Always wrap map work in `onHook`.

```js
plugin.mapHook.onHook(function () {
  plugin.log('Map is ready');
});
```

### Read Map State

```js
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();
```

Bounds shape:

```js
{
  south: number,
  west: number,
  north: number,
  east: number
}
```

View helpers:

```js
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

Use `onMoveEnd` to reload data when the map moves, zooms, pans, or finishes changing bounds.

```js
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 Map Clicks

Use `onClick` to receive the latitude and longitude where the user clicked the map.

```js
plugin.mapHook.onHook(function () {
  plugin.mapHook.onClick(function (e) {
    alert(
      'Lat: ' + e.lat +
      '\nLng: ' + e.lng
    );
  });
});
```

Other common map events:

```js
plugin.mapHook.onHook(function () {
  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) {
    // Use this sparingly because it fires often.
  });

  plugin.mapHook.onZoomEnd(function () {
    plugin.log('Zoom:', plugin.mapHook.getZoom());
  });

  plugin.mapHook.onDragEnd(function () {
    plugin.log('Center:', plugin.mapHook.getCenter());
  });
});
```

Use debouncing for network requests:

```js
var debounce;

function schedule(bounds) {
  clearTimeout(debounce);
  debounce = setTimeout(function () {
    loadForBounds(bounds);
  }, 500);
}
```

## Layers

Plugin-created layers are automatically disposed when the plugin is disabled, edited, deleted, or re-run.

### Marker Layer

```js
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:

```js
'click'
'doubleClick'
'rightClick'
'mouseOver'
'mouseOut'
```

### Polyline Layer

```js
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

```js
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

```js
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. Pick one fixed hostname.

### GeoJSON Layer

```js
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

```js
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 automatically when a plugin is disabled, edited, or
deleted. Anything else the plugin creates — an injected `<style>`, a timer, a listener on the
page — must be cleaned up explicitly, or it survives until the page reloads.

```js
var style = document.createElement('style');
style.textContent = '.leaflet-tile-pane{filter:invert(1)}';
document.head.appendChild(style);

plugin.onDispose(function () {
  style.remove();
});
```

### Custom Control

```js
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 after the call returns
(it is synchronous) you can find your own markup and wire it up:

```js
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 CSS applies to it too. Put unique
`data-*` attributes on the elements you need to find, and always null-check the lookup.

### Simple Drawing Pattern

```js
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
      });
    }
  });
});
```

### WMS Layer

```js
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);
});
```

## Recommended Plugin Pattern

Use this pattern for plugins that load data based on the current viewport.

```js
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;

        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);
});
```

## Good AI Request Template

Paste this into an AI chat together with this file:

```text
You are writing a Map Extender plugin.

Follow the Map Extender AI Skills API exactly.
Return:
1. Plugin name
2. Description
3. Match patterns
4. Settings schema as valid JSON (array), if needed
5. Default settings, if needed
6. JavaScript plugin code only, with no imports/exports/TypeScript/JSX

User request:
<describe the plugin here>
```

Example user request:

```text
Create a plugin that shows all bicycle parking locations in the current map bounds.
Use Overpass API.
Add blue markers.
Show the name or "Bicycle parking" in the popup.
Reload when the map moves, debounced by 500ms.
Add a setting for marker color.
```

## AI Output Checklist

Before accepting generated code, check that:

- It uses `plugin.mapHook.onHook(...)`
- It uses `plugin.fetch(...)` for remote requests
- The settings schema is valid JSON (if present) and can be pasted into the plugins page's JSON editor
- It debounces repeated map movements before network calls
- It ignores stale async responses with a sequence counter
- It clears old layer data before drawing new results
- It logs useful success and error messages
- It does not use imports, exports, TypeScript syntax, JSX, or npm packages
- It does not require a page refresh to enable or disable
- It creates layers through `plugin.mapHook.createMarkerLayer`, `createPolylineLayer`, or `createWmsLayer`

## Built-In Plugin Examples

Current built-in plugins demonstrate:

- Train station markers from Overpass (viewport-based marker loading)
- A public-transport stop finder from Overpass (click-to-search markers)
- An OpenRailwayMap raster overlay (always-on XYZ tile layer)
- A GeoJSON/GPX file viewer (loading a local file onto the map)
- A dark-map filter (a plugin-injected stylesheet with cleanup on disable)
- Click-to-measure distance (map click handling with no network access)

The [plugin registry](https://ssz360.github.io/map-extender-plugins/) adds further examples — a
WMS overlay (Italy Cadastral) and polyline rendering (Rail Tracks) among them.
