What a plugin is
A zip containing a folder <id>/ with plugin.json (the manifest) and one or more of: run.py (Python, runs on the server with GDAL + numpy), run.php (PHP, runs inside the API with the SQL helpers), client.js (runs in the browser with the GT API). Administrators install it from Analysis › Plugins ▾ › Plugin Manager (catalogue, zip or URL). Enabled plugins appear in the Plugins menu, grouped by category, and in Command Search.
Starter examples (all in plugins/repo/*.zip on this server): graticule (simplest Python), datetime_tools (PHP, in place), coordinate_capture (client only), dem_profile (client + server), csv_wkt_import (file upload).
plugin.json
{
"id": "graticule", // a-z 0-9 _ ; folder name and zip root
"name": "Graticule / Grid Lines",
"version": "1.0", "author": "GT Digital", "category": "Cartography",
"description": "One or two sentences shown in the menu and manager.",
"help": "Shown at the bottom of the dialog.",
"client": false, // true when client.js registers the tool itself
"output": "layer", // layer | rows | inplace | none (button label and result handling)
"fields": [ // the dialog, rendered automatically
{"id":"layer","type":"layer","label":"Extent from layer","optional":true},
{"id":"raster","type":"layer","label":"DEM","geom":"raster"},
{"id":"field","type":"field","label":"Value field","layerRef":"layer","numeric":true},
{"id":"interval","type":"number","label":"Interval (°)","default":0.5},
{"id":"mode","type":"select","label":"Mode","options":[["a","Option A"],["b","Option B"]],"default":"a"},
{"id":"flag","type":"check","label":"Tick me"},
{"id":"title","type":"text","label":"Title","default":""},
{"id":"notes","type":"textarea","label":"Notes"},
{"id":"file","type":"file","label":"CSV file","accept":".csv"}
]
}
run.py — the server contract
The script gets one argument: a JSON context file. Import the helper library (on the path automatically):
from geotee_plugin import ctx, load, save, done, fail, param, utm, points_of, haversine_m, bearing
c = ctx() # {params, layers:{field:{path,name,geom_type,bbox,schema,raster,id}}, files:{field:path}, out_geojson, out_tif, utm_epsg}
gj = load(c['layers']['layer']['path']) # GeoJSON dict (EPSG:4326)
iv = param('interval', 0.5, float)
# ... build features ...
save({'type':'FeatureCollection','features':feats}) # writes c['out_geojson']
done(layer={'name':'My result','geom':'LineString'}) # new layer
# other endings:
done(rows=[{'a':1,'b':2}], field='my_table') # table in the dialog + CSV download
done(inplace='layer', message='42 features updated') # replace the input layer (version kept)
done(message='Nothing to do') # just a message
fail('Explain what went wrong') # error shown in the dialog
A raster result: write a GeoTIFF to c['out_tif'] and call done(layer={'path': c['out_tif'], 'name': 'Slope', 'ramp': 'terrain', 'maxzoom': 14}). Rasters given as inputs ("geom":"raster") arrive as GeoTIFF paths. Python has GDAL/OGR (osgeo), numpy, and the command-line tools (ogr2ogr, gdalwarp…). Print nothing else to stdout after done().
run.php
Runs inside the API with $ctx (same shape) and every helper from _vec.php: layer($id), mkgpkg(), to_gpkg(), sql_to_layer(), replace_in_place(), rows_query(), qid(), has_field(), utm_epsg(). Return an array: ['layer' => ['path'=>…,'name'=>…]], ['rows' => […]], ['inplace' => ['field'=>'layer','path'=>…]], ['done_layer' => $layerRow] (when you already used replace_in_place / sql_to_layer), or ['error' => '…'].
client.js — the browser API
GT.registerTool({ id:'my_plugin', run(manifest) {
GT.dialog('Title', '<div class="field">…</div><div id="anmsg"></div>', [['Cancel', GT.closeDialog], ['Go', async () => {
const d = await GT.runPlugin('my_plugin', { some:'params', points }, { file:fileObject }); // calls run.py / run.php
GT.addLayer(d.layer); // library row → map
}, 'primary']], /*wide*/ false);
} });
| GT.… | What it gives |
| map | The MapLibre map (GT.map.on('click', …), getCenter()…) |
| layers(), selectedLayer(), library() | Map layers ({id, libId, name, raster, fields, geo}), the selected one, the catalogue rows |
| pickPoints(max, cb) | Crosshair pick mode with a Done/Cancel bar; cb([[lon,lat],…]) |
| dialog(title, html, buttons, wide), closeDialog(), toast(msg), esc(text) | Standard dialog and helpers; put results in #anmsg |
| runPlugin(id, params, files) | Runs the server part; resolves to {layer|rows|data|message} |
| addLayer(row), reloadCatalog(), api(url, opt), GF | Add a result to the map, refresh the catalogue, raw API access, the runtime library |
A plugin with "client": true and no run.* is client-only (no server call). A plugin with both is “hybrid”: the client draws the UI, the server does the work (see dem_profile).
Packaging and sharing
Zip the folder so the zip contains my_plugin/plugin.json. Install it from the manager (zip or URL). To add it to this server's catalogue, put the zip in plugins/repo/ and add an entry to plugins/repo/catalog.json. Only admins can install; editors can run; viewers see nothing. Plugins run with the server's rights — install only code you trust.